Compare commits

..

2 Commits

Author SHA1 Message Date
jared 2d59be9dd3 fix: DM preview shows message body for E2EE rooms; filter inputs match members panel style
CI / Build & Quality Checks (push) Successful in 10m23s
- RoomNavItem: change isEncrypted() to isDecryptionFailure() so DM
  previews show actual message body for successfully decrypted E2EE
  events instead of always showing 'Encrypted message'
- Home.tsx / Direct.tsx: upgrade filter inputs to size 400 / radii 400
  with search icon prefix to match the members list search bar style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:39:39 -04:00
jared bb65c96454 fix: media gallery encrypted rooms + Ctrl+K double-menu
- MediaGallery: switch from createMessagesRequest (returns raw encrypted
  events) to room.getLiveTimeline().getEvents() which gives already-
  decrypted MatrixEvent objects. Load More uses paginateEventTimeline().
- QuickSwitcher: change hotkey from Ctrl+K to Ctrl+P to avoid conflict
  with the existing SearchModalRenderer mod+k handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:36:54 -04:00
5 changed files with 45 additions and 51 deletions
+1 -1
View File
@@ -454,7 +454,7 @@ function RoomNavItem_({
// Skip pure membership events // Skip pure membership events
if (type === StateEvent.RoomMember) return null; if (type === StateEvent.RoomMember) return null;
let body: string; let body: string;
if (latestEvent.isEncrypted()) { if (latestEvent.isDecryptionFailure()) {
body = 'Encrypted message'; body = 'Encrypted message';
} else if (type === MessageEvent.Sticker) { } else if (type === MessageEvent.Sticker) {
body = 'Sticker'; body = 'Sticker';
+33 -41
View File
@@ -13,7 +13,7 @@ import {
TooltipProvider, TooltipProvider,
config, config,
} from 'folds'; } from 'folds';
import { Direction, EventType, MatrixEvent, MsgType, Room } from 'matrix-js-sdk'; import { EventType, MsgType, Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { mxcUrlToHttp } from '../../utils/matrix'; import { mxcUrlToHttp } from '../../utils/matrix';
@@ -65,53 +65,45 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [tab, setTab] = useState<GalleryTab>('image'); const [tab, setTab] = useState<GalleryTab>('image');
const [events, setEvents] = useState<MatrixEvent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [paginationToken, setPaginationToken] = useState<string | null>(null); const [canLoadMore, setCanLoadMore] = useState(true);
const msgtype = TAB_MSGTYPES[tab]; const msgtype = TAB_MSGTYPES[tab];
const loadMedia = useCallback( // Read already-decrypted events from the live timeline (works for E2EE rooms)
async (fromToken: string | null, append: boolean) => { const getFilteredEvents = useCallback(() => {
setLoading(true); const timeline = room.getLiveTimeline();
try { return timeline
const response = await mx.createMessagesRequest( .getEvents()
room.roomId, .filter((ev) => {
fromToken, if (ev.isRedacted()) return false;
100, const content = ev.getContent();
Direction.Backward, return ev.getType() === EventType.RoomMessage && content.msgtype === msgtype;
undefined, })
); .slice()
const { end, chunk } = response; .reverse(); // newest first
const filtered = chunk }, [room, msgtype]);
.filter(
(ev) =>
ev.type === EventType.RoomMessage &&
ev.content?.msgtype === msgtype &&
!ev.unsigned?.redacted_because,
)
.map((ev) => new MatrixEvent(ev));
setEvents((prev) => (append ? [...prev, ...filtered] : filtered)); const [events, setEvents] = useState(() => getFilteredEvents());
setPaginationToken(end ?? null);
} catch {
// silently swallow fetch errors — gallery stays showing what it has
} finally {
setLoading(false);
}
},
[mx, room.roomId, msgtype],
);
useEffect(() => { useEffect(() => {
setEvents([]); setEvents(getFilteredEvents());
setPaginationToken(null); setCanLoadMore(true);
loadMedia(null, false).catch(() => undefined); }, [getFilteredEvents]);
}, [loadMedia]);
const handleLoadMore = () => { const handleLoadMore = useCallback(async () => {
if (paginationToken) loadMedia(paginationToken, true).catch(() => undefined); setLoading(true);
}; try {
const timeline = room.getLiveTimeline();
const hasMore = await mx.paginateEventTimeline(timeline, { backwards: true, limit: 100 });
setEvents(getFilteredEvents());
setCanLoadMore(hasMore);
} catch {
// silently swallow
} finally {
setLoading(false);
}
}, [mx, room, getFilteredEvents]);
return ( return (
<Box <Box
@@ -306,7 +298,7 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
)} )}
{/* Load more */} {/* Load more */}
{paginationToken !== null && !loading && ( {canLoadMore && !loading && events.length > 0 && (
<Box justifyContent="Center" style={{ padding: config.space.S200 }}> <Box justifyContent="Center" style={{ padding: config.space.S200 }}>
<Button <Button
size="300" size="300"
+1 -1
View File
@@ -267,7 +267,7 @@ function QuickSwitcherFeature() {
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
e.preventDefault(); e.preventDefault();
setOpen(true); setOpen(true);
} }
+5 -4
View File
@@ -270,14 +270,15 @@ export function Direct() {
} }
placeholder="Filter DMs…" placeholder="Filter DMs…"
variant="Surface" variant="Surface"
size="300" size="400"
radii="300" radii="400"
before={<Icon size="50" src={Icons.Search} />}
after={ after={
filterQuery ? ( filterQuery ? (
<IconButton <IconButton
onClick={() => setFilterQuery('')} onClick={() => setFilterQuery('')}
size="300" size="400"
radii="300" radii="Pill"
variant="Background" variant="Background"
fill="None" fill="None"
aria-label="Clear filter" aria-label="Clear filter"
+5 -4
View File
@@ -402,14 +402,15 @@ export function Home() {
onChange={(e: ChangeEvent<HTMLInputElement>) => setFilterQuery(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setFilterQuery(e.target.value)}
placeholder="Filter rooms…" placeholder="Filter rooms…"
variant="Surface" variant="Surface"
size="300" size="400"
radii="300" radii="400"
before={<Icon size="50" src={Icons.Search} />}
after={ after={
filterQuery ? ( filterQuery ? (
<IconButton <IconButton
onClick={() => setFilterQuery('')} onClick={() => setFilterQuery('')}
size="300" size="400"
radii="300" radii="Pill"
variant="Background" variant="Background"
fill="None" fill="None"
aria-label="Clear filter" aria-label="Clear filter"