@devchitchat/chat 4.0.7 → 4.1.1

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.0.7",
3
+ "version": "4.1.1",
4
4
  "description": "A small chat app. p2p video and screenshare.",
5
5
  "scripts": {
6
6
  "dev": "bun --watch index.js",
@@ -137,7 +137,7 @@
137
137
  <article class="message" data-seq="{{seq}}" data-msg-id="{{msg_id}}" data-key="message_id" data-user-id="{{user_id}}" data-raw-text="{{raw_text}}" data-edited-at="{{edited_at}}" data-attachments="{{attachments_json}}" data-reactions="{{reactions_json}}">
138
138
  <span class="message-handle" data-user-id="{{user_id}}">{{user_display_name}}</span>
139
139
  <time class="message-time" datetime="{{ts}}">{{ts_fmt}}{{#if edited_at}}<span class="message-edited">(edited)</span>{{/if}}</time>
140
- <p class="message-text">{{{text}}}</p>
140
+ <div class="message-text">{{{text}}}</div>
141
141
  <div class="reaction-bar"></div>
142
142
  </article>
143
143
  {{/each}}
@@ -210,6 +210,8 @@ export default function CallIsland(root) {
210
210
  renderReactionBar(article, reactions, msgId)
211
211
  }
212
212
 
213
+ enableTaskCheckboxes(article)
214
+
213
215
  // Date separator before this article if date changed
214
216
  const ts = parseInt(article.querySelector('time')?.getAttribute('datetime') ?? '0', 10)
215
217
  if (ts) {
@@ -430,7 +432,7 @@ export default function CallIsland(root) {
430
432
  const article = messages.querySelector(`[data-msg-id="${msg_id}"]`)
431
433
  if (!article) return
432
434
  const textEl = article.querySelector('.message-text')
433
- if (textEl) textEl.innerHTML = sanitizeHtml(rendered_text)
435
+ if (textEl) { textEl.innerHTML = sanitizeHtml(rendered_text); enableTaskCheckboxes(article) }
434
436
  article.dataset.rawText = text
435
437
  article.dataset.editedAt = edited_at
436
438
  const timeEl = article.querySelector('.message-time')
@@ -692,6 +694,7 @@ export default function CallIsland(root) {
692
694
  article.appendChild(bar)
693
695
  }
694
696
 
697
+ enableTaskCheckboxes(article)
695
698
  messages.appendChild(article)
696
699
  renderReactionBar(article, reactions ?? [], msg_id)
697
700
  messages.scrollTop = messages.scrollHeight
@@ -703,6 +706,13 @@ export default function CallIsland(root) {
703
706
  return String(html ?? '').replaceAll('<script>', '').replaceAll('</script>', '')
704
707
  }
705
708
 
709
+ // Enable task-list checkboxes so they're clickable (renderer marks them disabled)
710
+ function enableTaskCheckboxes(article) {
711
+ for (const cb of article.querySelectorAll('.task-list-item-checkbox[disabled]')) {
712
+ cb.removeAttribute('disabled')
713
+ }
714
+ }
715
+
706
716
  // Delegated click: message sender name → open DM
707
717
  messages.addEventListener('click', e => {
708
718
  const handle = e.target.closest('.dm-trigger')
@@ -829,7 +839,6 @@ export default function CallIsland(root) {
829
839
  picker.onEmojiPickHandler = (e) => {
830
840
  const { emoji } = e.detail
831
841
  closeEmojiPicker()
832
- closeReactionContextMenu()
833
842
  ws.send({ t: 'reaction.add', body: { msg_id: msgId, channel_id: channelId, emoji } })
834
843
  }
835
844
  picker.removeEventListener('emoji:pick', picker._boundEmojiPick)
@@ -867,55 +876,7 @@ export default function CallIsland(root) {
867
876
  emojiPickerTarget = null
868
877
  }
869
878
 
870
- // ── Reaction context menu (desktop right-click) ────────────────────────────
871
-
872
- let activeReactionContextMenu = null
873
-
874
- function closeReactionContextMenu() {
875
- if (activeReactionContextMenu) { activeReactionContextMenu.remove(); activeReactionContextMenu = null }
876
- }
877
-
878
- function showReactionContextMenu(article, x, y) {
879
- closeReactionContextMenu()
880
- const msgId = article.dataset.msgId
881
- if (!msgId) return
882
-
883
- const menu = document.createElement('div')
884
- menu.className = 'msg-context-menu msg-context-menu--reaction'
885
-
886
- const reactBtn = document.createElement('button')
887
- reactBtn.className = 'msg-context-menu-item'
888
- reactBtn.type = 'button'
889
- reactBtn.textContent = 'React'
890
- reactBtn.addEventListener('click', (e) => {
891
- e.stopPropagation()
892
- openEmojiPickerAt(reactBtn, msgId)
893
- })
894
- menu.appendChild(reactBtn)
895
-
896
- document.body.appendChild(menu)
897
- activeReactionContextMenu = menu
898
-
899
- // Position near cursor, viewport-aware
900
- const menuRect = menu.getBoundingClientRect()
901
- let left = x + window.scrollX
902
- let top = y + window.scrollY
903
- if (left + menuRect.width > window.innerWidth - 8) left = window.innerWidth - 8 - menuRect.width
904
- if (left < 8) left = 8
905
- menu.style.top = `${top}px`
906
- menu.style.left = `${left}px`
907
- }
908
-
909
- // Right-click on a message article
910
- messages.addEventListener('contextmenu', e => {
911
- const article = e.target.closest('article.message')
912
- if (!article) return
913
- e.preventDefault()
914
- showReactionContextMenu(article, e.clientX, e.clientY)
915
- })
916
-
917
879
  document.addEventListener('click', e => {
918
- if (activeReactionContextMenu && !activeReactionContextMenu.contains(e.target)) closeReactionContextMenu()
919
880
  // Close emoji picker on click-outside
920
881
  if (emojiPickerEl && emojiPickerEl.parentNode && !emojiPickerEl.contains(e.target)) {
921
882
  const isReactionAddBtn = e.target.closest('.reaction-add') || e.target.closest('.btn-react')
@@ -925,11 +886,33 @@ export default function CallIsland(root) {
925
886
 
926
887
  document.addEventListener('keydown', e => {
927
888
  if (e.key === 'Escape') {
928
- closeReactionContextMenu()
929
889
  closeEmojiPicker()
930
890
  }
931
891
  })
932
892
 
893
+ // Delegated click on task-list checkboxes — toggle [ ] ↔ [x] and save via msg.edit
894
+ messages.addEventListener('click', e => {
895
+ const cb = e.target.closest('.task-list-item-checkbox')
896
+ if (!cb) return
897
+ e.preventDefault() // we control the toggle ourselves
898
+ const article = cb.closest('article.message')
899
+ if (!article) return
900
+ const rawText = article.dataset.rawText
901
+ if (!rawText) return
902
+ const allCbs = Array.from(article.querySelectorAll('.task-list-item-checkbox'))
903
+ const idx = allCbs.indexOf(cb)
904
+ if (idx === -1) return
905
+ let count = 0
906
+ const newText = rawText.replace(/\[([ xX])\]/g, (match, state) => {
907
+ if (count++ !== idx) return match
908
+ return state.trim() === '' ? '[x]' : '[ ]'
909
+ })
910
+ if (newText === rawText) return
911
+ cb.checked = !cb.checked // optimistic toggle
912
+ article.dataset.rawText = newText
913
+ ws.send({ t: 'msg.edit', body: { msg_id: article.dataset.msgId, channel_id: channelId, text: newText } })
914
+ })
915
+
933
916
  // Delegated click on .reaction-pill — toggle reaction
934
917
  messages.addEventListener('click', e => {
935
918
  const pill = e.target.closest('.reaction-pill')
@@ -139,7 +139,7 @@ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, tex
139
139
  article.innerHTML = `
140
140
  <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
141
  <time class="message-time" datetime="${ts}">${time}${editedHtml}</time>
142
- ${textHtml ? `<p class="message-text">${textHtml}</p>` : ''}
142
+ ${textHtml ? `<div class="message-text">${textHtml}</div>` : ''}
143
143
  ${attachmentHtml}
144
144
  ${actionsHtml}
145
145
  `
@@ -537,8 +537,20 @@ details summary {
537
537
  .message-handle.dm-trigger { cursor: pointer; }
538
538
  .message-handle.dm-trigger:hover { text-decoration: underline; }
539
539
  .message-time { font-size: 11px; color: var(--text-muted); grid-column: 2; grid-row: 1; align-self: end; }
540
- .message-text { grid-column: 1 / -1; grid-row: 2; line-height: 1.5; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
540
+ .message-text { grid-column: 1 / -1; grid-row: 2; line-height: 1.5; color: var(--text-primary); word-break: break-word; overflow-wrap: anywhere; }
541
+ .message-text p { white-space: pre-wrap; margin: 0 0 4px; }
542
+ .message-text p:last-child { margin-bottom: 0; }
541
543
  .message-text a { word-break: break-all; }
544
+ .message-text h1,.message-text h2,.message-text h3,.message-text h4 { margin: 8px 0 4px; font-weight: 600; line-height: 1.3; }
545
+ .message-text h1 { font-size: 1.3em; }
546
+ .message-text h2 { font-size: 1.15em; }
547
+ .message-text h3,.message-text h4 { font-size: 1em; }
548
+ .message-text ul,.message-text ol { margin: 4px 0; padding-left: 1.75em; }
549
+ .message-text li { margin: 2px 0; }
550
+ .message-text code { font-family: var(--font-mono, monospace); font-size: 0.875em; background: color-mix(in srgb, var(--accent) 12%, transparent); border-radius: 3px; padding: 1px 4px; }
551
+ .message-text pre { background: var(--bg-input, var(--bg-sidebar)); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 10px 14px; overflow-x: auto; margin: 6px 0; }
552
+ .message-text pre code { background: none; padding: 0; font-size: 0.85em; }
553
+ .message-text blockquote { border-left: 3px solid var(--accent); margin: 4px 0; padding: 2px 10px; color: var(--text-secondary); }
542
554
 
543
555
  /* ── Reaction bar ────────────────────────────────────────────────────────── */
544
556
  .reaction-bar {
@@ -42,6 +42,8 @@ export function handleMsgList(ws, msg, ctx) {
42
42
  const { messageService, sendWs } = ctx
43
43
  const { channel_id, after_seq, before_seq, limit } = msg.body || {}
44
44
 
45
+ const withRendered = messages => messages.map(m => ({ ...m, rendered_text: renderMarkdown(m.text).html }))
46
+
45
47
  if (before_seq != null) {
46
48
  const result = messageService.listMessagesBefore({
47
49
  channelId: channel_id,
@@ -49,12 +51,12 @@ export function handleMsgList(ws, msg, ctx) {
49
51
  beforeSeq: before_seq,
50
52
  limit: limit ?? 50,
51
53
  })
52
- sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, channel_id, direction: 'before' } })
54
+ sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, messages: withRendered(result.messages), channel_id, direction: 'before' } })
53
55
  return
54
56
  }
55
57
 
56
58
  const result = messageService.listMessages({ channelId: channel_id, userId: ws.data.userId, afterSeq: after_seq ?? 0, limit: limit ?? 50 })
57
- sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, channel_id, direction: 'after' } })
59
+ sendWs(ws, { t: 'msg.list_result', reply_to: msg.id, ok: true, body: { ...result, messages: withRendered(result.messages), channel_id, direction: 'after' } })
58
60
  }
59
61
 
60
62
  export function handleMsgDelete(ws, msg, ctx) {