Skip to content
This repository was archived by the owner on Apr 23, 2026. It is now read-only.

Commit ee09cac

Browse files
committed
fix: emoji and profile suggestion UX/UI fixes
1 parent 15fe8b6 commit ee09cac

7 files changed

Lines changed: 318 additions & 67 deletions

src/components/chat/EmojiPickerDialog.tsx

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ function updateReactionHistory(emoji: string): void {
4646
/**
4747
* EmojiPickerDialog - Searchable emoji picker for reactions
4848
*
49+
* Layout: top (recently used) emojis → search bar → scrollable list
50+
* This keeps the dialog close button away from the search input.
51+
*
4952
* Features:
53+
* - Recently used emojis shown as quick-pick buttons at the top
5054
* - Real-time search using FlexSearch with scrollable virtualized results
51-
* - Frequently used emoji at top when no search query
5255
* - Supports both unicode and NIP-30 custom emoji
5356
* - Keyboard navigation (arrow keys, enter, escape)
5457
* - Tracks usage in localStorage
@@ -94,14 +97,31 @@ export function EmojiPickerDialog({
9497
}
9598
}, [open]);
9699

97-
// Get frequently used emojis from history
100+
// Get frequently used emojis from history (sorted by use count)
98101
const frequentlyUsed = useMemo(() => {
99102
const history = getReactionHistory();
100103
return Object.entries(history)
101104
.sort((a, b) => b[1] - a[1])
102105
.map(([emoji]) => emoji);
103106
}, []);
104107

108+
// Resolve top 8 recently used emojis to EmojiSearchResult for rendering
109+
const topEmojis = useMemo<EmojiSearchResult[]>(() => {
110+
if (frequentlyUsed.length === 0) return [];
111+
const results: EmojiSearchResult[] = [];
112+
for (const emojiStr of frequentlyUsed.slice(0, 8)) {
113+
if (emojiStr.startsWith(":") && emojiStr.endsWith(":")) {
114+
const shortcode = emojiStr.slice(1, -1);
115+
const custom = service.getByShortcode(shortcode);
116+
if (custom) results.push(custom);
117+
} else {
118+
const found = searchResults.find((r) => r.url === emojiStr);
119+
if (found) results.push(found);
120+
}
121+
}
122+
return results;
123+
}, [frequentlyUsed, searchResults, service]);
124+
105125
// When no search query: show recently used first, then fill with search results
106126
// When searching: show search results only
107127
const displayEmojis = useMemo(() => {
@@ -216,7 +236,7 @@ export function EmojiPickerDialog({
216236
/>
217237
)}
218238
</span>
219-
<span className="truncate text-sm text-popover-foreground/80">
239+
<span className="truncate text-sm text-popover-foreground">
220240
:{item.shortcode}:
221241
</span>
222242
</button>
@@ -227,26 +247,64 @@ export function EmojiPickerDialog({
227247

228248
return (
229249
<Dialog open={open} onOpenChange={onOpenChange}>
230-
<DialogContent className="max-w-xs p-4 gap-2">
231-
{/* Search input */}
232-
<div className="relative">
233-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 size-4 text-muted-foreground" />
234-
<Input
235-
type="text"
236-
placeholder="Search emojis..."
237-
value={searchQuery}
238-
onChange={(e) => setSearchQuery(e.target.value)}
239-
onKeyDown={handleKeyDown}
240-
className="pl-9"
241-
autoFocus
242-
/>
250+
<DialogContent className="max-w-xs p-0 gap-0 overflow-hidden">
251+
{/* Top emojis — recently used quick-picks.
252+
This section also provides natural spacing for the dialog close (X) button,
253+
which is absolutely positioned at top-right of the dialog. */}
254+
<div className="flex items-center gap-1 px-3 pt-3 pb-2 pr-10 min-h-[48px]">
255+
{topEmojis.length > 0 ? (
256+
<div
257+
className="flex items-center gap-1 overflow-x-auto [&::-webkit-scrollbar]:hidden"
258+
style={{ scrollbarWidth: "none" }}
259+
>
260+
{topEmojis.map((emoji) => (
261+
<button
262+
key={emoji.shortcode}
263+
onClick={() => handleEmojiClick(emoji)}
264+
title={`:${emoji.shortcode}:`}
265+
className="flex size-8 items-center justify-center rounded hover:bg-muted/60 transition-colors flex-shrink-0"
266+
>
267+
{emoji.source === "unicode" ? (
268+
<span className="text-lg leading-none">{emoji.url}</span>
269+
) : (
270+
<img
271+
src={emoji.url}
272+
alt={`:${emoji.shortcode}:`}
273+
className="size-6 object-contain"
274+
loading="lazy"
275+
/>
276+
)}
277+
</button>
278+
))}
279+
</div>
280+
) : (
281+
<span className="text-xs font-medium text-muted-foreground select-none">
282+
Emoji
283+
</span>
284+
)}
285+
</div>
286+
287+
{/* Search bar */}
288+
<div className="px-3 pb-2">
289+
<div className="relative">
290+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
291+
<Input
292+
type="text"
293+
placeholder="Search emojis..."
294+
value={searchQuery}
295+
onChange={(e) => setSearchQuery(e.target.value)}
296+
onKeyDown={handleKeyDown}
297+
className="pl-9"
298+
autoFocus
299+
/>
300+
</div>
243301
</div>
244302

245303
{/* Scrollable emoji list */}
246304
{displayEmojis.length > 0 ? (
247305
<div
248306
role="listbox"
249-
className="rounded-md border border-border/50 bg-popover text-popover-foreground overflow-hidden"
307+
className="border-t border-border/50 bg-popover text-popover-foreground"
250308
>
251309
<Virtuoso
252310
ref={virtuosoRef}
@@ -257,11 +315,12 @@ export function EmojiPickerDialog({
257315
overflow:
258316
displayEmojis.length <= MAX_VISIBLE ? "hidden" : "auto",
259317
}}
318+
className="[&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/60 [&::-webkit-scrollbar-track]:bg-transparent"
260319
itemContent={renderItem}
261320
/>
262321
</div>
263322
) : (
264-
<div className="flex items-center justify-center py-6 text-sm text-muted-foreground">
323+
<div className="flex items-center justify-center py-6 text-sm text-muted-foreground border-t border-border/50">
265324
No emojis found
266325
</div>
267326
)}

src/components/editor/EmojiSuggestionList.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const EmojiSuggestionList = forwardRef<
103103
/>
104104
)}
105105
</span>
106-
<span className="truncate text-sm text-popover-foreground/80">
106+
<span className="truncate text-sm text-popover-foreground">
107107
:{item.shortcode}:
108108
</span>
109109
</button>
@@ -112,23 +112,14 @@ export const EmojiSuggestionList = forwardRef<
112112
[items, selectedIndex, command],
113113
);
114114

115-
if (items.length === 0) {
116-
return (
117-
<div className="rounded-md border border-border/50 bg-popover p-4 text-sm text-popover-foreground/60 shadow-md">
118-
No emoji found
119-
</div>
120-
);
121-
}
122-
123-
const listHeight = Math.max(
124-
Math.min(items.length, MAX_VISIBLE) * ITEM_HEIGHT,
125-
ITEM_HEIGHT + 8,
126-
);
115+
if (items.length === 0) return null;
116+
117+
const listHeight = Math.min(items.length, MAX_VISIBLE) * ITEM_HEIGHT;
127118

128119
return (
129120
<div
130121
role="listbox"
131-
className="w-[260px] rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md overflow-hidden"
122+
className="w-[260px] max-w-full rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md overflow-hidden"
132123
>
133124
<Virtuoso
134125
ref={virtuosoRef}

src/components/editor/ProfileSuggestionList.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const ProfileSuggestionList = forwardRef<
106106
<UserName pubkey={item.pubkey} />
107107
</div>
108108
{item.nip05 && (
109-
<div className="truncate text-xs text-popover-foreground/60">
109+
<div className="truncate text-xs text-muted-foreground">
110110
{item.nip05}
111111
</div>
112112
)}
@@ -117,23 +117,14 @@ export const ProfileSuggestionList = forwardRef<
117117
[items, selectedIndex, command],
118118
);
119119

120-
if (items.length === 0) {
121-
return (
122-
<div className="rounded-md border border-border/50 bg-popover p-4 text-sm text-popover-foreground/60 shadow-md">
123-
No profiles found
124-
</div>
125-
);
126-
}
127-
128-
const listHeight = Math.max(
129-
Math.min(items.length, MAX_VISIBLE) * ITEM_HEIGHT,
130-
ITEM_HEIGHT + 8,
131-
);
120+
if (items.length === 0) return null;
121+
122+
const listHeight = Math.min(items.length, MAX_VISIBLE) * ITEM_HEIGHT;
132123

133124
return (
134125
<div
135126
role="listbox"
136-
className="w-[320px] rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md overflow-hidden"
127+
className="w-[320px] max-w-full rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md overflow-hidden"
137128
>
138129
<Virtuoso
139130
ref={virtuosoRef}

src/components/editor/SlashCommandSuggestionList.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,13 @@ export const SlashCommandSuggestionList = forwardRef<
6969
setSelectedIndex(0);
7070
}, [items]);
7171

72-
if (items.length === 0) {
73-
return (
74-
<div className="border border-border/50 bg-popover p-4 text-sm text-popover-foreground/60 shadow-md">
75-
No commands available
76-
</div>
77-
);
78-
}
72+
if (items.length === 0) return null;
7973

8074
return (
8175
<div
8276
ref={listRef}
8377
role="listbox"
84-
className="max-h-[300px] w-full max-w-[320px] overflow-y-auto border border-border/50 bg-popover text-popover-foreground shadow-md"
78+
className="max-h-[300px] w-full max-w-[320px] overflow-y-auto rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md"
8579
>
8680
{items.map((item, index) => (
8781
<button
@@ -94,12 +88,12 @@ export const SlashCommandSuggestionList = forwardRef<
9488
index === selectedIndex ? "bg-muted/60" : "hover:bg-muted/60"
9589
}`}
9690
>
97-
<Terminal className="size-5 md:size-4 flex-shrink-0 text-popover-foreground/60" />
91+
<Terminal className="size-5 md:size-4 flex-shrink-0 text-muted-foreground" />
9892
<div className="min-w-0 flex-1">
9993
<div className="truncate text-sm font-medium font-mono">
10094
/{item.name}
10195
</div>
102-
<div className="truncate text-xs text-popover-foreground/60">
96+
<div className="truncate text-xs text-muted-foreground">
10397
{item.description}
10498
</div>
10599
</div>

src/components/editor/SuggestionPopover.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ export function SuggestionPopover({
8181
}, [clientRect, refs, update]);
8282

8383
return createPortal(
84-
<div ref={refs.setFloating} style={{ ...floatingStyles, zIndex: 50 }}>
84+
<div
85+
ref={refs.setFloating}
86+
style={{ ...floatingStyles, zIndex: 50, maxWidth: "calc(100vw - 16px)" }}
87+
>
8588
{children}
8689
</div>,
8790
document.body,

0 commit comments

Comments
 (0)