feat: add Vite configuration with custom plugins and server settings
This commit is contained in:
244
src/app/components/title-bar/TitleBar.tsx
Normal file
244
src/app/components/title-bar/TitleBar.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
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 { 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 { activeRoomIdAtom } from '../../state/activeRoom';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { getDirectRoomPath, getHomeRoomPath, getSpaceRoomPath } from '../../pages/pathUtils';
|
||||
|
||||
type NewMessageNotification = {
|
||||
roomId: string;
|
||||
eventId: string;
|
||||
senderName: string;
|
||||
preview: string;
|
||||
};
|
||||
|
||||
export type TitleBarProps = {
|
||||
mx?: MatrixClient;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
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 navigate = useNavigate();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
// Clear dismiss timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-dismiss notification after 10 seconds
|
||||
const scheduleDismiss = useCallback(() => {
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
}
|
||||
dismissTimerRef.current = setTimeout(() => {
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
setNewMessage(null);
|
||||
setIsExiting(false);
|
||||
}, 200); // Wait for exit animation
|
||||
}, 10000);
|
||||
}, []);
|
||||
|
||||
// Navigate to the new message
|
||||
const handleNotificationClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!newMessage || !mx) return;
|
||||
|
||||
const targetRoomId = newMessage.roomId;
|
||||
const eventId = newMessage.eventId;
|
||||
|
||||
// Dismiss notification
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
setNewMessage(null);
|
||||
setIsExiting(false);
|
||||
}, 200);
|
||||
|
||||
if (dismissTimerRef.current) {
|
||||
clearTimeout(dismissTimerRef.current);
|
||||
}
|
||||
|
||||
// Simple navigation approach - just try all paths
|
||||
try {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, targetRoomId);
|
||||
|
||||
// Check if it's a direct message
|
||||
if (mDirects.has(targetRoomId)) {
|
||||
const path = getDirectRoomPath(roomIdOrAlias, eventId);
|
||||
console.log('Navigating to direct room:', path);
|
||||
navigate(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it belongs to a space
|
||||
const orphanParents = getOrphanParents(roomToParents, targetRoomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const parentSpace = guessPerfectParent(mx, targetRoomId, orphanParents) ?? orphanParents[0];
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||
const path = getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId);
|
||||
console.log('Navigating to space room:', path);
|
||||
navigate(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to home room
|
||||
const path = getHomeRoomPath(roomIdOrAlias, eventId);
|
||||
console.log('Navigating to home room:', path);
|
||||
navigate(path);
|
||||
} catch (error) {
|
||||
console.error('Error navigating to room:', error);
|
||||
// Last resort - navigate without event ID
|
||||
try {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, targetRoomId);
|
||||
navigate(getHomeRoomPath(roomIdOrAlias));
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback navigation also failed:', fallbackError);
|
||||
}
|
||||
}
|
||||
}, [newMessage, mx, roomToParents, mDirects, navigate]);
|
||||
|
||||
// Subscribe to room changes for unread updates and new messages
|
||||
useEffect(() => {
|
||||
if (!mx) return;
|
||||
|
||||
const update = () => setUpdateCounter(c => c + 1);
|
||||
|
||||
const handleTimelineEvent = (event: MatrixEvent, room: Room | undefined) => {
|
||||
update();
|
||||
|
||||
// Check if this is a new message we should notify about
|
||||
if (!room || !event) return;
|
||||
|
||||
// Ignore if it's the currently active room
|
||||
if (room.roomId === roomId) return;
|
||||
|
||||
// Ignore if it's our own message
|
||||
if (event.getSender() === mx.getUserId()) return;
|
||||
|
||||
// Only notify for actual messages
|
||||
if (event.getType() !== 'm.room.message') return;
|
||||
|
||||
// Get content
|
||||
const content = event.getContent();
|
||||
if (!content.body) return;
|
||||
|
||||
// Get sender display name
|
||||
const sender = room.getMember(event.getSender() || '');
|
||||
const senderName = sender?.name || event.getSender() || 'Unknown';
|
||||
|
||||
// Create preview (truncate if needed)
|
||||
const preview = content.body.length > 30
|
||||
? content.body.substring(0, 30) + '...'
|
||||
: content.body;
|
||||
|
||||
setNewMessage({
|
||||
roomId: room.roomId,
|
||||
eventId: event.getId() || '',
|
||||
senderName,
|
||||
preview,
|
||||
});
|
||||
setIsExiting(false);
|
||||
scheduleDismiss();
|
||||
};
|
||||
|
||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
mx.on(RoomEvent.Receipt, update);
|
||||
mx.on(ClientEvent.Room, update);
|
||||
|
||||
return () => {
|
||||
mx.off(RoomEvent.Timeline, handleTimelineEvent);
|
||||
mx.off(RoomEvent.Receipt, update);
|
||||
mx.off(ClientEvent.Room, update);
|
||||
};
|
||||
}, [mx, roomId, scheduleDismiss]);
|
||||
|
||||
// Get room name
|
||||
const roomName = useMemo(() => {
|
||||
if (!mx || !roomId) return null;
|
||||
const room = mx.getRoom(roomId);
|
||||
return room?.name || null;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mx, roomId, updateCounter]);
|
||||
|
||||
// Get total unread count
|
||||
const totalUnread = useMemo(() => {
|
||||
if (!mx) return 0;
|
||||
const unreadInfos = getUnreadInfos(mx);
|
||||
return unreadInfos.reduce((sum, info) => sum + info.total, 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mx, updateCounter]);
|
||||
|
||||
// Build title parts
|
||||
const titleParts = useMemo(() => {
|
||||
let suffix = '';
|
||||
if (roomName) {
|
||||
suffix += ` - ${roomName}`;
|
||||
}
|
||||
if (totalUnread > 0) {
|
||||
suffix += ` (${totalUnread})`;
|
||||
}
|
||||
return { suffix };
|
||||
}, [roomName, totalUnread]);
|
||||
|
||||
const handleDragStart = async () => {
|
||||
try {
|
||||
await invoke('window_start_drag');
|
||||
} catch (error) {
|
||||
console.error('Failed to start window drag:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Only render on desktop (when Tauri is available)
|
||||
if (typeof (window as any).__TAURI__ === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={css.TitleBar} alignItems="Center" justifyContent="SpaceBetween">
|
||||
<Box
|
||||
className={css.TitleBarDragRegion}
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
grow="Yes"
|
||||
onMouseDown={handleDragStart}
|
||||
>
|
||||
<Text size="T200" truncate style={{ color: 'inherit' }}>
|
||||
<b>Paarrot</b>{titleParts.suffix}
|
||||
</Text>
|
||||
{newMessage && (
|
||||
<div
|
||||
className={`${css.NewMessagePill} ${isExiting ? css.NewMessagePillExiting : ''}`}
|
||||
onClick={handleNotificationClick}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title={`${newMessage.senderName}: ${newMessage.preview}`}
|
||||
>
|
||||
<span>{newMessage.senderName}: {newMessage.preview}</span>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
<WindowControls />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user