feat: Add base UI styling, ChatArea and VoiceStage components, and a screenshare sound asset.

This commit is contained in:
Bryan1029384756
2026-02-12 18:20:23 -06:00
parent 7a5b789ece
commit 751f428adc
5 changed files with 77 additions and 11 deletions

View File

@@ -498,6 +498,9 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const isLoadingMoreRef = useRef(false); const isLoadingMoreRef = useRef(false);
const userSentMessageRef = useRef(false); const userSentMessageRef = useRef(false);
const topSentinelRef = useRef(null); const topSentinelRef = useRef(null);
const notifiedMessageIdsRef = useRef(new Set());
const pendingNotificationIdsRef = useRef(new Set());
const lastPingTimeRef = useRef(0);
const convex = useConvex(); const convex = useConvex();
@@ -688,6 +691,8 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
// Don't clear messageDecryptionCache — it persists across channel switches // Don't clear messageDecryptionCache — it persists across channel switches
setDecryptedMessages([]); setDecryptedMessages([]);
isInitialLoadRef.current = true; isInitialLoadRef.current = true;
notifiedMessageIdsRef.current = new Set();
pendingNotificationIdsRef.current = new Set();
setReplyingTo(null); setReplyingTo(null);
setEditingMessage(null); setEditingMessage(null);
setMentionQuery(null); setMentionQuery(null);
@@ -695,6 +700,58 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
onTogglePinned(); onTogglePinned();
}, [channelId, channelKey]); }, [channelId, channelKey]);
// Play ping sound when a new message mentions us (by username or role)
useEffect(() => {
if (!decryptedMessages.length) return;
// Initial load: seed all IDs, no sound
if (isInitialLoadRef.current) {
for (const msg of decryptedMessages) {
if (msg.id) notifiedMessageIdsRef.current.add(msg.id);
}
return;
}
let shouldPing = false;
// Check newest messages (end of array) backwards — stop at first known ID
for (let i = decryptedMessages.length - 1; i >= 0; i--) {
const msg = decryptedMessages[i];
if (!msg.id) continue;
if (notifiedMessageIdsRef.current.has(msg.id)) break;
// Skip own messages
if (msg.sender_id === currentUserId) {
notifiedMessageIdsRef.current.add(msg.id);
continue;
}
// Still decrypting — mark pending
if (msg.content === '[Decrypting...]') {
pendingNotificationIdsRef.current.add(msg.id);
continue;
}
notifiedMessageIdsRef.current.add(msg.id);
pendingNotificationIdsRef.current.delete(msg.id);
if (isMentionedInContent(msg.content)) shouldPing = true;
}
// Re-check previously pending messages now decrypted
if (!shouldPing && pendingNotificationIdsRef.current.size > 0) {
for (const msg of decryptedMessages) {
if (!pendingNotificationIdsRef.current.has(msg.id)) continue;
if (msg.content === '[Decrypting...]') continue;
pendingNotificationIdsRef.current.delete(msg.id);
notifiedMessageIdsRef.current.add(msg.id);
if (isMentionedInContent(msg.content)) shouldPing = true;
}
}
if (shouldPing) playPingSound();
}, [decryptedMessages, currentUserId, isMentionedInContent, playPingSound]);
// Capture the unread divider position when read state loads for a channel // Capture the unread divider position when read state loads for a channel
const unreadDividerCapturedRef = useRef(null); const unreadDividerCapturedRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -740,6 +797,23 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
]; ];
const myRoleNames = members?.find(m => m.username === username)?.roles?.map(r => r.name) || []; const myRoleNames = members?.find(m => m.username === username)?.roles?.map(r => r.name) || [];
const isMentionedInContent = useCallback((content) => {
if (!content) return false;
return content.includes(`@${username}`) ||
myRoleNames.some(rn =>
rn.startsWith('@') ? content.includes(rn) : content.includes(`@role:${rn}`)
);
}, [username, myRoleNames]);
const playPingSound = useCallback(() => {
const now = Date.now();
if (now - lastPingTimeRef.current < 1000) return;
lastPingTimeRef.current = now;
const audio = new Audio(PingSound);
audio.volume = 0.5;
audio.play().catch(() => {});
}, []);
const scrollToBottom = useCallback((force = false) => { const scrollToBottom = useCallback((force = false) => {
const container = messagesContainerRef.current; const container = messagesContainerRef.current;
if (!container) return; if (!container) return;
@@ -1194,14 +1268,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
{decryptedMessages.map((msg, idx) => { {decryptedMessages.map((msg, idx) => {
const currentDate = new Date(msg.created_at); const currentDate = new Date(msg.created_at);
const previousDate = idx > 0 ? new Date(decryptedMessages[idx - 1].created_at) : null; const previousDate = idx > 0 ? new Date(decryptedMessages[idx - 1].created_at) : null;
const isMentioned = msg.content && ( const isMentioned = isMentionedInContent(msg.content);
msg.content.includes(`@${username}`) ||
myRoleNames.some(rn =>
rn.startsWith('@')
? msg.content.includes(rn)
: msg.content.includes(`@role:${rn}`)
)
);
const isOwner = msg.username === username; const isOwner = msg.username === username;
const canDelete = isOwner || !!myPermissions?.manage_messages; const canDelete = isOwner || !!myPermissions?.manage_messages;

View File

@@ -526,8 +526,6 @@ const FocusedStreamView = ({
height: participantsCollapsed ? 0 : BOTTOM_BAR_HEIGHT, height: participantsCollapsed ? 0 : BOTTOM_BAR_HEIGHT,
overflow: 'hidden', overflow: 'hidden',
transition: 'height 0.25s ease', transition: 'height 0.25s ease',
backgroundColor: '#1e1f22',
borderTop: participantsCollapsed ? 'none' : '1px solid #2f3136',
}}> }}>
<div style={{ <div style={{
height: BOTTOM_BAR_HEIGHT, height: BOTTOM_BAR_HEIGHT,

View File

@@ -1958,6 +1958,7 @@ body {
color: var(--text-muted); color: var(--text-muted);
transition: background-color 0.1s; transition: background-color 0.1s;
position: relative; position: relative;
margin-bottom: 4px;
} }
.dm-item:hover { .dm-item:hover {

View File

@@ -2,7 +2,7 @@
<!-- - When a user messages you, you should get a notification. On the server list that user profile picture should be their above all servers. right under the discord and above the server-separator. With a red dot next to it. If you get a private dm you should hear the ping sound also --> <!-- - When a user messages you, you should get a notification. On the server list that user profile picture should be their above all servers. right under the discord and above the server-separator. With a red dot next to it. If you get a private dm you should hear the ping sound also -->
- We should play a sound when a user mentions you also in the main server. - We should play a sound (the ping sound) when a user mentions you or you recieve a private message.
<!-- - In the mention list we should also have roles, so if people do @everyone they should mention everyone in the server, or they can @ a certain role they want to mention from the server. This does not apply to private messages. --> <!-- - In the mention list we should also have roles, so if people do @everyone they should mention everyone in the server, or they can @ a certain role they want to mention from the server. This does not apply to private messages. -->