diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index ea4361a94..d7dceef3c 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -410,7 +410,7 @@ export function MLocation({ content }: MLocationProps) { }} scrolling="no" loading="lazy" - sandbox="allow-scripts allow-same-origin" + sandbox="allow-scripts" /> {`${lat.toFixed(5)}, ${lon.toFixed(5)}`} diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index 478486a4b..08019b1c7 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/media-has-caption */ -import React, { ReactNode, useCallback, useRef, useState } from 'react'; +import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { Badge, Chip, Icon, IconButton, Icons, ProgressBar, Spinner, Text, toRem } from 'folds'; import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import { Range } from 'react-range'; @@ -65,6 +65,19 @@ export function AudioContent({ const audioRef = useRef(null); + useEffect( + () => () => { + if ( + srcState.status === AsyncStatus.Success && + typeof srcState.data === 'string' && + srcState.data.startsWith('blob:') + ) { + URL.revokeObjectURL(srcState.data); + } + }, + [srcState] + ); + const [currentTime, setCurrentTime] = useState(0); // duration in seconds. (NOTE: info.duration is in milliseconds) const infoDuration = info.duration ?? 0; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 91c5747b6..f8b1ee5ee 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -802,7 +802,7 @@ export const RoomInput = forwardRef( )} - + diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 2f08f2d1f..6bbf72a36 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -341,7 +341,7 @@ const useTimelinePagination = ( }) ); if (err) { - // TODO: handle pagination error. + fetching = false; return; } const fetchedTimeline = @@ -622,7 +622,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli // Check if the document is in focus (user is actively viewing the app), // and either there are no unread messages or the latest message is from the current user. // If either condition is met, trigger the markAsRead function to send a read receipt. - requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!, hideActivity)); + const _roomId = mEvt.getRoomId(); + if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity)); } if (!document.hasFocus() && !unreadInfo) { @@ -826,7 +827,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const evtTimeline = getEventTimeline(room, readUptoEventId); const absoluteIndex = evtTimeline && getEventIdAbsoluteIndex(linkedTimelines, evtTimeline, readUptoEventId); - if (absoluteIndex) { + if (typeof absoluteIndex === 'number') { scrollToItem(absoluteIndex, { behavior: 'instant', align: 'start', diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index d8c306652..b5c34c886 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { Box, Text, config } from 'folds'; import { EventType } from 'matrix-js-sdk'; import { ReactEditor } from 'slate-react'; @@ -98,8 +98,13 @@ export function RoomView({ eventId }: { eventId?: string }) { ) ); + const chatBgStyle = useMemo( + () => getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark), + [chatBackground, lotusTerminal, isDark] + ); + return ( - + ( const mx = useMatrixClient(); const typingMembers = useRoomTypingMember(room.roomId); - const typingNames = typingMembers - .filter((receipt) => receipt.userId !== mx.getUserId()) - .map( - (receipt) => getMemberDisplayName(room, receipt.userId) ?? getMxIdLocalPart(receipt.userId) - ) - .reverse(); + const myUserId = mx.getUserId(); + const typingNames = useMemo( + () => + typingMembers + .filter((receipt) => receipt.userId !== myUserId) + .map( + (receipt) => + getMemberDisplayName(room, receipt.userId) ?? getMxIdLocalPart(receipt.userId) + ) + .reverse(), + [typingMembers, myUserId, room] + ); if (typingNames.length === 0) { return null; diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index c02b0e94e..4f6f951b6 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -232,9 +232,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { const roomIdOrAliases = rawIds.filter( (idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias) ); - roomIdOrAliases.forEach(async (idOrAlias) => { - await mx.joinRoom(idOrAlias); - }); + await Promise.all(roomIdOrAliases.map((idOrAlias) => mx.joinRoom(idOrAlias))); }, }, [Command.Leave]: { diff --git a/src/app/hooks/useComposingCheck.ts b/src/app/hooks/useComposingCheck.ts index 687a49274..6a6932da0 100644 --- a/src/app/hooks/useComposingCheck.ts +++ b/src/app/hooks/useComposingCheck.ts @@ -21,7 +21,7 @@ export function useCompositionEndTracking(): void { return () => { window.removeEventListener('compositionend', recordCompositionEnd, { capture: true }); }; - }); + }, [recordCompositionEnd]); } interface IsComposingLike { diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts index ba5b0bc9b..6ecfe7a94 100644 --- a/src/app/pages/auth/login/loginUtil.ts +++ b/src/app/pages/auth/login/loginUtil.ts @@ -117,7 +117,9 @@ export const useLoginComplete = (data?: CustomLoginResponse) => { setFallbackSession(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl); const afterLoginRedirectUrl = getAfterLoginRedirectPath(); deleteAfterLoginRedirectPath(); - navigate(afterLoginRedirectUrl ?? getHomePath(), { replace: true }); + const _redir = afterLoginRedirectUrl; + const _safePath = (_redir && /^\/(?!\/)/.test(_redir)) ? _redir : getHomePath(); + navigate(_safePath, { replace: true }); } }, [data, navigate]); }; diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index f9eef4d66..c5b01313c 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -461,11 +461,12 @@ export const getReactCustomHtmlParser = ( { if (e.key === 'Enter' || e.key === ' ') params.handleSpoilerClick?.(e as any); }} onClick={params.handleSpoilerClick} className={css.Spoiler()} - aria-pressed + aria-label="Spoiler — click to reveal" + aria-pressed={false} style={{ cursor: 'pointer' }} > {domToReact(children, opts)} diff --git a/src/app/state/hooks/settings.ts b/src/app/state/hooks/settings.ts index d90c76646..52d3cf959 100644 --- a/src/app/state/hooks/settings.ts +++ b/src/app/state/hooks/settings.ts @@ -26,7 +26,8 @@ export const useSetting = ( key: K ): [Settings[K], ReturnType>] => { const selector = useMemo(() => (s: Settings) => s[key], [key]); - const setting = useAtomValue(selectAtom(settingsAtom, selector)); + const derivedAtom = useMemo(() => selectAtom(settingsAtom, selector), [settingsAtom, selector]); + const setting = useAtomValue(derivedAtom); const setter = useSetSetting(settingsAtom, key); return [setting, setter]; diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 005b64227..5c098a956 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -102,17 +102,19 @@ const defaultSettings: Settings = { pttKey: 'Space', }; -export const getSettings = () => { - const settings = localStorage.getItem(STORAGE_KEY); - if (settings === null) return defaultSettings; - return { - ...defaultSettings, - ...(JSON.parse(settings) as Settings), - }; +export const getSettings = (): Settings => { + try { + const settings = localStorage.getItem(STORAGE_KEY); + if (settings === null) return defaultSettings; + return { ...defaultSettings, ...(JSON.parse(settings) as Settings) }; + } catch { + localStorage.removeItem(STORAGE_KEY); + return defaultSettings; + } }; export const setSettings = (settings: Settings) => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch { /* quota */ } }; const baseSettings = atom(getSettings()); diff --git a/src/app/state/typingMembers.ts b/src/app/state/typingMembers.ts index e94ba9726..a070e5038 100644 --- a/src/app/state/typingMembers.ts +++ b/src/app/state/typingMembers.ts @@ -89,27 +89,34 @@ export const roomIdToTypingMembersAtom = atom< // remove typing receipt after some timeout // to prevent stuck typing members - setTimeout(() => { - const { roomId, userId } = action; - const timeout = timeoutReceipt( - get(baseRoomIdToTypingMembersAtom), - roomId, - userId, - TYPING_TIMEOUT_MS - ); - if (timeout) { - set( - baseRoomIdToTypingMembersAtom, - produce(get(baseRoomIdToTypingMembersAtom), (draft) => - deleteTypingMember(draft, { - type: 'DELETE', - roomId, - userId, - }) - ) + const timerKey = `${action.roomId}:${action.userId}`; + const existingTimer = typingTimers.get(timerKey); + if (existingTimer !== undefined) clearTimeout(existingTimer); + typingTimers.set( + timerKey, + setTimeout(() => { + typingTimers.delete(timerKey); + const { roomId, userId } = action; + const timeout = timeoutReceipt( + get(baseRoomIdToTypingMembersAtom), + roomId, + userId, + TYPING_TIMEOUT_MS ); - } - }, TYPING_TIMEOUT_MS); + if (timeout) { + set( + baseRoomIdToTypingMembersAtom, + produce(get(baseRoomIdToTypingMembersAtom), (draft) => + deleteTypingMember(draft, { + type: 'DELETE', + roomId, + userId, + }) + ) + ); + } + }, TYPING_TIMEOUT_MS) + ); } if ( diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index 6bda28021..74cb51846 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -117,15 +117,10 @@ export const nameInitials = (str: string | undefined | null, len = 1): string => }; export const randomStr = (len = 12): string => { - let str = ''; - const minCode = 'A'.charCodeAt(0); - const maxCode = 'Z'.charCodeAt(0); - - for (let i = 0; i < len; i += 1) { - const code = Math.floor(Math.random() * (maxCode - minCode + 1) + minCode); - str += String.fromCharCode(code); - } - return str; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const buf = new Uint8Array(len); + crypto.getRandomValues(buf); + return Array.from(buf, (b) => chars[b % chars.length]).join(''); }; export const suffixRename = (name: string, validator: (newName: string) => boolean): string => { diff --git a/src/lotus-terminal.css.ts b/src/lotus-terminal.css.ts index 137d4a06b..b1e4ea922 100644 --- a/src/lotus-terminal.css.ts +++ b/src/lotus-terminal.css.ts @@ -88,7 +88,7 @@ export const lotusTerminalBodyClass = style({ }); // Font on all descendants -globalStyle(`body.${lotusTerminalBodyClass} *`, { +globalStyle(`body.${lotusTerminalBodyClass}`, { fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace", }); diff --git a/vite.config.js b/vite.config.js index a5af94d8b..e8c016ddf 100644 --- a/vite.config.js +++ b/vite.config.js @@ -22,10 +22,6 @@ const copyFiles = { dest: '', rename: 'pdf.worker.min.js', }, - { - src: 'netlify.toml', - dest: '', - }, { src: 'config.json', dest: '', @@ -129,7 +125,7 @@ export default defineConfig({ }, build: { outDir: 'dist', - sourcemap: true, + sourcemap: false, copyPublicDir: false, rollupOptions: { plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],