From ed5f29824c1e3225d53c613912587f05efdfbb2e Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:45:45 +0200 Subject: [PATCH] feat: chat step 3, emoji search --- apps/chat/package.json | 1 + apps/chat/src/index.ts | 45 ++++- .../components/app/ChatPanel/ChatPanel.tsx | 60 +++++- .../components/app/ChatPanel/EmojiSearch.tsx | 178 ++++++++++++++++++ yarn.lock | 5 + 5 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/app/ChatPanel/EmojiSearch.tsx diff --git a/apps/chat/package.json b/apps/chat/package.json index b7c90a6..704906c 100644 --- a/apps/chat/package.json +++ b/apps/chat/package.json @@ -12,6 +12,7 @@ "@hctv/hono-ws": "*", "@hono/node-server": "^1.14.0", "@hono/node-ws": "^1.1.0", + "@leeoniya/ufuzzy": "^1.0.18", "@oslojs/encoding": "^1.1.0", "hono": "^4.7.5" }, diff --git a/apps/chat/src/index.ts b/apps/chat/src/index.ts index 1b566b9..b8c5b1e 100644 --- a/apps/chat/src/index.ts +++ b/apps/chat/src/index.ts @@ -6,10 +6,13 @@ import { lucia } from '@hctv/auth'; import { getCookie } from 'hono/cookie'; import { getPersonalChannel } from './utils/personalChannel.js'; import { getRedisConnection, prisma } from '@hctv/db'; +import uFuzzy from '@leeoniya/ufuzzy'; +const redis = getRedisConnection(); const MESSAGE_HISTORY_SIZE = 15; const MESSAGE_TTL = 60 * 60 * 24; const threed = await readFile('./src/3d.txt', 'utf-8'); +const uf = new uFuzzy(); const app = new Hono(); const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); @@ -103,7 +106,6 @@ app.get( }); }, async onMessage(evt, ws) { - const redis = getRedisConnection(); const msg = JSON.parse(evt.data.toString()); if (msg.type === 'ping') { ws.send( @@ -141,7 +143,6 @@ app.get( ws.wss.clients.forEach((c) => { const client = c as ModifiedWebSocket; if (client.readyState === client.OPEN && client.targetUsername === ws.targetUsername) { - console.log('Sending message to client:', msgStr); c.send(msgStr); } }); @@ -172,6 +173,46 @@ app.get( }) ); } + if (msg.type === 'emojiSearch') { + console.log('emoji search request:', msg); + const searchTerm = msg.searchTerm as string; + + const emojis = await redis.hgetall('emojis'); + const emojiKeys = Object.keys(emojis); + const idxs = uf.filter(emojiKeys, searchTerm); + console.log(`Emoji search for "${searchTerm}" found ${idxs?.length || 0} results.`); + + if (idxs && idxs.length > 0) { + const results: string[] = []; + + if (idxs.length <= 150) { + const info = uf.info(idxs, emojiKeys, searchTerm); + const order = uf.sort(info, emojiKeys, searchTerm); + for (let i = 0; i < order.length && i < 10; i++) { + results.push(emojiKeys[idxs[order[i]]]); + } + } else { + for (let i = 0; i < idxs.length && i < 10; i++) { + results.push(emojiKeys[idxs[i]]); + } + } + + ws.send( + JSON.stringify({ + type: 'emojiSearchResponse', + results: results, + }) + ); + console.log(`Sending emoji search results: ${results.join(', ')}`); + } else { + ws.send( + JSON.stringify({ + type: 'emojiSearchResponse', + results: [], + }) + ); + } + } }, })) ); diff --git a/apps/web/src/components/app/ChatPanel/ChatPanel.tsx b/apps/web/src/components/app/ChatPanel/ChatPanel.tsx index 60fb5cb..6d454b0 100644 --- a/apps/web/src/components/app/ChatPanel/ChatPanel.tsx +++ b/apps/web/src/components/app/ChatPanel/ChatPanel.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'; import { useParams } from 'next/navigation'; import { Message } from './message'; import { useMap } from '@uidotdev/usehooks'; +import { EmojiSearch } from './EmojiSearch'; export default function ChatPanel() { const { username } = useParams(); @@ -16,6 +17,8 @@ export default function ChatPanel() { const socketRef = useRef(null); const emojiMap = useMap() as Map; const [emojisToReq, setEmojisToReq] = useState([]); + const [cursorPosition, setCursorPosition] = useState(0); + const textareaRef = useRef(null); useEffect(() => { console.log('Initializing WebSocket connection for user:', username); @@ -213,6 +216,39 @@ export default function ChatPanel() { } }, [emojisToReq, emojiMap, username]); + const handleEmojiSelect = (emojiName: string) => { + if (!textareaRef.current) return; + + const textarea = textareaRef.current; + const beforeCursor = message.substring(0, cursorPosition); + const afterCursor = message.substring(cursorPosition); + + const match = beforeCursor.match(/:[\w\-+]*$/); + if (!match) return; + + const startPos = beforeCursor.lastIndexOf(match[0]); + const newBeforeCursor = beforeCursor.substring(0, startPos); + + const newMessage = `${newBeforeCursor}:${emojiName}: ${afterCursor}`; + setMessage(newMessage); + + // 3 for colons and space + const newCursorPos = newBeforeCursor.length + emojiName.length + 3; + + setTimeout(() => { + textarea.focus(); + textarea.selectionStart = newCursorPos; + textarea.selectionEnd = newCursorPos; + setCursorPosition(newCursorPos); + }, 0); + }; + + const isEmojiSearchOpen = () => { + const beforeCursor = message.substring(0, cursorPosition); + const match = beforeCursor.match(/:[\w\-+]*$/); + return match !== null; + }; + return (
@@ -228,17 +264,27 @@ export default function ChatPanel() { ))}
-
+