From 70a71d421251f8a5969cec466859fd5ec5d7e9ad Mon Sep 17 00:00:00 2001 From: koplenov Date: Thu, 7 May 2026 19:47:08 +0300 Subject: [PATCH] pin chats merge set avatar button and avatar --- xmpp.view.css | 46 +++++++++++++++++++++++++++++++++- xmpp.view.tree | 52 +++++++++++++++++++++++---------------- xmpp.view.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 23 deletions(-) diff --git a/xmpp.view.css b/xmpp.view.css index 4e1a12b..f8ee0bf 100644 --- a/xmpp.view.css +++ b/xmpp.view.css @@ -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; diff --git a/xmpp.view.tree b/xmpp.view.tree index db9d9f2..2284665 100644 --- a/xmpp.view.tree +++ b/xmpp.view.tree @@ -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 / diff --git a/xmpp.view.ts b/xmpp.view.ts index 9fa65ae..f2b20f2 100644 --- a/xmpp.view.ts +++ b/xmpp.view.ts @@ -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 { + this.pins_ver() + return (this.$.$mol_state_local.value('xmpp_pins') as Record | null) ?? {} + } + + private _save_pins(obj: Record) { + 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 { return (this.$.$mol_state_local.value('xmpp_folders') as Record | 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.