pin chats

merge set avatar button and avatar
This commit is contained in:
koplenov 2026-05-07 19:47:08 +03:00
parent deae55c0d1
commit 70a71d4212
3 changed files with 142 additions and 23 deletions

View file

@ -30,6 +30,13 @@
width: 56px;
height: 56px;
margin: 0 auto .5rem auto;
cursor: pointer;
transition: filter .15s, transform .15s;
}
[xmpp_Folders_pane] [xmpp_My_avatar]:hover {
filter: brightness(.7);
transform: scale(1.05);
}
[xmpp_Folders_spacer] {
@ -127,6 +134,18 @@
opacity: 1;
}
[xmpp_Roster_contact] {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
}
[xmpp_Roster_contact_drag] {
flex: 1;
min-width: 0;
}
[xmpp_Roster_contact_link] {
display: flex;
flex-direction: row;
@ -144,6 +163,32 @@
opacity: .4;
}
[xmpp_Roster_contact_drop][mol_drop_status="drag"] [xmpp_Roster_contact_link] {
background: var(--mol_theme_focus, rgba(74,158,255,.15));
}
[xmpp_Pin_button] {
flex-shrink: 0;
width: 1.8rem;
min-width: 1.8rem;
padding: 0;
opacity: 0;
transition: opacity .15s;
}
[xmpp_Roster_contact]:hover [xmpp_Pin_button],
[xmpp_Roster_contact][xmpp_pinned="true"] [xmpp_Pin_button] {
opacity: .6;
}
[xmpp_Pin_button]:hover {
opacity: 1 !important;
}
[xmpp_Roster_contact][xmpp_pinned="true"] [xmpp_Pin_button] {
color: var(--mol_theme_focus, #4a9eff);
}
[xmpp_Roster_pane] {
width: 320px;
flex-shrink: 0;
@ -190,7 +235,6 @@
}
[xmpp_Folders_pane] [xmpp_Disconnect_button],
[xmpp_Folders_pane] [xmpp_Set_avatar_button],
[xmpp_Folders_pane] [xmpp_Lights2] {
min-height: 2rem;
padding: .35rem;

View file

@ -44,6 +44,9 @@ $xmpp $mol_view
^
src <= my_avatar_uri \
alt \
event *
^
click? <=> do_set_avatar? null
<= Folder_all $mol_link
arg *
folder null
@ -68,11 +71,6 @@ $xmpp $mol_view
sub /
<= new_folder_text @ \+ Drop chat
<= Folders_spacer $mol_view
<= Set_avatar_button $mol_button_minor
hint @ \Set avatar
click? <=> do_set_avatar? null
sub /
<= Set_avatar_icon $mol_icon_camera
<= Lights2 $mol_lights_toggle
<= Disconnect_button $mol_button_minor
hint <= disconnect_label @ \Disconnect
@ -170,22 +168,34 @@ $xmpp $mol_view
title <= send_label @ \Send
click? <=> do_send*? null
-
Roster_contact* $mol_drag
transfer *
text/plain <= roster_jid* \
Sub <= Roster_contact_link* $mol_link
arg *
chat <= roster_jid* \
sub /
<= Contact_avatar* $mol_view
dom_name \img
attr *
^
src <= contact_avatar_uri* \
alt \
<= Contact_label* $mol_view
sub /
<= contact_display* \
Roster_contact* $mol_view
attr *
^
xmpp_pinned <= contact_pinned* false
sub /
<= Roster_contact_drag* $mol_drag
transfer *
text/plain <= roster_jid* \
Sub <= Roster_contact_drop* $mol_drop
adopt?transfer <=> folder_adopt?transfer null
receive?obj <=> reorder_pin*?obj null
Sub <= Roster_contact_link* $mol_link
arg *
chat <= roster_jid* \
sub /
<= Contact_avatar* $mol_view
dom_name \img
attr *
^
src <= contact_avatar_uri* \
alt \
<= Contact_label* $mol_view
sub /
<= contact_display* \
<= Pin_button* $mol_button_minor
click? <=> toggle_pin*? null
sub /
<= Pin_icon* $mol_icon_pin
-
Msg* $mol_view
sub /

View file

@ -765,6 +765,66 @@ private _handle_message(el: Element) {
@ $mol_mem
folders_ver(next?: number) { return next ?? 0 }
// ── Pins per folder ─────────────────────────────────────────────────
@ $mol_mem
pins_ver(next?: number) { return next ?? 0 }
// Folder key used in pin storage. Empty/null URL arg → 'all'.
private _folder_key(): string {
const f = this.folder()
return f ? f : 'all'
}
private _pins_obj(): Record<string, string[]> {
this.pins_ver()
return (this.$.$mol_state_local.value('xmpp_pins') as Record<string, string[]> | null) ?? {}
}
private _save_pins(obj: Record<string, string[]>) {
this.$.$mol_state_local.value('xmpp_pins', obj)
this.pins_ver(this.pins_ver() + 1)
}
pinned_jids(folder: string): string[] {
return this._pins_obj()[folder] ?? []
}
contact_pinned(jid: string): boolean {
return this.pinned_jids(this._folder_key()).includes(jid)
}
toggle_pin(jid: string) {
const folder = this._folder_key()
const obj = { ...this._pins_obj() }
const arr = [...(obj[folder] ?? [])]
const idx = arr.indexOf(jid)
if (idx >= 0) arr.splice(idx, 1)
else arr.unshift(jid)
if (arr.length === 0) delete obj[folder]
else obj[folder] = arr
this._save_pins(obj)
}
// Drop handler on a contact row: reorder pins. `target_jid` is this row's jid (anchor),
// `source_jid` is the dragged contact. Insert source before target in the pin list.
reorder_pin(target_jid: string, source_jid: string) {
if (!source_jid || target_jid === source_jid) return
const folder = this._folder_key()
const obj = { ...this._pins_obj() }
let arr = [...(obj[folder] ?? [])].filter(j => j !== source_jid)
const idx = arr.indexOf(target_jid)
if (idx >= 0) {
arr.splice(idx, 0, source_jid)
} else {
// Target not pinned → pin it first, then place source above it.
arr.push(target_jid)
arr.unshift(source_jid)
}
obj[folder] = arr
this._save_pins(obj)
}
private _folders_obj(): Record<string, string[]> {
return (this.$.$mol_state_local.value('xmpp_folders') as Record<string, string[]> | null) ?? {}
}
@ -1059,6 +1119,7 @@ private _handle_message(el: Element) {
@ $mol_mem
roster_rows() {
this.folders_ver()
this.pins_ver()
const folder = this.folder()
const contacts = this.contacts()
const rooms = this.rooms()
@ -1076,7 +1137,11 @@ private _handle_message(el: Element) {
return jid.toLowerCase().includes(q) || name.includes(q)
})
}
return jids.map(j => this.Roster_contact(j))
// Pinned at top in pin order, rest below preserving original order.
const pinned_set = new Set(this.pinned_jids(this._folder_key()))
const pinned = this.pinned_jids(this._folder_key()).filter(j => jids.includes(j))
const rest = jids.filter(j => !pinned_set.has(j))
return [...pinned, ...rest].map(j => this.Roster_contact(j))
}
// Suggestion buttons appear when the search query looks like a JID and isn't already in the list.