@devchitchat/chat 4.4.3 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devchitchat/chat",
3
- "version": "4.4.3",
3
+ "version": "4.5.0",
4
4
  "description": "A small chat app. p2p video and screenshare.",
5
5
  "scripts": {
6
6
  "dev": "bun --watch index.js",
@@ -29,7 +29,7 @@
29
29
  "styles.css"
30
30
  ],
31
31
  "dependencies": {
32
- "@devchitchat/index97": "^3.2.0",
32
+ "@devchitchat/index97": "^3.3.0",
33
33
  "@devchitchat/rdbljs": "^2.0.1"
34
34
  },
35
35
  "main": "index.js",
@@ -460,11 +460,13 @@ export default function CallIsland(root) {
460
460
 
461
461
  ws.on('msg.deleted', ({ msg_id }) => {
462
462
  const article = messages.querySelector(`[data-msg-id="${msg_id}"]`)
463
+ ?? threadRepliesEl?.querySelector(`[data-msg-id="${msg_id}"]`)
463
464
  if (article) article.remove()
464
465
  })
465
466
 
466
467
  ws.on('msg.edited', ({ msg_id, text, edited_at, rendered_text }) => {
467
468
  const article = messages.querySelector(`[data-msg-id="${msg_id}"]`)
469
+ ?? threadRepliesEl?.querySelector(`[data-msg-id="${msg_id}"]`)
468
470
  if (!article) return
469
471
  const textEl = article.querySelector('.message-text')
470
472
  if (textEl) { textEl.innerHTML = sanitizeHtml(rendered_text); enableTaskCheckboxes(article) }
@@ -483,7 +485,9 @@ export default function CallIsland(root) {
483
485
  })
484
486
 
485
487
  ws.on('reaction.event', ({ msg_id, reactions }) => {
488
+ // Check both the main message list and the open thread panel
486
489
  const article = messages.querySelector(`[data-msg-id="${msg_id}"]`)
490
+ ?? threadRepliesEl?.querySelector(`[data-msg-id="${msg_id}"]`)
487
491
  if (article) renderReactionBar(article, reactions ?? [], msg_id)
488
492
  })
489
493
 
@@ -589,7 +593,9 @@ export default function CallIsland(root) {
589
593
  return
590
594
  }
591
595
  for (const reply of replies) {
592
- const article = makeMessageEl(reply, { userId, userHandle })
596
+ const article = makeMessageEl(reply, { userId, userHandle, isThreadReply: true })
597
+ renderQuickPicks(article.querySelector('.message-hover-actions'))
598
+ if (reply.reactions?.length) renderReactionBar(article, reply.reactions, reply.msg_id)
593
599
  threadRepliesEl.appendChild(article)
594
600
  }
595
601
  if (threadBodyEl) threadBodyEl.scrollTop = threadBodyEl.scrollHeight
@@ -611,7 +617,8 @@ export default function CallIsland(root) {
611
617
  if (activeThreadParentId === parent_msg_id && threadRepliesEl) {
612
618
  const emptyEl = threadRepliesEl.querySelector('.thread-empty')
613
619
  if (emptyEl) emptyEl.remove()
614
- const article = makeMessageEl(reply, { userId, userHandle })
620
+ const article = makeMessageEl(reply, { userId, userHandle, isThreadReply: true })
621
+ renderQuickPicks(article.querySelector('.message-hover-actions'))
615
622
  threadRepliesEl.appendChild(article)
616
623
  if (threadBodyEl) threadBodyEl.scrollTop = threadBodyEl.scrollHeight
617
624
  }
@@ -1101,6 +1108,11 @@ export default function CallIsland(root) {
1101
1108
  for (const toolbar of messages.querySelectorAll('.message-hover-actions')) {
1102
1109
  renderQuickPicks(toolbar)
1103
1110
  }
1111
+ if (threadRepliesEl) {
1112
+ for (const toolbar of threadRepliesEl.querySelectorAll('.message-hover-actions')) {
1113
+ renderQuickPicks(toolbar)
1114
+ }
1115
+ }
1104
1116
  }
1105
1117
 
1106
1118
  let emojiPickerEl = null
@@ -1251,6 +1263,7 @@ export default function CallIsland(root) {
1251
1263
 
1252
1264
  document.addEventListener('keydown', e => {
1253
1265
  if (e.key === 'Escape') {
1266
+ if (activeEditCancel) { activeEditCancel(); return }
1254
1267
  if (composeOpen) { closeComposeMode(); return }
1255
1268
  closeEmojiPicker()
1256
1269
  }
@@ -1323,6 +1336,66 @@ export default function CallIsland(root) {
1323
1336
  openEmojiPickerAt(btn, msgId)
1324
1337
  })
1325
1338
 
1339
+ // ── Thread panel: mirror all message-level interaction handlers ───────────
1340
+ // threadRepliesEl is a sibling outside the island root, so handlers on
1341
+ // `messages` don't reach it — duplicate the relevant delegated clicks here.
1342
+
1343
+ threadRepliesEl?.addEventListener('click', e => {
1344
+ const btn = e.target.closest('.btn-quick-react')
1345
+ if (!btn) return
1346
+ e.stopPropagation()
1347
+ const emoji = btn.dataset.emoji
1348
+ const msgId = btn.closest('article.message')?.dataset.msgId
1349
+ if (!emoji || !msgId) return
1350
+ saveRecentEmoji(emoji)
1351
+ ws.send({ t: 'reaction.add', body: { msg_id: msgId, channel_id: channelId, emoji } })
1352
+ })
1353
+
1354
+ threadRepliesEl?.addEventListener('click', e => {
1355
+ const pill = e.target.closest('.reaction-pill')
1356
+ if (!pill) return
1357
+ e.stopPropagation()
1358
+ const emoji = pill.dataset.emoji
1359
+ const msgId = pill.dataset.msgId
1360
+ if (!emoji || !msgId) return
1361
+ if (pill.classList.contains('reacted')) {
1362
+ ws.send({ t: 'reaction.remove', body: { msg_id: msgId, channel_id: channelId, emoji } })
1363
+ } else {
1364
+ ws.send({ t: 'reaction.add', body: { msg_id: msgId, channel_id: channelId, emoji } })
1365
+ }
1366
+ })
1367
+
1368
+ threadRepliesEl?.addEventListener('click', e => {
1369
+ const addBtn = e.target.closest('.reaction-add')
1370
+ const reactBtn = e.target.closest('.btn-react')
1371
+ const btn = addBtn ?? reactBtn
1372
+ if (!btn) return
1373
+ e.stopPropagation()
1374
+ const msgId = addBtn?.dataset.msgId ?? btn.closest('article.message')?.dataset.msgId
1375
+ if (!msgId) return
1376
+ if (emojiPickerEl && emojiPickerEl.parentNode && emojiPickerTarget === msgId) {
1377
+ closeEmojiPicker()
1378
+ return
1379
+ }
1380
+ openEmojiPickerAt(btn, msgId)
1381
+ })
1382
+
1383
+ threadRepliesEl?.addEventListener('click', e => {
1384
+ const btn = e.target.closest('.btn-msg-actions')
1385
+ if (!btn) return
1386
+ e.stopPropagation()
1387
+ const article = btn.closest('article.message')
1388
+ if (article) showContextMenu(article, btn)
1389
+ })
1390
+
1391
+ threadRepliesEl?.addEventListener('click', e => {
1392
+ const handle = e.target.closest('.dm-trigger')
1393
+ if (!handle) return
1394
+ const targetUserId = handle.dataset.userId
1395
+ if (!targetUserId || targetUserId === userId) return
1396
+ ws.send({ t: 'dm.open', body: { target_user_id: targetUserId } })
1397
+ })
1398
+
1326
1399
  // ── Mobile long-press → action sheet with emoji picker ────────────────────
1327
1400
 
1328
1401
  addLongPress(messages, (e) => {
@@ -1356,6 +1429,8 @@ export default function CallIsland(root) {
1356
1429
 
1357
1430
  // ── Inline edit ────────────────────────────────────────────────────────────
1358
1431
 
1432
+ let activeEditCancel = null
1433
+
1359
1434
  function startInlineEdit(article) {
1360
1435
  if (article.querySelector('.message-edit-wrap')) return
1361
1436
  const textEl = article.querySelector('.message-text')
@@ -1420,13 +1495,16 @@ export default function CallIsland(root) {
1420
1495
  })
1421
1496
 
1422
1497
  function cancel() {
1498
+ if (!wrap.parentNode) return
1423
1499
  wrap.replaceWith(textEl)
1500
+ activeEditCancel = null
1424
1501
  }
1425
1502
 
1503
+ activeEditCancel = cancel
1504
+
1426
1505
  function save() {
1427
1506
  const newText = textarea.value.trim()
1428
1507
  if (!newText || newText === rawText) { cancel(); return }
1429
- textEl.textContent = newText
1430
1508
  cancel()
1431
1509
  article.dataset.rawText = newText
1432
1510
  ws.send({ t: 'msg.edit', body: { msg_id: article.dataset.msgId, channel_id: channelId, text: newText } })
@@ -120,9 +120,10 @@ export function renderAttachment(a) {
120
120
  /**
121
121
  * Build a <article class="message"> element.
122
122
  * @param {{ msg_id, seq, user_id, user_display_name, ts, text, attachments }} msg
123
- * @param {{ userId?: string, userHandle?: string }} [ctx] — caller's identity, used for self-styling and @mention highlighting
123
+ * @param {{ userId?: string, userHandle?: string, isThreadReply?: boolean }} [ctx]
124
+ * isThreadReply — omits the "Reply in thread" button (threads can't be nested)
124
125
  */
125
- export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, text, rendered_text, edited_at, attachments }, { userId, userHandle } = {}) {
126
+ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, text, rendered_text, edited_at, attachments }, { userId, userHandle, isThreadReply = false } = {}) {
126
127
  const article = document.createElement('article')
127
128
  article.className = 'message'
128
129
  article.dataset.seq = seq
@@ -134,13 +135,15 @@ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, tex
134
135
  const isSelf = userId != null && user_id === userId
135
136
  const attachmentHtml = (attachments ?? []).map(a => renderAttachment(a)).join('')
136
137
  const editedHtml = edited_at ? '<span class="message-edited">(edited)</span>' : ''
137
- const actionsHtml = `<div class="message-hover-actions"><span class="quick-picks"></span><button class="btn-reply btn-icon" type="button" title="Reply in thread" aria-label="Reply in thread">&#x21A9;</button><button class="btn-react btn-icon" type="button" title="Add reaction" aria-label="Add reaction">🙂</button>${isSelf ? '<button class="btn-msg-actions btn-icon" type="button" title="Message actions">…</button>' : ''}</div>`
138
+ const replyBtn = isThreadReply ? '' : '<button class="btn-reply btn-icon" type="button" title="Reply in thread" aria-label="Reply in thread">&#x21A9;</button>'
139
+ const actionsHtml = `<div class="message-hover-actions"><span class="quick-picks"></span>${replyBtn}<button class="btn-react btn-icon" type="button" title="Add reaction" aria-label="Add reaction">🙂</button>${isSelf ? '<button class="btn-msg-actions btn-icon" type="button" title="Message actions">…</button>' : ''}</div>`
138
140
  const textHtml = rendered_text ?? (text ? renderText(text, { userHandle }) : '')
139
141
  article.innerHTML = `
140
142
  <span class="message-handle${isSelf ? '' : ' dm-trigger'}" data-user-id="${escHtml(user_id)}" title="${isSelf ? '' : 'Send a direct message'}">${escHtml(user_display_name ?? user_id)}</span>
141
143
  <time class="message-time" datetime="${ts}">${time}${editedHtml}</time>
142
144
  ${textHtml ? `<div class="message-text">${textHtml}</div>` : ''}
143
145
  ${attachmentHtml}
146
+ <div class="reaction-bar"></div>
144
147
  ${actionsHtml}
145
148
  `
146
149
  return article