diff --git a/Frontend/Electron/src/assets/sounds/screenshare_viewer_join.mp3 b/Frontend/Electron/src/assets/sounds/screenshare_viewer_join.mp3 new file mode 100644 index 0000000..fd51c45 Binary files /dev/null and b/Frontend/Electron/src/assets/sounds/screenshare_viewer_join.mp3 differ diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index c639728..6f0846b 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -498,6 +498,9 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u const isLoadingMoreRef = useRef(false); const userSentMessageRef = useRef(false); const topSentinelRef = useRef(null); + const notifiedMessageIdsRef = useRef(new Set()); + const pendingNotificationIdsRef = useRef(new Set()); + const lastPingTimeRef = useRef(0); const convex = useConvex(); @@ -688,6 +691,8 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u // Don't clear messageDecryptionCache — it persists across channel switches setDecryptedMessages([]); isInitialLoadRef.current = true; + notifiedMessageIdsRef.current = new Set(); + pendingNotificationIdsRef.current = new Set(); setReplyingTo(null); setEditingMessage(null); setMentionQuery(null); @@ -695,6 +700,58 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u onTogglePinned(); }, [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 const unreadDividerCapturedRef = useRef(null); 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 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 container = messagesContainerRef.current; if (!container) return; @@ -1194,14 +1268,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u {decryptedMessages.map((msg, idx) => { const currentDate = new Date(msg.created_at); const previousDate = idx > 0 ? new Date(decryptedMessages[idx - 1].created_at) : null; - const isMentioned = msg.content && ( - msg.content.includes(`@${username}`) || - myRoleNames.some(rn => - rn.startsWith('@') - ? msg.content.includes(rn) - : msg.content.includes(`@role:${rn}`) - ) - ); + const isMentioned = isMentionedInContent(msg.content); const isOwner = msg.username === username; const canDelete = isOwner || !!myPermissions?.manage_messages; diff --git a/Frontend/Electron/src/components/VoiceStage.jsx b/Frontend/Electron/src/components/VoiceStage.jsx index 430b18a..d9cf76a 100644 --- a/Frontend/Electron/src/components/VoiceStage.jsx +++ b/Frontend/Electron/src/components/VoiceStage.jsx @@ -526,8 +526,6 @@ const FocusedStreamView = ({ height: participantsCollapsed ? 0 : BOTTOM_BAR_HEIGHT, overflow: 'hidden', transition: 'height 0.25s ease', - backgroundColor: '#1e1f22', - borderTop: participantsCollapsed ? 'none' : '1px solid #2f3136', }}>
-- 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.