feat: Implement Direct Messaging with new frontend components, user search, and backend read state management.
All checks were successful
Build and Release / build-and-release (push) Successful in 10m9s

This commit is contained in:
Bryan1029384756
2026-02-11 08:01:51 -06:00
parent 44814011fe
commit e773ab41ae
10 changed files with 154 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useConvex, useMutation, useQuery } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
@@ -21,6 +21,7 @@ import disconnectIcon from '../assets/icons/disconnect.svg';
import cameraIcon from '../assets/icons/camera.svg';
import screenIcon from '../assets/icons/screen.svg';
import inviteUserIcon from '../assets/icons/invite_user.svg';
import PingSound from '../assets/sounds/ping.mp3';
const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
@@ -348,7 +349,10 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
const convex = useConvex();
// Unread tracking
const channelIds = React.useMemo(() => channels.map(c => c._id), [channels]);
const channelIds = React.useMemo(() => [
...channels.map(c => c._id),
...dmChannels.map(dm => dm.channel_id)
], [channels, dmChannels]);
const allReadStates = useQuery(
api.readState.getAllReadStates,
userId ? { userId } : "skip"
@@ -373,6 +377,38 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
return set;
}, [allReadStates, latestTimestamps]);
const unreadDMs = React.useMemo(() =>
dmChannels.filter(dm =>
unreadChannels.has(dm.channel_id) &&
!(view === 'me' && activeDMChannel?.channel_id === dm.channel_id)
),
[dmChannels, unreadChannels, view, activeDMChannel]
);
const prevUnreadDMsRef = useRef(null);
useEffect(() => {
const currentIds = new Set(
dmChannels.filter(dm => unreadChannels.has(dm.channel_id)).map(dm => dm.channel_id)
);
if (prevUnreadDMsRef.current === null) {
prevUnreadDMsRef.current = currentIds;
return;
}
for (const id of currentIds) {
if (!prevUnreadDMsRef.current.has(id)) {
const audio = new Audio(PingSound);
audio.volume = 0.5;
audio.play().catch(() => {});
break;
}
}
prevUnreadDMsRef.current = currentIds;
}, [dmChannels, unreadChannels]);
const onRenameChannel = () => {};
const onDeleteChannel = (id) => {
@@ -621,7 +657,6 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
{Object.entries(groupedChannels).map(([category, catChannels]) => (
<div key={category}>
<div className="channel-category-header" onClick={() => toggleCategory(category)}>
<span className={`category-chevron ${collapsedCategories[category] ? 'collapsed' : ''}`}></span>
<span className="category-label">{category}</span>
<button className="category-add-btn" onClick={(e) => { e.stopPropagation(); handleStartCreate(); }} title="Create Channel">
+
@@ -712,6 +747,32 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
</Tooltip>
</div>
{unreadDMs.map(dm => (
<div key={dm.channel_id} className="server-item-wrapper">
<div className={`server-pill ${
view === 'me' && activeDMChannel?.channel_id === dm.channel_id ? 'active' : ''
}`} />
<Tooltip text={dm.other_username} position="right">
<div
className={`server-icon dm-server-icon ${
view === 'me' && activeDMChannel?.channel_id === dm.channel_id ? 'active' : ''
}`}
onClick={() => {
setActiveDMChannel(dm);
onViewChange('me');
}}
>
<Avatar
username={dm.other_username}
avatarUrl={dm.other_user_avatar_url}
size={48}
/>
<div className="dm-notification-dot" />
</div>
</Tooltip>
</div>
))}
<div className="server-separator" />
<div className="server-item-wrapper">