diff --git a/inventory-server/chat/routes.js b/inventory-server/chat/routes.js index b0d366a..9cbba9c 100644 --- a/inventory-server/chat/routes.js +++ b/inventory-server/chat/routes.js @@ -132,18 +132,124 @@ router.get('/files/by-id/:mongoId', async (req, res) => { } }); -// Serve avatars +// Serve user avatars by mongo_id +router.get('/avatar/:mongoId', async (req, res) => { + try { + const { mongoId } = req.params; + + console.log(`[Avatar Debug] Looking up avatar for user mongo_id: ${mongoId}`); + + // First try to find avatar by user's avataretag + const userResult = await global.pool.query(` + SELECT avataretag, username FROM users WHERE mongo_id = $1 + `, [mongoId]); + + let avatarPath = null; + + if (userResult.rows.length > 0) { + const username = userResult.rows[0].username; + const avataretag = userResult.rows[0].avataretag; + + // Try method 1: Look up by avataretag -> etag (for users with avataretag set) + if (avataretag) { + console.log(`[Avatar Debug] Found user ${username} with avataretag: ${avataretag}`); + + const avatarResult = await global.pool.query(` + SELECT url, path FROM avatars WHERE etag = $1 + `, [avataretag]); + + if (avatarResult.rows.length > 0) { + const dbPath = avatarResult.rows[0].path || avatarResult.rows[0].url; + console.log(`[Avatar Debug] Found avatar record with path: ${dbPath}`); + + if (dbPath) { + const pathParts = dbPath.split('/'); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i].includes('AmazonS3:Avatars') && i + 1 < pathParts.length) { + const avatarMongoId = pathParts[i + 1]; + avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', avatarMongoId); + console.log(`[Avatar Debug] Extracted avatar mongo_id: ${avatarMongoId}, full path: ${avatarPath}`); + break; + } + } + } + } else { + console.log(`[Avatar Debug] No avatar record found for etag: ${avataretag}`); + } + } + + // Try method 2: Look up by userid directly (for users without avataretag) + if (!avatarPath) { + console.log(`[Avatar Debug] Trying direct userid lookup for user ${username} (${mongoId})`); + + const avatarResult = await global.pool.query(` + SELECT url, path FROM avatars WHERE userid = $1 + `, [mongoId]); + + if (avatarResult.rows.length > 0) { + const dbPath = avatarResult.rows[0].path || avatarResult.rows[0].url; + console.log(`[Avatar Debug] Found avatar record by userid with path: ${dbPath}`); + + if (dbPath) { + const pathParts = dbPath.split('/'); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i].includes('AmazonS3:Avatars') && i + 1 < pathParts.length) { + const avatarMongoId = pathParts[i + 1]; + avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', avatarMongoId); + console.log(`[Avatar Debug] Extracted avatar mongo_id: ${avatarMongoId}, full path: ${avatarPath}`); + break; + } + } + } + } else { + console.log(`[Avatar Debug] No avatar record found for userid: ${mongoId}`); + } + } + } else { + console.log(`[Avatar Debug] No user found for mongo_id: ${mongoId}`); + } + + // Fallback: try direct lookup by user mongo_id + if (!avatarPath) { + avatarPath = path.join(__dirname, 'db-convert/db/files/avatars', mongoId); + console.log(`[Avatar Debug] Using fallback path: ${avatarPath}`); + } + + // Set proper content type for images + res.set('Content-Type', 'image/jpeg'); // Most avatars are likely JPEG + + // Send the file + res.sendFile(avatarPath, (err) => { + if (err) { + // If avatar doesn't exist, send a default 404 or generate initials + console.log(`[Avatar Debug] Avatar file not found at path: ${avatarPath}, error:`, err.message); + if (!res.headersSent) { + res.status(404).json({ error: 'Avatar not found' }); + } + } else { + console.log(`[Avatar Debug] Successfully served avatar from: ${avatarPath}`); + } + }); + + } catch (error) { + console.error('Error serving avatar:', error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Serve avatars statically as fallback router.use('/files/avatars', express.static(path.join(__dirname, 'db-convert/db/files/avatars'))); -// Get all active users for the "view as" dropdown +// Get all users for the "view as" dropdown (active and inactive) router.get('/users', async (req, res) => { try { const result = await global.pool.query(` SELECT id, username, name, type, active, status, lastlogin, - statustext, utcoffset, statusconnection + statustext, utcoffset, statusconnection, mongo_id, avataretag FROM users - WHERE active = true AND type = 'user' + WHERE type = 'user' ORDER BY + active DESC, -- Active users first CASE WHEN status = 'online' THEN 1 WHEN status = 'away' THEN 2 @@ -188,6 +294,7 @@ router.get('/users/:userId/rooms', async (req, res) => { const currentUsername = userResult.rows[0].username; // Get rooms where the user is a member with proper naming from subscription table + // Include archived and closed rooms but sort them at the bottom const result = await global.pool.query(` SELECT DISTINCT r.id, @@ -201,6 +308,9 @@ router.get('/users/:userId/rooms', async (req, res) => { r.uids, r.userscount, r.description, + r.teamid, + r.archived, + s.open, -- Use the subscription's name for direct messages (excludes current user) -- For channels/groups, use room's fname or name CASE @@ -210,15 +320,32 @@ router.get('/users/:userId/rooms', async (req, res) => { FROM room r JOIN subscription s ON s.rid = r.mongo_id WHERE s.u->>'_id' = $1 - AND r.archived IS NOT TRUE - AND s.open = true - ORDER BY r.lm DESC NULLS LAST + ORDER BY + s.open DESC NULLS LAST, -- Open rooms first + r.archived NULLS FIRST, -- Non-archived first (nulls treated as false) + r.lm DESC NULLS LAST LIMIT 50 `, [currentUserMongoId]); + + // Enhance rooms with participant information for direct messages + const enhancedRooms = await Promise.all(result.rows.map(async (room) => { + if (room.type === 'd' && room.uids) { + // Get participant info (excluding current user) for direct messages + const participantResult = await global.pool.query(` + SELECT u.username, u.name, u.mongo_id, u.avataretag + FROM users u + WHERE u.mongo_id = ANY($1::text[]) + AND u.mongo_id != $2 + `, [room.uids, currentUserMongoId]); + + room.participants = participantResult.rows; + } + return room; + })); res.json({ status: 'success', - rooms: result.rows + rooms: enhancedRooms }); } catch (error) { console.error('Error fetching user rooms:', error); @@ -233,11 +360,12 @@ router.get('/users/:userId/rooms', async (req, res) => { // Get room details including participants router.get('/rooms/:roomId', async (req, res) => { const { roomId } = req.params; + const { userId } = req.query; // Accept current user ID as query parameter try { const result = await global.pool.query(` SELECT r.id, r.name, r.fname, r.t as type, r.msgs, r.description, - r.lm as last_message_date, r.usernames, r.uids, r.userscount + r.lm as last_message_date, r.usernames, r.uids, r.userscount, r.teamid FROM room r WHERE r.id = $1 `, [roomId]); @@ -251,8 +379,38 @@ router.get('/rooms/:roomId', async (req, res) => { const room = result.rows[0]; - // Get room participants for direct messages - if (room.type === 'd' && room.uids) { + // For direct messages, get the proper display name based on current user + if (room.type === 'd' && room.uids && userId) { + // Get current user's mongo_id + const userResult = await global.pool.query(` + SELECT mongo_id FROM users WHERE id = $1 + `, [userId]); + + if (userResult.rows.length > 0) { + const currentUserMongoId = userResult.rows[0].mongo_id; + + // Get display name from subscription table for this user + // Use room mongo_id to match with subscription.rid + const roomMongoResult = await global.pool.query(` + SELECT mongo_id FROM room WHERE id = $1 + `, [roomId]); + + if (roomMongoResult.rows.length > 0) { + const roomMongoId = roomMongoResult.rows[0].mongo_id; + + const subscriptionResult = await global.pool.query(` + SELECT fname, name FROM subscription + WHERE rid = $1 AND u->>'_id' = $2 + `, [roomMongoId, currentUserMongoId]); + + if (subscriptionResult.rows.length > 0) { + const sub = subscriptionResult.rows[0]; + room.display_name = sub.fname || sub.name || 'Unknown User'; + } + } + } + + // Get all participants for additional info const participantResult = await global.pool.query(` SELECT username, name FROM users @@ -260,6 +418,9 @@ router.get('/rooms/:roomId', async (req, res) => { `, [room.uids]); room.participants = participantResult.rows; + } else { + // For channels/groups, use room's fname or name + room.display_name = room.fname || room.name || 'Unnamed Room'; } res.json({ diff --git a/inventory/src/components/chat/ChatRoom.tsx b/inventory/src/components/chat/ChatRoom.tsx index aa07a01..9f1f2a9 100644 --- a/inventory/src/components/chat/ChatRoom.tsx +++ b/inventory/src/components/chat/ChatRoom.tsx @@ -2,9 +2,11 @@ import React, { useState, useEffect, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Loader2, Hash, Lock, MessageSquare, ChevronUp, Search, ExternalLink, FileText, Image, Download } from 'lucide-react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Loader2, Hash, Lock, MessageSquare, ChevronUp, Search, ExternalLink, FileText, Image, Download, MessageCircle, Users2 } from 'lucide-react'; import { Input } from '@/components/ui/input'; import config from '@/config'; +import { convertEmojiShortcodes } from '@/utils/emojiUtils'; interface Message { id: number; @@ -44,6 +46,7 @@ interface Room { last_message_date: string; display_name: string; description?: string; + teamid?: string; participants?: { username: string; name: string }[]; } @@ -72,7 +75,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { const fetchRoom = async () => { try { - const response = await fetch(`${config.chatUrl}/rooms/${roomId}`); + const response = await fetch(`${config.chatUrl}/rooms/${roomId}?userId=${selectedUserId}`); const data = await response.json(); if (data.status === 'success') { @@ -194,12 +197,17 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { } }, [roomId, selectedUserId]); - const getRoomIcon = (roomType: string) => { - switch (roomType) { + const getRoomIcon = (room: Room) => { + switch (room.type) { case 'c': return ; case 'p': - return ; + // Distinguish between teams and discussions based on teamid + if (room.teamid) { + return ; // Teams + } else { + return ; // Discussions + } case 'd': return ; default: @@ -219,6 +227,30 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { } }; + const renderMessageText = (text: string, urls?: any[]) => { + if (!text) return ''; + + // First, convert emoji shortcodes to actual emoji + let processedText = convertEmojiShortcodes(text); + + // Then, handle markdown links [text](url) and convert them to HTML + processedText = processedText.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s\)]+)\)/g, + '$1' + ); + + // If we have URL previews, replace standalone URLs (that aren't already in markdown) with just the preview + if (urls && urls.length > 0) { + urls.forEach((urlData) => { + // Only replace standalone URLs that aren't part of markdown links + const standaloneUrlRegex = new RegExp(`(?; + }; + const renderURLPreviews = (urls: any[]) => { if (!urls || urls.length === 0) return null; @@ -235,11 +267,8 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { rel="noopener noreferrer" className="text-blue-600 hover:underline text-sm break-all" > - {urlData.url} + {urlData.meta?.pageTitle || urlData.url} - {urlData.meta?.pageTitle && ( -
{urlData.meta.pageTitle}
- )} {urlData.meta?.ogDescription && (
{urlData.meta.ogDescription}
)} @@ -251,14 +280,32 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { ); }; - const renderAttachments = (attachments: any[]) => { + const renderAttachments = (attachments: any[]) => { if (!attachments || attachments.length === 0) return null; + // Filter out thumbnail attachments (they're usually lower quality versions) + const filteredAttachments = attachments.filter(attachment => + !attachment.name?.toLowerCase().startsWith('thumb-') + ); + + if (filteredAttachments.length === 0) return null; + return (
- {attachments.map((attachment, index) => { + {filteredAttachments.map((attachment, index) => { const isImage = attachment.typegroup === 'image'; - const filePath = `${config.chatUrl}/files/by-id/${attachment.mongo_id}`; + const filePath = `${config.chatUrl}/files/by-id/${attachment.mongo_id}`; + + const handleDownload = () => { + // Create a temporary anchor element to trigger download + const link = document.createElement('a'); + link.href = filePath; + link.download = attachment.name || 'download'; + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; return (
@@ -277,7 +324,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { )}
- @@ -286,8 +333,9 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { {attachment.name} window.open(filePath, '_blank')} /> )} @@ -301,7 +349,10 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { const renderMentions = (text: string, mentions: any[]) => { if (!mentions || mentions.length === 0) return text; - let renderedText = text; + // First, convert emoji shortcodes to actual emoji + let renderedText = convertEmojiShortcodes(text); + + // Then process mentions mentions.forEach((mention) => { if (mention.username) { const mentionPattern = new RegExp(`@${mention.username}`, 'g'); @@ -325,9 +376,15 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
{!isConsecutive && (
-
- {(message.u.name || message.u.username).charAt(0).toUpperCase()} -
+ + + + {(message.u.name || message.u.username).charAt(0).toUpperCase()} + + {message.u.name || message.u.username} @@ -340,7 +397,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
{message.mentions && message.mentions.length > 0 ? renderMentions(message.msg, message.mentions) - : message.msg + : renderMessageText(message.msg, message.urls) }
{message.urls && renderURLPreviews(message.urls)} @@ -388,15 +445,19 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
- {room && getRoomIcon(room.type)} + {room && getRoomIcon(room)}
- {room?.display_name || room?.fname || room?.name || 'Unnamed Room'} + {room?.type === 'd' + ? `Direct message with ${room?.display_name || 'Unknown User'}` + : room?.display_name || room?.fname || room?.name || 'Unnamed Room' + } - {room?.description && ( + {room?.description && room?.type !== 'd' && (

{room.description}

)} - {room?.participants && room.participants.length > 0 && ( + {/* Only show participants for non-direct messages since DM names are already in the title */} + {room?.participants && room.participants.length > 0 && room?.type !== 'd' && (

{room.participants.map(p => p.name || p.username).join(', ')}

@@ -412,7 +473,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { > - {room?.msgs || 0} messages +
diff --git a/inventory/src/components/chat/RoomList.tsx b/inventory/src/components/chat/RoomList.tsx index b257175..bafa04e 100644 --- a/inventory/src/components/chat/RoomList.tsx +++ b/inventory/src/components/chat/RoomList.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Loader2, Hash, Lock, Users, MessageSquare, Search } from 'lucide-react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Loader2, Hash, Lock, Users, MessageSquare, Search, MessageCircle, Users2 } from 'lucide-react'; import { Input } from '@/components/ui/input'; import config from '@/config'; @@ -15,6 +16,15 @@ interface Room { display_name: string; userscount?: number; description?: string; + teamid?: string; + archived?: boolean; + open?: boolean; + participants?: { + username: string; + name: string; + mongo_id: string; + avataretag?: string; + }[]; } interface RoomListProps { @@ -75,48 +85,119 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL } }, [searchFilter, rooms]); - const getRoomIcon = (roomType: string) => { - switch (roomType) { - case 'c': - return ; - case 'p': - return ; - case 'd': - return ; - default: - return ; - } + const groupRoomsByType = (rooms: Room[]) => { + const teams: Room[] = []; + const discussions: Room[] = []; + const channels: Room[] = []; + const directMessages: Room[] = []; + + rooms.forEach(room => { + switch (room.type) { + case 'p': + if (room.teamid) { + teams.push(room); + } else { + discussions.push(room); + } + break; + case 'c': + channels.push(room); + break; + case 'd': + directMessages.push(room); + break; + default: + channels.push(room); // fallback for unknown types + } + }); + + // Sort each group by message count descending + const sortByMessages = (a: Room, b: Room) => (b.msgs || 0) - (a.msgs || 0); + + teams.sort(sortByMessages); + discussions.sort(sortByMessages); + channels.sort(sortByMessages); + directMessages.sort(sortByMessages); + + return { teams, discussions, channels, directMessages }; }; - const getRoomTypeLabel = (roomType: string) => { - switch (roomType) { - case 'c': - return 'Channel'; - case 'p': - return 'Private'; - case 'd': - return 'Direct'; - default: - return 'Unknown'; + const renderRoomIcon = (room: Room) => { + // For direct messages, show participant avatars + if (room.type === 'd' && room.participants && room.participants.length > 0) { + if (room.participants.length === 1) { + // Single participant - show their avatar + const participant = room.participants[0]; + return ( + + + + {(participant.name || participant.username).charAt(0).toUpperCase()} + + + ); + } else { + // Multiple participants - show overlapping avatars + return ( +
+ {room.participants.slice(0, 3).map((participant, index) => ( + 0 ? '-ml-4' : ''}`} + style={{ zIndex: 30 - index }} + > + + + {(participant.name || participant.username).charAt(0).toUpperCase()} + + + ))} +
+ ); + } } - }; - const formatLastMessageDate = (dateString: string) => { - if (!dateString) return ''; - - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now.getTime() - date.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 1) { - return 'Today'; - } else if (diffDays === 2) { - return 'Yesterday'; - } else if (diffDays <= 7) { - return `${diffDays - 1} days ago`; - } else { - return date.toLocaleDateString(); + // For other room types, use icons + switch (room.type) { + case 'c': + return ( +
+ +
+ ); + case 'p': + // Distinguish between teams and discussions based on teamid + if (room.teamid) { + return ( +
+ +
+ ); // Teams + } else { + return ( +
+ +
+ ); // Discussions + } + case 'd': + return ( +
+ +
+ ); + default: + return ( +
+ +
+ ); } }; @@ -167,16 +248,7 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL return ( - Rooms ({filteredRooms.length}) -
- - setSearchFilter(e.target.value)} - className="pl-8" - /> -
+ Rooms
@@ -188,60 +260,69 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL

) : ( -
- {filteredRooms.map((room) => ( -
onRoomSelect(room.id.toString())} - className={` - flex items-center justify-between p-3 rounded-lg cursor-pointer transition-colors - hover:bg-gray-100 - ${selectedRoomId === room.id.toString() ? 'bg-blue-50 border-l-4 border-blue-500' : ''} - `} - > -
- {getRoomIcon(room.type)} -
-
- {room.display_name || room.fname || room.name || 'Unnamed Room'} +
+ {(() => { + const { teams, discussions, channels, directMessages } = groupRoomsByType(filteredRooms); + + const renderRoomGroup = (title: string, rooms: Room[], icon: React.ReactNode) => { + if (rooms.length === 0) return null; + + return ( +
+
+ {icon} +

+ {title} ({rooms.length}) +

-
- - {getRoomTypeLabel(room.type)} - - {room.userscount && room.userscount > 0 && ( - - {room.userscount} member{room.userscount !== 1 ? 's' : ''} - - )} - {room.msgs > 0 && ( - - {room.msgs} msgs - - )} +
+ {rooms.map((room) => ( +
onRoomSelect(room.id.toString())} + className={` + flex items-center justify-between py-0.5 px-3 rounded-lg cursor-pointer transition-colors + hover:bg-gray-100 + ${selectedRoomId === room.id.toString() ? 'bg-blue-50 border-l-4 border-blue-500' : ''} + ${(room.open === false || room.archived === true) ? 'opacity-60' : ''} + `} + > +
+ {renderRoomIcon(room)} +
+
+
+ {room.display_name || room.fname || room.name || 'Unnamed Room'} +
+ {room.msgs > 0 && ( + + {room.msgs} msgs + + )} +
+ {room.description && ( +
+ {room.description} +
+ )} +
+
+
+ ))}
- {room.description && ( -
- {room.description} -
- )} - {room.last_message_date && ( -
- {formatLastMessageDate(room.last_message_date)} -
- )}
-
-
- ))} + ); + }; + + return ( + <> + {renderRoomGroup('Teams', teams, )} + {renderRoomGroup('Discussions', discussions, )} + {renderRoomGroup('Channels', channels, )} + {renderRoomGroup('Direct Messages', directMessages, )} + + ); + })()}
)}
diff --git a/inventory/src/components/chat/SearchResults.tsx b/inventory/src/components/chat/SearchResults.tsx index 35c0d8f..73c2c34 100644 --- a/inventory/src/components/chat/SearchResults.tsx +++ b/inventory/src/components/chat/SearchResults.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Hash, Lock, MessageSquare, X } from 'lucide-react'; +import { convertEmojiShortcodes } from '@/utils/emojiUtils'; interface SearchResult { id: number; @@ -40,10 +41,13 @@ export function SearchResults({ results, query, onClose, onRoomSelect }: SearchR }; const highlightText = (text: string, query: string) => { - if (!query) return text; + if (!query) return convertEmojiShortcodes(text); + + // First convert emoji shortcodes + const textWithEmoji = convertEmojiShortcodes(text); const regex = new RegExp(`(${query})`, 'gi'); - const parts = text.split(regex); + const parts = textWithEmoji.split(regex); return parts.map((part, index) => regex.test(part) ? ( diff --git a/inventory/src/pages/Chat.tsx b/inventory/src/pages/Chat.tsx index e5466fe..7f9e432 100644 --- a/inventory/src/pages/Chat.tsx +++ b/inventory/src/pages/Chat.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Loader2, Search } from 'lucide-react'; import { RoomList } from '@/components/chat/RoomList'; import { ChatRoom } from '@/components/chat/ChatRoom'; @@ -19,6 +20,8 @@ interface User { lastlogin?: string; statustext?: string; statusconnection?: string; + mongo_id?: string; + avataretag?: string; } interface SearchResult { @@ -186,11 +189,19 @@ export function Chat() { {users.map((user) => (
-
- - {user.name || user.username} -
- + + + + {(user.name || user.username).charAt(0).toUpperCase()} + + + + {user.name || user.username} + {!user.active && (inactive)} +
))} diff --git a/inventory/src/utils/emojiUtils.ts b/inventory/src/utils/emojiUtils.ts new file mode 100644 index 0000000..6133358 --- /dev/null +++ b/inventory/src/utils/emojiUtils.ts @@ -0,0 +1,213 @@ +// Emoji shortcode to Unicode mapping +// Based on common emoji shortcodes used in chat platforms +const emojiMap: Record = { + // Smileys & Emotion + 'joy': '😂', + 'heart_eyes': '😍', + 'sob': '😭', + 'blush': '😊', + 'kissing_heart': '😘', + 'smiling': 'â˜ēī¸', + 'weary': '😩', + 'pensive': '😔', + 'smirk': '😏', + 'grin': '😁', + 'wink': '😉', + 'relieved': '😌', + 'flushed': 'đŸ˜ŗ', + 'cry': 'đŸ˜ĸ', + 'sunglasses': '😎', + 'sweat_smile': '😅', + 'sleeping': '😴', + 'smile': '😄', + 'purple_heart': '💜', + 'broken_heart': '💔', + 'expressionless': '😑', + 'sparkling_heart': '💖', + 'blue_heart': '💙', + 'confused': '😕', + 'stuck_out_tongue_winking_eye': '😜', + 'disappointed': '😞', + 'yum': '😋', + 'neutral_face': '😐', + 'sleepy': 'đŸ˜Ē', + 'cupid': '💘', + 'heartpulse': '💗', + 'revolving_hearts': '💞', + 'speak_no_evil': '🙊', + 'see_no_evil': '🙈', + 'rage': '😡', + 'smiley': '😃', + 'tired_face': 'đŸ˜Ģ', + 'stuck_out_tongue_closed_eyes': '😝', + 'muscle': 'đŸ’Ē', + 'skull': '💀', + 'sunny': 'â˜€ī¸', + 'yellow_heart': '💛', + 'triumph': '😤', + 'new_moon_with_face': '🌚', + 'laughing': '😆', + 'sweat': '😓', + 'heavy_check_mark': 'âœ”ī¸', + 'heart_eyes_cat': 'đŸ˜ģ', + 'grinning': '😀', + 'mask': '😷', + 'green_heart': '💚', + 'persevere': 'đŸ˜Ŗ', + 'heartbeat': '💓', + 'angry': '😠', + 'grimacing': 'đŸ˜Ŧ', + 'gun': 'đŸ”Ģ', + 'thumbsdown': '👎', + 'dancer': '💃', + 'musical_note': 'đŸŽĩ', + 'no_mouth': 'đŸ˜ļ', + 'dizzy': 'đŸ’Ģ', + 'fist': '✊', + 'unamused': '😒', + 'cold_sweat': '😰', + 'gem': '💎', + 'pizza': '🍕', + 'joy_cat': '😹', + 'sun_with_face': '🌞', + + // Hearts + 'heart': 'â¤ī¸', + 'two_hearts': '💕', + 'kiss': '💋', + + // Hand gestures + 'thumbsup': '👍', + 'thumbs_up': '👍', + 'thumbs_down': '👎', + 'ok_hand': '👌', + 'pray': '🙏', + 'raised_hands': '🙌', + 'clap': '👏', + 'point_right': '👉', + 'point_left': '👈', + 'point_up': 'â˜ī¸', + 'point_down': '👇', + 'raised_hand': '✋', + 'wave': '👋', + 'v': 'âœŒī¸', + 'oncoming_fist': '👊', + 'facepunch': '👊', + 'punch': '👊', + + // Objects & symbols + 'fire': 'đŸ”Ĩ', + 'tada': '🎉', + 'camera': '📷', + 'notes': 'đŸŽļ', + 'sparkles': '✨', + 'star2': '🌟', + 'crown': '👑', + 'headphones': '🎧', + 'white_check_mark': '✅', + 'arrow_right': 'âžĄī¸', + 'arrow_left': 'âŦ…ī¸', + 'arrow_forward': 'â–ļī¸', + 'arrow_backward': 'â—€ī¸', + 'arrow_right_hook': 'â†Ēī¸', + 'leftwards_arrow_with_hook': 'â†Šī¸', + 'red_circle': '🔴', + 'boom': 'đŸ’Ĩ', + 'collision': 'đŸ’Ĩ', + 'copyright': 'ÂŠī¸', + 'thought_balloon': '💭', + 'recycle': 'â™ģī¸', + + // Nature + 'cherry_blossom': '🌸', + 'rose': '🌹', + 'scream': '😱', + + // Body parts + 'eyes': '👀', + 'tongue': '👅', + + // Misc + 'poop': '💩', + 'poo': '💩', + 'shit': '💩', + 'hankey': '💩', + 'innocent': '😇', + 'kissing_closed_eyes': '😚', + 'stuck_out_tongue': '😛', + 'disappointed_relieved': 'đŸ˜Ĩ', + 'confounded': '😖', + 'raising_hand': '🙋', + 'no_good': '🙅', + 'ok_woman': '🙆', + 'information_desk_person': '💁', + 'man_tipping_hand': 'đŸ’â€â™‚ī¸', + 'woman_tipping_hand': 'đŸ’â€â™€ī¸', + 'man_gesturing_no': 'đŸ™…â€â™‚ī¸', + 'woman_gesturing_no': 'đŸ™…â€â™€ī¸', + 'man_gesturing_ok': 'đŸ™†â€â™‚ī¸', + 'woman_gesturing_ok': 'đŸ™†â€â™€ī¸', + 'man_raising_hand': 'đŸ™‹â€â™‚ī¸', + 'woman_raising_hand': 'đŸ™‹â€â™€ī¸', + + // Common variations and aliases + 'slightly_smiling_face': '🙂', + 'upside_down_face': '🙃', + 'thinking_face': '🤔', + 'shrug': '🤷', + 'facepalm': 'đŸ¤Ļ', + 'man_shrugging': 'đŸ¤ˇâ€â™‚ī¸', + 'woman_shrugging': 'đŸ¤ˇâ€â™€ī¸', + 'man_facepalming': 'đŸ¤Ļâ€â™‚ī¸', + 'woman_facepalming': 'đŸ¤Ļâ€â™€ī¸', + 'hugging_face': '🤗', + 'money_mouth_face': '🤑', + 'nerd_face': '🤓', + 'face_with_rolling_eyes': '🙄', + 'zipper_mouth_face': '🤐', + 'nauseated_face': 'đŸ¤ĸ', + 'vomiting_face': '🤮', + 'sneezing_face': '🤧', + 'lying_face': 'đŸ¤Ĩ', + 'drooling_face': '🤤', + 'sleeping_face': '😴', +}; + +/** + * Convert emoji shortcodes (like :thumbsup: or :joy:) to Unicode emoji characters + */ +export function convertEmojiShortcodes(text: string): string { + if (!text || typeof text !== 'string') { + return text; + } + + return text.replace(/:([a-zA-Z0-9_+-]+):/g, (match, shortcode) => { + const emoji = emojiMap[shortcode.toLowerCase()]; + return emoji || match; // Return the emoji if found, otherwise return the original text + }); +} + +/** + * Check if a string contains emoji shortcodes + */ +export function hasEmojiShortcodes(text: string): boolean { + if (!text || typeof text !== 'string') { + return false; + } + + return /:([a-zA-Z0-9_+-]+):/.test(text); +} + +/** + * Get available emoji shortcodes + */ +export function getAvailableEmojis(): string[] { + return Object.keys(emojiMap).sort(); +} + +/** + * Get emoji for a specific shortcode + */ +export function getEmoji(shortcode: string): string | null { + return emojiMap[shortcode.toLowerCase()] || null; +} \ No newline at end of file