added persist:

* auto login if credentials exists
* storing messages in indexdb ($mol_db)
This commit is contained in:
koplenov 2026-05-07 10:51:50 +03:00
parent 3b55ecc693
commit e90ae849f3

View file

@ -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 <delay>.
private _connect_at = 0
// IndexedDB for persistent message history (via $mol_db).
private _db: $mol_db_database<Xmpp_db_schema> | null = null
private _db_init: Promise<$mol_db_database<Xmpp_db_schema>> | 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<Xmpp_db_schema>> {
if (this._db) return Promise.resolve(this._db)
if (this._db_init) return this._db_init
this._db_init = this.$.$mol_db<Xmpp_db_schema>('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) {