feat(chat): modern emojis via Twemoji (#5)
Loads @twemoji/api (jsDelivr) and renders emojis as Twemoji images in messages, the emoji picker, and reactions (twemojify at each render point). Picker inserts via data-emoji so it survives the <img> swap. Falls back to plain Unicode if the CDN is unavailable. build batch25. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-5
@@ -225,6 +225,8 @@
|
||||
selector would override .panel{position:absolute;inset:0} and collapse the pane. */
|
||||
#chatPanel.drag-over::after{content:"Drop to send";position:absolute;inset:10px;border:2.5px dashed var(--blue);border-radius:14px;background:rgba(31,59,115,.07);display:grid;place-items:center;color:var(--blue);font-weight:700;font-size:1.15rem;z-index:60;pointer-events:none;}
|
||||
.ic{display:inline-block;vertical-align:middle;}
|
||||
img.emoji{height:1.15em;width:1.15em;margin:0 .05em;vertical-align:-0.15em;display:inline-block;}
|
||||
.emoji-grid img.emoji,.emoji-tabs img.emoji{height:1.4em;width:1.4em;}
|
||||
.convo-back{border:none;background:var(--blue-soft);color:var(--blue);width:34px;height:34px;border-radius:9px;cursor:pointer;display:grid;place-items:center;flex:0 0 auto;}
|
||||
.convo-back:hover{background:#dbe6fb;}
|
||||
.convo-head .nm{font-weight:700;color:var(--ink);}
|
||||
@@ -733,7 +735,11 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=4"></script>
|
||||
<script>window.__BUILD='2026-07-02-batch24';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||
<script>window.__BUILD='2026-07-02-batch25';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
||||
// (emojis stay as plain Unicode). (#5)
|
||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||
<div class="loading" id="loading">Loading…</div>
|
||||
|
||||
<header>
|
||||
@@ -1337,8 +1343,9 @@ function renderEmojiPop(pop){
|
||||
// make the outside-click handler think the click was outside and close the picker.
|
||||
pop.querySelectorAll('.emoji-tabs button').forEach(b=>b.onclick=(ev)=>{ ev.stopPropagation(); emojiCat=+b.dataset.i; renderEmojiPop(pop); });
|
||||
const grid=pop.querySelector('#emojiGrid');
|
||||
grid.innerHTML=EMOJI_CATS[emojiCat].list.trim().split(/\s+/).map(e=>'<button type="button">'+e+'</button>').join('');
|
||||
grid.querySelectorAll('button').forEach(b=>b.onclick=(ev)=>{ ev.stopPropagation(); onEmojiPick(b.textContent); });
|
||||
grid.innerHTML=EMOJI_CATS[emojiCat].list.trim().split(/\s+/).map(e=>'<button type="button" data-emoji="'+e+'">'+e+'</button>').join('');
|
||||
grid.querySelectorAll('button').forEach(b=>b.onclick=(ev)=>{ ev.stopPropagation(); onEmojiPick(b.dataset.emoji); }); // data-emoji survives twemoji's <img> swap
|
||||
twemojify(pop); // modern emoji images in the picker
|
||||
}
|
||||
function onEmojiPick(e){ if(emojiMode && emojiMode.react){ reactToMessage(emojiMode.react, e); closeEmoji(); } else { insertEmoji(e); } }
|
||||
function openEmojiForReact(messageId, anchorEl){ openEmoji({ react: messageId }, anchorEl); }
|
||||
@@ -1517,7 +1524,7 @@ function openLightbox(src){
|
||||
function updateBubble(m){
|
||||
const sel=(window.CSS&&CSS.escape)?CSS.escape(m.id):m.id;
|
||||
const el=document.querySelector('#msgs .bubble[data-id="'+sel+'"]');
|
||||
if(el) el.outerHTML=bubbleHTML(m);
|
||||
if(el){ el.outerHTML=bubbleHTML(m); const n=document.querySelector('#msgs .bubble[data-id="'+sel+'"]'); twemojify(n); }
|
||||
}
|
||||
// Floating date pill (updates to the day at the top of the viewport) + jump-to-latest button.
|
||||
function onMsgsScroll(){
|
||||
@@ -1540,7 +1547,7 @@ function renderThread(){
|
||||
if(!THREAD.length){ box.innerHTML='<div class="empty-thread">No messages yet — say hello 👋</div>'; return; }
|
||||
let html='';
|
||||
for(const m of THREAD){ const dk=dayKey(m.created_at); if(dk!==_lastDay){ html+='<div class="day-sep"><span>'+pEsc(dayLabel(m.created_at))+'</span></div>'; _lastDay=dk; } rendered.add(m.id); html+=bubbleHTML(m); }
|
||||
box.innerHTML=html; box.scrollTop=box.scrollHeight;
|
||||
box.innerHTML=html; twemojify(box); box.scrollTop=box.scrollHeight;
|
||||
}
|
||||
function appendBubble(m){
|
||||
if(rendered.has(m.id)) return; rendered.add(m.id);
|
||||
@@ -1550,6 +1557,7 @@ function appendBubble(m){
|
||||
if(m.from===ME.id && !m.system){ if(_lastMineId){ const pe=box.querySelector('.bubble[data-id="'+((window.CSS&&CSS.escape)?CSS.escape(_lastMineId):_lastMineId)+'"] .seenby'); if(pe) pe.remove(); } _lastMineId=m.id; }
|
||||
const dk=dayKey(m.created_at); if(dk!==_lastDay){ box.insertAdjacentHTML('beforeend', '<div class="day-sep"><span>'+pEsc(dayLabel(m.created_at))+'</span></div>'); _lastDay=dk; }
|
||||
box.insertAdjacentHTML('beforeend', bubbleHTML(m));
|
||||
twemojify(box.lastElementChild);
|
||||
box.scrollTop=box.scrollHeight;
|
||||
}
|
||||
let _openUnread=0; // set by selectChat (the unread count before it's reset) so we can open at the first unread
|
||||
|
||||
Reference in New Issue
Block a user