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 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@@ -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. -->
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user