@devchitchat/chat 4.5.0 → 5.0.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/index.js +0 -9
- package/package.json +2 -3
- package/pages/_layout.html +0 -7
- package/pages/admin/_layout.html +0 -7
- package/pages/channels/[channelId].phtml +17 -42
- package/pages/design/_layout.html +349 -0
- package/pages/design/_layout.js +13 -0
- package/pages/design/components/index.js +3 -0
- package/pages/design/components/index.phtml +380 -0
- package/pages/design/index.js +3 -0
- package/pages/design/index.phtml +78 -0
- package/pages/design/principles/index.js +3 -0
- package/pages/design/principles/index.phtml +147 -0
- package/pages/design/tokens/index.js +3 -0
- package/pages/design/tokens/index.phtml +236 -0
- package/pages/public/client/app.js +171 -13
- package/pages/public/client/controllers/ChatController.js +204 -0
- package/pages/public/client/controllers/WebSocketController.js +191 -0
- package/pages/public/client/model/AppModel.js +351 -0
- package/pages/public/client/model/events.js +41 -0
- package/pages/public/client/resizable.js +74 -0
- package/pages/public/client/rtc-peer-manager.js +5 -2
- package/pages/public/client/settings-sync.js +45 -7
- package/pages/public/client/shared/messages.js +21 -9
- package/pages/public/client/theme.js +6 -4
- package/pages/public/client/views/CallView.js +754 -0
- package/pages/public/client/views/ChatHeaderView.js +67 -0
- package/pages/public/client/views/ComposerView.js +491 -0
- package/pages/public/client/views/MessageListView.js +461 -0
- package/pages/public/client/views/SidebarView.js +977 -0
- package/pages/public/client/views/ThreadPanelView.js +260 -0
- package/pages/public/client/views/shared/EmojiPickerSingleton.js +201 -0
- package/pages/public/client/views/shared/MentionPicker.js +139 -0
- package/pages/public/client/views/shared/MessageInteractions.js +353 -0
- package/pages/public/themes/base.css +29 -31
- package/src/ws/ChatServer.js +2 -1
- package/src/ws/handlers/rtcHandlers.js +7 -0
- package/pages/public/client/islands/call.js +0 -2282
- package/pages/public/client/islands/sidebar.js +0 -1198
|
@@ -1,25 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* settings-sync.js — client-side
|
|
2
|
+
* settings-sync.js — all client-side persistence in one place.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two namespaces:
|
|
5
|
+
* settings — synced to server (last_channel_id, mobile_chat_open, …)
|
|
6
|
+
* prefs — local-only UI preferences (theme, panel widths, devices, …)
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Callers never touch localStorage directly — they use the exports below.
|
|
9
|
+
*
|
|
10
|
+
* Storage keys:
|
|
11
|
+
* devchitchat_settings { settings: {…}, updated_at: number }
|
|
12
|
+
* devchitchat_prefs { theme, sidebar_width, thread_panel_width,
|
|
13
|
+
* tile_panel_width, tile_layout, devices, … }
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
|
-
const
|
|
16
|
+
const SETTINGS_KEY = 'devchitchat_settings'
|
|
17
|
+
const PREFS_KEY = 'devchitchat_prefs'
|
|
11
18
|
const BASE_PATH = window.__BASE_PATH__ ?? ''
|
|
12
19
|
|
|
13
20
|
function readLocal() {
|
|
14
21
|
try {
|
|
15
|
-
return JSON.parse(localStorage.getItem(
|
|
22
|
+
return JSON.parse(localStorage.getItem(SETTINGS_KEY) ?? '{}')
|
|
16
23
|
} catch {
|
|
17
24
|
return {}
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
function writeLocal(data) {
|
|
22
|
-
localStorage.setItem(
|
|
29
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Local-only UI preferences ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function readPrefs() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(localStorage.getItem(PREFS_KEY) ?? '{}')
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read a UI preference. Returns `defaultValue` when the key has never been set.
|
|
44
|
+
* @param {string} key
|
|
45
|
+
* @param {*} [defaultValue]
|
|
46
|
+
*/
|
|
47
|
+
export function getPref(key, defaultValue = null) {
|
|
48
|
+
const prefs = readPrefs()
|
|
49
|
+
return key in prefs ? prefs[key] : defaultValue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write one or more UI preferences.
|
|
54
|
+
* @param {string|Record<string,*>} keyOrPatch — key string or { key: value } map
|
|
55
|
+
* @param {*} [value] — value when keyOrPatch is a string
|
|
56
|
+
*/
|
|
57
|
+
export function setPref(keyOrPatch, value) {
|
|
58
|
+
const prefs = readPrefs()
|
|
59
|
+
const patch = typeof keyOrPatch === 'string' ? { [keyOrPatch]: value } : keyOrPatch
|
|
60
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify({ ...prefs, ...patch }))
|
|
23
61
|
}
|
|
24
62
|
|
|
25
63
|
// Returns current settings object (instant, synchronous)
|
|
@@ -33,8 +33,9 @@ export function makeDateSeparator(dateKey) {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Escape HTML then wrap @handles in <span class="mention"> (or mention-self for current user).
|
|
36
|
+
* Only handles that appear in `knownHandles` are styled; unrecognised @words are left as plain text.
|
|
36
37
|
* @param {string} text
|
|
37
|
-
* @param {{ userHandle?: string }} [opts]
|
|
38
|
+
* @param {{ userHandle?: string, knownHandles?: Set<string> }} [opts]
|
|
38
39
|
*/
|
|
39
40
|
// Combined regex (operates on raw text before HTML-escaping):
|
|
40
41
|
// group 1 (+ inner 2, 3) — markdown link: [text](url)
|
|
@@ -42,7 +43,7 @@ export function makeDateSeparator(dateKey) {
|
|
|
42
43
|
// group 5 — @mention
|
|
43
44
|
const INLINE_RE = /(\[([^\]]*)\]\((https?:\/\/[^)]+)\))|(https?:\/\/[^\s<>"'[\]()*]+)|(@[a-zA-Z0-9_.-]+)/g
|
|
44
45
|
|
|
45
|
-
export function renderText(text, { userHandle } = {}) {
|
|
46
|
+
export function renderText(text, { userHandle, knownHandles } = {}) {
|
|
46
47
|
let result = ''
|
|
47
48
|
let lastIndex = 0
|
|
48
49
|
INLINE_RE.lastIndex = 0
|
|
@@ -58,9 +59,14 @@ export function renderText(text, { userHandle } = {}) {
|
|
|
58
59
|
const trailing = bareUrl.slice(trimmed.length)
|
|
59
60
|
result += `<a href="${escHtml(trimmed)}" target="_blank" rel="noopener noreferrer">${escHtml(trimmed)}</a>${escHtml(trailing)}`
|
|
60
61
|
} else if (mention) {
|
|
61
|
-
const handle = mention.slice(1)
|
|
62
|
-
const
|
|
63
|
-
|
|
62
|
+
const handle = mention.slice(1).toLowerCase()
|
|
63
|
+
const known = !knownHandles || knownHandles.has(handle)
|
|
64
|
+
if (known) {
|
|
65
|
+
const isSelf = userHandle && handle === userHandle.toLowerCase()
|
|
66
|
+
result += `<span class="mention${isSelf ? ' mention-self' : ''}">${escHtml(mention)}</span>`
|
|
67
|
+
} else {
|
|
68
|
+
result += escHtml(mention)
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
lastIndex = m.index + full.length
|
|
66
72
|
}
|
|
@@ -75,7 +81,7 @@ export function renderText(text, { userHandle } = {}) {
|
|
|
75
81
|
* @param {Element} el
|
|
76
82
|
* @param {{ userHandle?: string }} [opts]
|
|
77
83
|
*/
|
|
78
|
-
export function applyInlineRenderingToTextNodes(el, { userHandle } = {}) {
|
|
84
|
+
export function applyInlineRenderingToTextNodes(el, { userHandle, knownHandles } = {}) {
|
|
79
85
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
80
86
|
const nodes = []
|
|
81
87
|
let n
|
|
@@ -86,7 +92,7 @@ export function applyInlineRenderingToTextNodes(el, { userHandle } = {}) {
|
|
|
86
92
|
for (const textNode of nodes) {
|
|
87
93
|
const raw = textNode.textContent
|
|
88
94
|
if (!raw.trim()) continue
|
|
89
|
-
const rendered = renderText(raw, { userHandle })
|
|
95
|
+
const rendered = renderText(raw, { userHandle, knownHandles })
|
|
90
96
|
if (rendered === escHtml(raw)) continue // nothing changed
|
|
91
97
|
const span = document.createElement('span')
|
|
92
98
|
span.innerHTML = rendered
|
|
@@ -123,7 +129,7 @@ export function renderAttachment(a) {
|
|
|
123
129
|
* @param {{ userId?: string, userHandle?: string, isThreadReply?: boolean }} [ctx]
|
|
124
130
|
* isThreadReply — omits the "Reply in thread" button (threads can't be nested)
|
|
125
131
|
*/
|
|
126
|
-
export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, text, rendered_text, edited_at, attachments }, { userId, userHandle, isThreadReply = false } = {}) {
|
|
132
|
+
export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, text, rendered_text, edited_at, attachments }, { userId, userHandle, knownHandles, isThreadReply = false } = {}) {
|
|
127
133
|
const article = document.createElement('article')
|
|
128
134
|
article.className = 'message'
|
|
129
135
|
article.dataset.seq = seq
|
|
@@ -137,7 +143,7 @@ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, tex
|
|
|
137
143
|
const editedHtml = edited_at ? '<span class="message-edited">(edited)</span>' : ''
|
|
138
144
|
const replyBtn = isThreadReply ? '' : '<button class="btn-reply btn-icon" type="button" title="Reply in thread" aria-label="Reply in thread">↩</button>'
|
|
139
145
|
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>`
|
|
140
|
-
const textHtml = rendered_text ?? (text ? renderText(text, { userHandle }) : '')
|
|
146
|
+
const textHtml = rendered_text ?? (text ? renderText(text, { userHandle, knownHandles }) : '')
|
|
141
147
|
article.innerHTML = `
|
|
142
148
|
<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>
|
|
143
149
|
<time class="message-time" datetime="${ts}">${time}${editedHtml}</time>
|
|
@@ -146,5 +152,11 @@ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, tex
|
|
|
146
152
|
<div class="reaction-bar"></div>
|
|
147
153
|
${actionsHtml}
|
|
148
154
|
`
|
|
155
|
+
// When the server provides rendered_text, @mention styling is not included.
|
|
156
|
+
// Apply it now so every code path gets consistent output.
|
|
157
|
+
if (rendered_text) {
|
|
158
|
+
const textEl = article.querySelector('.message-text')
|
|
159
|
+
if (textEl) applyInlineRenderingToTextNodes(textEl, { userHandle, knownHandles })
|
|
160
|
+
}
|
|
149
161
|
return article
|
|
150
162
|
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each theme is a separate CSS file in /themes/<name>.css.
|
|
5
5
|
* The <html data-theme> attribute is set so themes can also use attribute selectors.
|
|
6
|
+
* Theme preference is stored via settings-sync getPref/setPref ('theme' key).
|
|
6
7
|
*/
|
|
8
|
+
import { getPref, setPref } from './settings-sync.js'
|
|
9
|
+
|
|
7
10
|
const THEMES = ['dark', 'light', 'ocean', 'forest', 'rose']
|
|
8
|
-
const STORAGE_KEY = 'devchitchat_theme'
|
|
9
11
|
const BASE_PATH = window.__BASE_PATH__ ?? ''
|
|
10
12
|
const stylesheet = document.getElementById('theme-stylesheet')
|
|
11
13
|
const picker = document.getElementById('theme-picker')
|
|
@@ -15,13 +17,13 @@ function applyTheme(name) {
|
|
|
15
17
|
document.documentElement.dataset.theme = theme
|
|
16
18
|
if (stylesheet) stylesheet.href = `${BASE_PATH}/themes/${theme}.css`
|
|
17
19
|
if (picker) picker.value = theme
|
|
18
|
-
|
|
20
|
+
setPref('theme', theme)
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// Restore saved theme immediately (before paint)
|
|
22
|
-
applyTheme(
|
|
24
|
+
applyTheme(getPref('theme', 'dark'))
|
|
23
25
|
|
|
24
26
|
// Wire picker
|
|
25
27
|
if (picker) {
|
|
26
|
-
picker.addEventListener('change',
|
|
28
|
+
picker.addEventListener('change', e => applyTheme(e.target.value))
|
|
27
29
|
}
|