feat: add Vite configuration with custom plugins and server settings

This commit is contained in:
2026-02-21 16:16:15 +11:00
parent 576e6d77c1
commit 6c741577ce
32 changed files with 1937 additions and 20 deletions

View 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>
);
}