@devchitchat/chat 4.0.5 → 4.0.6

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.5",
3
+ "version": "4.0.6",
4
4
  "description": "A small chat app. p2p video and screenshare.",
5
5
  "scripts": {
6
6
  "dev": "bun --watch index.js",
@@ -141,9 +141,7 @@ export default function CallIsland(root) {
141
141
  data-emoji="${escHtml(r.emoji)}" data-msg-id="${escHtml(msgId)}"
142
142
  type="button" title="${r.count} reaction${r.count !== 1 ? 's' : ''}">
143
143
  ${r.emoji} <span class="reaction-count">${r.count}</span>
144
- </button>`).join('') +
145
- `<button class="reaction-add" data-msg-id="${escHtml(msgId)}" type="button"
146
- title="Add reaction" aria-label="Add reaction">+</button>`
144
+ </button>`).join('')
147
145
  }
148
146
 
149
147
  // ── Hydrate seed message attachments ──────────────────────────────────────
@@ -157,18 +155,32 @@ export default function CallIsland(root) {
157
155
  if (article.dataset.hydrated) continue
158
156
  article.dataset.hydrated = '1'
159
157
 
160
- // Add dm-trigger to non-self sender handles; add … actions button to own messages
158
+ // Add dm-trigger to non-self sender handles
161
159
  const handle = article.querySelector('.message-handle[data-user-id]')
162
160
  if (handle && handle.dataset.userId !== userId) {
163
161
  handle.classList.add('dm-trigger')
164
162
  handle.title = 'Send a direct message'
165
- } else if (article.dataset.userId === userId && !article.querySelector('.btn-msg-actions')) {
166
- const btn = document.createElement('button')
167
- btn.className = 'btn-msg-actions btn-icon'
168
- btn.type = 'button'
169
- btn.title = 'Message actions'
170
- btn.textContent = ''
171
- article.appendChild(btn)
163
+ }
164
+ // Add hover action toolbar (react for all messages; … only for own)
165
+ if (!article.querySelector('.message-hover-actions')) {
166
+ const toolbar = document.createElement('div')
167
+ toolbar.className = 'message-hover-actions'
168
+ const reactBtn = document.createElement('button')
169
+ reactBtn.className = 'btn-react btn-icon'
170
+ reactBtn.type = 'button'
171
+ reactBtn.title = 'Add reaction'
172
+ reactBtn.setAttribute('aria-label', 'Add reaction')
173
+ reactBtn.textContent = '🙂'
174
+ toolbar.appendChild(reactBtn)
175
+ if (article.dataset.userId === userId) {
176
+ const actionsBtn = document.createElement('button')
177
+ actionsBtn.className = 'btn-msg-actions btn-icon'
178
+ actionsBtn.type = 'button'
179
+ actionsBtn.title = 'Message actions'
180
+ actionsBtn.textContent = '…'
181
+ toolbar.appendChild(actionsBtn)
182
+ }
183
+ article.appendChild(toolbar)
172
184
  }
173
185
 
174
186
  // Apply inline rendering (URLs, @mentions) to server-rendered message text.
@@ -906,7 +918,7 @@ export default function CallIsland(root) {
906
918
  if (activeReactionContextMenu && !activeReactionContextMenu.contains(e.target)) closeReactionContextMenu()
907
919
  // Close emoji picker on click-outside
908
920
  if (emojiPickerEl && emojiPickerEl.parentNode && !emojiPickerEl.contains(e.target)) {
909
- const isReactionAddBtn = e.target.closest('.reaction-add')
921
+ const isReactionAddBtn = e.target.closest('.reaction-add') || e.target.closest('.btn-react')
910
922
  if (!isReactionAddBtn) closeEmojiPicker()
911
923
  }
912
924
  }, { capture: true })
@@ -933,12 +945,14 @@ export default function CallIsland(root) {
933
945
  }
934
946
  })
935
947
 
936
- // Delegated click on .reaction-add — open emoji picker
948
+ // Delegated click on .reaction-add or .btn-react — open emoji picker
937
949
  messages.addEventListener('click', e => {
938
- const btn = e.target.closest('.reaction-add')
950
+ const addBtn = e.target.closest('.reaction-add')
951
+ const reactBtn = e.target.closest('.btn-react')
952
+ const btn = addBtn ?? reactBtn
939
953
  if (!btn) return
940
954
  e.stopPropagation()
941
- const msgId = btn.dataset.msgId
955
+ const msgId = addBtn?.dataset.msgId ?? btn.closest('article.message')?.dataset.msgId
942
956
  if (!msgId) return
943
957
  // Toggle: close if already open for this message
944
958
  if (emojiPickerEl && emojiPickerEl.parentNode && emojiPickerTarget === msgId) {
@@ -134,7 +134,7 @@ export function makeMessageEl({ msg_id, seq, user_id, user_display_name, ts, tex
134
134
  const isSelf = userId != null && user_id === userId
135
135
  const attachmentHtml = (attachments ?? []).map(a => renderAttachment(a)).join('')
136
136
  const editedHtml = edited_at ? '<span class="message-edited">(edited)</span>' : ''
137
- const actionsHtml = isSelf ? '<button class="btn-msg-actions btn-icon" type="button" title="Message actions">…</button>' : ''
137
+ const actionsHtml = `<div class="message-hover-actions"><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>`
138
138
  const textHtml = rendered_text ?? (text ? renderText(text, { userHandle }) : '')
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>
@@ -989,10 +989,11 @@ mark { background: color-mix(in srgb, var(--accent) 30%, transparent); color: va
989
989
  }
990
990
  .btn-back-mobile:hover { color: var(--text-primary); }
991
991
 
992
- /* Actions button: no hover on touch — always visible for own messages */
993
- .btn-msg-actions {
992
+ /* Hover toolbar: no hover on touch — always visible, tucked inside message */
993
+ .message-hover-actions {
994
994
  opacity: 1;
995
995
  pointer-events: auto;
996
+ top: 2px;
996
997
  }
997
998
  }
998
999
 
@@ -1767,19 +1768,43 @@ form.inline { display: inline; }
1767
1768
  font-size: 14px;
1768
1769
  }
1769
1770
 
1770
- /* actions button on message hover */
1771
- .btn-msg-actions {
1771
+ /* Hover action toolbar floats above the message on hover (Discord-style) */
1772
+ article.message { position: relative; }
1773
+ .message-hover-actions {
1772
1774
  position: absolute;
1773
- top: 2px;
1775
+ top: -20px;
1774
1776
  right: 4px;
1775
- font-size: 16px;
1776
- line-height: 1;
1777
+ display: flex;
1778
+ gap: 1px;
1779
+ align-items: center;
1777
1780
  opacity: 0;
1778
1781
  pointer-events: none;
1779
1782
  transition: opacity 0.1s;
1783
+ background: var(--bg-sidebar);
1784
+ border: 1px solid var(--border);
1785
+ border-radius: var(--radius-md);
1786
+ padding: 2px;
1787
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1788
+ z-index: 10;
1789
+ }
1790
+ article.message:hover .message-hover-actions { opacity: 1; pointer-events: auto; }
1791
+ .btn-react,
1792
+ .btn-msg-actions {
1793
+ background: none;
1794
+ border: none;
1795
+ cursor: pointer;
1796
+ padding: 3px 7px;
1797
+ border-radius: calc(var(--radius-md) - 2px);
1798
+ font-size: 15px;
1799
+ line-height: 1;
1800
+ color: var(--text-secondary);
1801
+ transition: background var(--transition), color var(--transition);
1802
+ }
1803
+ .btn-react:hover,
1804
+ .btn-msg-actions:hover {
1805
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
1806
+ color: var(--text-primary);
1780
1807
  }
1781
- article.message:hover .btn-msg-actions { opacity: 1; pointer-events: auto; }
1782
- article.message { position: relative; }
1783
1808
 
1784
1809
  /* Context menu */
1785
1810
  .msg-context-menu {