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

@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
import { useSetAtom } from 'jotai';
import { RoomView } from './RoomView';
import { MembersDrawer } from './MembersDrawer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
@@ -13,11 +14,13 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../utils/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
import { activeRoomIdAtom } from '../../state/activeRoom';
export function Room() {
const { eventId } = useParams();
const room = useRoom();
const mx = useMatrixClient();
const setActiveRoomId = useSetAtom(activeRoomIdAtom);
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
@@ -25,6 +28,12 @@ export function Room() {
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
// Update titlebar with current room ID
useEffect(() => {
setActiveRoomId(room.roomId);
return () => setActiveRoomId(undefined);
}, [room.roomId, setActiveRoomId]);
useKeyDown(
window,
useCallback(

View File

@@ -174,6 +174,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
selectedFiles.map((f) => f.file)
);
const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
const editorRef = useRef<HTMLDivElement>(null);
const imagePackRooms: Room[] = useImagePackRooms(roomId, roomToParents);
@@ -384,7 +385,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
resetEditorHistory(editor);
setReplyDraft(undefined);
sendTypingStatus(false);
}, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands]);
}, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, editorRef]);
const handleKeyDown: KeyboardEventHandler = useCallback(
(evt) => {
@@ -544,6 +545,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
/>
)}
<CustomEditor
ref={editorRef}
editableName="RoomInput"
editor={editor}
placeholder="Send a message..."

View File

@@ -118,6 +118,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
import { useIsDirectRoom } from '../../hooks/useRoom';
import { setupCopyHandler } from '../../utils/copyHandler';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useRoomCreators } from '../../hooks/useRoomCreators';
@@ -906,6 +907,28 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}
}, [scrollToElement, editId]);
// Setup copy handler for better copy formatting (Name - Time: Message)
useEffect(() => {
const getMessageData = (eventId: string) => {
const timelineSet = room.getUnfilteredTimelineSet();
const evtTimeline = timelineSet.getTimelineForEvent(eventId);
if (!evtTimeline) return null;
const mEvent = evtTimeline.getEvents().find((e) => e.getId() === eventId);
if (!mEvent) return null;
const sender = getMemberDisplayName(room, mEvent.getSender() ?? '')
?? getMxIdLocalPart(mEvent.getSender() ?? '')
?? mEvent.getSender()
?? 'Unknown';
const content = mEvent.getContent().body ?? '';
return {
sender,
ts: mEvent.getTs(),
content,
};
};
return setupCopyHandler(scrollRef, getMessageData, hour24Clock);
}, [room, hour24Clock]);
const handleJumpToLatest = () => {
if (eventId) {
navigateRoom(room.roomId, undefined, { replace: true });

View File

@@ -28,6 +28,7 @@ import React, {
MouseEventHandler,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import FocusTrap from 'focus-trap-react';
@@ -353,6 +354,42 @@ export const MessageCopyLinkItem = as<
);
});
export const MessageCopyRawTextItem = as<
'button',
{
mEvent: MatrixEvent;
onClose?: () => void;
}
>(({ mEvent, onClose, ...props }, ref) => {
const handleCopy = () => {
const content = mEvent.getContent();
const rawText = content.body ?? '';
if (rawText) {
copyToClipboard(rawText);
}
onClose?.();
};
// Only show for text messages
const content = mEvent.getContent();
if (!content.body || mEvent.isRedacted()) return null;
return (
<MenuItem
size="300"
after={<Icon size="100" src={Icons.File} />}
radii="300"
onClick={handleCopy}
{...props}
ref={ref}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Copy Raw Text
</Text>
</MenuItem>
);
});
export const MessagePinItem = as<
'button',
{
@@ -742,6 +779,23 @@ export const Message = as<'div', MessageProps>(
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
const [shiftHeld, setShiftHeld] = useState(false);
// Track shift key state
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') setShiftHeld(true);
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') setShiftHeld(false);
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
@@ -986,6 +1040,36 @@ export const Message = as<'div', MessageProps>(
<Icon src={Icons.Pencil} size="100" />
</IconButton>
)}
{mEvent.getContent().body && !mEvent.isRedacted() && (
<IconButton
onClick={() => {
const rawText = mEvent.getContent().body ?? '';
if (rawText) copyToClipboard(rawText);
}}
variant="SurfaceVariant"
size="300"
radii="300"
title="Copy Raw Text"
>
<Icon src={Icons.File} size="100" />
</IconButton>
)}
{shiftHeld && canDelete && (
<IconButton
onClick={() => {
const eventId = mEvent.getId();
if (eventId) {
mx.redactEvent(room.roomId, eventId);
}
}}
variant="Critical"
size="300"
radii="300"
title="Delete Message (Shift held)"
>
<Icon src={Icons.Delete} size="100" />
</IconButton>
)}
<PopOut
anchor={menuAnchor}
position="Bottom"
@@ -1112,6 +1196,7 @@ export const Message = as<'div', MessageProps>(
/>
)}
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
<MessageCopyRawTextItem mEvent={mEvent} onClose={closeMenu} />
{canPinEvent && (
<MessagePinItem room={room} mEvent={mEvent} onClose={closeMenu} />
)}
@@ -1281,6 +1366,7 @@ export const Event = as<'div', EventProps>(
/>
)}
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
<MessageCopyRawTextItem mEvent={mEvent} onClose={closeMenu} />
</Box>
{((!mEvent.isRedacted() && canDelete && !stateEvent) ||
(mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (

View File

@@ -15,6 +15,9 @@ export const MessageOptionsBase = style([
top: toRem(-30),
right: 0,
zIndex: 1,
// Extend hitbox to the left for easier hover targeting
paddingLeft: toRem(100),
marginLeft: toRem(-100),
},
]);
export const MessageOptionsBar = style([