feat(gif-picker): add gif groups
Some checks failed
/ build-web (push) Successful in 8m40s
/ build-mobile (push) Failing after 11m17s
/ build-desktop (linux) (push) Successful in 14m8s
/ release (push) Has been skipped

feat(gif-picker): increase default size
This commit is contained in:
Alois 2026-08-11 13:01:09 +02:00
commit 6f4cc62530
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
11 changed files with 399 additions and 82 deletions

View file

@ -0,0 +1,29 @@
import { Button } from "@methanium/ui";
import { Emoji } from "@methanium/ui/markdown";
import { getRecentEmojis, useEmojiRanks } from "./emojiRanks";
export default function EmojiPicker({
onSelect,
}: {
onSelect: (emoji: string) => void;
}) {
const { ranks } = useEmojiRanks();
const emojis = getRecentEmojis(ranks, 3);
return (
<div className="flex flex-row gap-1 p-1">
{emojis.map((emoji) => (
<Button
key={emoji}
aria-label={`Select ${emoji}`}
className="h-10 w-10 p-0"
onClick={() => onSelect(emoji)}
type="button"
variant="ghost"
>
<Emoji shortcode={emoji} />
</Button>
))}
</div>
);
}

View file

@ -0,0 +1,95 @@
import { useStorage } from "@tensamin/storage/context";
import { useCallback, useEffect, useState } from "react";
import { normalizeShortcode } from "@methanium/ui/markdown";
const RANKS_CHANGED_EVENT = "tensamin-reaction-ranks-changed";
let recordQueue = Promise.resolve();
function normalizeRanks(ranks: Record<string, number>) {
return Object.entries(ranks).reduce<Record<string, number>>(
(normalized, [value, frequency]) => {
const shortcode = normalizeShortcode(value);
if (!shortcode || !Number.isFinite(frequency) || frequency <= 0) {
return normalized;
}
normalized[shortcode] = (normalized[shortcode] ?? 0) + frequency;
return normalized;
},
{},
);
}
function rankedEmojis(ranks: Record<string, number>) {
return Object.entries(ranks)
.filter(
([emoji, frequency]) =>
normalizeShortcode(emoji) !== undefined &&
Number.isFinite(frequency) &&
frequency > 0,
)
.sort(([emojiA, frequencyA], [emojiB, frequencyB]) =>
frequencyB === frequencyA
? emojiA.localeCompare(emojiB)
: frequencyB - frequencyA,
)
.flatMap(([emoji]) => {
const shortcode = normalizeShortcode(emoji);
return shortcode ? [shortcode] : [];
});
}
export function getRecentEmojis(ranks: Record<string, number>, amount: number) {
return rankedEmojis(ranks).slice(0, amount);
}
export function useEmojiRanks() {
const { load, save } = useStorage();
const [ranks, setRanks] = useState<Record<string, number>>({});
useEffect(() => {
void load("reactions").then((storedRanks) => {
const normalized = normalizeRanks(storedRanks);
setRanks(normalized);
if (JSON.stringify(normalized) !== JSON.stringify(storedRanks)) {
void save("reactions", normalized);
}
});
function handleRanksChanged(event: Event) {
setRanks((event as CustomEvent<Record<string, number>>).detail);
}
window.addEventListener(RANKS_CHANGED_EVENT, handleRanksChanged);
return () =>
window.removeEventListener(RANKS_CHANGED_EVENT, handleRanksChanged);
}, [load, save]);
return { ranks };
}
export function useRecordEmojiUse() {
const { load, save } = useStorage();
return useCallback(
(emoji: string) => {
const shortcode = normalizeShortcode(emoji);
if (!shortcode) return;
recordQueue = recordQueue
.catch(() => undefined)
.then(async () => {
const normalized = normalizeRanks(await load("reactions"));
const next = {
...normalized,
[shortcode]: (normalized[shortcode] ?? 0) + 1,
};
await save("reactions", next);
window.dispatchEvent(
new CustomEvent(RANKS_CHANGED_EVENT, { detail: next }),
);
});
},
[load, save],
);
}