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] 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 ( @@ -411,79 +77,20 @@ export const ThreadsMenu = forwardRef( - - - {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, }); };