* messages times

* sending message on Enter key
* multiline message
* new chat on contact
* paste file from buffer on ctrl+v
This commit is contained in:
koplenov 2026-05-07 10:34:19 +03:00
parent 596f2a1210
commit 3b55ecc693
3 changed files with 111 additions and 2 deletions

View file

@ -40,6 +40,26 @@
[xmpp_Compose_input] {
flex: 1;
min-height: 2.5rem;
max-height: 8rem;
resize: vertical;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font: inherit;
}
[xmpp_Msg_head] {
display: flex;
flex-direction: row;
align-items: baseline;
gap: .5rem;
}
[xmpp_Msg_time] {
font-size: .7rem;
opacity: .5;
margin-left: auto;
}
/* media */

View file

@ -54,6 +54,17 @@ $xmpp $mol_book2
body /
<= 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
@ -96,8 +107,13 @@ $xmpp $mol_book2
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
@ -131,9 +147,14 @@ $xmpp $mol_book2
alt \
<= Msg_content* $mol_view
sub /
<= Msg_from* $mol_view
<= Msg_head* $mol_view
sub /
<= msg_from* \
<= Msg_from* $mol_view
sub /
<= msg_from* \
<= Msg_time* $mol_view
sub /
<= msg_time* \
<= Msg_body* $mol_view
sub /
<= msg_body* \

View file

@ -702,6 +702,9 @@ private _handle_message(el: Element) {
@ $mol_mem
room_nick(next?: string) { return next ?? '' }
@ $mol_mem
new_chat_jid(next?: string) { return next ?? '' }
@ $mol_mem_key
compose(_jid: string, next?: string) { return next ?? '' }
@ -905,6 +908,16 @@ private _handle_message(el: Element) {
open_chat(jid: string) { this.$.$mol_state_arg.value('chat', jid) }
do_new_chat() {
const jid = this.new_chat_jid().trim()
if (!jid || !jid.includes('@')) {
this.error_text('Enter a valid JID like user@server')
return
}
this.new_chat_jid('')
this.$.$mol_state_arg.value('chat', jid)
}
// ── Rooms ─────────────────────────────────────────────────────────────
do_join_room() {
@ -999,6 +1012,23 @@ private _handle_message(el: Element) {
@ $mol_mem_key // ← FIX: make reactive
msg_link_uri(id: string) { return this._msg_by_id.get(id)?.body.trim() ?? '' }
@ $mol_mem_key
msg_time(id: string) {
const msg = this._msg_by_id.get(id)
if (!msg) return ''
const d = new Date(msg.time)
const same_day = (() => {
const now = new Date()
return d.getFullYear() === now.getFullYear()
&& d.getMonth() === now.getMonth()
&& d.getDate() === now.getDate()
})()
const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
if (same_day) return time
const date = d.toLocaleDateString([], { day: '2-digit', month: '2-digit' })
return `${ date } ${ time }`
}
// XEP-0333 status for outgoing messages.
// ✓ — sent locally; ✓✓ — peer received; ✓✓ (filled) — peer displayed.
@ $mol_mem_key
@ -1014,6 +1044,37 @@ private _handle_message(el: Element) {
return '✓'
}
// ── Compose input event handlers ──────────────────────────────────────
// Enter sends; Shift+Enter inserts a newline (browser default).
compose_keydown(jid: string, e?: Event | null) {
const ke = e as KeyboardEvent | null | undefined
if (!ke) return null
if (ke.key === 'Enter' && !ke.shiftKey && !ke.ctrlKey && !ke.altKey && !ke.metaKey) {
ke.preventDefault()
this.do_send(jid)
}
return null
}
// Paste image / file from clipboard → upload via XEP-0363 and send the link.
compose_paste(jid: string, e?: Event | null) {
const ce = e as ClipboardEvent | null | undefined
const items = ce?.clipboardData?.items
if (!items) return null
for (const item of Array.from(items)) {
if (item.kind === 'file') {
const file = item.getAsFile()
if (file) {
ce!.preventDefault()
void this._upload_and_send(jid, file).catch(err => this.error_text(String(err)))
return null
}
}
}
return null
}
// ── Send text ─────────────────────────────────────────────────────────
do_send(jid: string) {
@ -1240,6 +1301,12 @@ private _handle_message(el: Element) {
el.addEventListener('scroll', () => {
if (el.scrollTop < 200) this._load_more_history(jid)
}, { passive: true })
// Late-loading images push content; if user is near the bottom, follow.
el.addEventListener('load', e => {
if (!(e.target instanceof HTMLImageElement)) return
const near = el.scrollHeight - el.scrollTop - el.clientHeight < 200
if (near) el.scrollTop = el.scrollHeight
}, true)
} catch {
this._scroll_setup.delete(jid)
}
@ -1299,6 +1366,7 @@ private _handle_message(el: Element) {
const id = `l${ Date.now() }_${ Math.random().toString(36).slice(2) }`
this._conn.send_message(jid, get, id)
this._add_message({ id, from: this.my_jid().split('/')[0], to: jid, body: get, time: Date.now() })
this._scroll_to_bottom(jid)
}
}
}