feat: implement Paarrot API for Electron integration with navigation and IPC handlers
This commit is contained in:
@@ -1,23 +1,29 @@
|
||||
import React, { ReactNode, useMemo, useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Box, Text } from 'folds';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room } from 'matrix-js-sdk';
|
||||
import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules } from 'matrix-js-sdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { WindowControls } from './WindowControls';
|
||||
import * as css from './TitleBar.css';
|
||||
import { getUnreadInfos, getOrphanParents, guessPerfectParent } from '../../utils/room';
|
||||
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
|
||||
import { getUnreadInfos, getOrphanParents, guessPerfectParent, getAllParents } from '../../utils/room';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { activeRoomIdAtom } from '../../state/activeRoom';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { getDirectRoomPath, getHomeRoomPath, getSpaceRoomPath } from '../../pages/pathUtils';
|
||||
import { RoomNotificationMode } from '../../hooks/useRoomsNotificationPreferences';
|
||||
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
||||
import { getNotificationMode, NotificationMode } from '../../hooks/useNotificationMode';
|
||||
import { isRoomId } from '../../utils/matrix';
|
||||
|
||||
type NewMessageNotification = {
|
||||
id: string;
|
||||
roomId: string;
|
||||
eventId: string;
|
||||
senderName: string;
|
||||
preview: string;
|
||||
exiting?: boolean;
|
||||
};
|
||||
|
||||
export type TitleBarProps = {
|
||||
@@ -28,56 +34,201 @@ export type TitleBarProps = {
|
||||
export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
const [updateCounter, setUpdateCounter] = useState(0);
|
||||
const roomId = useAtomValue(activeRoomIdAtom);
|
||||
const [newMessage, setNewMessage] = useState<NewMessageNotification | null>(null);
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [notifications, setNotifications] = useState<NewMessageNotification[]>([]);
|
||||
const dismissTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
const lastMxRef = useRef<MatrixClient | undefined>();
|
||||
const navigate = useNavigate();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
// Clear dismiss timer on unmount
|
||||
// Notification-related state - only populated when mx is available
|
||||
const [notificationData, setNotificationData] = useState<{
|
||||
preferences: { mute: Set<string>; specialMessages: Set<string>; allMessages: Set<string> };
|
||||
keywordPatterns: string[];
|
||||
userId: string | undefined;
|
||||
displayName: string | undefined;
|
||||
}>({
|
||||
preferences: { mute: new Set(), specialMessages: new Set(), allMessages: new Set() },
|
||||
keywordPatterns: [],
|
||||
userId: undefined,
|
||||
displayName: undefined,
|
||||
});
|
||||
|
||||
// Update notification data when mx is available (only when mx changes)
|
||||
useEffect(() => {
|
||||
// Only update if mx actually changed
|
||||
if (lastMxRef.current === mx) return;
|
||||
lastMxRef.current = mx;
|
||||
|
||||
if (!mx) {
|
||||
setNotificationData({
|
||||
preferences: { mute: new Set(), specialMessages: new Set(), allMessages: new Set() },
|
||||
keywordPatterns: [],
|
||||
userId: undefined,
|
||||
displayName: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't use hooks here, so we'll fetch the data directly
|
||||
const userId = mx.getUserId();
|
||||
|
||||
// Get push rules for keywords
|
||||
const pushRulesEvt = mx.getAccountData(AccountDataEvent.PushRules);
|
||||
const pushRules = pushRulesEvt?.getContent<IPushRules>() ?? { global: {} };
|
||||
const content = pushRules.global.content ?? [];
|
||||
const keywordPatterns = content
|
||||
.filter((rule: any) => rule.default === false && typeof rule.pattern === 'string')
|
||||
.map((rule: any) => rule.pattern as string);
|
||||
|
||||
// Get notification preferences
|
||||
const global = pushRules?.global;
|
||||
const room = global?.room ?? [];
|
||||
const override = global?.override ?? [];
|
||||
|
||||
const preferences = {
|
||||
mute: new Set<string>(),
|
||||
specialMessages: new Set<string>(),
|
||||
allMessages: new Set<string>(),
|
||||
};
|
||||
|
||||
// Check override rules for muted rooms
|
||||
override.forEach((rule: any) => {
|
||||
if (isRoomId(rule.rule_id) && getNotificationMode(rule.actions) === NotificationMode.OFF) {
|
||||
preferences.mute.add(rule.rule_id);
|
||||
}
|
||||
});
|
||||
|
||||
// Check room rules for special messages (OFF mode = mentions only)
|
||||
room.forEach((rule: any) => {
|
||||
if (getNotificationMode(rule.actions) === NotificationMode.OFF) {
|
||||
preferences.specialMessages.add(rule.rule_id);
|
||||
}
|
||||
});
|
||||
|
||||
// Check room rules for all messages (Notify or NotifyLoud mode)
|
||||
room.forEach((rule: any) => {
|
||||
if (getNotificationMode(rule.actions) !== NotificationMode.OFF) {
|
||||
preferences.allMessages.add(rule.rule_id);
|
||||
}
|
||||
});
|
||||
|
||||
// Get display name
|
||||
const user = userId ? mx.getUser(userId) : null;
|
||||
const displayName = user?.displayName;
|
||||
|
||||
console.log('[TitleBar] Notification preferences loaded:', {
|
||||
muted: Array.from(preferences.mute),
|
||||
specialMessages: Array.from(preferences.specialMessages),
|
||||
allMessages: Array.from(preferences.allMessages),
|
||||
keywordPatterns,
|
||||
});
|
||||
|
||||
setNotificationData({
|
||||
preferences,
|
||||
keywordPatterns,
|
||||
userId,
|
||||
displayName,
|
||||
});
|
||||
}, [mx]);
|
||||
|
||||
// Check if message should trigger a notification for SpecialMessages mode
|
||||
const shouldNotifySpecialMessage = useCallback((event: MatrixEvent): boolean => {
|
||||
if (!notificationData.userId) return false;
|
||||
|
||||
const content = event.getContent();
|
||||
const body = content.body?.toLowerCase() || '';
|
||||
|
||||
// Check for user mentions in m.mentions
|
||||
const mentions = content['m.mentions'];
|
||||
if (mentions?.user_ids?.includes(notificationData.userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for @room mention
|
||||
if (mentions?.room === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if body contains @room
|
||||
if (body.includes('@room')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if body contains user's display name
|
||||
if (notificationData.displayName && body.includes(notificationData.displayName.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if body contains user's username/localpart
|
||||
const username = getMxIdLocalPart(notificationData.userId);
|
||||
if (username && body.includes(username.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if body contains any of the user's keywords
|
||||
for (const keyword of notificationData.keywordPatterns) {
|
||||
if (body.includes(keyword.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [notificationData]);
|
||||
|
||||
// Clear all dismiss timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
}
|
||||
dismissTimersRef.current.forEach(timer => clearTimeout(timer));
|
||||
dismissTimersRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-dismiss notification after 10 seconds
|
||||
const scheduleDismiss = useCallback(() => {
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
const scheduleDismiss = useCallback((notificationId: string) => {
|
||||
// Clear existing timer for this notification if any
|
||||
const existingTimer = dismissTimersRef.current.get(notificationId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
dismissTimerRef.current = setTimeout(() => {
|
||||
setIsExiting(true);
|
||||
|
||||
// Schedule new dismiss timer
|
||||
const timer = setTimeout(() => {
|
||||
setNotifications(prev =>
|
||||
prev.map(n => n.id === notificationId ? { ...n, exiting: true } : n)
|
||||
);
|
||||
setTimeout(() => {
|
||||
setNewMessage(null);
|
||||
setIsExiting(false);
|
||||
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
||||
dismissTimersRef.current.delete(notificationId);
|
||||
}, 200); // Wait for exit animation
|
||||
}, 10000);
|
||||
|
||||
dismissTimersRef.current.set(notificationId, timer);
|
||||
}, []);
|
||||
|
||||
// Navigate to the new message
|
||||
const handleNotificationClick = useCallback((e: React.MouseEvent) => {
|
||||
const handleNotificationClick = useCallback((notification: NewMessageNotification) => (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!newMessage || !mx) return;
|
||||
if (!mx) return;
|
||||
|
||||
const targetRoomId = newMessage.roomId;
|
||||
const eventId = newMessage.eventId;
|
||||
const targetRoomId = notification.roomId;
|
||||
const eventId = notification.eventId;
|
||||
|
||||
// Dismiss notification
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
setNewMessage(null);
|
||||
setIsExiting(false);
|
||||
}, 200);
|
||||
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
// Dismiss this notification
|
||||
const timer = dismissTimersRef.current.get(notification.id);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
dismissTimersRef.current.delete(notification.id);
|
||||
}
|
||||
|
||||
setNotifications(prev =>
|
||||
prev.map(n => n.id === notification.id ? { ...n, exiting: true } : n)
|
||||
);
|
||||
setTimeout(() => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== notification.id));
|
||||
}, 200);
|
||||
|
||||
// Simple navigation approach - just try all paths
|
||||
try {
|
||||
@@ -116,7 +267,7 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
console.error('Fallback navigation also failed:', fallbackError);
|
||||
}
|
||||
}
|
||||
}, [newMessage, mx, roomToParents, mDirects, navigate]);
|
||||
}, [mx, roomToParents, mDirects, navigate]);
|
||||
|
||||
// Subscribe to room changes for unread updates and new messages
|
||||
useEffect(() => {
|
||||
@@ -131,13 +282,99 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
if (!room || !event) return;
|
||||
|
||||
// Ignore if it's the currently active room
|
||||
if (room.roomId === roomId) return;
|
||||
if (room.roomId === roomId) {
|
||||
console.log('[TitleBar] Skipping: active room');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if it's our own message
|
||||
if (event.getSender() === mx.getUserId()) return;
|
||||
if (event.getSender() === mx.getUserId()) {
|
||||
console.log('[TitleBar] Skipping: own message');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only notify for actual messages
|
||||
if (event.getType() !== 'm.room.message') return;
|
||||
if (event.getType() !== 'm.room.message') {
|
||||
console.log('[TitleBar] Skipping: not a message event, type:', event.getType());
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show for recent messages (within 15 seconds) to avoid showing notifications
|
||||
// for old messages when loading history
|
||||
const eventTime = event.getTs();
|
||||
const now = Date.now();
|
||||
if (now - eventTime > 15000) {
|
||||
console.log('[TitleBar] Skipping: message too old', (now - eventTime) / 1000, 'seconds');
|
||||
return; // More than 15 seconds old
|
||||
}
|
||||
|
||||
// Only show for unread messages - check if event is after the last read receipt
|
||||
const readUpToEventId = room.getEventReadUpTo(mx.getUserId() ?? '');
|
||||
if (readUpToEventId) {
|
||||
const timeline = room.getLiveTimeline().getEvents();
|
||||
const readUpToIndex = timeline.findIndex(e => e.getId() === readUpToEventId);
|
||||
const eventIndex = timeline.findIndex(e => e.getId() === event.getId());
|
||||
|
||||
// If we found both events and the new event is not after the read marker, skip it
|
||||
if (readUpToIndex !== -1 && eventIndex !== -1 && eventIndex <= readUpToIndex) {
|
||||
console.log('[TitleBar] Skipping: already read (eventIndex:', eventIndex, ', readUpToIndex:', readUpToIndex, ')');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine notification mode for this room
|
||||
const roomIdStr = room.roomId;
|
||||
let notificationMode: RoomNotificationMode;
|
||||
|
||||
if (notificationData.preferences.mute.has(roomIdStr)) {
|
||||
notificationMode = RoomNotificationMode.Mute;
|
||||
} else if (notificationData.preferences.specialMessages.has(roomIdStr)) {
|
||||
notificationMode = RoomNotificationMode.SpecialMessages;
|
||||
} else if (notificationData.preferences.allMessages.has(roomIdStr)) {
|
||||
notificationMode = RoomNotificationMode.AllMessages;
|
||||
} else {
|
||||
notificationMode = RoomNotificationMode.Unset;
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log('[TitleBar Notification]', {
|
||||
roomId: roomIdStr,
|
||||
roomName: room.name,
|
||||
notificationMode,
|
||||
isDM: mDirects.has(roomIdStr),
|
||||
preferences: {
|
||||
muted: notificationData.preferences.mute.has(roomIdStr),
|
||||
specialMessages: notificationData.preferences.specialMessages.has(roomIdStr),
|
||||
allMessages: notificationData.preferences.allMessages.has(roomIdStr),
|
||||
},
|
||||
});
|
||||
|
||||
// Never show for muted rooms
|
||||
if (notificationMode === RoomNotificationMode.Mute) return;
|
||||
|
||||
// DMs with Unset mode should notify for all messages (underride rule behavior)
|
||||
const isDM = mDirects.has(roomIdStr);
|
||||
const treatAsAllMessages = notificationMode === RoomNotificationMode.AllMessages ||
|
||||
(notificationMode === RoomNotificationMode.Unset && isDM);
|
||||
|
||||
console.log('[TitleBar] treatAsAllMessages:', treatAsAllMessages, 'isDM:', isDM);
|
||||
|
||||
// For SpecialMessages mode, only show if message would trigger a notification
|
||||
// For Unset mode (non-DM), also check for special messages
|
||||
if (!treatAsAllMessages && notificationMode === RoomNotificationMode.SpecialMessages) {
|
||||
const shouldNotify = shouldNotifySpecialMessage(event);
|
||||
console.log('[TitleBar] SpecialMessages mode check:', shouldNotify);
|
||||
if (!shouldNotify) return;
|
||||
}
|
||||
if (!treatAsAllMessages && notificationMode === RoomNotificationMode.Unset && !isDM) {
|
||||
const shouldNotify = shouldNotifySpecialMessage(event);
|
||||
console.log('[TitleBar] Unset mode (non-DM) check:', shouldNotify);
|
||||
if (!shouldNotify) return;
|
||||
}
|
||||
|
||||
console.log('[TitleBar] Passed all checks, showing notification bubble');
|
||||
|
||||
// For AllMessages mode, show all messages (no additional check needed)
|
||||
|
||||
// Get content
|
||||
const content = event.getContent();
|
||||
@@ -152,14 +389,25 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
? content.body.substring(0, 30) + '...'
|
||||
: content.body;
|
||||
|
||||
setNewMessage({
|
||||
roomId: room.roomId,
|
||||
eventId: event.getId() || '',
|
||||
senderName,
|
||||
preview,
|
||||
// Generate unique ID for this notification
|
||||
const notificationId = `${room.roomId}-${event.getId()}-${Date.now()}`;
|
||||
|
||||
// Add to notifications array (limit to 4 notifications)
|
||||
setNotifications(prev => {
|
||||
const newNotification: NewMessageNotification = {
|
||||
id: notificationId,
|
||||
roomId: room.roomId,
|
||||
eventId: event.getId() || '',
|
||||
senderName,
|
||||
preview,
|
||||
};
|
||||
|
||||
// Remove oldest if we already have 4
|
||||
const updated = prev.length >= 4 ? prev.slice(1) : prev;
|
||||
return [...updated, newNotification];
|
||||
});
|
||||
setIsExiting(false);
|
||||
scheduleDismiss();
|
||||
|
||||
scheduleDismiss(notificationId);
|
||||
};
|
||||
|
||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
@@ -171,9 +419,137 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
mx.off(RoomEvent.Receipt, update);
|
||||
mx.off(ClientEvent.Room, update);
|
||||
};
|
||||
}, [mx, roomId, scheduleDismiss]);
|
||||
}, [mx, roomId, scheduleDismiss, notificationData, shouldNotifySpecialMessage]);
|
||||
|
||||
// Get room name
|
||||
// Clear notifications when user reads messages in a room
|
||||
useEffect(() => {
|
||||
if (!mx) return;
|
||||
|
||||
const handleReceipt = (event: MatrixEvent, room: Room) => {
|
||||
const myUserId = mx.getUserId();
|
||||
if (!myUserId) return;
|
||||
|
||||
const content = event.getContent();
|
||||
const isMyReceipt = Object.keys(content).find((eventId) =>
|
||||
(Object.keys(content[eventId]) as any[]).find(
|
||||
(receiptType) => content[eventId][receiptType][myUserId]
|
||||
)
|
||||
);
|
||||
|
||||
// If this is my receipt, clear all notifications for this room
|
||||
if (isMyReceipt) {
|
||||
setNotifications(prev => {
|
||||
const updated = prev.filter(n => n.roomId !== room.roomId);
|
||||
// Clear timers for removed notifications
|
||||
prev.forEach(n => {
|
||||
if (n.roomId === room.roomId) {
|
||||
const timer = dismissTimersRef.current.get(n.id);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
dismissTimersRef.current.delete(n.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mx.on(RoomEvent.Receipt, handleReceipt);
|
||||
return () => {
|
||||
mx.off(RoomEvent.Receipt, handleReceipt);
|
||||
};
|
||||
}, [mx]);
|
||||
|
||||
// Clear notifications for the active room when it changes
|
||||
useEffect(() => {
|
||||
if (!roomId) return;
|
||||
|
||||
setNotifications(prev => {
|
||||
const updated = prev.filter(n => n.roomId !== roomId);
|
||||
// Clear timers for removed notifications
|
||||
prev.forEach(n => {
|
||||
if (n.roomId === roomId) {
|
||||
const timer = dismissTimersRef.current.get(n.id);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
dismissTimersRef.current.delete(n.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}, [roomId]);
|
||||
|
||||
// Build breadcrumb path for room (Space > Subspace > RoomName)
|
||||
const breadcrumbPath = useMemo(() => {
|
||||
if (!mx || !roomId) return null;
|
||||
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
const roomName = room.name;
|
||||
if (!roomName) return null;
|
||||
|
||||
// DMs just show the room name without hierarchy
|
||||
if (mDirects.has(roomId)) {
|
||||
return roomName;
|
||||
}
|
||||
|
||||
// Get the parent chain
|
||||
const directParents = roomToParents.get(roomId);
|
||||
if (!directParents || directParents.size === 0) {
|
||||
// No parents, just the room name
|
||||
return roomName;
|
||||
}
|
||||
|
||||
// Build the parent chain recursively
|
||||
// This returns the chain of parent names ABOVE the given roomId, not including roomId itself
|
||||
const buildParentChain = (currentRoomId: string): string[] => {
|
||||
const parents = roomToParents.get(currentRoomId);
|
||||
if (!parents || parents.size === 0) {
|
||||
// No parents above this room
|
||||
return [];
|
||||
}
|
||||
|
||||
// Pick the best parent (or first if only one)
|
||||
const parentsList = Array.from(parents);
|
||||
let chosenParent: string;
|
||||
|
||||
if (parentsList.length === 1) {
|
||||
chosenParent = parentsList[0];
|
||||
} else {
|
||||
// Use heuristic to pick best parent
|
||||
// Prefer a parent that is itself a top-level space (orphan)
|
||||
const orphanParents = parentsList.filter(p => !roomToParents.has(p));
|
||||
if (orphanParents.length > 0) {
|
||||
// Use guessPerfectParent if available, otherwise just pick first orphan
|
||||
chosenParent = guessPerfectParent(mx, currentRoomId, orphanParents) ?? orphanParents[0];
|
||||
} else {
|
||||
// All parents have parents themselves, just pick first and continue
|
||||
chosenParent = parentsList[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Get the parent's name
|
||||
const parentRoom = mx.getRoom(chosenParent);
|
||||
if (!parentRoom?.name) return [];
|
||||
|
||||
// Get the chain above the parent
|
||||
const upperChain = buildParentChain(chosenParent);
|
||||
|
||||
// Return the upper chain plus this parent
|
||||
return [...upperChain, parentRoom.name];
|
||||
};
|
||||
|
||||
const parentChain = buildParentChain(roomId);
|
||||
const path = [...parentChain, roomName];
|
||||
|
||||
return path.join(' > ');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mx, roomId, roomToParents, mDirects, updateCounter]);
|
||||
|
||||
// Get room name (for backward compatibility, keeping simple name too)
|
||||
const roomName = useMemo(() => {
|
||||
if (!mx || !roomId) return null;
|
||||
const room = mx.getRoom(roomId);
|
||||
@@ -192,14 +568,14 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
// Build title parts
|
||||
const titleParts = useMemo(() => {
|
||||
let suffix = '';
|
||||
if (roomName) {
|
||||
suffix += ` - ${roomName}`;
|
||||
if (breadcrumbPath) {
|
||||
suffix += ` | ${breadcrumbPath}`;
|
||||
}
|
||||
if (totalUnread > 0) {
|
||||
suffix += ` (${totalUnread})`;
|
||||
}
|
||||
return { suffix };
|
||||
}, [roomName, totalUnread]);
|
||||
}, [breadcrumbPath, totalUnread]);
|
||||
|
||||
const handleDragStart = async () => {
|
||||
try {
|
||||
@@ -224,18 +600,19 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
||||
onMouseDown={handleDragStart}
|
||||
>
|
||||
<Text size="T200" truncate style={{ color: 'inherit' }}>
|
||||
<b>Paarrot</b>{titleParts.suffix}
|
||||
<span style={{ fontWeight: 700 }}>Paarrot</span>{titleParts.suffix}
|
||||
</Text>
|
||||
{newMessage && (
|
||||
{notifications.map((notification) => (
|
||||
<div
|
||||
className={`${css.NewMessagePill} ${isExiting ? css.NewMessagePillExiting : ''}`}
|
||||
onClick={handleNotificationClick}
|
||||
key={notification.id}
|
||||
className={`${css.NewMessagePill} ${notification.exiting ? css.NewMessagePillExiting : ''}`}
|
||||
onClick={handleNotificationClick(notification)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title={`${newMessage.senderName}: ${newMessage.preview}`}
|
||||
title={`${notification.senderName}: ${notification.preview}`}
|
||||
>
|
||||
<span>{newMessage.senderName}: {newMessage.preview}</span>
|
||||
<span>{notification.senderName}: {notification.preview}</span>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
{children}
|
||||
</Box>
|
||||
<WindowControls />
|
||||
|
||||
Reference in New Issue
Block a user