tg stickers v3

This commit is contained in:
2026-07-15 23:10:35 +02:00
parent 46d4b49157
commit ab9f2daaf9

View File

@@ -497,9 +497,13 @@ if (window.__dmLoaded) {
if (oldestMsgId === null || m.id < oldestMsgId) oldestMsgId = m.id; if (oldestMsgId === null || m.id < oldestMsgId) oldestMsgId = m.id;
if (prepend) { if (prepend) {
thread.insertBefore(buildMessageBubble(m), thread.firstChild); const bubble = buildMessageBubble(m);
thread.insertBefore(bubble, thread.firstChild);
playDmEmojiVideos(bubble);
} else { } else {
thread.appendChild(buildMessageBubble(m)); const bubble = buildMessageBubble(m);
thread.appendChild(bubble);
playDmEmojiVideos(bubble);
} }
} }
@@ -531,7 +535,9 @@ if (window.__dmLoaded) {
renderedIds.add(m.id); renderedIds.add(m.id);
threadMessages.push(m); threadMessages.push(m);
if (m.id > latestMsgId) latestMsgId = m.id; if (m.id > latestMsgId) latestMsgId = m.id;
thread.appendChild(buildMessageBubble(m)); const bubble = buildMessageBubble(m);
thread.appendChild(bubble);
playDmEmojiVideos(bubble);
appended = true; appended = true;
} }
@@ -582,10 +588,22 @@ if (window.__dmLoaded) {
return text.replace(/:([a-z0-9_]+):/g, (match, name) => { return text.replace(/:([a-z0-9_]+):/g, (match, name) => {
const url = emojis[name]; const url = emojis[name];
if (!url) return match; if (!url) return match;
if (url.endsWith('.webm')) {
return `<video src="${url}" class="emoji" title="${match}" autoplay loop muted playsinline></video>`;
}
return `<img src="${url}" class="emoji" alt="${match}" title="${match}" loading="lazy">`; return `<img src="${url}" class="emoji" alt="${match}" title="${match}" loading="lazy">`;
}); });
} }
function playDmEmojiVideos(el) {
if (!el) return;
el.querySelectorAll('video.emoji').forEach(v => {
v.play().catch(() => {
v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true });
});
});
}
function renderDmContent(plaintext) { function renderDmContent(plaintext) {
if (!plaintext) return ''; if (!plaintext) return '';
@@ -1748,39 +1766,127 @@ if (window.__dmLoaded) {
window.addEventListener('resize', () => { if (autocomplete.style.display !== 'none') positionAC(); }); window.addEventListener('resize', () => { if (autocomplete.style.display !== 'none') positionAC(); });
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
function renderEmojisIntoPicker(emojis) { // ── Helper: img or video element for a given emoji URL ────────────────
picker.innerHTML = ''; function makeDmEmojiEl(url, name) {
Object.entries(emojis).forEach(([name, url]) => { let el;
const img = document.createElement('img'); if (url && url.endsWith('.webm')) {
img.src = url; el = document.createElement('video');
img.title = `:${name}:`; el.src = url;
img.loading = 'lazy'; el.autoplay = true;
img.onerror = () => { img.style.display = 'none'; }; el.loop = true;
img.addEventListener('click', (ev) => { el.muted = true;
ev.stopPropagation(); el.playsInline = true;
const pos = textarea.selectionStart ?? textarea.value.length; } else {
const val = textarea.value; el = document.createElement('img');
textarea.value = val.slice(0, pos) + ` :${name}: ` + val.slice(pos); el.src = url;
textarea.focus(); el.loading = 'lazy';
textarea.selectionStart = textarea.selectionEnd = pos + name.length + 4; }
}); el.title = `:${name}:`;
picker.appendChild(img); el.onerror = () => { el.style.display = 'none'; };
}); return el;
} }
function populatePicker() { function buildDmPickerContent(emojis, packs) {
if (picker.querySelector('img')) return; // already populated picker.innerHTML = '';
picker.classList.remove('has-tabs');
// Prefer CommentSystem static cache (filled on any item page in this session) const hasPacks = packs && packs.length > 0;
const cached = (typeof CommentSystem !== 'undefined' && CommentSystem.emojiCache)
|| window.commentSystem?.customEmojis;
if (cached && Object.keys(cached).length) { if (!hasPacks) {
renderEmojisIntoPicker(cached); // Flat list — old behaviour
Object.entries(emojis).forEach(([name, url]) => {
const el = makeDmEmojiEl(url, name);
el.addEventListener('click', (ev) => {
ev.stopPropagation();
const pos = textarea.selectionStart ?? textarea.value.length;
const val = textarea.value;
textarea.value = val.slice(0, pos) + ` :${name}: ` + val.slice(pos);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = pos + name.length + 4;
});
picker.appendChild(el);
});
return; return;
} }
// No cache — fetch directly (happens when /messages is the first page visited) // ── Tabbed picker ──
picker.classList.add('has-tabs');
const tabBar = document.createElement('div');
tabBar.className = 'emoji-picker-tabs';
const gridArea = document.createElement('div');
gridArea.className = 'emoji-picker-grid';
let activeTabId = null;
const showTab = (packId) => {
activeTabId = packId;
tabBar.querySelectorAll('.ep-tab').forEach(t =>
t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))
);
gridArea.innerHTML = '';
const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
if (!pack) return;
pack.emojis.forEach(({ name, url }) => {
const el = makeDmEmojiEl(url, name);
el.addEventListener('click', (ev) => {
ev.stopPropagation();
const pos = textarea.selectionStart ?? textarea.value.length;
const val = textarea.value;
textarea.value = val.slice(0, pos) + `:${name}:` + val.slice(pos);
textarea.focus();
const newPos = pos + name.length + 2;
textarea.setSelectionRange(newPos, newPos);
});
gridArea.appendChild(el);
});
// Autoplay any webm stickers in the grid
gridArea.querySelectorAll('video').forEach(v =>
v.play().catch(() => v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }))
);
};
packs.forEach((pack) => {
const tab = document.createElement('button');
tab.className = 'ep-tab';
tab.dataset.packId = String(pack.id ?? null);
tab.title = pack.name || 'Emojis';
tab.type = 'button';
// Always use static img for tab icon (thumb_url or first non-video emoji)
if (pack.emojis && pack.emojis.length > 0) {
const iconUrl = pack.thumb_url
|| (pack.emojis.find(e => !e.url.endsWith('.webm')) || pack.emojis[0]).url;
const icon = document.createElement('img');
icon.src = iconUrl;
icon.alt = pack.name;
icon.loading = 'lazy';
tab.appendChild(icon);
} else {
tab.textContent = '☺';
}
tab.addEventListener('mousedown', e => e.preventDefault());
tab.addEventListener('click', () => showTab(pack.id ?? null));
tabBar.appendChild(tab);
});
picker.appendChild(tabBar);
picker.appendChild(gridArea);
showTab(packs[0]?.id ?? null);
}
function populatePicker() {
// Only use cache if BOTH emojis and packs are present — otherwise fetch fresh
// so the tab bar is always built from the full /api/v2/emojis response.
const cachedPacks = (typeof CommentSystem !== 'undefined' && CommentSystem.emojiPacks) || null;
const cachedEmojis = (typeof CommentSystem !== 'undefined' && CommentSystem.emojiCache) || null;
if (cachedEmojis && Object.keys(cachedEmojis).length && cachedPacks && cachedPacks.length > 0) {
buildDmPickerContent(cachedEmojis, cachedPacks);
return;
}
// Fetch fresh — ensures packs are always included
picker.innerHTML = '<div style="padding:8px;color:#aaa;font-size:0.82em;">Loading…</div>'; picker.innerHTML = '<div style="padding:8px;color:#aaa;font-size:0.82em;">Loading…</div>';
fetch('/api/v2/emojis') fetch('/api/v2/emojis')
.then(r => r.json()) .then(r => r.json())
@@ -1788,9 +1894,11 @@ if (window.__dmLoaded) {
if (data.success && data.emojis?.length) { if (data.success && data.emojis?.length) {
const map = {}; const map = {};
data.emojis.forEach(e => { map[e.name] = e.url; }); data.emojis.forEach(e => { map[e.name] = e.url; });
// Populate CommentSystem cache for future use if (typeof CommentSystem !== 'undefined') {
if (typeof CommentSystem !== 'undefined') CommentSystem.emojiCache = map; CommentSystem.emojiCache = map;
renderEmojisIntoPicker(map); CommentSystem.emojiPacks = data.packs || [];
}
buildDmPickerContent(map, data.packs || []);
} else { } else {
picker.innerHTML = '<div style="padding:8px;color:#aaa;font-size:0.82em;">No emojis yet</div>'; picker.innerHTML = '<div style="padding:8px;color:#aaa;font-size:0.82em;">No emojis yet</div>';
} }
@@ -1807,6 +1915,9 @@ if (window.__dmLoaded) {
if (isHidden) { if (isHidden) {
populatePicker(); populatePicker();
picker.style.display = ''; picker.style.display = '';
requestAnimationFrame(() => requestAnimationFrame(() =>
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
));
closeHandler = (ev) => { closeHandler = (ev) => {
if (!picker.contains(ev.target) && ev.target !== trigger) { if (!picker.contains(ev.target) && ev.target !== trigger) {
picker.style.display = 'none'; picker.style.display = 'none';
@@ -1821,8 +1932,10 @@ if (window.__dmLoaded) {
} }
}); });
// Ensure the emoji picker is updated when emojis are ready // Refresh picker when emojis are ready (e.g. first page load)
window.addEventListener('f0ck:emojis_ready', populatePicker); window.addEventListener('f0ck:emojis_ready', () => {
if (picker.style.display !== 'none') populatePicker();
});
} }
function setupSendForm() { function setupSendForm() {
@@ -1908,7 +2021,9 @@ if (window.__dmLoaded) {
renderedIds.add(res.id); renderedIds.add(res.id);
if (res.id > latestMsgId) latestMsgId = res.id; if (res.id > latestMsgId) latestMsgId = res.id;
const msg = { id: res.id, sender_id: myId, plaintext: text, created_at: res.created_at || new Date().toISOString() }; const msg = { id: res.id, sender_id: myId, plaintext: text, created_at: res.created_at || new Date().toISOString() };
thread.appendChild(buildMessageBubble(msg)); const sentBubble = buildMessageBubble(msg);
thread.appendChild(sentBubble);
playDmEmojiVideos(sentBubble);
snapToBottom(thread, true); // Force scroll for your own sent message snapToBottom(thread, true); // Force scroll for your own sent message
} }
} else { } else {
@@ -2429,7 +2544,9 @@ if (window.__dmLoaded) {
// Re-render everyone // Re-render everyone
for (const m of threadMessages) { for (const m of threadMessages) {
renderedIds.add(m.id); renderedIds.add(m.id);
thread.appendChild(buildMessageBubble(m)); const bubble = buildMessageBubble(m);
thread.appendChild(bubble);
playDmEmojiVideos(bubble);
} }
if (isAtBottom) snapToBottom(thread, true); if (isAtBottom) snapToBottom(thread, true);