favorites tab for emojis
This commit is contained in:
@@ -4069,10 +4069,33 @@ class CommentSystem {
|
|||||||
toggle(name, url) {
|
toggle(name, url) {
|
||||||
const favs = this.get();
|
const favs = this.get();
|
||||||
const idx = favs.findIndex(f => f.name === name);
|
const idx = favs.findIndex(f => f.name === name);
|
||||||
|
const action = idx >= 0 ? 'remove' : 'add';
|
||||||
if (idx >= 0) favs.splice(idx, 1); else favs.push({ name, url });
|
if (idx >= 0) favs.splice(idx, 1); else favs.push({ name, url });
|
||||||
localStorage.setItem(this.KEY, JSON.stringify(favs));
|
localStorage.setItem(this.KEY, JSON.stringify(favs));
|
||||||
|
// Sync to DB for logged-in users (fire-and-forget)
|
||||||
|
const csrf = window.f0ckSession?.csrf_token;
|
||||||
|
if (csrf) {
|
||||||
|
fetch('/api/v2/user/emoji-favorites/toggle', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
body: JSON.stringify({ name, url, action })
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncFromDB() {
|
||||||
|
fetch('/api/v2/user/emoji-favorites', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(data => {
|
||||||
|
if (data?.success && Array.isArray(data.favorites)) {
|
||||||
|
localStorage.setItem(this.KEY, JSON.stringify(data.favorites));
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Seed from DB on first open if logged in
|
||||||
|
if (window.f0ckSession?.id) {
|
||||||
|
window._emojiPickerFavUtils.syncFromDB();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!window._favContextMenu) {
|
if (!window._favContextMenu) {
|
||||||
const menu = document.createElement('div');
|
const menu = document.createElement('div');
|
||||||
@@ -4135,21 +4158,40 @@ class CommentSystem {
|
|||||||
badge.className = 'fa-solid fa-star ep-fav-badge';
|
badge.className = 'fa-solid fa-star ep-fav-badge';
|
||||||
el.appendChild(badge);
|
el.appendChild(badge);
|
||||||
}
|
}
|
||||||
// Hold-to-preview
|
// Hold-to-preview (400ms) + Long-hold-to-favorite (700ms, mobile-friendly)
|
||||||
let holdTimer = null;
|
let holdTimer = null;
|
||||||
|
let favHoldTimer = null;
|
||||||
let holdFired = false;
|
let holdFired = false;
|
||||||
|
let favHoldFired = false;
|
||||||
const startHold = () => {
|
const startHold = () => {
|
||||||
holdFired = false;
|
holdFired = false;
|
||||||
|
favHoldFired = false;
|
||||||
holdTimer = setTimeout(() => {
|
holdTimer = setTimeout(() => {
|
||||||
holdFired = true;
|
holdFired = true;
|
||||||
window.stickerPreview?.show(url, isVideo);
|
window.stickerPreview?.show(url, isVideo);
|
||||||
}, 400);
|
}, 400);
|
||||||
|
// 700ms: transition from preview to favorites menu (mobile long-press)
|
||||||
|
favHoldTimer = setTimeout(() => {
|
||||||
|
if (holdFired) {
|
||||||
|
window.stickerPreview?.hide();
|
||||||
|
holdFired = false;
|
||||||
|
favHoldFired = true;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
window._favContextMenu?.show(
|
||||||
|
rect.left + rect.width / 2, rect.top,
|
||||||
|
name, url,
|
||||||
|
() => showTab(activeTabId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 700);
|
||||||
};
|
};
|
||||||
const cancelHold = () => { clearTimeout(holdTimer); };
|
const cancelHold = () => { clearTimeout(holdTimer); clearTimeout(favHoldTimer); };
|
||||||
const endHold = () => {
|
const endHold = () => {
|
||||||
clearTimeout(holdTimer);
|
clearTimeout(holdTimer);
|
||||||
if (holdFired) {
|
clearTimeout(favHoldTimer);
|
||||||
|
if (holdFired || favHoldFired) {
|
||||||
holdFired = false;
|
holdFired = false;
|
||||||
|
favHoldFired = false;
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener('click', (e) => {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -4166,12 +4208,13 @@ class CommentSystem {
|
|||||||
media.addEventListener('touchstart', startHold, { passive: true });
|
media.addEventListener('touchstart', startHold, { passive: true });
|
||||||
media.addEventListener('touchend', endHold);
|
media.addEventListener('touchend', endHold);
|
||||||
media.addEventListener('touchcancel', endHold);
|
media.addEventListener('touchcancel', endHold);
|
||||||
// Right-click → favorites context menu
|
// Right-click (desktop) → favorites; suppressed on mobile when hold gesture is active
|
||||||
media.addEventListener('contextmenu', (e) => {
|
media.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (holdFired || favHoldFired) return;
|
||||||
window._favContextMenu?.show(e.clientX, e.clientY, name, url, () => {
|
window._favContextMenu?.show(e.clientX, e.clientY, name, url, () => {
|
||||||
showTab(activeTabId); // refresh to update star badges
|
showTab(activeTabId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return el;
|
return el;
|
||||||
|
|||||||
@@ -430,6 +430,60 @@ export default (router, tpl) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── User Emoji Favorites ─────────────────────────────────────────
|
||||||
|
// Create table lazily on first use (idempotent)
|
||||||
|
const ensureFavTable = async () => {
|
||||||
|
await db`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_emoji_favorites (
|
||||||
|
user_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||||
|
emoji_name text NOT NULL,
|
||||||
|
emoji_url text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, emoji_name)
|
||||||
|
)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET /api/v2/user/emoji-favorites — returns logged-in user's favorites
|
||||||
|
router.get('/api/v2/user/emoji-favorites', lib.loggedin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureFavTable();
|
||||||
|
const rows = await db`
|
||||||
|
SELECT emoji_name AS name, emoji_url AS url
|
||||||
|
FROM user_emoji_favorites
|
||||||
|
WHERE user_id = ${req.session.id}
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`;
|
||||||
|
return res.reply({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, favorites: rows }) });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[EMOJI FAVS GET]', e);
|
||||||
|
return res.reply({ code: 500, body: JSON.stringify({ success: false }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/v2/user/emoji-favorites/toggle — add or remove a single favorite
|
||||||
|
router.post('/api/v2/user/emoji-favorites/toggle', lib.loggedin, async (req, res) => {
|
||||||
|
const csrfToken = req.headers['x-csrf-token'];
|
||||||
|
if (!req.session.csrf_token || !csrfToken || csrfToken !== req.session.csrf_token) {
|
||||||
|
return res.reply({ code: 403, body: JSON.stringify({ success: false, message: 'Invalid CSRF token' }) });
|
||||||
|
}
|
||||||
|
let body = {};
|
||||||
|
try { const raw = req.body || req.post || {}; body = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch (_) {}
|
||||||
|
const { name, url, action } = body;
|
||||||
|
if (!name || !url) return res.reply({ code: 400, body: JSON.stringify({ success: false, message: 'name and url required' }) });
|
||||||
|
try {
|
||||||
|
await ensureFavTable();
|
||||||
|
if (action === 'remove') {
|
||||||
|
await db`DELETE FROM user_emoji_favorites WHERE user_id = ${req.session.id} AND emoji_name = ${name}`;
|
||||||
|
} else {
|
||||||
|
await db`INSERT INTO user_emoji_favorites (user_id, emoji_name, emoji_url) VALUES (${req.session.id}, ${name}, ${url}) ON CONFLICT (user_id, emoji_name) DO NOTHING`;
|
||||||
|
}
|
||||||
|
return res.reply({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true }) });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[EMOJI FAVS TOGGLE]', e);
|
||||||
|
return res.reply({ code: 500, body: JSON.stringify({ success: false }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Edit emoji (Admin only) — handled by bypass middleware in index.mjs
|
// Edit emoji (Admin only) — handled by bypass middleware in index.mjs
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
Reference in New Issue
Block a user