telegram stickers v1

This commit is contained in:
2026-07-15 22:56:26 +02:00
parent afc776d948
commit a7c811025e
7 changed files with 1065 additions and 168 deletions

View File

@@ -5638,6 +5638,15 @@ body[type='login'] {
margin-top: 5px;
}
/* Tabbed mode — switched to column layout by JS when packs are present */
.emoji-picker.has-tabs {
max-height: 200px;
overflow: hidden;
flex-direction: column;
flex-wrap: nowrap;
padding: 0;
}
.emoji-picker img {
width: 60px;
height: 60px;
@@ -5653,6 +5662,95 @@ body[type='login'] {
background: rgba(255, 255, 255, 0.1);
}
/* ── Tabbed picker ── */
.emoji-picker-tabs {
display: flex;
align-items: center;
gap: 0;
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
background: rgba(0,0,0,0.25);
border-bottom: 1px solid rgba(255,255,255,0.07);
scrollbar-width: none;
padding: 0 2px;
}
.emoji-picker-tabs::-webkit-scrollbar { display: none; }
.ep-tab {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border: none;
background: transparent;
cursor: pointer;
opacity: 0.55;
border-radius: 6px;
padding: 4px;
}
.ep-tab:hover { opacity: 1; background: rgba(255,255,255,0.07); }
.ep-tab.active {
opacity: 1;
background: rgba(255,255,255,0.12);
border-bottom: 2px solid var(--accent, #fff);
}
.ep-tab img,
.ep-tab video {
width: 28px;
height: 28px;
object-fit: contain;
pointer-events: none;
}
.emoji-picker-grid {
display: flex;
flex-wrap: wrap;
gap: 0;
overflow-y: auto;
flex: 1;
padding: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
.emoji-picker-grid img,
.emoji-picker-grid video {
width: 52px;
height: 52px;
object-fit: contain;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}
.emoji-picker-grid img:hover,
.emoji-picker-grid video:hover {
background: rgba(255, 255, 255, 0.1);
}
/* video emojis in comment content */
.comment-content video.emoji,
video.emoji {
height: 200px;
vertical-align: middle;
display: inline-block;
}
/* video in emoji autocomplete dropdown */
.emoji-ac-item video {
width: 24px;
height: 24px;
object-fit: contain;
flex-shrink: 0;
}
/* User Mentions */
.mention {
text-decoration: none !important;
@@ -13902,7 +14000,8 @@ body.layout-modern .xd-score-wrapper {
transform: translateY(-2px);
}
.emoji-card .emoji-preview {
.emoji-card .emoji-preview,
.emoji-card video.emoji-preview {
height: 80px;
max-width: 90px;
object-fit: contain;

View File

@@ -263,7 +263,9 @@ class CommentSystem {
this.customEmojis[e.name] = e.url;
});
CommentSystem.emojiCache = this.customEmojis;
_f0ckDebug('Loaded Emojis:', this.customEmojis);
// Store pack structure for tabbed picker
CommentSystem.emojiPacks = data.packs || [];
_f0ckDebug('Loaded Emojis:', this.customEmojis, 'Packs:', CommentSystem.emojiPacks);
if (this.container && this.lastData) {
const state = this.saveState();
@@ -304,7 +306,11 @@ class CommentSystem {
renderEmoji(match, name) {
// _f0ckDebug('Rendering Emoji:', name, this.customEmojis ? this.customEmojis[name] : 'No list');
if (this.customEmojis && this.customEmojis[name]) {
return `<img src="${this.customEmojis[name]}" style="height:60px;vertical-align:middle;" alt="${name}">`;
const url = this.customEmojis[name];
if (url.endsWith('.webm')) {
return `<video src="${url}" style="height:60px;vertical-align:middle;" autoplay loop muted playsinline title=":${name}:"></video>`;
}
return `<img src="${url}" style="height:60px;vertical-align:middle;" alt=":${name}:">`;
}
return match;
}
@@ -559,6 +565,7 @@ class CommentSystem {
contentEl.dataset.raw = this.escapeHtml(fullContent);
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
CommentSystem.autoplayConvertedGifs(contentEl);
CommentSystem.playEmojiVideos(contentEl);
} catch (e) {
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
}
@@ -583,6 +590,7 @@ class CommentSystem {
if (contentEl) {
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
CommentSystem.autoplayConvertedGifs(contentEl);
CommentSystem.playEmojiVideos(contentEl);
// Flash effect to draw attention
el.classList.remove('new-item-fade');
@@ -1296,6 +1304,7 @@ class CommentSystem {
}
this.syncSubscribeButton(isSubscribed);
CommentSystem.autoplayConvertedGifs(this.container);
CommentSystem.playEmojiVideos(this.container);
// Attach media load listeners to re-stabilize scroll if a hash is active.
// Only during the initial anchor scroll — never on subsequent renders (tab re-focus,
@@ -1415,6 +1424,7 @@ class CommentSystem {
_f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`);
contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id);
CommentSystem.autoplayConvertedGifs(contentEl);
CommentSystem.playEmojiVideos(contentEl);
contentEl.dataset.raw = incoming.content;
}
@@ -1929,6 +1939,15 @@ class CommentSystem {
});
}
static playEmojiVideos(container) {
if (!container) return;
container.querySelectorAll('video.emoji').forEach(v => {
v.play().catch(() => {
v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true });
});
});
}
buildBacklinkMap(comments) {
this.backlinkMap = {};
const process = (c) => {
@@ -1959,7 +1978,11 @@ class CommentSystem {
renderEmoji(match, name) {
if (this.customEmojis && this.customEmojis[name]) {
return `<img src="${this.customEmojis[name]}" class="emoji" alt="${match}" title="${match}">`;
const url = this.customEmojis[name];
if (url.endsWith('.webm')) {
return `<video src="${url}" class="emoji" autoplay loop muted playsinline title=":${name}:"></video>`;
}
return `<img src="${url}" class="emoji" alt="${match}" title="${match}">`;
}
return match;
}
@@ -3383,6 +3406,7 @@ class CommentSystem {
const newEl = tmp.firstElementChild;
if (newEl) {
parent.replaceChild(newEl, existingReply);
CommentSystem.playEmojiVideos(newEl);
requestAnimationFrame(() => {
newEl.classList.add('comment-entering', 'new-item-fade');
newEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -3404,6 +3428,7 @@ class CommentSystem {
if (commentEl) {
repliesEl.appendChild(commentEl);
this._ensureTruncationButton(commentEl, newComment.content);
CommentSystem.playEmojiVideos(commentEl);
requestAnimationFrame(() => {
commentEl.classList.add('comment-entering', 'new-item-fade');
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -3421,6 +3446,7 @@ class CommentSystem {
const newEl = tmp.firstElementChild;
if (newEl) {
parent.replaceChild(newEl, existingTop);
CommentSystem.playEmojiVideos(newEl);
requestAnimationFrame(() => {
newEl.classList.add('comment-entering', 'new-item-fade');
newEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -3442,6 +3468,7 @@ class CommentSystem {
list.appendChild(commentEl);
}
this._ensureTruncationButton(commentEl, newComment.content);
CommentSystem.playEmojiVideos(commentEl);
requestAnimationFrame(() => {
commentEl.classList.add('comment-entering', 'new-item-fade');
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -3586,6 +3613,7 @@ class CommentSystem {
const cs = window.commentSystem;
if (!cs) return;
CommentSystem.emojiCache = null;
CommentSystem.emojiPacks = null;
CommentSystem.loadingEmojis = false;
cs.loadEmojis(true); // force=true: bypass page-scan, admin just changed emojis
});
@@ -3797,13 +3825,24 @@ class CommentSystem {
const item = document.createElement('div');
item.className = 'emoji-ac-item';
item.dataset.idx = idx;
const img = document.createElement('img');
img.src = this.customEmojis[name];
img.alt = name;
img.loading = 'lazy';
const url = this.customEmojis[name];
let preview;
if (url && url.endsWith('.webm')) {
preview = document.createElement('video');
preview.src = url;
preview.autoplay = true;
preview.loop = true;
preview.muted = true;
preview.playsInline = true;
} else {
preview = document.createElement('img');
preview.src = url;
preview.alt = name;
preview.loading = 'lazy';
}
const label = document.createElement('span');
label.textContent = `:${name}:`;
item.appendChild(img);
item.appendChild(preview);
item.appendChild(label);
item.addEventListener('mousedown', ev => {
ev.preventDefault(); // don't blur textarea
@@ -3978,35 +4017,120 @@ class CommentSystem {
const buildPickerContent = () => {
if (!picker) return;
picker.innerHTML = '';
if (this.customEmojis && Object.keys(this.customEmojis).length > 0) {
const packs = CommentSystem.emojiPacks || [];
const hasPacks = packs.length > 0;
if (!this.customEmojis || Object.keys(this.customEmojis).length === 0) {
picker.innerHTML = '<div style="padding:8px 10px;color:rgba(255,255,255,0.5);font-size:0.8em;">No emojis found</div>';
return;
}
// Helper: create img or video element for a given emoji URL
const makeEmojiEl = (url, name) => {
let el;
if (url && url.endsWith('.webm')) {
el = document.createElement('video');
el.src = url;
el.autoplay = true;
el.loop = true;
el.muted = true;
el.playsInline = true;
} else {
el = document.createElement('img');
el.src = url;
el.loading = 'lazy';
}
el.title = `:${name}:`;
el.onerror = () => { el.style.display = 'none'; };
return el;
};
if (!hasPacks) {
// Flat list — old behaviour for sites without packs
picker.classList.remove('has-tabs');
Object.keys(this.customEmojis).forEach(name => {
const url = this.customEmojis[name];
const img = document.createElement('img');
img.src = url;
img.title = `:${name}:`;
img.loading = 'lazy'; // Use native lazy loading
// Add error handling for failed loads
img.onerror = () => {
console.warn(`Failed to load emoji: ${name}`);
img.style.display = 'none';
};
img.onclick = (ev) => {
const el = makeEmojiEl(url, name);
el.onclick = (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();
// Move cursor after the inserted emoji
const newPos = pos + name.length + 2;
textarea.setSelectionRange(newPos, newPos);
};
picker.appendChild(img);
picker.appendChild(el);
});
} else {
picker.innerHTML = '<div style="padding:5px;color:white;font-size:0.8em;">No emojis found</div>';
return;
}
// ── Tabbed picker with sticker packs ──
picker.classList.add('has-tabs');
// Tab bar
const tabBar = document.createElement('div');
tabBar.className = 'emoji-picker-tabs';
// Emoji grid area
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 = makeEmojiEl(url, name);
el.onclick = (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);
});
};
// Build tab buttons
packs.forEach((pack, i) => {
const tab = document.createElement('button');
tab.className = 'ep-tab';
tab.dataset.packId = String(pack.id ?? null);
tab.title = pack.name || 'Emojis';
tab.type = 'button';
// Tab icon — always use a static img (thumb_url from import, 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);
// Show first pack by default
showTab(packs[0]?.id ?? null);
};
trigger.addEventListener('click', (e) => {