fix autoscroll
add settings window
This commit is contained in:
parent
70a71d4212
commit
17913397e6
3 changed files with 483 additions and 6 deletions
183
xmpp.view.css
183
xmpp.view.css
|
|
@ -235,6 +235,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[xmpp_Folders_pane] [xmpp_Disconnect_button],
|
[xmpp_Folders_pane] [xmpp_Disconnect_button],
|
||||||
|
[xmpp_Folders_pane] [xmpp_Settings_button],
|
||||||
[xmpp_Folders_pane] [xmpp_Lights2] {
|
[xmpp_Folders_pane] [xmpp_Lights2] {
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
padding: .35rem;
|
padding: .35rem;
|
||||||
|
|
@ -243,6 +244,154 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Settings modal ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
[xmpp_Settings_modal] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Settings_dim] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Settings_dialog] {
|
||||||
|
position: relative;
|
||||||
|
width: min(560px, 90vw);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--mol_theme_back);
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
border-radius: .75rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, .25);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Settings_header] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--mol_theme_line);
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Settings_title] {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_section],
|
||||||
|
[xmpp_Sound_section],
|
||||||
|
[xmpp_Theme_section] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_section_title],
|
||||||
|
[xmpp_Sound_section_title],
|
||||||
|
[xmpp_Theme_section_title] {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .9rem;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_section] {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_section_title] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_inapp_button],
|
||||||
|
[xmpp_Notif_system_button] {
|
||||||
|
padding: .5rem .9rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Notif_inapp_button][xmpp_active="true"],
|
||||||
|
[xmpp_Notif_system_button][xmpp_active="true"] {
|
||||||
|
background: var(--mol_theme_focus);
|
||||||
|
color: var(--mol_theme_card);
|
||||||
|
border-color: var(--mol_theme_focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Sound_section] {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Sound_section_title] { width: 100%; }
|
||||||
|
|
||||||
|
[xmpp_Sound_status] {
|
||||||
|
width: 100%;
|
||||||
|
font-size: .85rem;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Sound_pick_button],
|
||||||
|
[xmpp_Sound_test_button],
|
||||||
|
[xmpp_Sound_clear_button] {
|
||||||
|
padding: .4rem .7rem;
|
||||||
|
border-radius: .4rem;
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Theme_presets] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Theme_preset] {
|
||||||
|
padding: .5rem .9rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Theme_preset][xmpp_active="true"] {
|
||||||
|
background: var(--mol_theme_focus);
|
||||||
|
color: var(--mol_theme_card);
|
||||||
|
border-color: var(--mol_theme_focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Theme_custom] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: .75rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
border-top: 1px solid var(--mol_theme_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Theme_color_input] {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
border-radius: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
[xmpp_Roster_list] {
|
[xmpp_Roster_list] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -288,12 +437,46 @@
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[xmpp_Messages_area] {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
[xmpp_Messages_list] {
|
[xmpp_Messages_list] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[xmpp_Scroll_down_button] {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mol_theme_card);
|
||||||
|
border: 1px solid var(--mol_theme_line);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Scroll_down_button]:hover {
|
||||||
|
background: var(--mol_theme_hover, var(--mol_theme_card));
|
||||||
|
}
|
||||||
|
|
||||||
|
[xmpp_Scroll_down_button][hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
[xmpp_Compose_pane] {
|
[xmpp_Compose_pane] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ $xmpp $mol_view
|
||||||
sub /
|
sub /
|
||||||
<= new_folder_text @ \+ Drop chat
|
<= new_folder_text @ \+ Drop chat
|
||||||
<= Folders_spacer $mol_view
|
<= Folders_spacer $mol_view
|
||||||
|
<= Settings_button $mol_button_minor
|
||||||
|
hint @ \Settings
|
||||||
|
click? <=> do_open_settings? null
|
||||||
|
sub /
|
||||||
|
<= Settings_icon $mol_icon_cog
|
||||||
<= Lights2 $mol_lights_toggle
|
<= Lights2 $mol_lights_toggle
|
||||||
<= Disconnect_button $mol_button_minor
|
<= Disconnect_button $mol_button_minor
|
||||||
hint <= disconnect_label @ \Disconnect
|
hint <= disconnect_label @ \Disconnect
|
||||||
|
|
@ -107,6 +112,86 @@ $xmpp $mol_view
|
||||||
title <= search_action_room_title \
|
title <= search_action_room_title \
|
||||||
click? <=> do_search_room? null
|
click? <=> do_search_room? null
|
||||||
-
|
-
|
||||||
|
Settings_modal $mol_view
|
||||||
|
sub /
|
||||||
|
<= Settings_dim $mol_view
|
||||||
|
event *
|
||||||
|
^
|
||||||
|
click? <=> do_close_settings? null
|
||||||
|
<= Settings_dialog $mol_view
|
||||||
|
sub /
|
||||||
|
<= Settings_header $mol_view
|
||||||
|
sub /
|
||||||
|
<= Settings_title $mol_view
|
||||||
|
sub /
|
||||||
|
<= settings_title @ \Settings
|
||||||
|
<= Settings_close $mol_button_minor
|
||||||
|
click? <=> do_close_settings? null
|
||||||
|
sub /
|
||||||
|
<= Settings_close_icon $mol_icon_close
|
||||||
|
<= Notif_section $mol_view
|
||||||
|
sub /
|
||||||
|
<= Notif_section_title $mol_view
|
||||||
|
sub /
|
||||||
|
<= notif_section_title @ \Notifications
|
||||||
|
<= Notif_inapp_button $mol_button_minor
|
||||||
|
click? <=> do_set_notif_inapp? null
|
||||||
|
attr *
|
||||||
|
^
|
||||||
|
xmpp_active <= notif_inapp_active false
|
||||||
|
sub /
|
||||||
|
<= notif_inapp_label @ \In-app toast
|
||||||
|
<= Notif_system_button $mol_button_minor
|
||||||
|
click? <=> do_set_notif_system? null
|
||||||
|
attr *
|
||||||
|
^
|
||||||
|
xmpp_active <= notif_system_active false
|
||||||
|
sub /
|
||||||
|
<= notif_system_label @ \System notification
|
||||||
|
<= Sound_section $mol_view
|
||||||
|
sub /
|
||||||
|
<= Sound_section_title $mol_view
|
||||||
|
sub /
|
||||||
|
<= sound_section_title @ \Notification sound
|
||||||
|
<= Sound_status $mol_view
|
||||||
|
sub /
|
||||||
|
<= sound_status_text \
|
||||||
|
<= Sound_pick_button $mol_button_minor
|
||||||
|
click? <=> do_pick_sound? null
|
||||||
|
sub /
|
||||||
|
<= sound_pick_label @ \Choose sound file
|
||||||
|
<= Sound_test_button $mol_button_minor
|
||||||
|
click? <=> do_test_sound? null
|
||||||
|
sub /
|
||||||
|
<= sound_test_label @ \Test
|
||||||
|
<= Sound_clear_button $mol_button_minor
|
||||||
|
click? <=> do_clear_sound? null
|
||||||
|
sub /
|
||||||
|
<= sound_clear_label @ \Reset to default
|
||||||
|
<= Theme_section $mol_view
|
||||||
|
sub /
|
||||||
|
<= Theme_section_title $mol_view
|
||||||
|
sub /
|
||||||
|
<= theme_section_title @ \Theme
|
||||||
|
<= Theme_presets $mol_view
|
||||||
|
sub <= theme_preset_views /
|
||||||
|
<= Theme_custom $mol_view
|
||||||
|
sub /
|
||||||
|
<= Theme_color_label $mol_view
|
||||||
|
sub /
|
||||||
|
<= theme_color_label @ \Custom hue
|
||||||
|
<= Theme_color_input $mol_string
|
||||||
|
type \color
|
||||||
|
value? <=> theme_color? \
|
||||||
|
-
|
||||||
|
Theme_preset* $mol_button_minor
|
||||||
|
click? <=> do_apply_preset*? null
|
||||||
|
attr *
|
||||||
|
^
|
||||||
|
xmpp_active <= theme_preset_active* false
|
||||||
|
sub /
|
||||||
|
<= theme_preset_label* \
|
||||||
|
-
|
||||||
Roster_static_header $mol_view
|
Roster_static_header $mol_view
|
||||||
sub /
|
sub /
|
||||||
<= roster_static_text \
|
<= roster_static_text \
|
||||||
|
|
@ -146,8 +231,18 @@ $xmpp $mol_view
|
||||||
attr *
|
attr *
|
||||||
^
|
^
|
||||||
hidden <= chat_leave_hidden* \
|
hidden <= chat_leave_hidden* \
|
||||||
|
<= Messages_area* $mol_view
|
||||||
|
sub /
|
||||||
<= Messages_list* $mol_list
|
<= Messages_list* $mol_list
|
||||||
rows <= message_rows* /
|
rows <= message_rows* /
|
||||||
|
<= Scroll_down_button* $mol_button_minor
|
||||||
|
hint @ \Scroll to bottom
|
||||||
|
click? <=> do_scroll_down*? null
|
||||||
|
attr *
|
||||||
|
^
|
||||||
|
hidden <= scroll_down_hidden* \
|
||||||
|
sub /
|
||||||
|
<= Scroll_down_icon* $mol_icon_chevron_double_down
|
||||||
<= Compose_pane* $mol_view
|
<= Compose_pane* $mol_view
|
||||||
sub /
|
sub /
|
||||||
<= Compose_input* $mol_string
|
<= Compose_input* $mol_string
|
||||||
|
|
|
||||||
207
xmpp.view.ts
207
xmpp.view.ts
|
|
@ -744,11 +744,165 @@ private _handle_message(el: Element) {
|
||||||
// ── Panes (Telegram-like 3-column layout) ─────────────────────────────
|
// ── Panes (Telegram-like 3-column layout) ─────────────────────────────
|
||||||
|
|
||||||
panes() {
|
panes() {
|
||||||
|
this._apply_theme()
|
||||||
if (this.status() !== 'connected') {
|
if (this.status() !== 'connected') {
|
||||||
this._maybe_auto_connect()
|
this._maybe_auto_connect()
|
||||||
return [this.Login_pane()]
|
return [this.Login_pane()]
|
||||||
}
|
}
|
||||||
return [this.Folders_pane(), this.Roster_pane(), this.Chat_pane()]
|
const list: $mol_view[] = [this.Folders_pane(), this.Roster_pane(), this.Chat_pane()]
|
||||||
|
if (this.settings_open()) list.push(this.Settings_modal())
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
@ $mol_mem
|
||||||
|
settings_open(next?: boolean) { return next ?? false }
|
||||||
|
|
||||||
|
do_open_settings() { this.settings_open(true) }
|
||||||
|
do_close_settings() { this.settings_open(false) }
|
||||||
|
|
||||||
|
// Per-chat reactive flag — true when scroll is near the bottom of Messages_list.
|
||||||
|
@ $mol_mem_key
|
||||||
|
scroll_at_bottom(_jid: string, next?: boolean) { return next ?? true }
|
||||||
|
|
||||||
|
scroll_down_hidden(jid: string): string {
|
||||||
|
return this.scroll_at_bottom(jid) ? 'hidden' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
do_scroll_down(jid: string) {
|
||||||
|
this._scroll_to_bottom(jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notifications mode ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
notif_mode(next?: 'inapp' | 'system') {
|
||||||
|
return (this.$.$mol_state_local.value('xmpp_notif_mode', next) as 'inapp' | 'system' | null) ?? 'inapp'
|
||||||
|
}
|
||||||
|
|
||||||
|
notif_inapp_active() { return this.notif_mode() === 'inapp' }
|
||||||
|
notif_system_active() { return this.notif_mode() === 'system' }
|
||||||
|
|
||||||
|
do_set_notif_inapp() { this.notif_mode('inapp') }
|
||||||
|
do_set_notif_system() {
|
||||||
|
this.notif_mode('system')
|
||||||
|
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
||||||
|
void Notification.requestPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notification sound ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
notif_sound_uri(next?: string | null) {
|
||||||
|
return (this.$.$mol_state_local.value('xmpp_notif_sound', next) as string | null) ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
sound_status_text() {
|
||||||
|
return this.notif_sound_uri() ? '✓ Custom sound loaded' : 'Default beep'
|
||||||
|
}
|
||||||
|
|
||||||
|
do_pick_sound() {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'audio/*'
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
if (file.size > 1_000_000) {
|
||||||
|
this.error_text('Sound file too large (max 1 MB)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
this.notif_sound_uri(String(reader.result))
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
do_test_sound() { this._beep() }
|
||||||
|
|
||||||
|
do_clear_sound() { this.notif_sound_uri(null) }
|
||||||
|
|
||||||
|
// ── Theme ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
theme_preset(next?: string | null) {
|
||||||
|
return (this.$.$mol_state_local.value('xmpp_theme_preset', next) as string | null) ?? 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_hue(next?: number | null) {
|
||||||
|
return (this.$.$mol_state_local.value('xmpp_theme_hue', next) as number | null) ?? 240
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in presets keyed by hue (deg).
|
||||||
|
private _theme_presets(): Record<string, { hue: number; label: string }> {
|
||||||
|
return {
|
||||||
|
default: { hue: 240, label: 'Default' },
|
||||||
|
ocean: { hue: 200, label: 'Ocean' },
|
||||||
|
forest: { hue: 130, label: 'Forest' },
|
||||||
|
sunset: { hue: 20, label: 'Sunset' },
|
||||||
|
cherry: { hue: 350, label: 'Cherry' },
|
||||||
|
violet: { hue: 290, label: 'Violet' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_preset_views() {
|
||||||
|
const presets = this._theme_presets()
|
||||||
|
return Object.keys(presets).map(name => this.Theme_preset(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_preset_label(name: string) { return this._theme_presets()[name]?.label ?? name }
|
||||||
|
theme_preset_active(name: string) { return this.theme_preset() === name }
|
||||||
|
|
||||||
|
do_apply_preset(name: string) {
|
||||||
|
this.theme_preset(name)
|
||||||
|
const p = this._theme_presets()[name]
|
||||||
|
if (p) this.theme_hue(p.hue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bridge between HSL hue and the <input type="color"> hex value.
|
||||||
|
theme_color(next?: string) {
|
||||||
|
if (next === undefined) {
|
||||||
|
return this._hsl_to_hex(this.theme_hue(), 60, 50)
|
||||||
|
}
|
||||||
|
const hue = this._hex_to_hue(next)
|
||||||
|
this.theme_preset('custom')
|
||||||
|
this.theme_hue(hue)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hsl_to_hex(h: number, s: number, l: number): string {
|
||||||
|
s /= 100; l /= 100
|
||||||
|
const k = (n: number) => (n + h / 30) % 12
|
||||||
|
const a = s * Math.min(l, 1 - l)
|
||||||
|
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))
|
||||||
|
const to = (x: number) => Math.round(x * 255).toString(16).padStart(2, '0')
|
||||||
|
return `#${ to(f(0)) }${ to(f(8)) }${ to(f(4)) }`
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hex_to_hue(hex: string): number {
|
||||||
|
const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
|
||||||
|
if (!m) return 240
|
||||||
|
const r = parseInt(m[1], 16) / 255
|
||||||
|
const g = parseInt(m[2], 16) / 255
|
||||||
|
const b = parseInt(m[3], 16) / 255
|
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||||
|
if (max === min) return 0
|
||||||
|
const d = max - min
|
||||||
|
let h = 0
|
||||||
|
if (max === r) h = ((g - b) / d) % 6
|
||||||
|
else if (max === g) h = (b - r) / d + 2
|
||||||
|
else h = (r - g) / d + 4
|
||||||
|
h = Math.round(h * 60)
|
||||||
|
if (h < 0) h += 360
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactively apply theme variables to the document root.
|
||||||
|
@ $mol_mem
|
||||||
|
_apply_theme() {
|
||||||
|
if (typeof document === 'undefined') return
|
||||||
|
const hue = this.theme_hue()
|
||||||
|
document.documentElement.style.setProperty('--mol_theme_hue', `${ hue }deg`)
|
||||||
}
|
}
|
||||||
|
|
||||||
chat_pane_content() {
|
chat_pane_content() {
|
||||||
|
|
@ -1420,7 +1574,8 @@ private _handle_message(el: Element) {
|
||||||
private _scroll_to_bottom(jid: string) {
|
private _scroll_to_bottom(jid: string) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
try {
|
try {
|
||||||
this.Chat_page(jid).Body().dom_node().scrollTop = 999999
|
const el = this.Messages_list(jid).dom_node()
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
} catch {}
|
} catch {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1494,10 +1649,39 @@ private _handle_message(el: Element) {
|
||||||
const open_peer = this.$.$mol_state_arg.value('chat')
|
const open_peer = this.$.$mol_state_arg.value('chat')
|
||||||
const visible = typeof document !== 'undefined' && document.visibilityState === 'visible'
|
const visible = typeof document !== 'undefined' && document.visibilityState === 'visible'
|
||||||
if (open_peer === peer && visible) return
|
if (open_peer === peer && visible) return
|
||||||
this._show_toast(peer, title, body)
|
if (this.notif_mode() === 'system') this._system_notify(title, body, peer)
|
||||||
|
else this._show_toast(peer, title, body)
|
||||||
this._beep()
|
this._beep()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _system_notify(title: string, body: string, peer: string) {
|
||||||
|
if (typeof Notification === 'undefined') {
|
||||||
|
this._show_toast(peer, title, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
void Notification.requestPermission().then(p => {
|
||||||
|
if (p === 'granted') this._system_notify(title, body, peer)
|
||||||
|
else this._show_toast(peer, title, body)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
this._show_toast(peer, title, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const n = new Notification(title, { body, tag: peer, requireInteraction: true })
|
||||||
|
n.onclick = () => {
|
||||||
|
try { window.focus() } catch {}
|
||||||
|
this.$.$mol_state_arg.value('chat', peer)
|
||||||
|
n.close()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this._show_toast(peer, title, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _ensure_toast_container(): HTMLDivElement {
|
private _ensure_toast_container(): HTMLDivElement {
|
||||||
if (this._toast_container && this._toast_container.isConnected) return this._toast_container
|
if (this._toast_container && this._toast_container.isConnected) return this._toast_container
|
||||||
const c = document.createElement('div')
|
const c = document.createElement('div')
|
||||||
|
|
@ -1535,6 +1719,15 @@ private _handle_message(el: Element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _beep() {
|
private _beep() {
|
||||||
|
const custom = this.notif_sound_uri()
|
||||||
|
if (custom) {
|
||||||
|
try {
|
||||||
|
const audio = new Audio(custom)
|
||||||
|
audio.volume = 0.5
|
||||||
|
void audio.play().catch(() => {})
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const Ctor = (window as any).AudioContext || (window as any).webkitAudioContext
|
const Ctor = (window as any).AudioContext || (window as any).webkitAudioContext
|
||||||
if (!Ctor) return
|
if (!Ctor) return
|
||||||
|
|
@ -1574,9 +1767,15 @@ private _handle_message(el: Element) {
|
||||||
this._scroll_setup.add(jid)
|
this._scroll_setup.add(jid)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
try {
|
try {
|
||||||
const el = this.Chat_page(jid).Body().dom_node() as HTMLElement
|
const el = this.Messages_list(jid).dom_node() as HTMLElement
|
||||||
|
const update_at_bottom = () => {
|
||||||
|
const near = el.scrollHeight - el.scrollTop - el.clientHeight < 100
|
||||||
|
this.scroll_at_bottom(jid, near)
|
||||||
|
}
|
||||||
|
update_at_bottom()
|
||||||
el.addEventListener('scroll', () => {
|
el.addEventListener('scroll', () => {
|
||||||
if (el.scrollTop < 200) this._load_more_history(jid)
|
if (el.scrollTop < 200) this._load_more_history(jid)
|
||||||
|
update_at_bottom()
|
||||||
}, { passive: true })
|
}, { passive: true })
|
||||||
// Late-loading images push content; if user is near the bottom, follow.
|
// Late-loading images push content; if user is near the bottom, follow.
|
||||||
el.addEventListener('load', e => {
|
el.addEventListener('load', e => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue