From b4a104f8c6d78c55e899fc13d50bcd5ca7d09f12 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sun, 30 Aug 2026 19:02:21 -0600 Subject: [PATCH] Move custom emoji management out of the emoji picker The picker's delete "x" overlaps the glyph in a tightly packed grid, which is too easy to hit by accident on a touch screen. Adding and deleting custom emoji now live in their own modal under the account menu, with delete gated behind the same confirm() every other destructive action in the app uses. Co-Authored-By: Claude Sonnet 5 --- .../src/components/CustomEmojiManageModal.tsx | 99 +++++++++++++++++++ frontend/src/components/EmojiPicker.css | 46 --------- frontend/src/components/EmojiPicker.tsx | 86 ++++------------ frontend/src/components/TopBar.tsx | 13 +++ 4 files changed, 130 insertions(+), 114 deletions(-) create mode 100644 frontend/src/components/CustomEmojiManageModal.tsx diff --git a/frontend/src/components/CustomEmojiManageModal.tsx b/frontend/src/components/CustomEmojiManageModal.tsx new file mode 100644 index 0000000..dd60200 --- /dev/null +++ b/frontend/src/components/CustomEmojiManageModal.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react' +import { deleteCustomEmoji } from '../api/customEmoji' +import { useAuth } from '../context/AuthContext' +import { useCustomEmoji } from '../context/CustomEmojiContext' +import type { CustomEmoji } from '../types' +import { CustomEmojiUploadModal } from './CustomEmojiUploadModal' +import { EmojiGlyph } from './MessageContent' +import './Modal.css' + +interface CustomEmojiManageModalProps { + onClose: () => void +} + +// Moved out of the reaction/composer emoji picker -- that grid packs items +// 9-to-a-row with a delete "x" overlapping the glyph itself, which on a +// touch screen is far too easy to hit by accident while just trying to +// react. A dedicated list with a normal-sized "Delete" button (plus the +// same confirm() every other destructive action in this app uses) needs a +// deliberate tap to actually delete something. +export function CustomEmojiManageModal({ onClose }: CustomEmojiManageModalProps) { + const { user } = useAuth() + const { list, refresh } = useCustomEmoji() + const [uploadOpen, setUploadOpen] = useState(false) + const [deletingId, setDeletingId] = useState(null) + + async function handleDelete(emoji: CustomEmoji) { + if (!confirm(`Delete :${emoji.shortcode}:? This can't be undone.`)) return + setDeletingId(emoji.id) + try { + await deleteCustomEmoji(emoji.id) + await refresh() + } finally { + setDeletingId(null) + } + } + + return ( +
+
e.stopPropagation()}> +
+

Custom emoji

+ +
+ +
Site emoji
+ {list.length === 0 ? ( +

No custom emoji yet.

+ ) : ( + list.map((emoji) => { + const canDelete = user?.id === emoji.uploaded_by || user?.is_site_admin + return ( +
+
+
+ :{emoji.shortcode}: +
+
+ Added {new Date(emoji.created_at).toLocaleDateString()} +
+
+ {canDelete && ( + + )} +
+ ) + }) + )} + +
+ + +
+
+ + {uploadOpen && ( + setUploadOpen(false)} + onUploaded={() => { + refresh() + setUploadOpen(false) + }} + /> + )} +
+ ) +} diff --git a/frontend/src/components/EmojiPicker.css b/frontend/src/components/EmojiPicker.css index b872a31..f60f9fe 100644 --- a/frontend/src/components/EmojiPicker.css +++ b/frontend/src/components/EmojiPicker.css @@ -69,26 +69,6 @@ padding: 4px 4px 2px; } -.emoji-picker-category-label-row { - display: flex; - align-items: center; - justify-content: space-between; -} - -.emoji-picker-add-custom { - background: transparent; - border: none; - color: var(--ds-accent); - font-size: 0.7rem; - font-weight: 600; - cursor: pointer; - padding: 2px 4px; -} - -.emoji-picker-add-custom:hover { - text-decoration: underline; -} - .emoji-picker-grid { display: grid; grid-template-columns: repeat(9, 1fr); @@ -109,32 +89,6 @@ background: var(--ds-surface-2); } -.emoji-picker-item-custom { - position: relative; -} - -.emoji-picker-item-remove { - position: absolute; - top: -2px; - right: -2px; - display: flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--ds-danger); - color: white; - font-size: 0.65rem; - line-height: 1; - opacity: 0; - cursor: pointer; -} - -.emoji-picker-item-custom:hover .emoji-picker-item-remove { - opacity: 1; -} - /* The picker is positioned absolutely relative to its trigger button, which can sit close enough to a narrow viewport's edge that the full 320px width runs off-screen (e.g. the composer's emoji trigger, near the left diff --git a/frontend/src/components/EmojiPicker.tsx b/frontend/src/components/EmojiPicker.tsx index 6e503a4..cb426b7 100644 --- a/frontend/src/components/EmojiPicker.tsx +++ b/frontend/src/components/EmojiPicker.tsx @@ -1,12 +1,9 @@ -import { useMemo, useState, type MouseEvent } from 'react' -import { deleteCustomEmoji } from '../api/customEmoji' -import { useAuth } from '../context/AuthContext' +import { useMemo, useState } from 'react' import { useCustomEmoji } from '../context/CustomEmojiContext' import { useEscapeKey } from '../hooks/useEscapeKey' import { ALL_EMOJI, EMOJI_CATEGORIES } from '../lib/emoji' import { EMOJI_NAMES } from '../lib/emojiNames' import { getRecentEmoji, recordEmojiUsed } from '../lib/recentEmoji' -import { CustomEmojiUploadModal } from './CustomEmojiUploadModal' import { EmojiGlyph } from './MessageContent' import './EmojiPicker.css' @@ -55,11 +52,8 @@ function searchEmoji(query: string, customShortcodes: string[]): string[] { export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'left' }: EmojiPickerProps) { useEscapeKey(onClose) - const { user } = useAuth() - const { list: customEmoji, refresh: refreshCustomEmoji } = useCustomEmoji() + const { list: customEmoji } = useCustomEmoji() const [query, setQuery] = useState('') - const [uploadOpen, setUploadOpen] = useState(false) - const [deletingId, setDeletingId] = useState(null) const customShortcodes = useMemo(() => customEmoji.map((e) => e.shortcode), [customEmoji]) const searchResults = useMemo(() => searchEmoji(query, customShortcodes), [query, customShortcodes]) const searching = query.trim().length > 0 @@ -74,18 +68,6 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef onPick(emoji) } - async function handleDeleteCustomEmoji(e: MouseEvent, emojiId: string) { - // Delete, not pick -- must never bubble to the button's own onClick. - e.stopPropagation() - setDeletingId(emojiId) - try { - await deleteCustomEmoji(emojiId) - await refreshCustomEmoji() - } finally { - setDeletingId(null) - } - } - return ( <>
@@ -122,48 +104,25 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef ) ) : ( <> -
-
+ {customEmoji.length > 0 && ( +
Custom
- -
- {customEmoji.length > 0 && (
- {customEmoji.map((e) => { - const canDelete = user?.id === e.uploaded_by || user?.is_site_admin - return ( - - ) - })} + {customEmoji.map((e) => ( + + ))}
- )} -
+
+ )} {recent.length > 0 && (
Recently used
@@ -205,15 +164,6 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef )}
- {uploadOpen && ( - setUploadOpen(false)} - onUploaded={() => { - refreshCustomEmoji() - setUploadOpen(false) - }} - /> - )} ) } diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index 22f523c..ecfdadd 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -13,6 +13,7 @@ import { } from '../lib/desktopBridge' import { getPushSubscriptionStatus, isPushSupported, subscribeToPush, unsubscribeFromPush } from '../lib/push' import { AboutModal } from './AboutModal' +import { CustomEmojiManageModal } from './CustomEmojiManageModal' import { ProfileModal } from './ProfileModal' import { UserAvatar } from './UserAvatar' import './TopBar.css' @@ -28,6 +29,7 @@ export function TopBar() { const navigate = useNavigate() const [menuOpen, setMenuOpen] = useState(false) const [profileModalOpen, setProfileModalOpen] = useState(false) + const [customEmojiModalOpen, setCustomEmojiModalOpen] = useState(false) const [aboutModalOpen, setAboutModalOpen] = useState(false) const [pushSubscribed, setPushSubscribed] = useState(false) const [pushBusy, setPushBusy] = useState(false) @@ -124,6 +126,16 @@ export function TopBar() { > Profile settings +