@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
|
@@ -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')
|
|
@@ -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 ? `<
|
|
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) {
|