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) {
)}
-