From 38a43a3a998a562ec66db776081343621cc7f99f Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Wed, 13 May 2026 04:59:13 +1000 Subject: [PATCH] feat: Add emoji usage tracking hook and integrate with EmojiBoard for usage statistics --- src/app/components/emoji-board/EmojiBoard.tsx | 26 ++++- src/app/hooks/useEmojiUsage.ts | 104 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/app/hooks/useEmojiUsage.ts diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 3db27e2..bc358f6 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -22,6 +22,7 @@ import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard import { useRelevantImagePacks } from '../../hooks/useImagePacks'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRecentEmoji } from '../../hooks/useRecentEmoji'; +import { useEmojiUsage } from '../../hooks/useEmojiUsage'; import { isUserId, mxcUrlToHttp } from '../../utils/matrix'; import { editableActiveElement, targetFromEvent } from '../../utils/dom'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; @@ -378,13 +379,30 @@ export function EmojiBoard({ addToRecentEmoji = true, }: EmojiBoardProps) { const mx = useMatrixClient(); + const { trackEmojiUsage, getMostUsedEmoji } = useEmojiUsage(); const emojiTab = tab === EmojiBoardTab.Emoji; const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; + // Get default emoji - use most used or fallback to slight_smile + const defaultPreview = useMemo((): PreviewData => { + if (!emojiTab) return undefined; + + const mostUsedShortcode = getMostUsedEmoji(); + if (mostUsedShortcode) { + // Find emoji by shortcode + const emoji = emojis.find((e) => e.shortcode === mostUsedShortcode); + if (emoji) { + return { key: emoji.unicode, shortcode: emoji.shortcode }; + } + } + // Fallback to default + return DefaultEmojiPreview; + }, [emojiTab, getMostUsedEmoji]); + const previewAtom = useMemo( - () => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined), - [emojiTab] + () => createPreviewDataAtom(defaultPreview), + [defaultPreview] ); const activeGroupIdAtom = useMemo(() => atom(undefined), []); const setActiveGroupId = useSetAtom(activeGroupIdAtom); @@ -439,6 +457,10 @@ export function EmojiBoard({ onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode); if (!evt.altKey && !evt.shiftKey && addToRecentEmoji) { addRecentEmoji(mx, emojiInfo.data); + // Track emoji usage for favorites + trackEmojiUsage(emojiInfo.shortcode).catch((err) => { + console.error('Failed to track emoji usage:', err); + }); } } if (emojiInfo.type === EmojiType.CustomEmoji) { diff --git a/src/app/hooks/useEmojiUsage.ts b/src/app/hooks/useEmojiUsage.ts new file mode 100644 index 0000000..ddc2538 --- /dev/null +++ b/src/app/hooks/useEmojiUsage.ts @@ -0,0 +1,104 @@ +import { useCallback, useMemo } from 'react'; +import { useMatrixClient } from './useMatrixClient'; +import { useAccountData } from './useAccountData'; + +const PAARROT_EMOJI_USAGE_EVENT = 'paarrot.favemojis'; +const MAX_TRACKED_EMOJIS = 50; + +export type EmojiUsageData = { + [shortcode: string]: { + count: number; + lastUsed: number; + }; +}; + +/** + * Hook for tracking emoji usage and storing favorites in Matrix account data + * Stores emoji usage stats under paarrot.favemojis in user's account data + */ +export function useEmojiUsage() { + const mx = useMatrixClient(); + const event = useAccountData(PAARROT_EMOJI_USAGE_EVENT); + + const emojiUsage = useMemo((): EmojiUsageData => { + const content = event?.getContent(); + return content || {}; + }, [event]); + + /** + * Track emoji usage (max 1 per message as per spec) + * @param shortcode - Emoji shortcode (e.g., 'thumbsup', 'smile') + */ + const trackEmojiUsage = useCallback( + async (shortcode: string) => { + const now = Date.now(); + const currentUsage = { ...emojiUsage }; + + // Update or create emoji entry + currentUsage[shortcode] = { + count: (currentUsage[shortcode]?.count || 0) + 1, + lastUsed: now, + }; + + // Keep only top MAX_TRACKED_EMOJIS by usage count + const entries = Object.entries(currentUsage); + if (entries.length > MAX_TRACKED_EMOJIS) { + entries.sort((a, b) => b[1].count - a[1].count); + const topEmojis = entries.slice(0, MAX_TRACKED_EMOJIS); + const newUsage: EmojiUsageData = {}; + topEmojis.forEach(([key, value]) => { + newUsage[key] = value; + }); + await mx.setAccountData(PAARROT_EMOJI_USAGE_EVENT, newUsage); + } else { + await mx.setAccountData(PAARROT_EMOJI_USAGE_EVENT, currentUsage); + } + }, + [mx, emojiUsage] + ); + + /** + * Get most used emoji shortcode + * @returns Shortcode of most used emoji, or undefined if none tracked + */ + const getMostUsedEmoji = useCallback((): string | undefined => { + const entries = Object.entries(emojiUsage); + if (entries.length === 0) return undefined; + + // Sort by count descending, then by lastUsed descending + entries.sort((a, b) => { + if (b[1].count !== a[1].count) { + return b[1].count - a[1].count; + } + return b[1].lastUsed - a[1].lastUsed; + }); + + return entries[0][0]; + }, [emojiUsage]); + + /** + * Get top N most used emojis + * @param count - Number of top emojis to return + * @returns Array of shortcodes sorted by usage + */ + const getTopEmojis = useCallback( + (count: number = 10): string[] => { + const entries = Object.entries(emojiUsage); + entries.sort((a, b) => { + if (b[1].count !== a[1].count) { + return b[1].count - a[1].count; + } + return b[1].lastUsed - a[1].lastUsed; + }); + return entries.slice(0, count).map(([shortcode]) => shortcode); + }, + [emojiUsage] + ); + + return { + emojiUsage, + trackEmojiUsage, + getMostUsedEmoji, + getTopEmojis, + }; +}