diff --git a/index.html b/index.html index 6854c5abc..8030bacba 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + Lotus Chat diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index 68c1d964f..4fc42deb6 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -431,6 +431,20 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) { } }, [pipMode, callVisible]); + React.useEffect(() => { + if (!pipMode) return; + const onPipWindowResize = (): void => { + const el = callEmbedRef.current; + if (!el) return; + const l = parseFloat(el.style.left); + const t = parseFloat(el.style.top); + if (!isNaN(l)) el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`; + if (!isNaN(t)) el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`; + }; + window.addEventListener('resize', onPipWindowResize); + return () => window.removeEventListener('resize', onPipWindowResize); + }, [pipMode, callEmbedRef]); + const handlePipMouseDown = (e: React.MouseEvent) => { const el = callEmbedRef.current; if (!el) return; const rect = el.getBoundingClientRect(); diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index bd848dd5a..bf623edad 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -140,6 +140,8 @@ export const CustomEditor = forwardRef( data-editable-name={editableName} className={css.EditorTextarea} placeholder={placeholder} + aria-label={placeholder ?? 'Message input'} + aria-multiline="true" renderPlaceholder={renderPlaceholder} renderElement={renderElement} renderLeaf={renderLeaf} diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index 32fe53501..ea4361a94 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -410,6 +410,7 @@ export function MLocation({ content }: MLocationProps) { }} scrolling="no" loading="lazy" + sandbox="allow-scripts allow-same-origin" /> {`${lat.toFixed(5)}, ${lon.toFixed(5)}`} diff --git a/src/app/features/room/RoomViewTyping.tsx b/src/app/features/room/RoomViewTyping.tsx index 1142a3a81..fd97f9497 100644 --- a/src/app/features/room/RoomViewTyping.tsx +++ b/src/app/features/room/RoomViewTyping.tsx @@ -44,7 +44,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( }; return ( -
+
( userIds={readReceiptUsers} /> )} - {isMine && readReceiptUsers.length === 0 && ( + {isMine && !mEvent.isState() && readReceiptUsers.length === 0 && ( )} diff --git a/src/app/hooks/useRoomReadPositions.ts b/src/app/hooks/useRoomReadPositions.ts index 316fbabbc..8ae7c30ee 100644 --- a/src/app/hooks/useRoomReadPositions.ts +++ b/src/app/hooks/useRoomReadPositions.ts @@ -1,12 +1,16 @@ -import { Room, RoomEvent, MatrixEvent } from 'matrix-js-sdk'; +import { Room, RoomEvent, RoomMemberEvent, MatrixEvent } from 'matrix-js-sdk'; import { useEffect, useState } from 'react'; import { useMatrixClient } from './useMatrixClient'; import { reactionOrEditEvent } from '../utils/room'; // Receipts can land on reaction/edit events which RoomTimeline skips (renders null). // Walk backwards from the receipt event to find the nearest event that IS rendered. -function nearestRenderableId(liveEvents: MatrixEvent[], evtId: string): string | null { - const idx = liveEvents.findIndex(e => e.getId() === evtId); +function nearestRenderableId( + liveEvents: MatrixEvent[], + eventIndex: Map, + evtId: string +): string | null { + const idx = eventIndex.get(evtId) ?? -1; if (idx === -1) return null; for (let i = idx; i >= 0; i--) { const e = liveEvents[i]; @@ -18,11 +22,15 @@ function nearestRenderableId(liveEvents: MatrixEvent[], evtId: string): string | function computePositions(room: Room, myUserId: string): Map { const map = new Map(); const liveEvents = room.getLiveTimeline().getEvents(); + // Build O(1) index once instead of O(T) findIndex per member + const eventIndex = new Map( + liveEvents.map((e, i) => [e.getId() ?? '', i]) + ); for (const member of room.getJoinedMembers()) { if (member.userId === myUserId) continue; const evtId = room.getEventReadUpTo(member.userId); if (!evtId) continue; - const targetId = nearestRenderableId(liveEvents, evtId); + const targetId = nearestRenderableId(liveEvents, eventIndex, evtId); if (!targetId) continue; const arr = map.get(targetId); if (arr) arr.push(member.userId); @@ -46,10 +54,13 @@ export function useRoomReadPositions(room: Room): Map { debounceTimer = null; }, 150); }; + const onMembership = (): void => setPositions(computePositions(room, myUserId)); room.on(RoomEvent.Receipt, onReceipt); + room.on(RoomMemberEvent.Membership, onMembership); return () => { if (debounceTimer !== null) clearTimeout(debounceTimer); room.removeListener(RoomEvent.Receipt, onReceipt); + room.removeListener(RoomMemberEvent.Membership, onMembership); }; }, [room, myUserId]); diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx index c394f5934..45d110894 100644 --- a/src/app/pages/ThemeManager.tsx +++ b/src/app/pages/ThemeManager.tsx @@ -50,6 +50,11 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) { ? (terminalIsLight ? LotusTerminalLightTheme : LotusTerminalTheme) : activeTheme; + // Boot animation only fires when lotusTerminal is toggled on, not on every theme change + useEffect(() => { + if (lotusTerminal) runLotusBootSequence(); + }, [lotusTerminal]); + useEffect(() => { document.body.className = ''; document.body.classList.add(configClass, varsClass); @@ -57,7 +62,6 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) { if (lotusTerminal) { document.documentElement.setAttribute('data-theme', terminalIsLight ? 'light' : 'dark'); document.body.classList.add(lotusTerminalBodyClass); - runLotusBootSequence(); } else { document.documentElement.removeAttribute('data-theme'); } diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 961131d9b..f9eef4d66 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -52,7 +52,7 @@ export const LINKIFY_OPTS: LinkifyOpts = { rel: 'noreferrer noopener', }, validate: { - url: (value) => /^(https|http|ftp|mailto|magnet)?:/.test(value), + url: (value) => /^(https?|ftp|mailto|magnet):/.test(value), }, ignoreTags: ['span'], }; diff --git a/src/app/utils/sanitize.ts b/src/app/utils/sanitize.ts index c199f69a4..530d37a67 100644 --- a/src/app/utils/sanitize.ts +++ b/src/app/utils/sanitize.ts @@ -84,7 +84,7 @@ const transformFontTag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, - style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, + style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(), }, }); @@ -92,7 +92,7 @@ const transformSpanTag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, - style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, + style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(), }, }); diff --git a/src/lotus-terminal.css.ts b/src/lotus-terminal.css.ts index baa9bad2f..ba540e92c 100644 --- a/src/lotus-terminal.css.ts +++ b/src/lotus-terminal.css.ts @@ -686,3 +686,34 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-url-p color: '#0062b8 !important' as any, }); +// ── GIF picker light TDS (dark-mode rules already exist via [data-gif-terminal]) ── +globalStyle( + `html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] input,` + + `html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] form`, { + background: '#f4f6fa !important' as any, + color: '#111827 !important' as any, + border: '1px solid rgba(196,78,0,0.28) !important' as any, + fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any, + fontSize: '12px !important' as any, + boxShadow: 'none !important' as any, +}); +globalStyle(`html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, { + borderColor: 'rgba(196,78,0,0.60) !important' as any, + boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any, + outline: 'none !important' as any, +}); +globalStyle(`html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`, { + color: 'rgba(196,78,0,0.45) !important' as any, +}); +globalStyle(`html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] svg,` + + `html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`, { + display: 'none !important' as any, +}); +globalStyle(`html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-track`, { + background: '#e2e7ef', +}); +globalStyle(`html[data-theme="light"] body.\${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-thumb`, { + background: 'rgba(196,78,0,0.35)', + borderRadius: '2px', +}); +