feat: Add base UI styling, ChatArea and VoiceStage components, and a screenshare sound asset.
This commit is contained in:
BIN
Frontend/Electron/src/assets/sounds/screenshare_viewer_join.mp3
Normal file
BIN
Frontend/Electron/src/assets/sounds/screenshare_viewer_join.mp3
Normal file
Binary file not shown.
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
}}>
|
||||
<div style={{
|
||||
height: BOTTOM_BAR_HEIGHT,
|
||||
|
||||
@@ -1958,6 +1958,7 @@ body {
|
||||
color: var(--text-muted);
|
||||
transition: background-color 0.1s;
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dm-item:hover {
|
||||
|
||||
Reference in New Issue
Block a user