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] 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; };
+ {mEvent.sender?.name}: {mEvent.getContent().body} +