Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/src/infra/rest/apis/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { post, get } from '../..';
import { ApiResponse } from '../../typings';
import { OnlineUser } from './typing';

export const reportPresence = async () => {
return post<undefined, ApiResponse<undefined>>(
'/api/chat/presence',
true,
undefined
);
};

export const getOnlineUsers = async () => {
return get<undefined, ApiResponse<OnlineUser[]>>(
'/api/chat/online-users',
true
);
};

export const getChatUsers = async () => {
return get<undefined, ApiResponse<OnlineUser[]>>('/api/chat/users', true);
};
11 changes: 11 additions & 0 deletions client/src/infra/rest/apis/chat/typing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { USER_PERSONAL_LIMITED_INFO } from '../../typings';

export interface OnlineUser {
_id: string;
personal_info: USER_PERSONAL_LIMITED_INFO;
/**
* Optional status flag used by the chats module.
* If true, the user is currently online; otherwise they are offline.
*/
isOnline?: boolean;
}
4 changes: 4 additions & 0 deletions client/src/modules/chats/v1/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const CHATS_SIDEBAR_WIDTH = 300;

export const ONLINE_USERS_POLL_INTERVAL_MS = 45 * 1000; // 45 seconds
export const PRESENCE_REFRESH_INTERVAL_MS = 60 * 1000; // 1 minute
192 changes: 189 additions & 3 deletions client/src/modules/chats/v1/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,195 @@
import { Box } from '@mui/material';
import { useEffect, useState, useCallback } from 'react';
import {
Box,
List,
ListItemButton,
Avatar,
Typography,
useTheme,
CircularProgress,
} from '@mui/material';
import { getChatUsers, reportPresence } from '../../../infra/rest/apis/chat';
import { OnlineUser } from '../../../infra/rest/apis/chat/typing';
import { CHATS_SIDEBAR_WIDTH, ONLINE_USERS_POLL_INTERVAL_MS, PRESENCE_REFRESH_INTERVAL_MS } from './constants';

const Chats = () => {
const theme = useTheme();
const [users, setUsers] = useState<OnlineUser[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<OnlineUser | null>(null);

const fetchPresenceAndUsers = useCallback(async () => {
try {
await reportPresence();
const res = await getChatUsers();
if (res.status === 'success' && res.data) {
setUsers(res.data);
}
} catch {
setUsers([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchPresenceAndUsers();
}, [fetchPresenceAndUsers]);

useEffect(() => {
const pollInterval = setInterval(fetchPresenceAndUsers, ONLINE_USERS_POLL_INTERVAL_MS);
return () => clearInterval(pollInterval);
}, [fetchPresenceAndUsers]);

useEffect(() => {
const presenceInterval = setInterval(reportPresence, PRESENCE_REFRESH_INTERVAL_MS);
return () => clearInterval(presenceInterval);
}, []);

return (
<Box>
<p>Chats</p>
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
bgcolor: 'background.default',
overflow: 'hidden',
}}
>
<Box
sx={{
width: CHATS_SIDEBAR_WIDTH,
minWidth: CHATS_SIDEBAR_WIDTH,
borderRight: `1px solid ${theme.palette.divider}`,
bgcolor: 'background.paper',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<Box
sx={{
p: 2,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="h6" component="h1">
Chats
</Typography>
<Typography variant="body2" color="text.secondary">
All registered users
</Typography>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress size={32} />
</Box>
) : users.length === 0 ? (
<Box sx={{ px: 2, py: 4 }}>
<Typography variant="body2" color="text.secondary" textAlign="center">
No other users found. Ask a friend to create an account and open the chat page.
</Typography>
</Box>
) : (
<List disablePadding>
{users.map((user) => (
<ListItemButton
key={user._id}
selected={selectedUser?._id === user._id}
onClick={() => setSelectedUser(user)}
sx={{
gap: 1.5,
py: 1.5,
}}
>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Avatar
src={user.personal_info?.profile_img}
alt={user.personal_info?.fullname}
sx={{ width: 40, height: 40 }}
>
{(user.personal_info?.fullname?.[0] || user.personal_info?.username?.[0] || '?').toUpperCase()}
</Avatar>
<Box
sx={{
position: 'absolute',
bottom: -1,
right: -1,
width: 12,
height: 12,
borderRadius: '50%',
bgcolor: user.isOnline ? 'success.main' : 'background.paper',
border: '2px solid',
borderColor: user.isOnline ? 'background.paper' : 'text.disabled',
}}
/>
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="body1" noWrap>
{user.personal_info?.fullname || user.personal_info?.username || 'Unknown'}
</Typography>
<Typography variant="body2" color="text.secondary" noWrap>
@{user.personal_info?.username || '—'}
</Typography>
</Box>
</ListItemButton>
))}
</List>
)}
</Box>
</Box>
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
p: 3,
}}
>
{!selectedUser ? (
<Typography variant="body1" color="text.secondary" textAlign="center">
Select a user to start a chat session. Real-time messaging will be available in a future update.
</Typography>
) : (
<Box sx={{ textAlign: 'center', maxWidth: 360 }}>
<Box sx={{ position: 'relative', width: 64, height: 64, mx: 'auto', mb: 1.5 }}>
<Avatar
src={selectedUser.personal_info?.profile_img}
alt={selectedUser.personal_info?.fullname}
sx={{ width: 64, height: 64 }}
>
{(selectedUser.personal_info?.fullname?.[0] || selectedUser.personal_info?.username?.[0] || '?').toUpperCase()}
</Avatar>
<Box
sx={{
position: 'absolute',
bottom: -2,
right: -2,
width: 14,
height: 14,
borderRadius: '50%',
bgcolor: selectedUser.isOnline ? 'success.main' : 'background.paper',
border: '2px solid',
borderColor: selectedUser.isOnline ? 'background.paper' : 'text.disabled',
}}
/>
</Box>
<Typography variant="h6">
Chat with {selectedUser.personal_info?.fullname || selectedUser.personal_info?.username || 'Unknown'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
@{selectedUser.personal_info?.username || '—'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
Chat session prepared. Real-time messaging will be available in a future update.
</Typography>
</Box>
)}
</Box>
</Box>
);
};
Expand Down
3 changes: 3 additions & 0 deletions client/src/modules/chats/v1/typings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { OnlineUser } from '../../../../infra/rest/apis/chat/typing';

export type SelectedUser = OnlineUser | null;
15 changes: 14 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { createServer } from 'http';
import server from './src/server.js';
import { PORT } from './src/config/env.js';
import { initializeSocketIO } from './src/config/socket.io.js';

server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
const httpServer = createServer(server);

// Initialize Socket.IO
const io = initializeSocketIO(httpServer);

// Make io available globally if needed
server.set('io', io);

httpServer.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Socket.IO server initialized`);
});
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"rate-limiter-flexible": "^7.3.0",
"resend": "^6.1.2",
"sanitize-html": "^2.17.0",
"socket.io": "^4.7.5",
"winston": "^3.18.3"
},
"devDependencies": {
Expand Down
Loading
Loading