@devchitchat/chat 4.0.6 → 4.1.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.0.6",
3
+ "version": "4.1.0",
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')
@@ -867,55 +877,7 @@ export default function CallIsland(root) {
867
877
  emojiPickerTarget = null
868
878
  }
869
879
 
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
880
  document.addEventListener('click', e => {
918
- if (activeReactionContextMenu && !activeReactionContextMenu.contains(e.target)) closeReactionContextMenu()
919
881
  // Close emoji picker on click-outside
920
882
  if (emojiPickerEl && emojiPickerEl.parentNode && !emojiPickerEl.contains(e.target)) {
921
883
  const isReactionAddBtn = e.target.closest('.reaction-add') || e.target.closest('.btn-react')
@@ -930,6 +892,29 @@ export default function CallIsland(root) {
930
892
  }
931
893
  })
932
894
 
895
+ // Delegated click on task-list checkboxes — toggle [ ] ↔ [x] and save via msg.edit
896
+ messages.addEventListener('click', e => {
897
+ const cb = e.target.closest('.task-list-item-checkbox')
898
+ if (!cb) return
899
+ e.preventDefault() // we control the toggle ourselves
900
+ const article = cb.closest('article.message')
901
+ if (!article) return
902
+ const rawText = article.dataset.rawText
903
+ if (!rawText) return
904
+ const allCbs = Array.from(article.querySelectorAll('.task-list-item-checkbox'))
905
+ const idx = allCbs.indexOf(cb)
906
+ if (idx === -1) return
907
+ let count = 0
908
+ const newText = rawText.replace(/\[([ xX])\]/g, (match, state) => {
909
+ if (count++ !== idx) return match
910
+ return state.trim() === '' ? '[x]' : '[ ]'
911
+ })
912
+ if (newText === rawText) return
913
+ cb.checked = !cb.checked // optimistic toggle
914
+ article.dataset.rawText = newText
915
+ ws.send({ t: 'msg.edit', body: { msg_id: article.dataset.msgId, channel_id: channelId, text: newText } })
916
+ })
917
+
933
918
  // Delegated click on .reaction-pill — toggle reaction
934
919
  messages.addEventListener('click', e => {
935
920
  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) {