mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
feat(desktop): replace hardcoded emoji pickers with full emoji-mart picker (#122)
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -34,6 +36,7 @@
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.1.0",
|
||||
"react-diff-view": "^3.3.2",
|
||||
|
||||
30
desktop/pnpm-lock.yaml
generated
30
desktop/pnpm-lock.yaml
generated
@@ -8,6 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@emoji-mart/data':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@emoji-mart/react':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(emoji-mart@5.6.0)(react@19.2.4)
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -50,6 +56,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
emoji-mart:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@19.2.4)
|
||||
@@ -260,6 +269,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@emoji-mart/data@1.2.1':
|
||||
resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
|
||||
|
||||
'@emoji-mart/react@1.1.1':
|
||||
resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==}
|
||||
peerDependencies:
|
||||
emoji-mart: ^5.2
|
||||
react: ^16.8 || ^17 || ^18
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1281,6 +1299,9 @@ packages:
|
||||
electron-to-chromium@1.5.307:
|
||||
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
|
||||
|
||||
emoji-mart@5.6.0:
|
||||
resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2142,6 +2163,13 @@ snapshots:
|
||||
'@biomejs/cli-win32-x64@2.4.6':
|
||||
optional: true
|
||||
|
||||
'@emoji-mart/data@1.2.1': {}
|
||||
|
||||
'@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.4)':
|
||||
dependencies:
|
||||
emoji-mart: 5.6.0
|
||||
react: 19.2.4
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
@@ -2984,6 +3012,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.307: {}
|
||||
|
||||
emoji-mart@5.6.0: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Picker from "@emoji-mart/react";
|
||||
import data from "@emoji-mart/data";
|
||||
import { SmilePlus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { DEFAULT_EMOJI_OPTIONS } from "./messageTimelineUtils";
|
||||
|
||||
type ComposerEmojiPickerProps = {
|
||||
disabled?: boolean;
|
||||
@@ -37,36 +38,20 @@ export function ComposerEmojiPicker({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-56 rounded-2xl p-3"
|
||||
className="w-auto p-0 rounded-2xl overflow-hidden border-0 bg-transparent shadow-none"
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
Emoji
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Insert an emoji into your message.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{DEFAULT_EMOJI_OPTIONS.map((emoji) => (
|
||||
<button
|
||||
aria-label={`Insert ${emoji}`}
|
||||
className="flex h-10 items-center justify-center rounded-xl border border-border/70 bg-muted/40 text-lg transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-testid="composer-emoji-option"
|
||||
key={emoji}
|
||||
onClick={() => {
|
||||
onEmojiSelect(emoji);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={(emoji: any) => onEmojiSelect(emoji.native)}
|
||||
theme="auto"
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
set="native"
|
||||
maxFrequentRows={2}
|
||||
perLine={8}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Picker from "@emoji-mart/react";
|
||||
import data from "@emoji-mart/data";
|
||||
import { CornerUpLeft, LoaderCircle, SmilePlus } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -8,7 +10,6 @@ import type {
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { getReactionOptions } from "./messageTimelineUtils";
|
||||
|
||||
export function MessageActionBar({
|
||||
activeReplyTargetId = null,
|
||||
@@ -39,7 +40,6 @@ export function MessageActionBar({
|
||||
const selectedReactionCount = reactions.filter(
|
||||
(reaction) => reaction.reactedByCurrentUser,
|
||||
).length;
|
||||
const reactionOptions = getReactionOptions(reactions);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -84,70 +84,39 @@ export function MessageActionBar({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-56 rounded-2xl p-3"
|
||||
className="w-auto p-0 rounded-2xl overflow-hidden border-0 bg-transparent shadow-none"
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||
React
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs",
|
||||
reactionErrorMessage
|
||||
? "text-destructive"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{reactionErrorMessage ??
|
||||
"Click any emoji. Click it again to remove your own reaction."}
|
||||
{reactionErrorMessage ? (
|
||||
<div className="px-3 pt-3 pb-0">
|
||||
<p className="text-xs text-destructive">
|
||||
{reactionErrorMessage}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{reactionOptions.map((emoji) => {
|
||||
const isActive = reactions.some(
|
||||
(reaction) =>
|
||||
reaction.emoji === emoji &&
|
||||
reaction.reactedByCurrentUser,
|
||||
);
|
||||
) : null}
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={(emoji: any) => {
|
||||
if (!onReactionSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={`React with ${emoji}`}
|
||||
aria-pressed={isActive}
|
||||
className={cn(
|
||||
"flex h-10 items-center justify-center rounded-xl border text-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
isActive
|
||||
? "border-primary/40 bg-primary/10"
|
||||
: "border-border/70 bg-muted/40 hover:bg-accent",
|
||||
)}
|
||||
data-emoji={emoji}
|
||||
data-testid={`react-option-${message.id}`}
|
||||
disabled={reactionPending}
|
||||
key={`${message.id}-${emoji}`}
|
||||
onClick={() => {
|
||||
if (!onReactionSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
void onReactionSelect(emoji)
|
||||
.then(() => {
|
||||
setIsReactionPickerOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
return;
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
void onReactionSelect(emoji.native)
|
||||
.then(() => {
|
||||
setIsReactionPickerOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
return;
|
||||
});
|
||||
}}
|
||||
theme="auto"
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
set="native"
|
||||
maxFrequentRows={2}
|
||||
perLine={8}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null}
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import type { TimelineReaction } from "@/features/messages/types";
|
||||
|
||||
const BOTTOM_THRESHOLD_PX = 72;
|
||||
export const DEFAULT_EMOJI_OPTIONS = [
|
||||
"👍",
|
||||
"❤️",
|
||||
"🎉",
|
||||
"🚀",
|
||||
"👀",
|
||||
"✅",
|
||||
"🔥",
|
||||
"👎",
|
||||
];
|
||||
|
||||
export function isNearBottom(container: HTMLDivElement) {
|
||||
return (
|
||||
@@ -18,28 +6,3 @@ export function isNearBottom(container: HTMLDivElement) {
|
||||
BOTTOM_THRESHOLD_PX
|
||||
);
|
||||
}
|
||||
|
||||
export function getReactionOptions(reactions: TimelineReaction[]) {
|
||||
const seen = new Set<string>();
|
||||
const options: string[] = [];
|
||||
|
||||
for (const reaction of reactions) {
|
||||
if (seen.has(reaction.emoji)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(reaction.emoji);
|
||||
options.push(reaction.emoji);
|
||||
}
|
||||
|
||||
for (const emoji of DEFAULT_EMOJI_OPTIONS) {
|
||||
if (seen.has(emoji)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(emoji);
|
||||
options.push(emoji);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,13 @@ test("emoji picker inserts emoji into the draft and keeps focus in the composer"
|
||||
await input.fill("Ship");
|
||||
|
||||
await page.getByTestId("composer-emoji-button").click();
|
||||
await page.getByRole("button", { name: "Insert 🚀" }).click();
|
||||
|
||||
// emoji-mart renders inside a Shadow DOM web component — use the search
|
||||
// input to find the rocket emoji, then click it.
|
||||
const pickerEl = page.locator("em-emoji-picker");
|
||||
const searchInput = pickerEl.locator("input[type='search']");
|
||||
await searchInput.fill("rocket");
|
||||
await pickerEl.locator("button[aria-label='🚀']").first().click();
|
||||
|
||||
await expect(input).toHaveValue("Ship🚀");
|
||||
await expect(input).toBeFocused();
|
||||
|
||||
Reference in New Issue
Block a user