From be4a3e928c3f9c56f023438746521a570aa14d0a Mon Sep 17 00:00:00 2001 From: koplenov Date: Sat, 9 May 2026 19:33:33 +0300 Subject: [PATCH] working on images --- xmpp.view.ts | 110 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 19 deletions(-) diff --git a/xmpp.view.ts b/xmpp.view.ts index b80965f..87e34a6 100644 --- a/xmpp.view.ts +++ b/xmpp.view.ts @@ -14,6 +14,13 @@ body: string time: number nick?: string // sender nick for MUC groupchat messages + media_uri?: string + media_mime?: string + media_name?: string + media_kind?: Media_type + media_size?: number + media_hash?: string + media_hash_algo?: string } type Xmpp_room = { @@ -22,6 +29,8 @@ nick: string // my nickname in this room } + type Media_type = 'image' | 'audio' | 'link' | null + // $mol_db schema for persisted messages. type Xmpp_db_schema = { Messages: { @@ -35,13 +44,18 @@ body: string time: number nick?: string + media_uri?: string + media_mime?: string + media_name?: string + media_kind?: Media_type + media_size?: number + media_hash?: string + media_hash_algo?: string } Indexes: {} } } - type Media_type = 'image' | 'audio' | 'link' | null - function media_type(url: string): Media_type { const s = url.trim() if (!/^https?:\/\/\S+$/.test(s)) return null @@ -51,6 +65,13 @@ return 'link' } + function media_type_from_mime(mime: string): Media_type { + const s = mime.trim().toLowerCase() + if (s.startsWith('image/')) return 'image' + if (s.startsWith('audio/')) return 'audio' + return null + } + // ─── XMPP over WebSocket (RFC 7590) ────────────────────────────────────── class Xmpp_conn { @@ -342,6 +363,27 @@ private _getBody(el: Element): string | null { return new DOMParser().parseFromString(data, 'text/xml') } + private _parse_media_sharing(el: Element) { + const reference = el.getElementsByTagNameNS('urn:xmpp:reference:0', 'reference')[0] + if (!reference || reference.getAttribute('type') !== 'data') return null + const media = reference.getElementsByTagNameNS('urn:xmpp:sims:1', 'media-sharing')[0] + if (!media) return null + const file = media.getElementsByTagNameNS('urn:xmpp:jingle:apps:file-transfer:5', 'file')[0] + if (!file) return null + const name = file.getElementsByTagNameNS('urn:xmpp:jingle:apps:file-transfer:5', 'name')[0]?.textContent || undefined + const mime = file.getElementsByTagNameNS('urn:xmpp:jingle:apps:file-transfer:5', 'media-type')[0]?.textContent || undefined + const sizeText = file.getElementsByTagNameNS('urn:xmpp:jingle:apps:file-transfer:5', 'size')[0]?.textContent + const hashElem = file.getElementsByTagNameNS('urn:xmpp:hashes:2', 'hash')[0] + const sourceRef = Array.from(media.getElementsByTagNameNS('urn:xmpp:reference:0', 'reference')) + .find(r => r.getAttribute('type') === 'data') + const uri = sourceRef?.getAttribute('uri') || undefined + const size = sizeText ? Number(sizeText) : undefined + const hash = hashElem?.textContent || undefined + const hash_algo = hashElem?.getAttribute('algo') || undefined + const kind = mime ? media_type_from_mime(mime) : uri ? media_type(uri) : null + return { media_uri: uri, media_mime: mime, media_name: name, media_kind: kind, media_size: size, media_hash: hash, media_hash_algo: hash_algo } + } + private _handle(data: string) { const doc = this._parse(data) const root = doc.documentElement @@ -552,16 +594,10 @@ private _handle_message(el: Element) { const type = el.getAttribute('type') || 'chat'; let body = this._getBody(el); + const media = this._parse_media_sharing(el) if (!body) { - const reference = this._find(el, 'urn:xmpp:reference:0', 'reference') - if (reference?.getAttribute('type') === 'data') { - const media = reference.querySelector('media-sharing') - const file = media?.querySelector('file') - const name = file?.querySelector('name')?.textContent - const source = media?.querySelector('sources reference')?.getAttribute('uri') - if (name) body = `Shared file: ${ name }` - else if (source) body = source - } + if (media?.media_name) body = `Shared file: ${ media.media_name }` + else if (media?.media_uri) body = media.media_uri } if (!body) return; @@ -589,6 +625,7 @@ private _handle_message(el: Element) { from: room_jid, to: (el.getAttribute('to') || '').split('/')[0], body, time, nick, + ...(media || {}), }); return; } @@ -599,6 +636,7 @@ private _handle_message(el: Element) { from: (el.getAttribute('from') || '').split('/')[0], to: (el.getAttribute('to') || '').split('/')[0], body, time, + ...(media || {}), }); } @@ -607,7 +645,12 @@ private _handle_message(el: Element) { if (!fwd) return const msg = fwd.querySelector('message') if (!msg) return - const body = msg.querySelector('body')?.textContent + let body = msg.querySelector('body')?.textContent + const media = this._parse_media_sharing(msg) + if (!body) { + if (media?.media_name) body = `Shared file: ${ media.media_name }` + else if (media?.media_uri) body = media.media_uri + } if (!body) return const type = msg.getAttribute('type') || 'chat' if (type !== 'chat' && type !== 'normal' && type !== 'groupchat') return @@ -625,6 +668,7 @@ private _handle_message(el: Element) { to: (msg.getAttribute('to') || '').split('/')[0], body, time, ...(nick !== undefined ? { nick } : {}), + ...(media || {}), }) } @@ -1473,14 +1517,19 @@ private _handle_message(el: Element) { @ $mol_mem_key // ← FIX: make reactive msg_body(id: string) { - const body = this._msg_by_id.get(id)?.body ?? '' + const msg = this._msg_by_id.get(id) + if (!msg) return '' + if (msg.media_uri || msg.media_kind) return '' + const body = msg.body ?? '' return media_type(body) ? '' : body } @ $mol_mem_key // ← FIX: make reactive msg_media(id: string): $mol_view[] { - const body = this._msg_by_id.get(id)?.body.trim() ?? '' - const type = media_type(body) + const msg = this._msg_by_id.get(id) + if (!msg) return [] + const uri = msg.media_uri || msg.body.trim() + const type = msg.media_kind ?? (msg.media_uri ? media_type(uri) : media_type(msg.body.trim())) if (type === 'image') return [this.Msg_image(id)] if (type === 'audio') return [this.Msg_audio(id)] if (type === 'link') return [this.Msg_link(id)] @@ -1488,13 +1537,13 @@ private _handle_message(el: Element) { } @ $mol_mem_key // ← FIX: make reactive - msg_image_uri(id: string) { return this._msg_by_id.get(id)?.body.trim() ?? '' } + msg_image_uri(id: string) { const msg = this._msg_by_id.get(id); return msg?.media_uri || (msg?.body.trim() ?? '') } @ $mol_mem_key // ← FIX: make reactive - msg_audio_src(id: string) { return this._msg_by_id.get(id)?.body.trim() ?? '' } + msg_audio_src(id: string) { const msg = this._msg_by_id.get(id); return msg?.media_uri || (msg?.body.trim() ?? '') } @ $mol_mem_key // ← FIX: make reactive - msg_link_uri(id: string) { return this._msg_by_id.get(id)?.body.trim() ?? '' } + msg_link_uri(id: string) { const msg = this._msg_by_id.get(id); return msg?.media_uri || (msg?.body.trim() ?? '') } @ $mol_mem_key msg_time(id: string) { @@ -1881,6 +1930,13 @@ private _handle_message(el: Element) { account, peer, id: msg.id, from: msg.from, to: msg.to, body: msg.body, time: msg.time, ...(msg.nick !== undefined ? { nick: msg.nick } : {}), + ...(msg.media_uri !== undefined ? { media_uri: msg.media_uri } : {}), + ...(msg.media_mime !== undefined ? { media_mime: msg.media_mime } : {}), + ...(msg.media_name !== undefined ? { media_name: msg.media_name } : {}), + ...(msg.media_kind !== undefined ? { media_kind: msg.media_kind } : {}), + ...(msg.media_size !== undefined ? { media_size: msg.media_size } : {}), + ...(msg.media_hash !== undefined ? { media_hash: msg.media_hash } : {}), + ...(msg.media_hash_algo !== undefined ? { media_hash_algo: msg.media_hash_algo } : {}), } await Messages.put(doc, [account, msg.id]) } catch (e) { console.warn('[xmpp] persist failed', e) } @@ -1901,6 +1957,13 @@ private _handle_message(el: Element) { id: doc.id, from: doc.from, to: doc.to, body: doc.body, time: doc.time, ...(doc.nick !== undefined ? { nick: doc.nick } : {}), + ...(doc.media_uri !== undefined ? { media_uri: doc.media_uri } : {}), + ...(doc.media_mime !== undefined ? { media_mime: doc.media_mime } : {}), + ...(doc.media_name !== undefined ? { media_name: doc.media_name } : {}), + ...(doc.media_kind !== undefined ? { media_kind: doc.media_kind } : {}), + ...(doc.media_size !== undefined ? { media_size: doc.media_size } : {}), + ...(doc.media_hash !== undefined ? { media_hash: doc.media_hash } : {}), + ...(doc.media_hash_algo !== undefined ? { media_hash_algo: doc.media_hash_algo } : {}), }) } } finally { @@ -2000,7 +2063,16 @@ private _handle_message(el: Element) { const id = `l${ Date.now() }_${ Math.random().toString(36).slice(2) }` const type = this._rooms.has(jid) ? 'groupchat' : 'chat' this._conn.send_media_sharing(jid, body, file.name, file.size, file.type || 'application/octet-stream', 'sha-256', hash, get, type, id) - this._add_message({ id, from: this.my_jid().split('/')[0], to: jid, body, time: Date.now() }) + this._add_message({ + id, from: this.my_jid().split('/')[0], to: jid, body, time: Date.now(), + media_uri: get, + media_mime: file.type || 'application/octet-stream', + media_name: file.name, + media_kind: media_type_from_mime(file.type) ?? media_type(get), + media_size: file.size, + media_hash: hash, + media_hash_algo: 'sha-256', + }) this._scroll_to_bottom(jid) } }