update interface, add folders support
This commit is contained in:
parent
8c6e7a774d
commit
2355d6836d
3 changed files with 623 additions and 107 deletions
314
xmpp.view.css
314
xmpp.view.css
|
|
@ -1,3 +1,310 @@
|
|||
/* ── Three-column Telegram-like layout ────────────────────────────── */
|
||||
|
||||
[mol_view_root="$xmpp"] > [xmpp] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[xmpp_Login_pane] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[xmpp_Folders_pane] {
|
||||
width: 84px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: .5rem;
|
||||
gap: .25rem;
|
||||
background: var(--mol_theme_field);
|
||||
border-right: 1px solid var(--mol_theme_line);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[xmpp_Folders_pane] [xmpp_My_avatar] {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto .5rem auto;
|
||||
}
|
||||
|
||||
[xmpp_Folders_spacer] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[xmpp_Folder_all],
|
||||
[xmpp_Folder_chats],
|
||||
[xmpp_Folder_rooms] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
font-size: .8rem;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[xmpp_Folder_all]:hover,
|
||||
[xmpp_Folder_chats]:hover,
|
||||
[xmpp_Folder_rooms]:hover,
|
||||
[xmpp_User_folder_link]:hover {
|
||||
background: var(--mol_theme_hover, rgba(0,0,0,.05));
|
||||
}
|
||||
|
||||
[xmpp_Folder_all][mol_link_current="true"],
|
||||
[xmpp_Folder_chats][mol_link_current="true"],
|
||||
[xmpp_Folder_rooms][mol_link_current="true"],
|
||||
[xmpp_User_folder_link][mol_link_current="true"] {
|
||||
background: var(--mol_theme_focus, #4a9eff);
|
||||
color: var(--mol_theme_card, #fff);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[xmpp_User_folders] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .15rem;
|
||||
}
|
||||
|
||||
[xmpp_User_folder] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: .15rem;
|
||||
}
|
||||
|
||||
[xmpp_User_folder_drop] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[xmpp_User_folder_link] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .4rem .25rem;
|
||||
border-radius: .5rem;
|
||||
font-size: .75rem;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[xmpp_User_folder_delete] {
|
||||
width: 1.4rem;
|
||||
min-width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
padding: 0;
|
||||
font-size: .9rem;
|
||||
opacity: .4;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
[xmpp_User_folder_delete]:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[xmpp_New_folder_zone] {
|
||||
margin-top: .25rem;
|
||||
padding: .5rem .25rem;
|
||||
border: 2px dashed var(--mol_theme_line, rgba(0,0,0,.2));
|
||||
border-radius: .5rem;
|
||||
font-size: .7rem;
|
||||
opacity: .5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[xmpp_New_folder_zone][mol_drop_status="drag"],
|
||||
[xmpp_User_folder_drop][mol_drop_status="drag"] [xmpp_User_folder_link] {
|
||||
background: var(--mol_theme_focus, rgba(74,158,255,.15));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[xmpp_Roster_contact_link] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .5rem .75rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[xmpp_Roster_contact_link]:hover {
|
||||
background: var(--mol_theme_hover, rgba(0,0,0,.05));
|
||||
}
|
||||
|
||||
[xmpp_Roster_contact][mol_drag_status="drag"] [xmpp_Roster_contact_link] {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
[xmpp_Roster_pane] {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--mol_theme_line);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[xmpp_Roster_header] {
|
||||
padding: .5rem 1rem;
|
||||
border-bottom: 1px solid var(--mol_theme_line);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[xmpp_Roster_static_header] {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
[xmpp_Roster_folder_header] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[xmpp_Roster_folder_input] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[xmpp_Roster_folder_delete] {
|
||||
flex-shrink: 0;
|
||||
font-size: .8rem;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
[xmpp_Roster_folder_delete]:hover {
|
||||
opacity: 1;
|
||||
color: #d33;
|
||||
}
|
||||
|
||||
[xmpp_Folders_pane] [xmpp_Disconnect_button],
|
||||
[xmpp_Folders_pane] [xmpp_Set_avatar_button],
|
||||
[xmpp_Folders_pane] [xmpp_Lights2] {
|
||||
min-height: 2rem;
|
||||
padding: .35rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[xmpp_Roster_list] {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
[xmpp_Chat_pane] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[xmpp_Chat_view] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
[xmpp_Chat_header] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .75rem 1rem;
|
||||
border-bottom: 1px solid var(--mol_theme_line);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[xmpp_Chat_title] {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[xmpp_Chat_avatar] {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
[xmpp_Messages_list] {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
[xmpp_Compose_pane] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: .5rem;
|
||||
padding: .75rem 1rem;
|
||||
border-top: 1px solid var(--mol_theme_line);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[xmpp_Chat_placeholder] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: .5;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
[xmpp_Search_input] {
|
||||
margin: .5rem 1rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
height: 2.25rem;
|
||||
min-height: 2.25rem;
|
||||
max-height: 2.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[xmpp_Search_actions] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .25rem;
|
||||
padding: .25rem .5rem .5rem .5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[xmpp_Search_actions]:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[xmpp_Search_action_chat],
|
||||
[xmpp_Search_action_room] {
|
||||
font-size: .85rem;
|
||||
padding: .5rem .75rem;
|
||||
border-radius: .5rem;
|
||||
background: var(--mol_theme_card);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[xmpp_Search_action_chat]:hover,
|
||||
[xmpp_Search_action_room]:hover {
|
||||
background: var(--mol_theme_hover, rgba(0,0,0,.05));
|
||||
}
|
||||
|
||||
/* ── Messages ─────────────────────────────────────────────────────── */
|
||||
|
||||
[xmpp_Msg] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -115,13 +422,6 @@
|
|||
}
|
||||
|
||||
|
||||
[xmpp_Roster_contact] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
[xmpp_Contact_label] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
|
|||
222
xmpp.view.tree
222
xmpp.view.tree
|
|
@ -1,8 +1,9 @@
|
|||
$xmpp $mol_book2
|
||||
$xmpp $mol_view
|
||||
plugins /
|
||||
<= Theme $mol_theme_auto
|
||||
sub <= panes /
|
||||
-
|
||||
Login_page $mol_page
|
||||
Login_pane $mol_page
|
||||
title @ \XMPP Client
|
||||
tools /
|
||||
<= Lights $mol_lights_toggle
|
||||
|
|
@ -35,107 +36,156 @@ $xmpp $mol_book2
|
|||
click? <=> do_connect? null
|
||||
disabled <= connecting false
|
||||
-
|
||||
Roster_page $mol_page
|
||||
title <= roster_title @ \Contacts
|
||||
tools /
|
||||
<= Lights $mol_lights_toggle
|
||||
Folders_pane $mol_view
|
||||
sub /
|
||||
<= My_avatar $mol_view
|
||||
dom_name \img
|
||||
attr *
|
||||
^
|
||||
src <= my_avatar_uri \
|
||||
alt \
|
||||
<= Folder_all $mol_link
|
||||
arg *
|
||||
folder null
|
||||
sub /
|
||||
<= folder_all_label @ \All
|
||||
<= Folder_chats $mol_link
|
||||
arg *
|
||||
folder \chats
|
||||
sub /
|
||||
<= folder_chats_label @ \Chats
|
||||
<= Folder_rooms $mol_link
|
||||
arg *
|
||||
folder \rooms
|
||||
sub /
|
||||
<= folder_rooms_label @ \Rooms
|
||||
<= User_folders $mol_view
|
||||
sub <= user_folder_views /
|
||||
<= New_folder_zone $mol_drop
|
||||
adopt?transfer <=> folder_adopt?transfer null
|
||||
receive?obj <=> new_folder_receive?obj null
|
||||
Sub <= New_folder_label $mol_view
|
||||
sub /
|
||||
<= new_folder_text @ \+ Drop chat
|
||||
<= Folders_spacer $mol_view
|
||||
<= Set_avatar_button $mol_button_minor
|
||||
title @ \Set avatar
|
||||
hint @ \Set avatar
|
||||
click? <=> do_set_avatar? null
|
||||
sub /
|
||||
<= Set_avatar_icon $mol_icon_camera
|
||||
<= Lights2 $mol_lights_toggle
|
||||
<= Disconnect_button $mol_button_minor
|
||||
title <= disconnect_label @ \Disconnect
|
||||
hint <= disconnect_label @ \Disconnect
|
||||
click? <=> do_disconnect? null
|
||||
body /
|
||||
sub /
|
||||
<= Disconnect_icon $mol_icon_logout
|
||||
-
|
||||
User_folder* $mol_drop
|
||||
adopt?transfer <=> folder_adopt?transfer null
|
||||
receive?obj <=> folder_receive*?obj null
|
||||
Sub <= User_folder_link* $mol_link
|
||||
arg *
|
||||
folder <= user_folder_name* \
|
||||
sub /
|
||||
<= user_folder_label* \
|
||||
-
|
||||
Roster_pane $mol_view
|
||||
sub /
|
||||
<= Roster_header $mol_view
|
||||
sub <= roster_header_sub /
|
||||
<= Search_input $mol_string
|
||||
hint @ \Search or enter JID…
|
||||
value? <=> search_query? \
|
||||
<= Roster_list $mol_list
|
||||
rows <= roster_rows /
|
||||
<= New_chat_form $mol_form
|
||||
form_fields /
|
||||
<= New_chat_field $mol_form_field
|
||||
name @ \New chat with
|
||||
control <= New_chat_input $mol_string
|
||||
hint \user@example.com
|
||||
value? <=> new_chat_jid? \
|
||||
buttons /
|
||||
<= New_chat_button $mol_button_major
|
||||
title @ \Start chat
|
||||
click? <=> do_new_chat? null
|
||||
<= Room_join_form $mol_form
|
||||
form_fields /
|
||||
<= Room_jid_field $mol_form_field
|
||||
name @ \Room JID
|
||||
control <= Room_jid_input $mol_string
|
||||
hint \room@conference.example.com
|
||||
value? <=> room_jid? \
|
||||
<= Room_nick_field $mol_form_field
|
||||
name @ \Nickname
|
||||
control <= Room_nick_input $mol_string
|
||||
hint @ \nickname
|
||||
value? <=> room_nick? \
|
||||
buttons /
|
||||
<= Room_join_button $mol_button_major
|
||||
title @ \Join Room
|
||||
click? <=> do_join_room? null
|
||||
<= Search_actions $mol_view
|
||||
sub <= search_actions_sub /
|
||||
-
|
||||
Chat_page* $mol_page
|
||||
title <= chat_with* \
|
||||
Logo <= Chat_avatar* $mol_view
|
||||
dom_name \img
|
||||
attr *
|
||||
^
|
||||
src <= chat_avatar_uri* \
|
||||
alt \
|
||||
tools /
|
||||
<= Chat_leave* $mol_button_minor
|
||||
title @ \Leave
|
||||
click? <=> do_leave_room*? null
|
||||
attr *
|
||||
^
|
||||
hidden <= chat_leave_hidden* \
|
||||
<= Chat_close* $mol_link
|
||||
arg *
|
||||
chat null
|
||||
Search_action_chat $mol_button_minor
|
||||
title <= search_action_chat_title \
|
||||
click? <=> do_search_chat? null
|
||||
-
|
||||
Search_action_room $mol_button_minor
|
||||
title <= search_action_room_title \
|
||||
click? <=> do_search_room? null
|
||||
-
|
||||
Roster_static_header $mol_view
|
||||
sub /
|
||||
<= roster_static_text \
|
||||
-
|
||||
Roster_folder_header $mol_view
|
||||
sub /
|
||||
<= Roster_folder_input $mol_string
|
||||
value? <=> roster_folder_input? \
|
||||
hint @ \Folder name
|
||||
<= Roster_folder_delete $mol_button_minor
|
||||
title @ \Delete folder
|
||||
click? <=> do_delete_current_folder? null
|
||||
-
|
||||
Chat_pane $mol_view
|
||||
sub <= chat_pane_content /
|
||||
-
|
||||
Chat_placeholder $mol_view
|
||||
sub /
|
||||
<= chat_placeholder_text @ \Select a chat to start messaging
|
||||
-
|
||||
Chat_view* $mol_view
|
||||
sub /
|
||||
<= Chat_header* $mol_view
|
||||
sub /
|
||||
<= Chat_close_icon* $mol_icon_close
|
||||
body /
|
||||
<= Chat_avatar* $mol_view
|
||||
dom_name \img
|
||||
attr *
|
||||
^
|
||||
src <= chat_avatar_uri* \
|
||||
alt \
|
||||
<= Chat_title* $mol_view
|
||||
sub /
|
||||
<= chat_with* \
|
||||
<= Chat_leave* $mol_button_minor
|
||||
title @ \Leave
|
||||
click? <=> do_leave_room*? null
|
||||
attr *
|
||||
^
|
||||
hidden <= chat_leave_hidden* \
|
||||
<= Messages_list* $mol_list
|
||||
rows <= message_rows* /
|
||||
foot /
|
||||
<= Compose_input* $mol_string
|
||||
dom_name \textarea
|
||||
hint @ \Type a message…
|
||||
value? <=> compose*? \
|
||||
event *
|
||||
^
|
||||
keydown? <=> compose_keydown*? null
|
||||
paste? <=> compose_paste*? null
|
||||
<= Attach_button* $mol_button_minor
|
||||
title @ \Attach
|
||||
click? <=> do_attach*? null
|
||||
<= Record_button* $mol_button_minor
|
||||
title <= record_title* @ \Record
|
||||
click? <=> do_record*? null
|
||||
<= Send_button* $mol_button_major
|
||||
title <= send_label @ \Send
|
||||
click? <=> do_send*? null
|
||||
-
|
||||
Roster_contact* $mol_button_minor
|
||||
click? <=> open_chat*? null
|
||||
sub /
|
||||
<= Contact_avatar* $mol_view
|
||||
dom_name \img
|
||||
attr *
|
||||
^
|
||||
src <= contact_avatar_uri* \
|
||||
alt \
|
||||
<= Contact_label* $mol_view
|
||||
<= Compose_pane* $mol_view
|
||||
sub /
|
||||
<= contact_display* \
|
||||
<= Compose_input* $mol_string
|
||||
dom_name \textarea
|
||||
hint @ \Type a message…
|
||||
value? <=> compose*? \
|
||||
event *
|
||||
^
|
||||
keydown? <=> compose_keydown*? null
|
||||
paste? <=> compose_paste*? null
|
||||
<= Attach_button* $mol_button_minor
|
||||
title @ \Attach
|
||||
click? <=> do_attach*? null
|
||||
<= Record_button* $mol_button_minor
|
||||
title <= record_title* @ \Record
|
||||
click? <=> do_record*? null
|
||||
<= Send_button* $mol_button_major
|
||||
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* \
|
||||
-
|
||||
Msg* $mol_view
|
||||
sub /
|
||||
|
|
|
|||
194
xmpp.view.ts
194
xmpp.view.ts
|
|
@ -732,29 +732,148 @@ private _handle_message(el: Element) {
|
|||
@ $mol_mem
|
||||
new_chat_jid(next?: string) { return next ?? '' }
|
||||
|
||||
@ $mol_mem
|
||||
search_query(next?: string) { return next ?? '' }
|
||||
|
||||
@ $mol_mem_key
|
||||
compose(_jid: string, next?: string) { return next ?? '' }
|
||||
|
||||
@ $mol_mem_key
|
||||
recording(_jid: string, next?: boolean) { return next ?? false }
|
||||
|
||||
// ── Pages ─────────────────────────────────────────────────────────────
|
||||
// ── Panes (Telegram-like 3-column layout) ─────────────────────────────
|
||||
|
||||
pages() {
|
||||
panes() {
|
||||
if (this.status() !== 'connected') {
|
||||
this._maybe_auto_connect()
|
||||
return [this.Login_page()]
|
||||
return [this.Login_pane()]
|
||||
}
|
||||
const pages: $mol_view[] = [this.Roster_page()]
|
||||
const peer = this.$.$mol_state_arg.value('chat')
|
||||
if (peer) {
|
||||
pages.push(this.Chat_page(peer))
|
||||
// load MAM history the first time this chat page is shown
|
||||
this._maybe_load_history(peer)
|
||||
}
|
||||
return pages
|
||||
return [this.Folders_pane(), this.Roster_pane(), this.Chat_pane()]
|
||||
}
|
||||
|
||||
chat_pane_content() {
|
||||
const peer = this.$.$mol_state_arg.value('chat')
|
||||
if (!peer) return [this.Chat_placeholder()]
|
||||
this._maybe_load_history(peer)
|
||||
return [this.Chat_view(peer)]
|
||||
}
|
||||
|
||||
folder() { return (this.$.$mol_state_arg.value('folder') as string | null) ?? '' }
|
||||
|
||||
// ── User folders (drag chat → drop on +zone or existing folder) ──────
|
||||
|
||||
@ $mol_mem
|
||||
folders_ver(next?: number) { return next ?? 0 }
|
||||
|
||||
private _folders_obj(): Record<string, string[]> {
|
||||
return (this.$.$mol_state_local.value('xmpp_folders') as Record<string, string[]> | null) ?? {}
|
||||
}
|
||||
|
||||
private _save_folders(obj: Record<string, string[]>) {
|
||||
this.$.$mol_state_local.value('xmpp_folders', obj)
|
||||
this.folders_ver(this.folders_ver() + 1)
|
||||
}
|
||||
|
||||
folders_list(): string[] {
|
||||
this.folders_ver()
|
||||
return Object.keys(this._folders_obj()).sort()
|
||||
}
|
||||
|
||||
private _add_to_folder(name: string, jid: string) {
|
||||
const obj = { ...this._folders_obj() }
|
||||
const arr = [...(obj[name] ?? [])]
|
||||
if (!arr.includes(jid)) arr.push(jid)
|
||||
obj[name] = arr
|
||||
this._save_folders(obj)
|
||||
}
|
||||
|
||||
private _remove_from_folder(name: string, jid: string) {
|
||||
const obj = { ...this._folders_obj() }
|
||||
const arr = (obj[name] ?? []).filter(j => j !== jid)
|
||||
if (arr.length === 0) delete obj[name]
|
||||
else obj[name] = arr
|
||||
this._save_folders(obj)
|
||||
}
|
||||
|
||||
// View bindings for the user folder list rendered in the left pane.
|
||||
user_folder_views() {
|
||||
return this.folders_list().map(name => this.User_folder(name))
|
||||
}
|
||||
|
||||
user_folder_name(name: string) { return name }
|
||||
user_folder_label(name: string) { return name }
|
||||
|
||||
delete_folder(name: string) {
|
||||
const obj = this._folders_obj()
|
||||
delete obj[name]
|
||||
this._save_folders(obj)
|
||||
if (this.folder() === name) this.$.$mol_state_arg.value('folder', null)
|
||||
}
|
||||
|
||||
// ── Roster header (shows folder name; user folders are renamable + deletable) ──
|
||||
|
||||
roster_header_sub() {
|
||||
const f = this.folder()
|
||||
if (!f || f === 'chats' || f === 'rooms') return [this.Roster_static_header()]
|
||||
return [this.Roster_folder_header()]
|
||||
}
|
||||
|
||||
roster_static_text() {
|
||||
const f = this.folder()
|
||||
if (f === 'chats') return 'Chats'
|
||||
if (f === 'rooms') return 'Rooms'
|
||||
return 'All'
|
||||
}
|
||||
|
||||
roster_folder_input(next?: string) {
|
||||
const cur = this.folder()
|
||||
if (next === undefined) return cur
|
||||
const trimmed = next.trim()
|
||||
if (!trimmed || trimmed === cur) return cur
|
||||
if (cur && cur !== 'chats' && cur !== 'rooms') {
|
||||
const obj = this._folders_obj()
|
||||
if (obj[cur] && !obj[trimmed]) {
|
||||
obj[trimmed] = obj[cur]
|
||||
delete obj[cur]
|
||||
this._save_folders(obj)
|
||||
this.$.$mol_state_arg.value('folder', trimmed)
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
do_delete_current_folder() {
|
||||
const f = this.folder()
|
||||
if (!f || f === 'chats' || f === 'rooms') return
|
||||
const confirmed = window.prompt(`Type "${ f }" to confirm deletion of this folder`)
|
||||
if (!confirmed || confirmed.trim() !== f) return
|
||||
this.delete_folder(f)
|
||||
}
|
||||
|
||||
// $mol_drag/$mol_drop integration.
|
||||
roster_jid(jid: string) { return jid }
|
||||
|
||||
folder_adopt(transfer: DataTransfer): string | null {
|
||||
const jid = transfer.getData('text/plain')
|
||||
return jid || null
|
||||
}
|
||||
|
||||
folder_receive(name: string, jid: string) {
|
||||
if (!jid) return
|
||||
const obj = this._folders_obj()
|
||||
const arr = obj[name] ?? []
|
||||
if (arr.includes(jid)) this._remove_from_folder(name, jid)
|
||||
else this._add_to_folder(name, jid)
|
||||
}
|
||||
|
||||
new_folder_receive(jid: string) {
|
||||
if (!jid) return
|
||||
const name = (window.prompt('Folder name')?.trim()) ?? ''
|
||||
if (!name) return
|
||||
this._add_to_folder(name, jid)
|
||||
}
|
||||
|
||||
|
||||
// On first render, if we have saved credentials and aren't connecting/connected, kick off auto-connect.
|
||||
private _maybe_auto_connect() {
|
||||
if (this._auto_connect_tried) return
|
||||
|
|
@ -937,10 +1056,57 @@ private _handle_message(el: Element) {
|
|||
|
||||
// ── Roster ────────────────────────────────────────────────────────────
|
||||
|
||||
@ $mol_mem
|
||||
roster_rows() {
|
||||
const contacts = this.contacts().map(c => this.Roster_contact(c.jid))
|
||||
const rooms = this.rooms().map(r => this.Roster_contact(r.jid))
|
||||
return [...contacts, ...rooms]
|
||||
this.folders_ver()
|
||||
const folder = this.folder()
|
||||
const contacts = this.contacts()
|
||||
const rooms = this.rooms()
|
||||
let jids: string[] = []
|
||||
if (folder === 'chats') jids = contacts.map(c => c.jid)
|
||||
else if (folder === 'rooms') jids = rooms.map(r => r.jid)
|
||||
else if (!folder) jids = [...contacts.map(c => c.jid), ...rooms.map(r => r.jid)]
|
||||
else jids = this._folders_obj()[folder] ?? []
|
||||
const q = this.search_query().trim().toLowerCase()
|
||||
if (q) {
|
||||
jids = jids.filter(jid => {
|
||||
const c = contacts.find(x => x.jid === jid)
|
||||
const r = this._rooms.get(jid)
|
||||
const name = (c?.name || r?.name || jid).toLowerCase()
|
||||
return jid.toLowerCase().includes(q) || name.includes(q)
|
||||
})
|
||||
}
|
||||
return jids.map(j => this.Roster_contact(j))
|
||||
}
|
||||
|
||||
// Suggestion buttons appear when the search query looks like a JID and isn't already in the list.
|
||||
search_actions_sub() {
|
||||
const q = this.search_query().trim()
|
||||
if (!q.includes('@')) return []
|
||||
const has_existing = this._rooms.has(q) || this.contacts().some(c => c.jid === q)
|
||||
return has_existing ? [] : [this.Search_action_chat(), this.Search_action_room()]
|
||||
}
|
||||
|
||||
search_action_chat_title() { return `+ Start chat with ${ this.search_query().trim() }` }
|
||||
search_action_room_title() { return `# Join room ${ this.search_query().trim() }` }
|
||||
|
||||
do_search_chat() {
|
||||
const jid = this.search_query().trim()
|
||||
if (!jid.includes('@')) return
|
||||
this.search_query('')
|
||||
this.$.$mol_state_arg.value('chat', jid)
|
||||
}
|
||||
|
||||
do_search_room() {
|
||||
const jid = this.search_query().trim()
|
||||
if (!jid.includes('@') || !this._conn) return
|
||||
const nick = this.my_jid().split('@')[0]
|
||||
this._conn.join_room(jid, nick)
|
||||
const room: Xmpp_room = { jid, name: jid.split('@')[0], nick }
|
||||
this._rooms.set(jid, room)
|
||||
this.rooms([ ...this._rooms.values() ])
|
||||
this.search_query('')
|
||||
this.$.$mol_state_arg.value('chat', jid)
|
||||
}
|
||||
|
||||
contact_display(jid: string) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue