From e90ae849f386a9903dcfbbd5270c95de2a16c822 Mon Sep 17 00:00:00 2001 From: koplenov Date: Thu, 7 May 2026 10:51:50 +0300 Subject: [PATCH] added persist: * auto login if credentials exists * storing messages in indexdb ($mol_db) --- xmpp.view.ts | 120 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 9 deletions(-) diff --git a/xmpp.view.ts b/xmpp.view.ts index b83b473..3656e4c 100644 --- a/xmpp.view.ts +++ b/xmpp.view.ts @@ -22,6 +22,24 @@ namespace $.$$ { nick: string // my nickname in this room } + // $mol_db schema for persisted messages. + type Xmpp_db_schema = { + Messages: { + Key: [ string, string ] // [account_bare_jid, msg_id] + Doc: { + account: string + peer: string + id: string + from: string + to: string + body: string + time: number + nick?: string + } + Indexes: {} + } + } + type Media_type = 'image' | 'audio' | 'link' | null function media_type(url: string): Media_type { @@ -616,20 +634,29 @@ private _handle_message(el: Element) { // — covers servers that replay offline messages without proper . private _connect_at = 0 + // IndexedDB for persistent message history (via $mol_db). + private _db: $mol_db_database | null = null + private _db_init: Promise<$mol_db_database> | null = null + private _loading_persisted = false + + private _auto_connect_tried = false + @ $mol_mem status(next?: 'disconnected' | 'connecting' | 'connected') { return next ?? 'disconnected' } @ $mol_mem my_jid(next?: string) { return next ?? '' } - @ $mol_mem - server(next?: string) { return next ?? '' } - - @ $mol_mem - jid(next?: string) { return next ?? '' } - - @ $mol_mem - password(next?: string) { return next ?? '' } + // Persisted in localStorage so the user doesn't have to re-enter on reload. + server(next?: string) { + return (this.$.$mol_state_local.value('xmpp_server', next) as string | null) ?? '' + } + jid(next?: string) { + return (this.$.$mol_state_local.value('xmpp_jid', next) as string | null) ?? '' + } + password(next?: string) { + return (this.$.$mol_state_local.value('xmpp_password', next) as string | null) ?? '' + } @ $mol_mem error_text(next?: string) { return next ?? '' } @@ -714,7 +741,10 @@ private _handle_message(el: Element) { // ── Pages ───────────────────────────────────────────────────────────── pages() { - if (this.status() !== 'connected') return [this.Login_page()] + if (this.status() !== 'connected') { + this._maybe_auto_connect() + return [this.Login_page()] + } const pages: $mol_view[] = [this.Roster_page()] const peer = this.$.$mol_state_arg.value('chat') if (peer) { @@ -725,6 +755,21 @@ private _handle_message(el: Element) { return pages } + // 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 + if (this.status() !== 'disconnected') return + const url = this.server().trim() + const jid = this.jid().trim() + const pw = this.password() + if (!url || !jid || !pw) return + this._auto_connect_tried = true + // Defer to escape the current reactive fiber — do_connect mutates memos. + Promise.resolve().then(() => { + if (this.status() === 'disconnected') this.do_connect() + }) + } + // ── Login ───────────────────────────────────────────────────────────── connecting() { return this.status() === 'connecting' } @@ -751,6 +796,7 @@ private _handle_message(el: Element) { this.my_jid(bound_jid) this.status('connected') this._connect_at = Date.now() + void this._load_persisted() } conn.on_bookmarks = bookmarks => { const my_nick = this.my_jid().split('@')[0] @@ -1322,6 +1368,62 @@ private _handle_message(el: Element) { if (msg.time < cur) this._oldest_time.set(msg.from, msg.time) } this.messages_ver(this.messages_ver() + 1) + if (!this._loading_persisted) void this._persist_msg(msg) + } + + // ── Persistence (IndexedDB via $mol_db) ────────────────────────────── + + private _ensure_db(): Promise<$mol_db_database> { + if (this._db) return Promise.resolve(this._db) + if (this._db_init) return this._db_init + this._db_init = this.$.$mol_db('xmpp', + mig => { mig.store_make('Messages') }, + ).then(db => { this._db = db; return db }) + return this._db_init + } + + private _peer_of(msg: Xmpp_message, account: string): string { + if (msg.nick !== undefined) return msg.from + return msg.from === account ? msg.to : msg.from + } + + private async _persist_msg(msg: Xmpp_message) { + const account = this.my_jid().split('/')[0] + if (!account) return + try { + const db = await this._ensure_db() + const peer = this._peer_of(msg, account) + const { Messages } = db.change('Messages').stores + const doc: Xmpp_db_schema['Messages']['Doc'] = { + account, peer, + id: msg.id, from: msg.from, to: msg.to, body: msg.body, time: msg.time, + ...(msg.nick !== undefined ? { nick: msg.nick } : {}), + } + await Messages.put(doc, [account, msg.id]) + } catch (e) { console.warn('[xmpp] persist failed', e) } + } + + private async _load_persisted() { + const account = this.my_jid().split('/')[0] + if (!account) return + try { + const db = await this._ensure_db() + const range = this.$.$mol_dom_context.IDBKeyRange.bound([account, ''], [account, '￿']) + const docs = await db.read('Messages').Messages.select(range, 100_000) + if (!docs?.length) return + this._loading_persisted = true + try { + for (const doc of docs) { + this._add_message({ + id: doc.id, from: doc.from, to: doc.to, + body: doc.body, time: doc.time, + ...(doc.nick !== undefined ? { nick: doc.nick } : {}), + }) + } + } finally { + this._loading_persisted = false + } + } catch (e) { console.warn('[xmpp] load persisted failed', e) } } private _maybe_load_history(jid: string) {