@devchitchat/chat 4.0.7 → 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
|
@@ -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
|
-
<
|
|
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 ? `<
|
|
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);
|
|
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) {
|