feat: Add emoji usage tracking hook and integrate with EmojiBoard for usage statistics
This commit is contained in:
@@ -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<string | undefined>(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) {
|
||||
|
||||
104
src/app/hooks/useEmojiUsage.ts
Normal file
104
src/app/hooks/useEmojiUsage.ts
Normal file
@@ -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<EmojiUsageData>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user