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
18 changes: 18 additions & 0 deletions client/src/infra/rest/apis/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
);
};
6 changes: 6 additions & 0 deletions client/src/infra/rest/apis/chat/typing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { USER_PERSONAL_LIMITED_INFO } from '../../typings';

export interface OnlineUser {
_id: string;
personal_info: USER_PERSONAL_LIMITED_INFO;
}
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 = 280;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increase width to 300px

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it


export const ONLINE_USERS_POLL_INTERVAL_MS = 45 * 1000; // 45 seconds
export const PRESENCE_REFRESH_INTERVAL_MS = 60 * 1000; // 1 minute
162 changes: 159 additions & 3 deletions client/src/modules/chats/v1/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,165 @@
import { Box } from '@mui/material';
import { useEffect, useState, useCallback } from 'react';
import {
Box,
List,
ListItemButton,
Avatar,
Typography,
useTheme,
CircularProgress,
} from '@mui/material';
import { getOnlineUsers, 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 [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<OnlineUser | null>(null);

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

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

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

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">
Logged-in users
</Typography>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress size={32} />
</Box>
) : onlineUsers.length === 0 ? (
<Box sx={{ px: 2, py: 4 }}>
<Typography variant="body2" color="text.secondary" textAlign="center">
No other users online. Open this page in another browser or ask a friend to open the chat page.
</Typography>
</Box>
) : (
<List disablePadding>
{onlineUsers.map((user) => (
<ListItemButton
key={user._id}
selected={selectedUser?._id === user._id}
onClick={() => setSelectedUser(user)}
sx={{
gap: 1.5,
py: 1.5,
}}
>
<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={{ 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 }}>
<Avatar
src={selectedUser.personal_info?.profile_img}
alt={selectedUser.personal_info?.fullname}
sx={{ width: 64, height: 64, mx: 'auto', mb: 1.5 }}
>
{(selectedUser.personal_info?.fullname?.[0] || selectedUser.personal_info?.username?.[0] || '?').toUpperCase()}
</Avatar>
<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