+
(
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',
+});
+