(
}}
>
{virtualizer.getVirtualItems().map((vItem) => {
- const eventId = sortedPinnedEvent[vItem.index];
- if (!eventId) return null;
+ const event = events[vItem.index];
+ if (!event.getId()) return null;
return (
(
variant="SurfaceVariant"
direction="Column"
>
-
@@ -441,7 +476,7 @@ export const ThreadsMenu = forwardRef
(
No Threads
- This room does not have any threads yet.
+ Threads you are participating in will appear here.
diff --git a/src/app/features/room/threads-menu/index.ts b/src/app/features/room/threads-menu/index.ts
new file mode 100644
index 00000000..98286736
--- /dev/null
+++ b/src/app/features/room/threads-menu/index.ts
@@ -0,0 +1 @@
+export * from './ThreadsMenu';
diff --git a/src/app/hooks/useRoomThreads.ts b/src/app/hooks/useRoomThreads.ts
index 8050a5d9..d5ef24d6 100644
--- a/src/app/hooks/useRoomThreads.ts
+++ b/src/app/hooks/useRoomThreads.ts
@@ -1,9 +1,29 @@
-// import { Room } from 'matrix-js-sdk';
-// import { useMatrixClient } from './useMatrixClient';
+import { useCallback } from 'react';
+import { Direction, MatrixEvent, Room, ThreadFilterType } from 'matrix-js-sdk';
+import { useMatrixClient } from './useMatrixClient';
+import { AsyncStatus, useAsyncCallbackValue } from './useAsyncCallback';
-// export const useRoomThreads = (room: Room) => {
-// const mx = useMatrixClient();
+export const useRoomMyThreads = (room: Room): MatrixEvent[] | undefined => {
+ const mx = useMatrixClient();
-// mx.createThreadListMessagesRequest;
-// mx.processThreadRoots;
-// };
+ const [fetchState] = useAsyncCallbackValue(
+ useCallback(
+ () =>
+ mx.createThreadListMessagesRequest(
+ room.roomId,
+ null,
+ 30,
+ Direction.Backward,
+ ThreadFilterType.My
+ ),
+ [mx, room]
+ )
+ );
+
+ if (fetchState.status === AsyncStatus.Success) {
+ const roomEvents = fetchState.data.chunk;
+ const mEvents = roomEvents.map((event) => new MatrixEvent(event)).reverse();
+ return mEvents;
+ }
+ return undefined;
+};
diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts
index b4bba2ad..e80edb20 100644
--- a/src/app/utils/room.ts
+++ b/src/app/utils/room.ts
@@ -8,6 +8,7 @@ import {
IPowerLevelsContent,
IPushRule,
IPushRules,
+ IThreadBundledRelationship,
JoinRule,
MatrixClient,
MatrixEvent,
@@ -16,6 +17,7 @@ import {
RelationType,
Room,
RoomMember,
+ THREAD_RELATION_TYPE,
} from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData';
@@ -551,3 +553,13 @@ export const guessPerfectParent = (
return perfectParent;
};
+
+export const getEventThreadDetail = (
+ mEvent: MatrixEvent
+): IThreadBundledRelationship | undefined => {
+ const details = mEvent.getServerAggregatedRelation(
+ THREAD_RELATION_TYPE.name
+ );
+
+ return details;
+};
From d36938e1fd92bdbd9135690a1ac81369796505b5 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 24 Sep 2025 16:32:05 +0530
Subject: [PATCH 003/220] fix typo
---
src/app/components/thread-selector/ThreadSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/components/thread-selector/ThreadSelector.tsx b/src/app/components/thread-selector/ThreadSelector.tsx
index 407f6b09..75eb1590 100644
--- a/src/app/components/thread-selector/ThreadSelector.tsx
+++ b/src/app/components/thread-selector/ThreadSelector.tsx
@@ -77,7 +77,7 @@ export function ThreadSelector({ room, senderId, threadDetail, outlined }: Threa
- {threadDetail.count} Replies
+ {threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
{/* TODO: date */}
From 67c6785bf3ba4e90db3f607e86e71b18426f5bd9 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Thu, 25 Sep 2025 12:17:14 +0530
Subject: [PATCH 004/220] inherit font weight for time component
---
src/app/components/message/Time.css.ts | 6 ++++++
src/app/components/message/Time.tsx | 13 +++++++++++--
2 files changed, 17 insertions(+), 2 deletions(-)
create mode 100644 src/app/components/message/Time.css.ts
diff --git a/src/app/components/message/Time.css.ts b/src/app/components/message/Time.css.ts
new file mode 100644
index 00000000..f2f5ac55
--- /dev/null
+++ b/src/app/components/message/Time.css.ts
@@ -0,0 +1,6 @@
+import { style } from '@vanilla-extract/css';
+
+export const Time = style({
+ fontWeight: 'inherit',
+ flexShrink: 0,
+});
diff --git a/src/app/components/message/Time.tsx b/src/app/components/message/Time.tsx
index 3eab5cc2..702e682d 100644
--- a/src/app/components/message/Time.tsx
+++ b/src/app/components/message/Time.tsx
@@ -1,6 +1,8 @@
import React, { ComponentProps } from 'react';
import { Text, as } from 'folds';
+import classNames from 'classnames';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
+import * as css from './Time.css';
export type TimeProps = {
compact?: boolean;
@@ -22,7 +24,7 @@ export type TimeProps = {
* @returns {React.ReactElement} A element with the formatted date/time.
*/
export const Time = as<'span', TimeProps & ComponentProps>(
- ({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
+ ({ compact, hour24Clock, dateFormatString, ts, className, ...props }, ref) => {
const formattedTime = timeHourMinute(ts, hour24Clock);
let time = '';
@@ -37,7 +39,14 @@ export const Time = as<'span', TimeProps & ComponentProps>(
}
return (
-
+
{time}
);
From a6a3ac3b24c5bddda4c109d3047c2c41113b0931 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Thu, 25 Sep 2025 12:17:44 +0530
Subject: [PATCH 005/220] redesign thread selector
---
.../thread-selector/ThreadSelector.tsx | 90 ---------------
.../components/thread-selector/styles.css.ts | 20 ----
src/app/features/room/RoomTimeline.tsx | 10 +-
.../thread-selector/ThreadSelector.tsx | 105 ++++++++++++++++++
.../room/message}/thread-selector/index.ts | 0
.../message/thread-selector/styles.css.ts | 37 ++++++
.../room/threads-menu/ThreadsMenu.tsx | 5 +-
src/app/hooks/useRoomThreads.ts | 2 +-
8 files changed, 154 insertions(+), 115 deletions(-)
delete mode 100644 src/app/components/thread-selector/ThreadSelector.tsx
delete mode 100644 src/app/components/thread-selector/styles.css.ts
create mode 100644 src/app/features/room/message/thread-selector/ThreadSelector.tsx
rename src/app/{components => features/room/message}/thread-selector/index.ts (100%)
create mode 100644 src/app/features/room/message/thread-selector/styles.css.ts
diff --git a/src/app/components/thread-selector/ThreadSelector.tsx b/src/app/components/thread-selector/ThreadSelector.tsx
deleted file mode 100644
index 75eb1590..00000000
--- a/src/app/components/thread-selector/ThreadSelector.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Avatar, Box, Icon, Icons, Text } from 'folds';
-import React, { ReactNode } from 'react';
-import classNames from 'classnames';
-import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
-import { ContainerColor } from '../../styles/ContainerColor.css';
-import * as css from './styles.css';
-import { UserAvatar } from '../user-avatar';
-import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
-
-export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
- return {children};
-}
-
-type ThreadSelectorProps = {
- room: Room;
- senderId: string;
- threadDetail: IThreadBundledRelationship;
- outlined?: boolean;
-};
-
-export function ThreadSelector({ room, senderId, threadDetail, outlined }: ThreadSelectorProps) {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
- const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
-
- const latestEvent = threadDetail.latest_event;
- const latestSenderId = latestEvent.sender;
- const latestSenderAvatarMxc = getMemberAvatarMxc(room, latestSenderId);
- const latestDisplayName =
- getMemberDisplayName(room, latestSenderId) ??
- getMxIdLocalPart(latestSenderId) ??
- latestSenderId;
-
- const latestEventTs = latestEvent.origin_server_ts;
-
- return (
-
-
-
- }
- />
-
- {latestSenderId && (
-
- }
- />
-
- )}
-
-
-
- {threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
-
-
- {/* TODO: date */}
- Last Reply by {latestDisplayName} at {new Date(latestEventTs).getTime()}
-
-
-
-
- );
-}
diff --git a/src/app/components/thread-selector/styles.css.ts b/src/app/components/thread-selector/styles.css.ts
deleted file mode 100644
index fe580b68..00000000
--- a/src/app/components/thread-selector/styles.css.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { style } from '@vanilla-extract/css';
-import { color, config } from 'folds';
-
-export const ThreadSelectorContainer = style({
- paddingTop: config.space.S100,
-});
-
-export const ThreadSelector = style({
- padding: config.space.S200,
- borderRadius: config.radii.R400,
-});
-
-export const ThreadSectorOutlined = style({
- borderWidth: config.borderWidth.B300,
-});
-
-export const ThreadRepliesCount = style({
- color: color.Primary.Main,
- flexShrink: 0,
-});
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 93c3cf66..71674b0f 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -127,7 +127,7 @@ import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/u
import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
-import { ThreadSelector, ThreadSelectorContainer } from '../../components/thread-selector';
+import { ThreadSelector, ThreadSelectorContainer } from './message/thread-selector';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
@@ -1113,7 +1113,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
{threadDetail && (
-
+
)}
diff --git a/src/app/features/room/message/thread-selector/ThreadSelector.tsx b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
new file mode 100644
index 00000000..3701948c
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
@@ -0,0 +1,105 @@
+import { Avatar, Box, Icon, Icons, Line, Text } from 'folds';
+import React, { ReactNode } from 'react';
+import classNames from 'classnames';
+import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
+import * as css from './styles.css';
+import { UserAvatar } from '../../../../components/user-avatar';
+import { getMemberAvatarMxc, getMemberDisplayName } from '../../../../utils/room';
+import { useMatrixClient } from '../../../../hooks/useMatrixClient';
+import { useMediaAuthentication } from '../../../../hooks/useMediaAuthentication';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../../../utils/matrix';
+import { Time } from '../../../../components/message';
+
+export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+type ThreadSelectorProps = {
+ room: Room;
+ threadDetail: IThreadBundledRelationship;
+ outlined?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+
+export function ThreadSelector({
+ room,
+ threadDetail,
+ outlined,
+ hour24Clock,
+ dateFormatString,
+}: ThreadSelectorProps) {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+
+ const latestEvent = threadDetail.latest_event;
+
+ const latestSenderId = latestEvent.sender;
+ const latestSenderAvatarMxc = getMemberAvatarMxc(room, latestSenderId);
+ const latestDisplayName =
+ getMemberDisplayName(room, latestSenderId) ??
+ getMxIdLocalPart(latestSenderId) ??
+ latestSenderId;
+
+ const latestEventTs = latestEvent.origin_server_ts;
+
+ return (
+
+
+
+
+ {threadDetail.count} {threadDetail.count === 1 ? 'Thread Reply' : 'Thread Replies'}
+
+
+ {latestSenderId && (
+ <>
+
+
+
+
+ }
+ />
+
+
+
+
+ Latest by {latestDisplayName} at{' '}
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/app/components/thread-selector/index.ts b/src/app/features/room/message/thread-selector/index.ts
similarity index 100%
rename from src/app/components/thread-selector/index.ts
rename to src/app/features/room/message/thread-selector/index.ts
diff --git a/src/app/features/room/message/thread-selector/styles.css.ts b/src/app/features/room/message/thread-selector/styles.css.ts
new file mode 100644
index 00000000..fbd15ecc
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/styles.css.ts
@@ -0,0 +1,37 @@
+import { style } from '@vanilla-extract/css';
+import { color, config, toRem } from 'folds';
+import { ContainerColor } from '../../../../styles/ContainerColor.css';
+
+export const ThreadSelectorContainer = style({
+ marginTop: config.space.S200,
+});
+
+export const ThreadSelector = style([
+ ContainerColor({ variant: 'SurfaceVariant' }),
+ {
+ padding: `${config.space.S200} ${config.space.S300}`,
+ borderRadius: config.radii.R400,
+ cursor: 'pointer',
+
+ selectors: {
+ '&:hover, &:focus-visible': {
+ backgroundColor: color.SurfaceVariant.ContainerHover,
+ },
+ '&:active': {
+ backgroundColor: color.SurfaceVariant.ContainerActive,
+ },
+ },
+ },
+]);
+
+export const ThreadSectorOutlined = style({
+ borderWidth: config.borderWidth.B300,
+});
+
+export const ThreadSelectorDivider = style({
+ height: toRem(16),
+});
+
+export const ThreadRepliesCount = style({
+ color: color.Primary.Main,
+});
diff --git a/src/app/features/room/threads-menu/ThreadsMenu.tsx b/src/app/features/room/threads-menu/ThreadsMenu.tsx
index 5ff308e6..375b97da 100644
--- a/src/app/features/room/threads-menu/ThreadsMenu.tsx
+++ b/src/app/features/room/threads-menu/ThreadsMenu.tsx
@@ -80,7 +80,7 @@ import {
} from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { useRoomMyThreads } from '../../../hooks/useRoomThreads';
-import { ThreadSelector, ThreadSelectorContainer } from '../../../components/thread-selector';
+import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
type ThreadMessageProps = {
room: Room;
@@ -287,9 +287,10 @@ export const ThreadsMenu = forwardRef(
)}
diff --git a/src/app/hooks/useRoomThreads.ts b/src/app/hooks/useRoomThreads.ts
index d5ef24d6..a8ed0933 100644
--- a/src/app/hooks/useRoomThreads.ts
+++ b/src/app/hooks/useRoomThreads.ts
@@ -14,7 +14,7 @@ export const useRoomMyThreads = (room: Room): MatrixEvent[] | undefined => {
null,
30,
Direction.Backward,
- ThreadFilterType.My
+ ThreadFilterType.All
),
[mx, room]
)
From 958ae8945d6f3e039e82471f594dd83f23a05e64 Mon Sep 17 00:00:00 2001
From: Krishan <33421343+kfiven@users.noreply.github.com>
Date: Mon, 29 Sep 2025 14:34:38 +1000
Subject: [PATCH 006/220] Release v4.10.1 (#2495)
---
package-lock.json | 4 ++--
package.json | 2 +-
src/app/features/settings/about/About.tsx | 2 +-
src/app/pages/auth/AuthFooter.tsx | 2 +-
src/app/pages/client/WelcomePage.tsx | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 39cc6c22..d032ace2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cinny",
- "version": "4.10.0",
+ "version": "4.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
- "version": "4.10.0",
+ "version": "4.10.1",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
diff --git a/package.json b/package.json
index 495d34f5..577e129d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cinny",
- "version": "4.10.0",
+ "version": "4.10.1",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
index 4fba564e..295fc3fe 100644
--- a/src/app/features/settings/about/About.tsx
+++ b/src/app/features/settings/about/About.tsx
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
Cinny
- v4.10.0
+ v4.10.1
Yet another matrix client.
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx
index 25c70e6e..6ac0d4e9 100644
--- a/src/app/pages/auth/AuthFooter.tsx
+++ b/src/app/pages/auth/AuthFooter.tsx
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
- v4.10.0
+ v4.10.1
Twitter
diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx
index 37ca10fa..4b6dd992 100644
--- a/src/app/pages/client/WelcomePage.tsx
+++ b/src/app/pages/client/WelcomePage.tsx
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank"
rel="noreferrer noopener"
>
- v4.10.0
+ v4.10.1
}
From e13d97aa989fed1271a8896ec1fa2f1dd07f7038 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 15 Oct 2025 17:27:11 +1100
Subject: [PATCH 007/220] Fix member are not sorted correctly after last js-sdk
update (#2504)
---
src/app/features/common-settings/members/Members.tsx | 5 +++--
src/app/features/room/MembersDrawer.tsx | 5 +++--
src/app/hooks/useMemberSort.ts | 7 +++++--
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx
index 9940a751..15cd1d08 100644
--- a/src/app/features/common-settings/members/Members.tsx
+++ b/src/app/features/common-settings/members/Members.tsx
@@ -27,7 +27,7 @@ import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
import { useRoomMembers } from '../../../hooks/useRoomMembers';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import { usePowerLevels } from '../../../hooks/usePowerLevels';
+import { useGetMemberPowerLevel, usePowerLevels } from '../../../hooks/usePowerLevels';
import { VirtualTile } from '../../../components/virtualizer';
import { MemberTile } from '../../../components/member-tile';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
@@ -87,12 +87,13 @@ export function Members({ requestClose }: MembersProps) {
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+ const getPowerLevel = useGetMemberPowerLevel(powerLevels);
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu());
const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu());
- const memberPowerSort = useMemberPowerSort(creators);
+ const memberPowerSort = useMemberPowerSort(creators, getPowerLevel);
const scrollRef = useRef(null);
const searchInputRef = useRef(null);
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx
index 46d2238a..d9205076 100644
--- a/src/app/features/room/MembersDrawer.tsx
+++ b/src/app/features/room/MembersDrawer.tsx
@@ -51,7 +51,7 @@ import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
-import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { useGetMemberPowerLevel, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
import { MemberSortMenu } from '../../components/MemberSortMenu';
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
@@ -185,6 +185,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+ const getPowerLevel = useGetMemberPowerLevel(powerLevels);
const fetchingMembers = members.length < room.getJoinedMemberCount();
const openUserRoomProfile = useOpenUserRoomProfile();
@@ -198,7 +199,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu);
- const memberPowerSort = useMemberPowerSort(creators);
+ const memberPowerSort = useMemberPowerSort(creators, getPowerLevel);
const typingMembers = useRoomTypingMember(room.roomId);
diff --git a/src/app/hooks/useMemberSort.ts b/src/app/hooks/useMemberSort.ts
index d8e403c5..48d2f605 100644
--- a/src/app/hooks/useMemberSort.ts
+++ b/src/app/hooks/useMemberSort.ts
@@ -47,7 +47,10 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
return item;
};
-export const useMemberPowerSort = (creators: Set): MemberSortFn => {
+export const useMemberPowerSort = (
+ creators: Set,
+ getPowerLevel: (userId: string) => number
+): MemberSortFn => {
const sort: MemberSortFn = useCallback(
(a, b) => {
if (creators.has(a.userId) && creators.has(b.userId)) {
@@ -56,7 +59,7 @@ export const useMemberPowerSort = (creators: Set): MemberSortFn => {
if (creators.has(a.userId)) return -1;
if (creators.has(b.userId)) return 1;
- return b.powerLevel - a.powerLevel;
+ return getPowerLevel(b.userId) - getPowerLevel(a.userId);
},
[creators]
);
From 46c02b89deda5a766f3b34ccad0c1eb0dc58d029 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 15 Oct 2025 17:30:03 +1100
Subject: [PATCH 008/220] Update folds to fix broken scrollbar color (#2505)
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d032ace2..6db1e02d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,7 +32,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.3.0",
+ "folds": "2.4.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -7157,9 +7157,9 @@
}
},
"node_modules/folds": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/folds/-/folds-2.3.0.tgz",
- "integrity": "sha512-1KoM21jrg5daxvKrmSY0V04wa946KlNT0z6h017Rsnw2fdtNC6J0f34Ce5GF46Tzi00gZ/7SvCDXMzW/7e5s0w==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/folds/-/folds-2.4.0.tgz",
+ "integrity": "sha512-Q5xCmvU3SIM8etQ9qLF6Y5Jtv01c9JpG3QcnF+Z3nlbMvtktfE13Pj7p0XgSPBcA3OuoU0zXiRwiTlMcbU7KhA==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
diff --git a/package.json b/package.json
index 577e129d..33fe68c1 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.3.0",
+ "folds": "2.4.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
From d73428ee3de68695a1063069d00e023f0a9413c6 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:20:22 +0530
Subject: [PATCH 009/220] add option to inherit priority in time component
---
src/app/components/message/Time.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/app/components/message/Time.tsx b/src/app/components/message/Time.tsx
index 702e682d..bc5e487a 100644
--- a/src/app/components/message/Time.tsx
+++ b/src/app/components/message/Time.tsx
@@ -9,6 +9,7 @@ export type TimeProps = {
ts: number;
hour24Clock: boolean;
dateFormatString: string;
+ inheritPriority?: boolean;
};
/**
@@ -24,7 +25,7 @@ export type TimeProps = {
* @returns {React.ReactElement} A element with the formatted date/time.
*/
export const Time = as<'span', TimeProps & ComponentProps>(
- ({ compact, hour24Clock, dateFormatString, ts, className, ...props }, ref) => {
+ ({ compact, hour24Clock, dateFormatString, ts, inheritPriority, className, ...props }, ref) => {
const formattedTime = timeHourMinute(ts, hour24Clock);
let time = '';
@@ -35,7 +36,7 @@ export const Time = as<'span', TimeProps & ComponentProps>(
} else if (yesterday(ts)) {
time = `Yesterday ${formattedTime}`;
} else {
- time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
+ time = `${timeDayMonYear(ts, dateFormatString)}, ${formattedTime}`;
}
return (
@@ -43,7 +44,7 @@ export const Time = as<'span', TimeProps & ComponentProps>(
as="time"
className={classNames(css.Time, className)}
size="T200"
- priority="300"
+ priority={inheritPriority ? undefined : '300'}
{...props}
ref={ref}
>
From 174b315278941d30b845e041cc4f18bd530e9cfe Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:21:16 +0530
Subject: [PATCH 010/220] move timeline utils functions to new file
---
src/app/features/room/RoomTimeline.tsx | 91 +++++--------------------
src/app/features/room/utils/index.ts | 1 +
src/app/features/room/utils/timeline.ts | 66 ++++++++++++++++++
3 files changed, 85 insertions(+), 73 deletions(-)
create mode 100644 src/app/features/room/utils/index.ts
create mode 100644 src/app/features/room/utils/timeline.ts
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 71674b0f..44696b0a 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -128,6 +128,16 @@ import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { ThreadSelector, ThreadSelectorContainer } from './message/thread-selector';
+import {
+ getEventIdAbsoluteIndex,
+ getFirstLinkedTimeline,
+ getLinkedTimelines,
+ getTimelineAndBaseIndex,
+ getTimelineEvent,
+ getTimelineRelativeIndex,
+ getTimelinesEventsCount,
+ timelineToEventsCount,
+} from './utils';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
@@ -152,79 +162,6 @@ const TimelineDivider = as<'div', { variant?: ContainerColor | 'Inherit' }>(
)
);
-export const getLiveTimeline = (room: Room): EventTimeline =>
- room.getUnfilteredTimelineSet().getLiveTimeline();
-
-export const getEventTimeline = (room: Room, eventId: string): EventTimeline | undefined => {
- const timelineSet = room.getUnfilteredTimelineSet();
- return timelineSet.getTimelineForEvent(eventId) ?? undefined;
-};
-
-export const getFirstLinkedTimeline = (
- timeline: EventTimeline,
- direction: Direction
-): EventTimeline => {
- const linkedTm = timeline.getNeighbouringTimeline(direction);
- if (!linkedTm) return timeline;
- return getFirstLinkedTimeline(linkedTm, direction);
-};
-
-export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
- const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
- const timelines: EventTimeline[] = [];
-
- for (
- let nextTimeline: EventTimeline | null = firstTimeline;
- nextTimeline;
- nextTimeline = nextTimeline.getNeighbouringTimeline(Direction.Forward)
- ) {
- timelines.push(nextTimeline);
- }
- return timelines;
-};
-
-export const timelineToEventsCount = (t: EventTimeline) => t.getEvents().length;
-export const getTimelinesEventsCount = (timelines: EventTimeline[]): number => {
- const timelineEventCountReducer = (count: number, tm: EventTimeline) =>
- count + timelineToEventsCount(tm);
- return timelines.reduce(timelineEventCountReducer, 0);
-};
-
-export const getTimelineAndBaseIndex = (
- timelines: EventTimeline[],
- index: number
-): [EventTimeline | undefined, number] => {
- let uptoTimelineLen = 0;
- const timeline = timelines.find((t) => {
- uptoTimelineLen += t.getEvents().length;
- if (index < uptoTimelineLen) return true;
- return false;
- });
- if (!timeline) return [undefined, 0];
- return [timeline, uptoTimelineLen - timeline.getEvents().length];
-};
-
-export const getTimelineRelativeIndex = (absoluteIndex: number, timelineBaseIndex: number) =>
- absoluteIndex - timelineBaseIndex;
-
-export const getTimelineEvent = (timeline: EventTimeline, index: number): MatrixEvent | undefined =>
- timeline.getEvents()[index];
-
-export const getEventIdAbsoluteIndex = (
- timelines: EventTimeline[],
- eventTimeline: EventTimeline,
- eventId: string
-): number | undefined => {
- const timelineIndex = timelines.findIndex((t) => t === eventTimeline);
- if (timelineIndex === -1) return undefined;
- const eventIndex = eventTimeline.getEvents().findIndex((evt) => evt.getId() === eventId);
- if (eventIndex === -1) return undefined;
- const baseIndex = timelines
- .slice(0, timelineIndex)
- .reduce((accValue, timeline) => timeline.getEvents().length + accValue, 0);
- return baseIndex + eventIndex;
-};
-
type RoomTimelineProps = {
room: Room;
eventId?: string;
@@ -234,6 +171,14 @@ type RoomTimelineProps = {
const PAGINATION_LIMIT = 80;
+export const getLiveTimeline = (room: Room): EventTimeline =>
+ room.getUnfilteredTimelineSet().getLiveTimeline();
+
+export const getEventTimeline = (room: Room, eventId: string): EventTimeline | undefined => {
+ const timelineSet = room.getUnfilteredTimelineSet();
+ return timelineSet.getTimelineForEvent(eventId) ?? undefined;
+};
+
type Timeline = {
linkedTimelines: EventTimeline[];
range: ItemRange;
diff --git a/src/app/features/room/utils/index.ts b/src/app/features/room/utils/index.ts
new file mode 100644
index 00000000..13d812d7
--- /dev/null
+++ b/src/app/features/room/utils/index.ts
@@ -0,0 +1 @@
+export * from './timeline';
diff --git a/src/app/features/room/utils/timeline.ts b/src/app/features/room/utils/timeline.ts
new file mode 100644
index 00000000..f9c45525
--- /dev/null
+++ b/src/app/features/room/utils/timeline.ts
@@ -0,0 +1,66 @@
+import { Room, EventTimeline, Direction, MatrixEvent } from 'matrix-js-sdk';
+
+export const getFirstLinkedTimeline = (
+ timeline: EventTimeline,
+ direction: Direction
+): EventTimeline => {
+ const linkedTm = timeline.getNeighbouringTimeline(direction);
+ if (!linkedTm) return timeline;
+ return getFirstLinkedTimeline(linkedTm, direction);
+};
+
+export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
+ const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
+ const timelines: EventTimeline[] = [];
+
+ for (
+ let nextTimeline: EventTimeline | null = firstTimeline;
+ nextTimeline;
+ nextTimeline = nextTimeline.getNeighbouringTimeline(Direction.Forward)
+ ) {
+ timelines.push(nextTimeline);
+ }
+ return timelines;
+};
+
+export const timelineToEventsCount = (t: EventTimeline) => t.getEvents().length;
+export const getTimelinesEventsCount = (timelines: EventTimeline[]): number => {
+ const timelineEventCountReducer = (count: number, tm: EventTimeline) =>
+ count + timelineToEventsCount(tm);
+ return timelines.reduce(timelineEventCountReducer, 0);
+};
+
+export const getTimelineAndBaseIndex = (
+ timelines: EventTimeline[],
+ index: number
+): [EventTimeline | undefined, number] => {
+ let uptoTimelineLen = 0;
+ const timeline = timelines.find((t) => {
+ uptoTimelineLen += t.getEvents().length;
+ if (index < uptoTimelineLen) return true;
+ return false;
+ });
+ if (!timeline) return [undefined, 0];
+ return [timeline, uptoTimelineLen - timeline.getEvents().length];
+};
+
+export const getTimelineRelativeIndex = (absoluteIndex: number, timelineBaseIndex: number) =>
+ absoluteIndex - timelineBaseIndex;
+
+export const getTimelineEvent = (timeline: EventTimeline, index: number): MatrixEvent | undefined =>
+ timeline.getEvents()[index];
+
+export const getEventIdAbsoluteIndex = (
+ timelines: EventTimeline[],
+ eventTimeline: EventTimeline,
+ eventId: string
+): number | undefined => {
+ const timelineIndex = timelines.findIndex((t) => t === eventTimeline);
+ if (timelineIndex === -1) return undefined;
+ const eventIndex = eventTimeline.getEvents().findIndex((evt) => evt.getId() === eventId);
+ if (eventIndex === -1) return undefined;
+ const baseIndex = timelines
+ .slice(0, timelineIndex)
+ .reduce((accValue, timeline) => timeline.getEvents().length + accValue, 0);
+ return baseIndex + eventIndex;
+};
From e44ca92422ee4fe6b9ccbeed255b48d16fe6ffcf Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:21:33 +0530
Subject: [PATCH 011/220] remove avatar from threads selector
---
.../thread-selector/ThreadSelector.tsx | 45 ++++---------------
.../message/thread-selector/styles.css.ts | 2 +-
2 files changed, 10 insertions(+), 37 deletions(-)
diff --git a/src/app/features/room/message/thread-selector/ThreadSelector.tsx b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
index 3701948c..b022a34f 100644
--- a/src/app/features/room/message/thread-selector/ThreadSelector.tsx
+++ b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
@@ -1,13 +1,10 @@
-import { Avatar, Box, Icon, Icons, Line, Text } from 'folds';
+import { Box, Icon, Icons, Line, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
import * as css from './styles.css';
-import { UserAvatar } from '../../../../components/user-avatar';
-import { getMemberAvatarMxc, getMemberDisplayName } from '../../../../utils/room';
-import { useMatrixClient } from '../../../../hooks/useMatrixClient';
-import { useMediaAuthentication } from '../../../../hooks/useMediaAuthentication';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../../../utils/matrix';
+import { getMemberDisplayName } from '../../../../utils/room';
+import { getMxIdLocalPart } from '../../../../utils/matrix';
import { Time } from '../../../../components/message';
export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
@@ -29,13 +26,9 @@ export function ThreadSelector({
hour24Clock,
dateFormatString,
}: ThreadSelectorProps) {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
-
const latestEvent = threadDetail.latest_event;
const latestSenderId = latestEvent.sender;
- const latestSenderAvatarMxc = getMemberAvatarMxc(room, latestSenderId);
const latestDisplayName =
getMemberDisplayName(room, latestSenderId) ??
getMxIdLocalPart(latestSenderId) ??
@@ -51,10 +44,10 @@ export function ThreadSelector({
alignItems="Center"
gap="300"
>
-
+
- {threadDetail.count} {threadDetail.count === 1 ? 'Thread Reply' : 'Thread Replies'}
+ {threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
{latestSenderId && (
@@ -65,35 +58,15 @@ export function ThreadSelector({
variant="SurfaceVariant"
/>
-
-
- }
- />
-
-
-
- Latest by {latestDisplayName} at{' '}
-
+ Last reply by
+ {latestDisplayName}
+ —
diff --git a/src/app/features/room/message/thread-selector/styles.css.ts b/src/app/features/room/message/thread-selector/styles.css.ts
index fbd15ecc..45bb7d62 100644
--- a/src/app/features/room/message/thread-selector/styles.css.ts
+++ b/src/app/features/room/message/thread-selector/styles.css.ts
@@ -9,7 +9,7 @@ export const ThreadSelectorContainer = style({
export const ThreadSelector = style([
ContainerColor({ variant: 'SurfaceVariant' }),
{
- padding: `${config.space.S200} ${config.space.S300}`,
+ padding: config.space.S200,
borderRadius: config.radii.R400,
cursor: 'pointer',
From 12bcbc2e78a27d49581d0e1fc43e39f342649acf Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Wed, 22 Oct 2025 16:22:31 +0530
Subject: [PATCH 012/220] load threads in My Threads menu
---
.../room/threads-menu/ThreadsError.tsx | 32 ++
.../room/threads-menu/ThreadsLoading.tsx | 23 +
.../room/threads-menu/ThreadsMenu.tsx | 507 ++----------------
.../room/threads-menu/ThreadsTimeline.tsx | 481 +++++++++++++++++
src/app/hooks/useRoomThreads.ts | 37 +-
src/client/initMatrix.ts | 1 +
6 files changed, 608 insertions(+), 473 deletions(-)
create mode 100644 src/app/features/room/threads-menu/ThreadsError.tsx
create mode 100644 src/app/features/room/threads-menu/ThreadsLoading.tsx
create mode 100644 src/app/features/room/threads-menu/ThreadsTimeline.tsx
diff --git a/src/app/features/room/threads-menu/ThreadsError.tsx b/src/app/features/room/threads-menu/ThreadsError.tsx
new file mode 100644
index 00000000..c72b7202
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsError.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Box, Icon, Icons, toRem, Text, config } from 'folds';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import { BreakWord } from '../../../styles/Text.css';
+
+export function ThreadsError({ error }: { error: Error }) {
+ return (
+
+
+
+
+ {error.name}
+
+
+ {error.message}
+
+
+
+ );
+}
diff --git a/src/app/features/room/threads-menu/ThreadsLoading.tsx b/src/app/features/room/threads-menu/ThreadsLoading.tsx
new file mode 100644
index 00000000..236435b2
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsLoading.tsx
@@ -0,0 +1,23 @@
+import { Box, config, Spinner } from 'folds';
+import React from 'react';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+
+export function ThreadsLoading() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/features/room/threads-menu/ThreadsMenu.tsx b/src/app/features/room/threads-menu/ThreadsMenu.tsx
index 375b97da..28e73f33 100644
--- a/src/app/features/room/threads-menu/ThreadsMenu.tsx
+++ b/src/app/features/room/threads-menu/ThreadsMenu.tsx
@@ -1,194 +1,48 @@
/* eslint-disable react/destructuring-assignment */
-import React, { forwardRef, MouseEventHandler, useMemo, useRef } from 'react';
-import { IRoomEvent, MatrixEvent, Room } from 'matrix-js-sdk';
-import {
- Avatar,
- Box,
- Chip,
- color,
- config,
- Header,
- Icon,
- IconButton,
- Icons,
- Menu,
- Scroll,
- Text,
- toRem,
-} from 'folds';
-import { Opts as LinkifyOpts } from 'linkifyjs';
-import { HTMLReactParserOptions } from 'html-react-parser';
-import { useVirtualizer } from '@tanstack/react-virtual';
+import React, { forwardRef, useMemo } from 'react';
+import { EventTimelineSet, Room } from 'matrix-js-sdk';
+import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text, toRem } from 'folds';
import * as css from './ThreadsMenu.css';
-import { SequenceCard } from '../../../components/sequence-card';
-import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
-import {
- AvatarBase,
- ImageContent,
- MessageNotDecryptedContent,
- MessageUnsupportedContent,
- ModernLayout,
- MSticker,
- RedactedContent,
- Reply,
- Time,
- Username,
- UsernameBold,
-} from '../../../components/message';
-import { UserAvatar } from '../../../components/user-avatar';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import {
- getEditedEvent,
- getEventThreadDetail,
- getMemberAvatarMxc,
- getMemberDisplayName,
-} from '../../../utils/room';
-import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room';
-import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
-import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
-import {
- factoryRenderLinkifyWithMention,
- getReactCustomHtmlParser,
- LINKIFY_OPTS,
- makeMentionCustomProps,
- renderMatrixMention,
-} from '../../../plugins/react-custom-html-parser';
-import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
-import { RenderMessageContent } from '../../../components/RenderMessageContent';
-import { useSetting } from '../../../state/hooks/settings';
-import { settingsAtom } from '../../../state/settings';
-import * as customHtmlCss from '../../../styles/CustomHtml.css';
-import { EncryptedContent } from '../message';
-import { Image } from '../../../components/media';
-import { ImageViewer } from '../../../components/image-viewer';
-import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
-import { VirtualTile } from '../../../components/virtualizer';
-import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { ContainerColor } from '../../../styles/ContainerColor.css';
-import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
-import { useTheme } from '../../../hooks/useTheme';
-import { PowerIcon } from '../../../components/power';
-import colorMXID from '../../../../util/colorMXID';
-import { useIsDirectRoom } from '../../../hooks/useRoom';
-import { useRoomCreators } from '../../../hooks/useRoomCreators';
-import {
- GetMemberPowerTag,
- getPowerTagIconSrc,
- useAccessiblePowerTagColors,
- useGetMemberPowerTag,
-} from '../../../hooks/useMemberPowerTag';
-import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { useRoomMyThreads } from '../../../hooks/useRoomThreads';
-import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
+import { AsyncStatus } from '../../../hooks/useAsyncCallback';
+import { getLinkedTimelines, getTimelinesEventsCount } from '../utils';
+import { ThreadsTimeline } from './ThreadsTimeline';
+import { ThreadsLoading } from './ThreadsLoading';
+import { ThreadsError } from './ThreadsError';
-type ThreadMessageProps = {
- room: Room;
- event: MatrixEvent;
- renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
- onOpen: (roomId: string, eventId: string) => void;
- getMemberPowerTag: GetMemberPowerTag;
- accessibleTagColors: Map;
- legacyUsernameColor: boolean;
- hour24Clock: boolean;
- dateFormatString: string;
+const getTimelines = (timelineSet: EventTimelineSet) => {
+ const liveTimeline = timelineSet.getLiveTimeline();
+ const linkedTimelines = getLinkedTimelines(liveTimeline);
+
+ return linkedTimelines;
};
-function ThreadMessage({
- room,
- event,
- renderContent,
- onOpen,
- getMemberPowerTag,
- accessibleTagColors,
- legacyUsernameColor,
- hour24Clock,
- dateFormatString,
-}: ThreadMessageProps) {
- const useAuthentication = useMediaAuthentication();
- const mx = useMatrixClient();
-
- const handleOpenClick: MouseEventHandler = (evt) => {
- evt.stopPropagation();
- const evtId = evt.currentTarget.getAttribute('data-event-id');
- if (!evtId) return;
- onOpen(room.roomId, evtId);
- };
-
- const renderOptions = () => (
-
-
- Open
-
-
- );
-
- const sender = event.getSender()!;
- const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
- const senderAvatarMxc = getMemberAvatarMxc(room, sender);
- const getContent = (() => event.getContent()) as GetContentCallback;
-
- const memberPowerTag = getMemberPowerTag(sender);
- const tagColor = memberPowerTag?.color
- ? accessibleTagColors?.get(memberPowerTag.color)
- : undefined;
- const tagIconSrc = memberPowerTag?.icon
- ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
- : undefined;
-
- const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
+function NoThreads() {
return (
-
-
- }
- />
-
-
- }
+
-
-
-
-
-
- {displayName}
-
-
- {tagIconSrc && }
-
-
-
- {renderOptions()}
+
+
+
+ No Threads Yet
+
+
+ Threads you’re participating in will appear here.
+
- {event.replyEventId && (
-
- )}
- {renderContent(event.getType(), false, event, displayName, getContent)}
-
+
);
}
@@ -198,204 +52,16 @@ type ThreadsMenuProps = {
};
export const ThreadsMenu = forwardRef(
({ room, requestClose }, ref) => {
- const mx = useMatrixClient();
- const powerLevels = usePowerLevelsContext();
- const creators = useRoomCreators(room);
+ const threadsState = useRoomMyThreads(room);
+ const threadsTimelineSet =
+ threadsState.status === AsyncStatus.Success ? threadsState.data : undefined;
- const creatorsTag = useRoomCreatorsTag();
- const powerLevelTags = usePowerLevelTags(room, powerLevels);
- const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+ const linkedTimelines = useMemo(() => {
+ if (!threadsTimelineSet) return undefined;
+ return getTimelines(threadsTimelineSet);
+ }, [threadsTimelineSet]);
- const theme = useTheme();
- const accessibleTagColors = useAccessiblePowerTagColors(
- theme.kind,
- creatorsTag,
- powerLevelTags
- );
-
- const useAuthentication = useMediaAuthentication();
- const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
- const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
-
- const direct = useIsDirectRoom();
- const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
-
- const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
- const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
-
- const { navigateRoom } = useRoomNavigate();
- const scrollRef = useRef(null);
-
- const events = useRoomMyThreads(room);
-
- const virtualizer = useVirtualizer({
- count: events?.length ?? 0,
- getScrollElement: () => scrollRef.current,
- estimateSize: () => 75,
- overscan: 4,
- });
-
- const mentionClickHandler = useMentionClickHandler(room.roomId);
- const spoilerClickHandler = useSpoilerClickHandler();
-
- const linkifyOpts = useMemo(
- () => ({
- ...LINKIFY_OPTS,
- render: factoryRenderLinkifyWithMention((href) =>
- renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
- ),
- }),
- [mx, room, mentionClickHandler]
- );
- const htmlReactParserOptions = useMemo(
- () =>
- getReactCustomHtmlParser(mx, room.roomId, {
- linkifyOpts,
- useAuthentication,
- handleSpoilerClick: spoilerClickHandler,
- handleMentionClick: mentionClickHandler,
- }),
- [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
- );
-
- const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>(
- {
- [MessageEvent.RoomMessage]: (event, displayName, getContent) => {
- if (event.isRedacted()) {
- return (
-
- );
- }
-
- const threadDetail = getEventThreadDetail(event);
-
- return (
- <>
-
- {threadDetail && (
-
-
-
- )}
- >
- );
- },
- [MessageEvent.RoomMessageEncrypted]: (mEvent, displayName) => {
- const eventId = mEvent.getId()!;
- const evtTimeline = room.getTimelineForEvent(eventId);
-
- return (
-
- {() => {
- if (mEvent.isRedacted()) return ;
- if (mEvent.getType() === MessageEvent.Sticker)
- return (
- (
- }
- renderViewer={(p) => }
- />
- )}
- />
- );
- if (mEvent.getType() === MessageEvent.RoomMessage) {
- const editedEvent =
- evtTimeline && getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
- const getContent = (() =>
- editedEvent?.getContent()['m.new_content'] ??
- mEvent.getContent()) as GetContentCallback;
-
- return (
-
- );
- }
- if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
- return (
-
-
-
- );
- return (
-
-
-
- );
- }}
-
- );
- },
- [MessageEvent.Sticker]: (event, displayName, getContent) => {
- if (event.isRedacted()) {
- return (
-
- );
- }
- return (
- (
- }
- renderViewer={(p) => }
- />
- )}
- />
- );
- },
- },
- undefined,
- (event) => {
- if (event.isRedacted()) {
- return ;
- }
- return (
-
-
- {event.getType()}
- {' event'}
-
-
- );
- }
- );
-
- const handleOpen = (roomId: string, eventId: string) => {
- navigateRoom(roomId, eventId);
- requestClose();
- };
+ const hasEvents = linkedTimelines && getTimelinesEventsCount(linkedTimelines) > 0;
return (
-
-
- {events && events.length > 0 ? (
-
- {virtualizer.getVirtualItems().map((vItem) => {
- const event = events[vItem.index];
- if (!event.getId()) return null;
-
- return (
-
-
-
-
-
- );
- })}
-
- ) : (
-
-
-
-
- No Threads
-
-
- Threads you are participating in will appear here.
-
-
-
- )}
-
-
+ {threadsState.status === AsyncStatus.Success && hasEvents ? (
+
+ ) : (
+
+
+ {(threadsState.status === AsyncStatus.Loading ||
+ threadsState.status === AsyncStatus.Idle) && }
+ {threadsState.status === AsyncStatus.Success && !hasEvents && }
+ {threadsState.status === AsyncStatus.Error && (
+
+ )}
+
+
+ )}
diff --git a/src/app/features/room/threads-menu/ThreadsTimeline.tsx b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
new file mode 100644
index 00000000..d6cbb62d
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
@@ -0,0 +1,481 @@
+/* eslint-disable react/destructuring-assignment */
+import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Direction, EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk';
+import { Avatar, Box, Chip, config, Icon, Icons, Scroll, Text } from 'folds';
+import { Opts as LinkifyOpts } from 'linkifyjs';
+import { HTMLReactParserOptions } from 'html-react-parser';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { SequenceCard } from '../../../components/sequence-card';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import {
+ AvatarBase,
+ ImageContent,
+ MessageNotDecryptedContent,
+ MessageUnsupportedContent,
+ ModernLayout,
+ MSticker,
+ RedactedContent,
+ Reply,
+ Time,
+ Username,
+ UsernameBold,
+} from '../../../components/message';
+import { UserAvatar } from '../../../components/user-avatar';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import {
+ getEditedEvent,
+ getEventThreadDetail,
+ getMemberAvatarMxc,
+ getMemberDisplayName,
+} from '../../../utils/room';
+import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room';
+import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
+import {
+ factoryRenderLinkifyWithMention,
+ getReactCustomHtmlParser,
+ LINKIFY_OPTS,
+ makeMentionCustomProps,
+ renderMatrixMention,
+} from '../../../plugins/react-custom-html-parser';
+import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
+import { RenderMessageContent } from '../../../components/RenderMessageContent';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import * as customHtmlCss from '../../../styles/CustomHtml.css';
+import { EncryptedContent } from '../message';
+import { Image } from '../../../components/media';
+import { ImageViewer } from '../../../components/image-viewer';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
+import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
+import { useTheme } from '../../../hooks/useTheme';
+import { PowerIcon } from '../../../components/power';
+import colorMXID from '../../../../util/colorMXID';
+import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import {
+ GetMemberPowerTag,
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
+import {
+ getTimelineAndBaseIndex,
+ getTimelineEvent,
+ getTimelineRelativeIndex,
+ getTimelinesEventsCount,
+} from '../utils';
+import { ThreadsLoading } from './ThreadsLoading';
+import { VirtualTile } from '../../../components/virtualizer';
+import { useAlive } from '../../../hooks/useAlive';
+import * as css from './ThreadsMenu.css';
+
+type ThreadMessageProps = {
+ room: Room;
+ event: MatrixEvent;
+ renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
+ onOpen: (roomId: string, eventId: string) => void;
+ getMemberPowerTag: GetMemberPowerTag;
+ accessibleTagColors: Map;
+ legacyUsernameColor: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+function ThreadMessage({
+ room,
+ event,
+ renderContent,
+ onOpen,
+ getMemberPowerTag,
+ accessibleTagColors,
+ legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
+}: ThreadMessageProps) {
+ const useAuthentication = useMediaAuthentication();
+ const mx = useMatrixClient();
+
+ const handleOpenClick: MouseEventHandler = (evt) => {
+ evt.stopPropagation();
+ const evtId = evt.currentTarget.getAttribute('data-event-id');
+ if (!evtId) return;
+ onOpen(room.roomId, evtId);
+ };
+
+ const renderOptions = () => (
+
+
+ Open
+
+
+ );
+
+ const sender = event.getSender()!;
+ const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
+ const senderAvatarMxc = getMemberAvatarMxc(room, sender);
+ const getContent = (() => event.getContent()) as GetContentCallback;
+
+ const memberPowerTag = getMemberPowerTag(sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
+ : undefined;
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
+ : undefined;
+
+ const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
+
+ return (
+
+
+ }
+ />
+
+
+ }
+ >
+
+
+
+
+
+ {displayName}
+
+
+ {tagIconSrc && }
+
+
+
+ {renderOptions()}
+
+ {event.replyEventId && (
+
+ )}
+ {renderContent(event.getType(), false, event, displayName, getContent)}
+
+ );
+}
+
+type ThreadsTimelineProps = {
+ timelines: EventTimeline[];
+ requestClose: () => void;
+};
+export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProps) {
+ const mx = useMatrixClient();
+ const { navigateRoom } = useRoomNavigate();
+ const alive = useAlive();
+ const scrollRef = useRef(null);
+
+ const room = useRoom();
+ const powerLevels = usePowerLevelsContext();
+ const creators = useRoomCreators(room);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
+ const theme = useTheme();
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
+
+ const useAuthentication = useMediaAuthentication();
+ const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
+ const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
+
+ const direct = useIsDirectRoom();
+ const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
+ const mentionClickHandler = useMentionClickHandler(room.roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
+
+ const linkifyOpts = useMemo(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
+ const htmlReactParserOptions = useMemo(
+ () =>
+ getReactCustomHtmlParser(mx, room.roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
+ }),
+ [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
+ );
+
+ const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>(
+ {
+ [MessageEvent.RoomMessage]: (event, displayName, getContent) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+
+ const threadDetail = getEventThreadDetail(event);
+
+ return (
+ <>
+
+ {threadDetail && (
+
+
+
+ )}
+ >
+ );
+ },
+ [MessageEvent.RoomMessageEncrypted]: (mEvent, displayName) => {
+ const eventId = mEvent.getId()!;
+ const evtTimeline = room.getTimelineForEvent(eventId);
+
+ return (
+
+ {() => {
+ if (mEvent.isRedacted()) return ;
+ if (mEvent.getType() === MessageEvent.Sticker)
+ return (
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
+ );
+ if (mEvent.getType() === MessageEvent.RoomMessage) {
+ const editedEvent =
+ evtTimeline && getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
+ const getContent = (() =>
+ editedEvent?.getContent()['m.new_content'] ??
+ mEvent.getContent()) as GetContentCallback;
+
+ return (
+
+ );
+ }
+ if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
+ return (
+
+
+
+ );
+ return (
+
+
+
+ );
+ }}
+
+ );
+ },
+ [MessageEvent.Sticker]: (event, displayName, getContent) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+
+ const threadDetail = getEventThreadDetail(event);
+
+ return (
+ <>
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
+ {threadDetail && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ undefined,
+ (event) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+ return (
+
+
+ {event.getType()}
+ {' event'}
+
+
+ );
+ }
+ );
+
+ const handleOpen = (roomId: string, eventId: string) => {
+ navigateRoom(roomId, eventId);
+ requestClose();
+ };
+
+ const eventsLength = getTimelinesEventsCount(timelines);
+ const timelineToPaginate = timelines[timelines.length - 1];
+ const [paginationToken, setPaginationToken] = useState(
+ timelineToPaginate.getPaginationToken(Direction.Backward)
+ );
+
+ const virtualizer = useVirtualizer({
+ count: eventsLength,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 122,
+ overscan: 10,
+ });
+ const vItems = virtualizer.getVirtualItems();
+
+ const paginate = useCallback(async () => {
+ const moreToLoad = await mx.paginateEventTimeline(timelineToPaginate, {
+ backwards: true,
+ limit: 30,
+ });
+
+ if (alive()) {
+ setPaginationToken(
+ moreToLoad ? timelineToPaginate.getPaginationToken(Direction.Backward) : null
+ );
+ }
+ }, [mx, alive, timelineToPaginate]);
+
+ // auto paginate when scroll reach bottom
+ useEffect(() => {
+ const lastVItem = vItems.length > 0 ? vItems[vItems.length - 1] : undefined;
+ if (paginationToken && lastVItem && lastVItem.index === eventsLength - 1) {
+ paginate();
+ }
+ }, [vItems, paginationToken, eventsLength, paginate]);
+
+ return (
+
+
+
+ {vItems.map((vItem) => {
+ const reverseTimelineIndex = eventsLength - vItem.index - 1;
+
+ const [timeline, baseIndex] = getTimelineAndBaseIndex(timelines, reverseTimelineIndex);
+ if (!timeline) return null;
+ const event = getTimelineEvent(
+ timeline,
+ getTimelineRelativeIndex(reverseTimelineIndex, baseIndex)
+ );
+
+ if (!event?.getId()) return null;
+
+ return (
+
+
+
+
+
+ );
+ })}
+
+ {paginationToken && }
+
+
+ );
+}
diff --git a/src/app/hooks/useRoomThreads.ts b/src/app/hooks/useRoomThreads.ts
index a8ed0933..7d5fa807 100644
--- a/src/app/hooks/useRoomThreads.ts
+++ b/src/app/hooks/useRoomThreads.ts
@@ -1,29 +1,20 @@
import { useCallback } from 'react';
-import { Direction, MatrixEvent, Room, ThreadFilterType } from 'matrix-js-sdk';
-import { useMatrixClient } from './useMatrixClient';
-import { AsyncStatus, useAsyncCallbackValue } from './useAsyncCallback';
+import { EventTimelineSet, Room } from 'matrix-js-sdk';
+import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
-export const useRoomMyThreads = (room: Room): MatrixEvent[] | undefined => {
- const mx = useMatrixClient();
+export const useRoomMyThreads = (room: Room): AsyncState => {
+ const [threadsState] = useAsyncCallbackValue(
+ useCallback(async () => {
+ await room.createThreadsTimelineSets();
+ await room.fetchRoomThreads();
- const [fetchState] = useAsyncCallbackValue(
- useCallback(
- () =>
- mx.createThreadListMessagesRequest(
- room.roomId,
- null,
- 30,
- Direction.Backward,
- ThreadFilterType.All
- ),
- [mx, room]
- )
+ const timelineSet = room.threadsTimelineSets[0];
+ if (timelineSet === undefined) {
+ throw new Error('Failed to fetch My Threads!');
+ }
+ return timelineSet;
+ }, [room])
);
- if (fetchState.status === AsyncStatus.Success) {
- const roomEvents = fetchState.data.chunk;
- const mEvents = roomEvents.map((event) => new MatrixEvent(event)).reverse();
- return mEvents;
- }
- return undefined;
+ return threadsState;
};
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
index 487c3f13..8ed4100e 100644
--- a/src/client/initMatrix.ts
+++ b/src/client/initMatrix.ts
@@ -42,6 +42,7 @@ export const initClient = async (session: Session): Promise => {
export const startClient = async (mx: MatrixClient) => {
await mx.startClient({
lazyLoadMembers: true,
+ threadSupport: true,
});
};
From 38cc6e6f3a0ff56ab62f034771e73f2d849fc6ae Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:27:02 +0530
Subject: [PATCH 013/220] add room to active thread atom
---
.../pages/client/ClientInitStorageAtom.tsx | 12 ++-
src/app/state/hooks/roomToActiveThread.ts | 55 ++++++++++++++
src/app/state/roomToActiveThread.ts | 75 +++++++++++++++++++
3 files changed, 139 insertions(+), 3 deletions(-)
create mode 100644 src/app/state/hooks/roomToActiveThread.ts
create mode 100644 src/app/state/roomToActiveThread.ts
diff --git a/src/app/pages/client/ClientInitStorageAtom.tsx b/src/app/pages/client/ClientInitStorageAtom.tsx
index 1abee707..308a0212 100644
--- a/src/app/pages/client/ClientInitStorageAtom.tsx
+++ b/src/app/pages/client/ClientInitStorageAtom.tsx
@@ -8,6 +8,8 @@ import { makeNavToActivePathAtom } from '../../state/navToActivePath';
import { NavToActivePathProvider } from '../../state/hooks/navToActivePath';
import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder';
import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder';
+import { makeRoomToActiveThreadAtom } from '../../state/roomToActiveThread';
+import { RoomToActiveThreadProvider } from '../../state/hooks/roomToActiveThread';
type ClientInitStorageAtomProps = {
children: ReactNode;
@@ -22,15 +24,19 @@ export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps)
const navToActivePathAtom = useMemo(() => makeNavToActivePathAtom(userId), [userId]);
+ const roomToActiveThreadAtom = useMemo(() => makeRoomToActiveThreadAtom(userId), [userId]);
+
const openedSidebarFolderAtom = useMemo(() => makeOpenedSidebarFolderAtom(userId), [userId]);
return (
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/src/app/state/hooks/roomToActiveThread.ts b/src/app/state/hooks/roomToActiveThread.ts
new file mode 100644
index 00000000..7abb5974
--- /dev/null
+++ b/src/app/state/hooks/roomToActiveThread.ts
@@ -0,0 +1,55 @@
+import { createContext, useCallback, useContext } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { RoomToActiveThreadAtom } from '../roomToActiveThread';
+
+const RoomToActiveThreadAtomContext = createContext(null);
+export const RoomToActiveThreadProvider = RoomToActiveThreadAtomContext.Provider;
+
+export const useRoomToActiveThreadAtom = (): RoomToActiveThreadAtom => {
+ const anAtom = useContext(RoomToActiveThreadAtomContext);
+
+ if (!anAtom) {
+ throw new Error('RoomToActiveThreadAtom is not provided!');
+ }
+
+ return anAtom;
+};
+
+export const useThreadSelector = (roomId: string): ((threadId: string) => void) => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
+
+ const onThreadSelect = useCallback(
+ (threadId: string) => {
+ setRoomToActiveThread({
+ type: 'PUT',
+ roomId,
+ threadId,
+ });
+ },
+ [roomId, setRoomToActiveThread]
+ );
+
+ return onThreadSelect;
+};
+
+export const useActiveThread = (roomId: string): string | undefined => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const roomToActiveThread = useAtomValue(roomToActiveThreadAtom);
+
+ return roomToActiveThread.get(roomId);
+};
+
+export const useThreadClose = (roomId: string): (() => void) => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
+
+ const closeThread = useCallback(() => {
+ setRoomToActiveThread({
+ type: 'DELETE',
+ roomId,
+ });
+ }, [roomId, setRoomToActiveThread]);
+
+ return closeThread;
+};
diff --git a/src/app/state/roomToActiveThread.ts b/src/app/state/roomToActiveThread.ts
new file mode 100644
index 00000000..e156c63a
--- /dev/null
+++ b/src/app/state/roomToActiveThread.ts
@@ -0,0 +1,75 @@
+import { atom, WritableAtom } from 'jotai';
+import produce from 'immer';
+import {
+ atomWithLocalStorage,
+ getLocalStorageItem,
+ setLocalStorageItem,
+} from './utils/atomWithLocalStorage';
+
+const ROOM_TO_ACTIVE_THREAD = 'roomToActiveThread';
+
+const getStoreKey = (userId: string): string => `${ROOM_TO_ACTIVE_THREAD}${userId}`;
+
+type RoomToActiveThread = Map;
+
+type RoomToActiveThreadAction =
+ | {
+ type: 'PUT';
+ roomId: string;
+ threadId: string;
+ }
+ | {
+ type: 'DELETE';
+ roomId: string;
+ };
+
+export type RoomToActiveThreadAtom = WritableAtom<
+ RoomToActiveThread,
+ [RoomToActiveThreadAction],
+ undefined
+>;
+
+export const makeRoomToActiveThreadAtom = (userId: string): RoomToActiveThreadAtom => {
+ const storeKey = getStoreKey(userId);
+
+ const baseRoomToActiveThread = atomWithLocalStorage(
+ storeKey,
+ (key) => {
+ const obj: Record = getLocalStorageItem(key, {});
+ return new Map(Object.entries(obj));
+ },
+ (key, value) => {
+ const obj: Record = Object.fromEntries(value);
+ setLocalStorageItem(key, obj);
+ }
+ );
+
+ const navToActivePathAtom = atom(
+ (get) => get(baseRoomToActiveThread),
+ (get, set, action) => {
+ if (action.type === 'DELETE') {
+ set(
+ baseRoomToActiveThread,
+ produce(get(baseRoomToActiveThread), (draft) => {
+ draft.delete(action.roomId);
+ })
+ );
+ return;
+ }
+ if (action.type === 'PUT') {
+ set(
+ baseRoomToActiveThread,
+ produce(get(baseRoomToActiveThread), (draft) => {
+ draft.set(action.roomId, action.threadId);
+ })
+ );
+ }
+ }
+ );
+
+ return navToActivePathAtom;
+};
+
+export const clearRoomToActiveThreadStore = (userId: string) => {
+ localStorage.removeItem(getStoreKey(userId));
+};
From 3d4c91c969534b17e97e726581b1c757afcab2b6 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:28:42 +0530
Subject: [PATCH 014/220] add onClick prop to thread selector
---
.../features/room/message/thread-selector/ThreadSelector.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/app/features/room/message/thread-selector/ThreadSelector.tsx b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
index b022a34f..41c87edc 100644
--- a/src/app/features/room/message/thread-selector/ThreadSelector.tsx
+++ b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
@@ -13,18 +13,22 @@ export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
type ThreadSelectorProps = {
room: Room;
+ threadId: string;
threadDetail: IThreadBundledRelationship;
outlined?: boolean;
hour24Clock: boolean;
dateFormatString: string;
+ onClick?: (threadId: string) => void;
};
export function ThreadSelector({
room,
+ threadId,
threadDetail,
outlined,
hour24Clock,
dateFormatString,
+ onClick,
}: ThreadSelectorProps) {
const latestEvent = threadDetail.latest_event;
@@ -43,6 +47,7 @@ export function ThreadSelector({
className={classNames(css.ThreadSelector, outlined && css.ThreadSectorOutlined)}
alignItems="Center"
gap="300"
+ onClick={() => onClick?.(threadId)}
>
From b5fd41f8628a5f149d3448120bb6e2e1bfcf4b1f Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:28:52 +0530
Subject: [PATCH 015/220] clear active thread state on logout
---
src/client/initMatrix.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
index 8ed4100e..f26b4f8d 100644
--- a/src/client/initMatrix.ts
+++ b/src/client/initMatrix.ts
@@ -2,6 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from
import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
+import { clearRoomToActiveThreadStore } from '../app/state/roomToActiveThread';
type Session = {
baseUrl: string;
@@ -49,6 +50,7 @@ export const startClient = async (mx: MatrixClient) => {
export const clearCacheAndReload = async (mx: MatrixClient) => {
mx.stopClient();
clearNavToActivePathStore(mx.getSafeUserId());
+ clearRoomToActiveThreadStore(mx.getSafeUserId());
await mx.store.deleteAllData();
window.location.reload();
};
From f35aa384e5d5a02ac3878d34961c369334459d3b Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:29:51 +0530
Subject: [PATCH 016/220] thread view - WIP
---
src/app/features/room/Room.tsx | 18 ++++-
src/app/features/room/RoomTimeline.tsx | 5 ++
.../features/room/thread-view/ThreadView.tsx | 79 +++++++++++++++++++
src/app/features/room/thread-view/index.tsx | 1 +
.../features/room/thread-view/styles.css.ts | 21 +++++
.../room/threads-menu/ThreadsTimeline.tsx | 23 ++++--
src/app/utils/room.ts | 1 +
7 files changed, 137 insertions(+), 11 deletions(-)
create mode 100644 src/app/features/room/thread-view/ThreadView.tsx
create mode 100644 src/app/features/room/thread-view/index.tsx
create mode 100644 src/app/features/room/thread-view/styles.css.ts
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index 24878d5e..47549fc7 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -13,6 +13,8 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../utils/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
+import { ThreadView } from './thread-view';
+import { useActiveThread } from '../../state/hooks/roomToActiveThread';
export function Room() {
const { eventId } = useParams();
@@ -25,6 +27,8 @@ export function Room() {
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
+ const threadId = useActiveThread(room.roomId);
+
useKeyDown(
window,
useCallback(
@@ -41,11 +45,19 @@ export function Room() {
- {screenSize === ScreenSize.Desktop && isDrawer && (
+ {threadId ? (
<>
-
-
+
+
>
+ ) : (
+ screenSize === ScreenSize.Desktop &&
+ isDrawer && (
+ <>
+
+
+ >
+ )
)}
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 44696b0a..5ca2fc8e 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -138,6 +138,7 @@ import {
getTimelinesEventsCount,
timelineToEventsCount,
} from './utils';
+import { useThreadSelector } from '../../state/hooks/roomToActiveThread';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
@@ -429,6 +430,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const spoilerClickHandler = useSpoilerClickHandler();
const openUserRoomProfile = useOpenUserRoomProfile();
const space = useSpaceOptionally();
+ const handleThreadClick = useThreadSelector(room.roomId);
const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents);
@@ -961,6 +963,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
},
[editor]
);
+
const { t } = useTranslation();
const renderMatrixEvent = useMatrixEventRenderer<
@@ -1060,10 +1063,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
)}
diff --git a/src/app/features/room/thread-view/ThreadView.tsx b/src/app/features/room/thread-view/ThreadView.tsx
new file mode 100644
index 00000000..6b6f4642
--- /dev/null
+++ b/src/app/features/room/thread-view/ThreadView.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { Box, Icon, IconButton, Icons, Scroll, Text, Tooltip, TooltipProvider } from 'folds';
+import classNames from 'classnames';
+import FocusTrap from 'focus-trap-react';
+import { Page, PageHeader } from '../../../components/page';
+import * as css from './styles.css';
+import { useRoom } from '../../../hooks/useRoom';
+import { useThreadClose } from '../../../state/hooks/roomToActiveThread';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+
+type ThreadViewProps = {
+ threadId: string;
+};
+export function ThreadView({ threadId }: ThreadViewProps) {
+ const room = useRoom();
+ const screenSize = useScreenSizeContext();
+ const floating = screenSize !== ScreenSize.Desktop;
+
+ const closeThread = useThreadClose(room.roomId);
+
+ const thread = room.getThread(threadId);
+ const events = thread?.events ?? [];
+
+ return (
+
+
+
+
+
+
+ Thread
+
+
+
+
+ Close
+
+ }
+ >
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {events.map((mEvent) => (
+
+ {mEvent.sender?.name}: {mEvent.getContent().body}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/features/room/thread-view/index.tsx b/src/app/features/room/thread-view/index.tsx
new file mode 100644
index 00000000..6ba05aab
--- /dev/null
+++ b/src/app/features/room/thread-view/index.tsx
@@ -0,0 +1 @@
+export * from './ThreadView';
diff --git a/src/app/features/room/thread-view/styles.css.ts b/src/app/features/room/thread-view/styles.css.ts
new file mode 100644
index 00000000..fe66d49b
--- /dev/null
+++ b/src/app/features/room/thread-view/styles.css.ts
@@ -0,0 +1,21 @@
+import { style } from '@vanilla-extract/css';
+import { config, toRem } from 'folds';
+
+export const ThreadView = style({
+ width: toRem(456),
+ flexShrink: 0,
+ flexGrow: 0,
+});
+
+export const ThreadViewFloating = style({
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 1,
+
+ maxWidth: toRem(456),
+ flexShrink: 1,
+ width: '100vw',
+ boxShadow: config.shadow.E400,
+});
diff --git a/src/app/features/room/threads-menu/ThreadsTimeline.tsx b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
index d6cbb62d..0e28f97d 100644
--- a/src/app/features/room/threads-menu/ThreadsTimeline.tsx
+++ b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
@@ -77,7 +77,7 @@ import * as css from './ThreadsMenu.css';
type ThreadMessageProps = {
room: Room;
event: MatrixEvent;
- renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
+ renderContent: RenderMatrixEvent<[string, MatrixEvent, string, GetContentCallback]>;
onOpen: (roomId: string, eventId: string) => void;
getMemberPowerTag: GetMemberPowerTag;
accessibleTagColors: Map;
@@ -134,6 +134,10 @@ function ThreadMessage({
const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
+ const mEventId = event.getId();
+
+ if (!mEventId) return null;
+
return (
)}
- {renderContent(event.getType(), false, event, displayName, getContent)}
+ {renderContent(event.getType(), false, mEventId, event, displayName, getContent)}
);
}
@@ -238,9 +242,11 @@ export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProp
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
);
- const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>(
+ const renderMatrixEvent = useMatrixEventRenderer<
+ [string, MatrixEvent, string, GetContentCallback]
+ >(
{
- [MessageEvent.RoomMessage]: (event, displayName, getContent) => {
+ [MessageEvent.RoomMessage]: (eventId, event, displayName, getContent) => {
if (event.isRedacted()) {
return ;
}
@@ -265,6 +271,7 @@ export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProp
);
},
- [MessageEvent.RoomMessageEncrypted]: (mEvent, displayName) => {
- const eventId = mEvent.getId()!;
+ [MessageEvent.RoomMessageEncrypted]: (eventId, mEvent, displayName) => {
const evtTimeline = room.getTimelineForEvent(eventId);
return (
@@ -333,7 +339,7 @@ export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProp
);
},
- [MessageEvent.Sticker]: (event, displayName, getContent) => {
+ [MessageEvent.Sticker]: (eventId, event, displayName, getContent) => {
if (event.isRedacted()) {
return ;
}
@@ -357,6 +363,7 @@ export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProp
{
+ (eventId, event) => {
if (event.isRedacted()) {
return ;
}
diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts
index e80edb20..f7b8f017 100644
--- a/src/app/utils/room.ts
+++ b/src/app/utils/room.ts
@@ -203,6 +203,7 @@ export const isNotificationEvent = (mEvent: MatrixEvent) => {
if (mEvent.isRedacted()) return false;
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
+ if (mEvent.getRelation()?.rel_type === 'm.thread') return false;
return true;
};
From 444b2feb9e3faa6ca3fa81f2fd47adb5ee0f54ae Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:30:52 +0530
Subject: [PATCH 017/220] update folds
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 39cc6c22..4abfac35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,7 +32,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.3.0",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -7157,9 +7157,9 @@
}
},
"node_modules/folds": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/folds/-/folds-2.3.0.tgz",
- "integrity": "sha512-1KoM21jrg5daxvKrmSY0V04wa946KlNT0z6h017Rsnw2fdtNC6J0f34Ce5GF46Tzi00gZ/7SvCDXMzW/7e5s0w==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz",
+ "integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
diff --git a/package.json b/package.json
index 495d34f5..5ad4daab 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.3.0",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
From c88cb4bca93e3a376836b8880e5e4c1fa5c7eba2 Mon Sep 17 00:00:00 2001
From: Krishan <33421343+kfiven@users.noreply.github.com>
Date: Wed, 5 Nov 2025 17:49:56 +1100
Subject: [PATCH 018/220] Release v4.10.2 (#2528)
---
package-lock.json | 4 ++--
package.json | 2 +-
src/app/features/settings/about/About.tsx | 2 +-
src/app/pages/auth/AuthFooter.tsx | 2 +-
src/app/pages/client/WelcomePage.tsx | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 6db1e02d..158a6112 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cinny",
- "version": "4.10.1",
+ "version": "4.10.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
- "version": "4.10.1",
+ "version": "4.10.2",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
diff --git a/package.json b/package.json
index 33fe68c1..7316dcce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cinny",
- "version": "4.10.1",
+ "version": "4.10.2",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
index 295fc3fe..f2cabf1d 100644
--- a/src/app/features/settings/about/About.tsx
+++ b/src/app/features/settings/about/About.tsx
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
Cinny
- v4.10.1
+ v4.10.2
Yet another matrix client.
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx
index 6ac0d4e9..d53ef9a6 100644
--- a/src/app/pages/auth/AuthFooter.tsx
+++ b/src/app/pages/auth/AuthFooter.tsx
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
- v4.10.1
+ v4.10.2
Twitter
diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx
index 4b6dd992..6eb33a16 100644
--- a/src/app/pages/client/WelcomePage.tsx
+++ b/src/app/pages/client/WelcomePage.tsx
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank"
rel="noreferrer noopener"
>
- v4.10.1
+ v4.10.2
}
From 0f61f2f3285371d13ec61591554d616f21c1e3f2 Mon Sep 17 00:00:00 2001
From: willow <42willow@pm.me>
Date: Thu, 27 Nov 2025 16:01:40 +1100
Subject: [PATCH 019/220] Fix typo: change "Advance Options" to "Advanced
Options" (#2537)
---
src/app/features/create-room/CreateRoom.tsx | 2 +-
src/app/features/create-space/CreateSpace.tsx | 2 +-
src/app/features/room-settings/general/General.tsx | 2 +-
src/app/features/space-settings/general/General.tsx | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx
index 6ad469c9..05c5c2e3 100644
--- a/src/app/features/create-room/CreateRoom.tsx
+++ b/src/app/features/create-room/CreateRoom.tsx
@@ -183,7 +183,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
onClick={() => setAdvance(!advance)}
type="button"
>
- Advance Options
+ Advanced Options
diff --git a/src/app/features/create-space/CreateSpace.tsx b/src/app/features/create-space/CreateSpace.tsx
index b1f9f649..530145af 100644
--- a/src/app/features/create-space/CreateSpace.tsx
+++ b/src/app/features/create-space/CreateSpace.tsx
@@ -184,7 +184,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
onClick={() => setAdvance(!advance)}
type="button"
>
- Advance Options
+ Advanced Options
diff --git a/src/app/features/room-settings/general/General.tsx b/src/app/features/room-settings/general/General.tsx
index d9c16c90..15157304 100644
--- a/src/app/features/room-settings/general/General.tsx
+++ b/src/app/features/room-settings/general/General.tsx
@@ -59,7 +59,7 @@ export function General({ requestClose }: GeneralProps) {
- Advance Options
+ Advanced Options
diff --git a/src/app/features/space-settings/general/General.tsx b/src/app/features/space-settings/general/General.tsx
index 641bfa7a..72040632 100644
--- a/src/app/features/space-settings/general/General.tsx
+++ b/src/app/features/space-settings/general/General.tsx
@@ -55,7 +55,7 @@ export function General({ requestClose }: GeneralProps) {
- Advance Options
+ Advanced Options
From 17d4bceb429d607edbc416d6aa911c3fae5f01c9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Nov 2025 16:16:20 +1100
Subject: [PATCH 020/220] Bump nginx from 1.29.1-alpine to 1.29.3-alpine
(#2525)
Bumps nginx from 1.29.1-alpine to 1.29.3-alpine.
---
updated-dependencies:
- dependency-name: nginx
dependency-version: 1.29.3-alpine
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index 46d0db6e..6489cb86 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ RUN npm run build
## App
-FROM nginx:1.29.1-alpine
+FROM nginx:1.29.3-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
From 655c1c9aff6e1a31a1a632bd4fce4b14bfbd4779 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Nov 2025 16:30:39 +1100
Subject: [PATCH 021/220] Bump docker/login-action from 3.5.0 to 3.6.0 (#2496)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.5.0...v3.6.0)
---
updated-dependencies:
- dependency-name: docker/login-action
dependency-version: 3.6.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/prod-deploy.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml
index 7b58f56f..fe8a722f 100644
--- a/.github/workflows/prod-deploy.yml
+++ b/.github/workflows/prod-deploy.yml
@@ -72,12 +72,12 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
- name: Login to Docker Hub
- uses: docker/login-action@v3.5.0
+ uses: docker/login-action@v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
- uses: docker/login-action@v3.5.0
+ uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
From 073a9f5786d676b9e98b98bd3f03e49d0324bb3b Mon Sep 17 00:00:00 2001
From: Santhoshkumar044
Date: Mon, 12 Jan 2026 17:51:00 +0530
Subject: [PATCH 022/220] Fix room alias mention triggering room-wide
notifications (#2562)
* fix: prevent room alias mentions from triggering @room notifications
* fix: Simplify room mention to exact match on @room
---
src/app/components/editor/output.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index dbdd51f3..5310012b 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -212,9 +212,10 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.type === BlockType.CodeBlock) return;
if (node.type === BlockType.Mention) {
- if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
+ if (node.name === '@room') {
mentionData.room = true;
}
+
if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id);
}
From 8ce33ee6ff6a9ec76dea47e458d4a24f50ffbe5e Mon Sep 17 00:00:00 2001
From: Zach
Date: Wed, 11 Feb 2026 17:39:58 -0600
Subject: [PATCH 023/220] Replace envs.net with unredacted.org in config
(#2601)
* Replace 'envs.net' with 'unredacted.org' in config
https://envs.net/ is shutting down their Matrix server
* Update defaultHomeserver and reorder servers list
* Remove 'monero.social' from homeserver list
---
config.json | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/config.json b/config.json
index de6015a1..644f0eb6 100644
--- a/config.json
+++ b/config.json
@@ -1,11 +1,10 @@
{
- "defaultHomeserver": 2,
+ "defaultHomeserver": 1,
"homeserverList": [
"converser.eu",
- "envs.net",
"matrix.org",
- "monero.social",
"mozilla.org",
+ "unredacted.org",
"xmr.se"
],
"allowCustomHomeservers": true,
@@ -15,7 +14,7 @@
"spaces": [
"#cinny-space:matrix.org",
"#community:matrix.org",
- "#space:envs.net",
+ "#space:unredacted.org",
"#science-space:matrix.org",
"#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org"
@@ -28,7 +27,7 @@
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
- "servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
+ "servers": [ "matrix.org", "mozilla.org", "unredacted.org" ]
},
"hashRouter": {
From ca82aa283a43faf65b43bcd150e156d4fe615581 Mon Sep 17 00:00:00 2001
From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>
Date: Thu, 12 Feb 2026 06:27:17 +0100
Subject: [PATCH 024/220] Hide "Delete Message" if it is forbidden (#2602)
hide "Delete Message" if it is forbidden
---
src/app/features/room/RoomTimeline.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 2281b59d..25061dac 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -471,6 +471,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const permissions = useRoomPermissions(creators, powerLevels);
const canRedact = permissions.action('redact', mx.getSafeUserId());
+ const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
const [editId, setEditId] = useState();
@@ -1047,7 +1048,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
- canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ canDelete={canRedact || canDeleteOwn}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1129,7 +1130,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
- canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ canDelete={canRedact || canDeleteOwn}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1247,7 +1248,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
- canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ canDelete={canRedact || canDeleteOwn}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
From 83e5125b37d3c3ea84918291f2c96c96f2d33f6b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 12 Feb 2026 16:56:47 +1100
Subject: [PATCH 025/220] fix(deps): update dependency folds to v2.5.0 (#2606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 158a6112..4c12e9db 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,7 +32,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.4.0",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -7157,9 +7157,9 @@
}
},
"node_modules/folds": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/folds/-/folds-2.4.0.tgz",
- "integrity": "sha512-Q5xCmvU3SIM8etQ9qLF6Y5Jtv01c9JpG3QcnF+Z3nlbMvtktfE13Pj7p0XgSPBcA3OuoU0zXiRwiTlMcbU7KhA==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz",
+ "integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
diff --git a/package.json b/package.json
index 7316dcce..00535bd8 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.4.0",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
From 1ce6ca2b07ceeff5e0def4cb531d864dc672839f Mon Sep 17 00:00:00 2001
From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>
Date: Thu, 12 Feb 2026 11:40:11 +0100
Subject: [PATCH 026/220] Re-add `mEvent.getSender() === mx.getUserId()` check
for deletion of messages (#2607)
* hide "Delete Message" if it is forbidden
* Fix the stuff I broke :/
---
src/app/features/room/RoomTimeline.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 25061dac..d1678b65 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -1048,7 +1048,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
- canDelete={canRedact || canDeleteOwn}
+ canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1130,7 +1130,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
- canDelete={canRedact || canDeleteOwn}
+ canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1248,7 +1248,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
- canDelete={canRedact || canDeleteOwn}
+ canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
From fd37dfe3f9203cfb836b741b49ac232a6b3f890c Mon Sep 17 00:00:00 2001
From: Andrew Murphy
Date: Thu, 12 Feb 2026 21:45:37 +1100
Subject: [PATCH 027/220] Fix muted rooms showing unread badges (#2581)
fix: detect muted rooms with empty actions array
The mute detection was checking for `actions[0] === "dont_notify"` but
Cinny sets `actions: []` (empty array) when muting a room, which is
the correct behavior per Matrix spec where empty actions means no
notification.
This caused muted rooms to still show unread badges and contribute to
space badge counts.
Fixes the isMutedRule check to handle both:
- Empty actions array (current Matrix spec)
- "dont_notify" string (deprecated but may exist in older rules)
---
src/app/utils/room.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts
index b4bba2ad..c282a0a2 100644
--- a/src/app/utils/room.ts
+++ b/src/app/utils/room.ts
@@ -160,7 +160,8 @@ export const getOrphanParents = (roomToParents: RoomToParents, roomId: string):
};
export const isMutedRule = (rule: IPushRule) =>
- rule.actions[0] === 'dont_notify' && rule.conditions?.[0]?.kind === 'event_match';
+ // Check for empty actions (new spec) or dont_notify (deprecated)
+ (rule.actions.length === 0 || rule.actions[0] === 'dont_notify') && rule.conditions?.[0]?.kind === 'event_match';
export const findMutedRule = (overrideRules: IPushRule[], roomId: string) =>
overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule));
From 206a927f30f8b29eea4445f4f599b1f16f66b85e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 14 Feb 2026 17:10:43 +1100
Subject: [PATCH 028/220] fix(deps): update dependency react-router-dom to
v6.30.3 (#2612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
package-lock.json | 29 ++++++++++++++++-------------
package.json | 2 +-
2 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 4c12e9db..823c38b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -56,7 +56,7 @@
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-range": "1.8.14",
- "react-router-dom": "6.20.0",
+ "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1",
"slate": "0.112.0",
"slate-dom": "0.112.2",
@@ -3699,9 +3699,10 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz",
- "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==",
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
"engines": {
"node": ">=14.0.0"
}
@@ -9605,11 +9606,12 @@
}
},
"node_modules/react-router": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz",
- "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==",
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
"dependencies": {
- "@remix-run/router": "1.13.0"
+ "@remix-run/router": "1.23.2"
},
"engines": {
"node": ">=14.0.0"
@@ -9619,12 +9621,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz",
- "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==",
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
"dependencies": {
- "@remix-run/router": "1.13.0",
- "react-router": "6.20.0"
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
},
"engines": {
"node": ">=14.0.0"
diff --git a/package.json b/package.json
index 00535bd8..9438330a 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-range": "1.8.14",
- "react-router-dom": "6.20.0",
+ "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1",
"slate": "0.112.0",
"slate-dom": "0.112.2",
From 074c5552943206ff47df0aa0f85b50b4d79af656 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:41:36 +0530
Subject: [PATCH 029/220] Post session info to service worker instead of asking
from sw (#2605)
post session info to service worker instead of asking from sw on each request
---
src/app/pages/Router.tsx | 5 ++-
src/client/initMatrix.ts | 2 +
src/index.tsx | 15 +++----
src/sw-session.ts | 10 +++++
src/sw.ts | 94 +++++++++++++++++++++++++++-------------
5 files changed, 84 insertions(+), 42 deletions(-)
create mode 100644 src/sw-session.ts
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index 18591f29..04d14a07 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -68,6 +68,7 @@ import { Create } from './client/create';
import { CreateSpaceModalRenderer } from '../features/create-space';
import { SearchModalRenderer } from '../features/search';
import { getFallbackSession } from '../state/sessions';
+import { pushSessionToSW } from '../../sw-session';
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
const { hashRouter } = clientConfig;
@@ -106,7 +107,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
{
- if (!getFallbackSession()) {
+ const session = getFallbackSession();
+ if (!session) {
const afterLoginPath = getAppPathFromHref(
getOriginBaseUrl(hashRouter),
window.location.href
@@ -114,6 +116,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
return redirect(getLoginPath());
}
+ pushSessionToSW(session.baseUrl, session.accessToken);
return null;
}}
element={
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
index 487c3f13..498d4f75 100644
--- a/src/client/initMatrix.ts
+++ b/src/client/initMatrix.ts
@@ -2,6 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from
import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
+import { pushSessionToSW } from '../sw-session';
type Session = {
baseUrl: string;
@@ -53,6 +54,7 @@ export const clearCacheAndReload = async (mx: MatrixClient) => {
};
export const logoutClient = async (mx: MatrixClient) => {
+ pushSessionToSW();
mx.stopClient();
try {
await mx.logout();
diff --git a/src/index.tsx b/src/index.tsx
index ddfd30b4..0019a224 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -15,6 +15,8 @@ import App from './app/pages/App';
// import i18n (needs to be bundled ;))
import './app/i18n';
+import { pushSessionToSW } from './sw-session';
+import { getFallbackSession } from './app/state/sessions';
document.body.classList.add(configClass, varsClass);
@@ -25,16 +27,9 @@ if ('serviceWorker' in navigator) {
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
- navigator.serviceWorker.register(swUrl);
- navigator.serviceWorker.addEventListener('message', (event) => {
- if (event.data?.type === 'token' && event.data?.responseKey) {
- // Get the token for SW.
- const token = localStorage.getItem('cinny_access_token') ?? undefined;
- event.source!.postMessage({
- responseKey: event.data.responseKey,
- token,
- });
- }
+ navigator.serviceWorker.register(swUrl).then(() => {
+ const session = getFallbackSession();
+ pushSessionToSW(session?.baseUrl, session?.accessToken);
});
}
diff --git a/src/sw-session.ts b/src/sw-session.ts
new file mode 100644
index 00000000..4b2ec055
--- /dev/null
+++ b/src/sw-session.ts
@@ -0,0 +1,10 @@
+export function pushSessionToSW(baseUrl?: string, accessToken?: string) {
+ if (!('serviceWorker' in navigator)) return;
+ if (!navigator.serviceWorker.controller) return;
+
+ navigator.serviceWorker.controller.postMessage({
+ type: 'setSession',
+ accessToken,
+ baseUrl,
+ });
+}
diff --git a/src/sw.ts b/src/sw.ts
index 2179dfcb..d4eebc02 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -3,22 +3,64 @@
export type {};
declare const self: ServiceWorkerGlobalScope;
-async function askForAccessToken(client: Client): Promise {
- return new Promise((resolve) => {
- const responseKey = Math.random().toString(36);
- const listener = (event: ExtendableMessageEvent) => {
- if (event.data.responseKey !== responseKey) return;
- resolve(event.data.token);
- self.removeEventListener('message', listener);
- };
- self.addEventListener('message', listener);
- client.postMessage({ responseKey, type: 'token' });
+self.addEventListener('install', () => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event: ExtendableEvent) => {
+ event.waitUntil(self.clients.claim());
+});
+
+type SessionInfo = {
+ accessToken: string;
+ baseUrl: string;
+};
+
+/**
+ * Store session per client (tab)
+ */
+const sessions = new Map();
+
+async function cleanupDeadClients() {
+ const activeClients = await self.clients.matchAll();
+ const activeIds = new Set(activeClients.map((c) => c.id));
+
+ Array.from(sessions.keys()).forEach((id) => {
+ if (!activeIds.has(id)) {
+ sessions.delete(id);
+ }
});
}
-function fetchConfig(token?: string): RequestInit | undefined {
- if (!token) return undefined;
+/**
+ * Receive session updates from clients
+ */
+self.addEventListener('message', (event: ExtendableMessageEvent) => {
+ const client = event.source as Client | null;
+ if (!client) return;
+ const { type, accessToken, baseUrl } = event.data || {};
+
+ if (type !== 'setSession') return;
+
+ cleanupDeadClients();
+
+ if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
+ sessions.set(client.id, { accessToken, baseUrl });
+ } else {
+ // Logout or invalid session
+ sessions.delete(client.id);
+ }
+});
+
+function validMediaRequest(url: string, baseUrl: string): boolean {
+ const downloadUrl = new URL('/_matrix/client/v1/media/download', baseUrl);
+ const thumbnailUrl = new URL('/_matrix/client/v1/media/thumbnail', baseUrl);
+
+ return url.startsWith(downloadUrl.href) || url.startsWith(thumbnailUrl.href);
+}
+
+function fetchConfig(token: string): RequestInit {
return {
headers: {
Authorization: `Bearer ${token}`,
@@ -27,26 +69,16 @@ function fetchConfig(token?: string): RequestInit | undefined {
};
}
-self.addEventListener('activate', (event: ExtendableEvent) => {
- event.waitUntil(clients.claim());
-});
-
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
- if (method !== 'GET') return;
- if (
- !url.includes('/_matrix/client/v1/media/download') &&
- !url.includes('/_matrix/client/v1/media/thumbnail')
- ) {
- return;
- }
- event.respondWith(
- (async (): Promise => {
- const client = await self.clients.get(event.clientId);
- let token: string | undefined;
- if (client) token = await askForAccessToken(client);
- return fetch(url, fetchConfig(token));
- })()
- );
+ if (method !== 'GET') return;
+ if (!event.clientId) return;
+
+ const session = sessions.get(event.clientId);
+ if (!session) return;
+
+ if (!validMediaRequest(url, session.baseUrl)) return;
+
+ event.respondWith(fetch(url, fetchConfig(session.accessToken)));
});
From 3522751a15ae194e1d9cf49512796ee5f4f3d74a Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:42:28 +0530
Subject: [PATCH 030/220] Prevent invalid mxc from getting used (#2609)
---
.../editor/autocomplete/EmoticonAutocomplete.tsx | 6 ++++--
src/app/components/emoji-board/EmojiBoard.tsx | 5 ++---
src/app/components/emoji-board/components/Item.tsx | 4 ++--
src/app/components/message/FileHeader.tsx | 3 ++-
src/app/components/message/content/AudioContent.tsx | 3 ++-
src/app/components/message/content/FileContent.tsx | 9 ++++++---
src/app/components/message/content/ImageContent.tsx | 3 ++-
src/app/components/message/content/ThumbnailContent.tsx | 3 ++-
src/app/components/message/content/VideoContent.tsx | 3 ++-
9 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
index cc0dff19..d358ff7d 100644
--- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
@@ -88,6 +88,8 @@ export function EmoticonAutocomplete({
{autoCompleteEmoticon.map((emoticon) => {
const isCustomEmoji = 'url' in emoticon;
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
+ const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
+
return (
+