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
All checks were successful
Build and Release / build-and-release (push) Successful in 10m9s
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user