mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	load threads in My Threads menu
This commit is contained in:
		
							parent
							
								
									e44ca92422
								
							
						
					
					
						commit
						12bcbc2e78
					
				
					 6 changed files with 608 additions and 473 deletions
				
			
		
							
								
								
									
										32
									
								
								src/app/features/room/threads-menu/ThreadsError.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/app/features/room/threads-menu/ThreadsError.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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 (
 | 
				
			||||||
 | 
					    <Box
 | 
				
			||||||
 | 
					      className={ContainerColor({ variant: 'SurfaceVariant' })}
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        marginBottom: config.space.S200,
 | 
				
			||||||
 | 
					        padding: `${config.space.S700} ${config.space.S400} ${toRem(60)}`,
 | 
				
			||||||
 | 
					        borderRadius: config.radii.R300,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      grow="Yes"
 | 
				
			||||||
 | 
					      direction="Column"
 | 
				
			||||||
 | 
					      gap="400"
 | 
				
			||||||
 | 
					      justifyContent="Center"
 | 
				
			||||||
 | 
					      alignItems="Center"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Icon src={Icons.Warning} size="600" />
 | 
				
			||||||
 | 
					      <Box style={{ maxWidth: toRem(300) }} direction="Column" gap="200" alignItems="Center">
 | 
				
			||||||
 | 
					        <Text size="H4" align="Center">
 | 
				
			||||||
 | 
					          {error.name}
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <Text className={BreakWord} size="T400" align="Center">
 | 
				
			||||||
 | 
					          {error.message}
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/app/features/room/threads-menu/ThreadsLoading.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/app/features/room/threads-menu/ThreadsLoading.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import { Box, config, Spinner } from 'folds';
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { ContainerColor } from '../../../styles/ContainerColor.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ThreadsLoading() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box
 | 
				
			||||||
 | 
					      className={ContainerColor({ variant: 'SurfaceVariant' })}
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        marginBottom: config.space.S200,
 | 
				
			||||||
 | 
					        padding: config.space.S700,
 | 
				
			||||||
 | 
					        borderRadius: config.radii.R300,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      grow="Yes"
 | 
				
			||||||
 | 
					      direction="Column"
 | 
				
			||||||
 | 
					      gap="400"
 | 
				
			||||||
 | 
					      justifyContent="Center"
 | 
				
			||||||
 | 
					      alignItems="Center"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Spinner variant="Secondary" />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,194 +1,48 @@
 | 
				
			||||||
/* eslint-disable react/destructuring-assignment */
 | 
					/* eslint-disable react/destructuring-assignment */
 | 
				
			||||||
import React, { forwardRef, MouseEventHandler, useMemo, useRef } from 'react';
 | 
					import React, { forwardRef, useMemo } from 'react';
 | 
				
			||||||
import { IRoomEvent, MatrixEvent, Room } from 'matrix-js-sdk';
 | 
					import { EventTimelineSet, Room } from 'matrix-js-sdk';
 | 
				
			||||||
import {
 | 
					import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text, toRem } from 'folds';
 | 
				
			||||||
  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 * as css from './ThreadsMenu.css';
 | 
					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 { 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 { 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 = {
 | 
					const getTimelines = (timelineSet: EventTimelineSet) => {
 | 
				
			||||||
  room: Room;
 | 
					  const liveTimeline = timelineSet.getLiveTimeline();
 | 
				
			||||||
  event: MatrixEvent;
 | 
					  const linkedTimelines = getLinkedTimelines(liveTimeline);
 | 
				
			||||||
  renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
 | 
					
 | 
				
			||||||
  onOpen: (roomId: string, eventId: string) => void;
 | 
					  return linkedTimelines;
 | 
				
			||||||
  getMemberPowerTag: GetMemberPowerTag;
 | 
					 | 
				
			||||||
  accessibleTagColors: Map<string, string>;
 | 
					 | 
				
			||||||
  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 = () => (
 | 
					 | 
				
			||||||
    <Box shrink="No" gap="200" alignItems="Center">
 | 
					 | 
				
			||||||
      <Chip
 | 
					 | 
				
			||||||
        data-event-id={event.getId()}
 | 
					 | 
				
			||||||
        onClick={handleOpenClick}
 | 
					 | 
				
			||||||
        variant="Secondary"
 | 
					 | 
				
			||||||
        radii="Pill"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Text size="T200">Open</Text>
 | 
					 | 
				
			||||||
      </Chip>
 | 
					 | 
				
			||||||
    </Box>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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 (
 | 
					  return (
 | 
				
			||||||
    <ModernLayout
 | 
					    <Box
 | 
				
			||||||
      before={
 | 
					      className={ContainerColor({ variant: 'SurfaceVariant' })}
 | 
				
			||||||
        <AvatarBase>
 | 
					      style={{
 | 
				
			||||||
          <Avatar size="300">
 | 
					        marginBottom: config.space.S200,
 | 
				
			||||||
            <UserAvatar
 | 
					        padding: `${config.space.S700} ${config.space.S400} ${toRem(60)}`,
 | 
				
			||||||
              userId={sender}
 | 
					        borderRadius: config.radii.R300,
 | 
				
			||||||
              src={
 | 
					      }}
 | 
				
			||||||
                senderAvatarMxc
 | 
					      grow="Yes"
 | 
				
			||||||
                  ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
 | 
					      direction="Column"
 | 
				
			||||||
                    undefined
 | 
					      gap="400"
 | 
				
			||||||
                  : undefined
 | 
					      justifyContent="Center"
 | 
				
			||||||
              }
 | 
					      alignItems="Center"
 | 
				
			||||||
              alt={displayName}
 | 
					 | 
				
			||||||
              renderFallback={() => <Icon size="200" src={Icons.User} filled />}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Avatar>
 | 
					 | 
				
			||||||
        </AvatarBase>
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
 | 
					      <Icon src={Icons.Thread} size="600" />
 | 
				
			||||||
        <Box gap="200" alignItems="Baseline">
 | 
					      <Box style={{ maxWidth: toRem(300) }} direction="Column" gap="200" alignItems="Center">
 | 
				
			||||||
          <Box alignItems="Center" gap="200">
 | 
					        <Text size="H4" align="Center">
 | 
				
			||||||
            <Username style={{ color: usernameColor }}>
 | 
					          No Threads Yet
 | 
				
			||||||
              <Text as="span" truncate>
 | 
					        </Text>
 | 
				
			||||||
                <UsernameBold>{displayName}</UsernameBold>
 | 
					        <Text size="T400" align="Center">
 | 
				
			||||||
              </Text>
 | 
					          Threads you’re participating in will appear here.
 | 
				
			||||||
            </Username>
 | 
					        </Text>
 | 
				
			||||||
            {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
 | 
					 | 
				
			||||||
          </Box>
 | 
					 | 
				
			||||||
          <Time ts={event.getTs()} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
 | 
					 | 
				
			||||||
        </Box>
 | 
					 | 
				
			||||||
        {renderOptions()}
 | 
					 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
      {event.replyEventId && (
 | 
					    </Box>
 | 
				
			||||||
        <Reply
 | 
					 | 
				
			||||||
          room={room}
 | 
					 | 
				
			||||||
          replyEventId={event.replyEventId}
 | 
					 | 
				
			||||||
          threadRootId={event.threadRootId}
 | 
					 | 
				
			||||||
          onClick={handleOpenClick}
 | 
					 | 
				
			||||||
          getMemberPowerTag={getMemberPowerTag}
 | 
					 | 
				
			||||||
          accessibleTagColors={accessibleTagColors}
 | 
					 | 
				
			||||||
          legacyUsernameColor={legacyUsernameColor}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      {renderContent(event.getType(), false, event, displayName, getContent)}
 | 
					 | 
				
			||||||
    </ModernLayout>
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,204 +52,16 @@ type ThreadsMenuProps = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export const ThreadsMenu = forwardRef<HTMLDivElement, ThreadsMenuProps>(
 | 
					export const ThreadsMenu = forwardRef<HTMLDivElement, ThreadsMenuProps>(
 | 
				
			||||||
  ({ room, requestClose }, ref) => {
 | 
					  ({ room, requestClose }, ref) => {
 | 
				
			||||||
    const mx = useMatrixClient();
 | 
					    const threadsState = useRoomMyThreads(room);
 | 
				
			||||||
    const powerLevels = usePowerLevelsContext();
 | 
					    const threadsTimelineSet =
 | 
				
			||||||
    const creators = useRoomCreators(room);
 | 
					      threadsState.status === AsyncStatus.Success ? threadsState.data : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const creatorsTag = useRoomCreatorsTag();
 | 
					    const linkedTimelines = useMemo(() => {
 | 
				
			||||||
    const powerLevelTags = usePowerLevelTags(room, powerLevels);
 | 
					      if (!threadsTimelineSet) return undefined;
 | 
				
			||||||
    const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
 | 
					      return getTimelines(threadsTimelineSet);
 | 
				
			||||||
 | 
					    }, [threadsTimelineSet]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const theme = useTheme();
 | 
					    const hasEvents = linkedTimelines && getTimelinesEventsCount(linkedTimelines) > 0;
 | 
				
			||||||
    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<HTMLDivElement>(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<LinkifyOpts>(
 | 
					 | 
				
			||||||
      () => ({
 | 
					 | 
				
			||||||
        ...LINKIFY_OPTS,
 | 
					 | 
				
			||||||
        render: factoryRenderLinkifyWithMention((href) =>
 | 
					 | 
				
			||||||
          renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
      [mx, room, mentionClickHandler]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        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 (
 | 
					 | 
				
			||||||
              <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const threadDetail = getEventThreadDetail(event);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <>
 | 
					 | 
				
			||||||
              <RenderMessageContent
 | 
					 | 
				
			||||||
                displayName={displayName}
 | 
					 | 
				
			||||||
                msgType={event.getContent().msgtype ?? ''}
 | 
					 | 
				
			||||||
                ts={event.getTs()}
 | 
					 | 
				
			||||||
                getContent={getContent}
 | 
					 | 
				
			||||||
                edited={!!event.replacingEvent()}
 | 
					 | 
				
			||||||
                mediaAutoLoad={mediaAutoLoad}
 | 
					 | 
				
			||||||
                urlPreview={urlPreview}
 | 
					 | 
				
			||||||
                htmlReactParserOptions={htmlReactParserOptions}
 | 
					 | 
				
			||||||
                linkifyOpts={linkifyOpts}
 | 
					 | 
				
			||||||
                outlineAttachment
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              {threadDetail && (
 | 
					 | 
				
			||||||
                <ThreadSelectorContainer>
 | 
					 | 
				
			||||||
                  <ThreadSelector
 | 
					 | 
				
			||||||
                    room={room}
 | 
					 | 
				
			||||||
                    threadDetail={threadDetail}
 | 
					 | 
				
			||||||
                    outlined
 | 
					 | 
				
			||||||
                    hour24Clock={hour24Clock}
 | 
					 | 
				
			||||||
                    dateFormatString={dateFormatString}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </ThreadSelectorContainer>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        [MessageEvent.RoomMessageEncrypted]: (mEvent, displayName) => {
 | 
					 | 
				
			||||||
          const eventId = mEvent.getId()!;
 | 
					 | 
				
			||||||
          const evtTimeline = room.getTimelineForEvent(eventId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <EncryptedContent mEvent={mEvent}>
 | 
					 | 
				
			||||||
              {() => {
 | 
					 | 
				
			||||||
                if (mEvent.isRedacted()) return <RedactedContent />;
 | 
					 | 
				
			||||||
                if (mEvent.getType() === MessageEvent.Sticker)
 | 
					 | 
				
			||||||
                  return (
 | 
					 | 
				
			||||||
                    <MSticker
 | 
					 | 
				
			||||||
                      content={mEvent.getContent()}
 | 
					 | 
				
			||||||
                      renderImageContent={(props) => (
 | 
					 | 
				
			||||||
                        <ImageContent
 | 
					 | 
				
			||||||
                          {...props}
 | 
					 | 
				
			||||||
                          autoPlay={mediaAutoLoad}
 | 
					 | 
				
			||||||
                          renderImage={(p) => <Image {...p} loading="lazy" />}
 | 
					 | 
				
			||||||
                          renderViewer={(p) => <ImageViewer {...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 (
 | 
					 | 
				
			||||||
                    <RenderMessageContent
 | 
					 | 
				
			||||||
                      displayName={displayName}
 | 
					 | 
				
			||||||
                      msgType={mEvent.getContent().msgtype ?? ''}
 | 
					 | 
				
			||||||
                      ts={mEvent.getTs()}
 | 
					 | 
				
			||||||
                      edited={!!editedEvent || !!mEvent.replacingEvent()}
 | 
					 | 
				
			||||||
                      getContent={getContent}
 | 
					 | 
				
			||||||
                      mediaAutoLoad={mediaAutoLoad}
 | 
					 | 
				
			||||||
                      urlPreview={urlPreview}
 | 
					 | 
				
			||||||
                      htmlReactParserOptions={htmlReactParserOptions}
 | 
					 | 
				
			||||||
                      linkifyOpts={linkifyOpts}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
 | 
					 | 
				
			||||||
                  return (
 | 
					 | 
				
			||||||
                    <Text>
 | 
					 | 
				
			||||||
                      <MessageNotDecryptedContent />
 | 
					 | 
				
			||||||
                    </Text>
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                return (
 | 
					 | 
				
			||||||
                  <Text>
 | 
					 | 
				
			||||||
                    <MessageUnsupportedContent />
 | 
					 | 
				
			||||||
                  </Text>
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            </EncryptedContent>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        [MessageEvent.Sticker]: (event, displayName, getContent) => {
 | 
					 | 
				
			||||||
          if (event.isRedacted()) {
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <MSticker
 | 
					 | 
				
			||||||
              content={getContent()}
 | 
					 | 
				
			||||||
              renderImageContent={(props) => (
 | 
					 | 
				
			||||||
                <ImageContent
 | 
					 | 
				
			||||||
                  {...props}
 | 
					 | 
				
			||||||
                  autoPlay={mediaAutoLoad}
 | 
					 | 
				
			||||||
                  renderImage={(p) => <Image {...p} loading="lazy" />}
 | 
					 | 
				
			||||||
                  renderViewer={(p) => <ImageViewer {...p} />}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      undefined,
 | 
					 | 
				
			||||||
      (event) => {
 | 
					 | 
				
			||||||
        if (event.isRedacted()) {
 | 
					 | 
				
			||||||
          return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          <Box grow="Yes" direction="Column">
 | 
					 | 
				
			||||||
            <Text size="T400" priority="300">
 | 
					 | 
				
			||||||
              <code className={customHtmlCss.Code}>{event.getType()}</code>
 | 
					 | 
				
			||||||
              {' event'}
 | 
					 | 
				
			||||||
            </Text>
 | 
					 | 
				
			||||||
          </Box>
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleOpen = (roomId: string, eventId: string) => {
 | 
					 | 
				
			||||||
      navigateRoom(roomId, eventId);
 | 
					 | 
				
			||||||
      requestClose();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Menu ref={ref} className={css.ThreadsMenu}>
 | 
					      <Menu ref={ref} className={css.ThreadsMenu}>
 | 
				
			||||||
| 
						 | 
					@ -411,79 +77,20 @@ export const ThreadsMenu = forwardRef<HTMLDivElement, ThreadsMenuProps>(
 | 
				
			||||||
            </Box>
 | 
					            </Box>
 | 
				
			||||||
          </Header>
 | 
					          </Header>
 | 
				
			||||||
          <Box grow="Yes">
 | 
					          <Box grow="Yes">
 | 
				
			||||||
            <Scroll ref={scrollRef} size="300" hideTrack visibility="Hover">
 | 
					            {threadsState.status === AsyncStatus.Success && hasEvents ? (
 | 
				
			||||||
              <Box className={css.ThreadsMenuContent} direction="Column" gap="100">
 | 
					              <ThreadsTimeline timelines={linkedTimelines} requestClose={requestClose} />
 | 
				
			||||||
                {events && events.length > 0 ? (
 | 
					            ) : (
 | 
				
			||||||
                  <div
 | 
					              <Scroll size="300" hideTrack visibility="Hover">
 | 
				
			||||||
                    style={{
 | 
					                <Box className={css.ThreadsMenuContent} direction="Column" gap="100">
 | 
				
			||||||
                      position: 'relative',
 | 
					                  {(threadsState.status === AsyncStatus.Loading ||
 | 
				
			||||||
                      height: virtualizer.getTotalSize(),
 | 
					                    threadsState.status === AsyncStatus.Idle) && <ThreadsLoading />}
 | 
				
			||||||
                    }}
 | 
					                  {threadsState.status === AsyncStatus.Success && !hasEvents && <NoThreads />}
 | 
				
			||||||
                  >
 | 
					                  {threadsState.status === AsyncStatus.Error && (
 | 
				
			||||||
                    {virtualizer.getVirtualItems().map((vItem) => {
 | 
					                    <ThreadsError error={threadsState.error} />
 | 
				
			||||||
                      const event = events[vItem.index];
 | 
					                  )}
 | 
				
			||||||
                      if (!event.getId()) return null;
 | 
					                </Box>
 | 
				
			||||||
 | 
					              </Scroll>
 | 
				
			||||||
                      return (
 | 
					            )}
 | 
				
			||||||
                        <VirtualTile
 | 
					 | 
				
			||||||
                          virtualItem={vItem}
 | 
					 | 
				
			||||||
                          style={{ paddingBottom: config.space.S200 }}
 | 
					 | 
				
			||||||
                          ref={virtualizer.measureElement}
 | 
					 | 
				
			||||||
                          key={vItem.index}
 | 
					 | 
				
			||||||
                        >
 | 
					 | 
				
			||||||
                          <SequenceCard
 | 
					 | 
				
			||||||
                            style={{ padding: config.space.S400, borderRadius: config.radii.R300 }}
 | 
					 | 
				
			||||||
                            variant="SurfaceVariant"
 | 
					 | 
				
			||||||
                            direction="Column"
 | 
					 | 
				
			||||||
                          >
 | 
					 | 
				
			||||||
                            <ThreadMessage
 | 
					 | 
				
			||||||
                              room={room}
 | 
					 | 
				
			||||||
                              event={event}
 | 
					 | 
				
			||||||
                              renderContent={renderMatrixEvent}
 | 
					 | 
				
			||||||
                              onOpen={handleOpen}
 | 
					 | 
				
			||||||
                              getMemberPowerTag={getMemberPowerTag}
 | 
					 | 
				
			||||||
                              accessibleTagColors={accessibleTagColors}
 | 
					 | 
				
			||||||
                              legacyUsernameColor={legacyUsernameColor || direct}
 | 
					 | 
				
			||||||
                              hour24Clock={hour24Clock}
 | 
					 | 
				
			||||||
                              dateFormatString={dateFormatString}
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                          </SequenceCard>
 | 
					 | 
				
			||||||
                        </VirtualTile>
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                ) : (
 | 
					 | 
				
			||||||
                  <Box
 | 
					 | 
				
			||||||
                    className={ContainerColor({ variant: 'SurfaceVariant' })}
 | 
					 | 
				
			||||||
                    style={{
 | 
					 | 
				
			||||||
                      marginBottom: config.space.S200,
 | 
					 | 
				
			||||||
                      padding: `${config.space.S700} ${config.space.S400} ${toRem(60)}`,
 | 
					 | 
				
			||||||
                      borderRadius: config.radii.R300,
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                    grow="Yes"
 | 
					 | 
				
			||||||
                    direction="Column"
 | 
					 | 
				
			||||||
                    gap="400"
 | 
					 | 
				
			||||||
                    justifyContent="Center"
 | 
					 | 
				
			||||||
                    alignItems="Center"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <Icon src={Icons.Thread} size="600" />
 | 
					 | 
				
			||||||
                    <Box
 | 
					 | 
				
			||||||
                      style={{ maxWidth: toRem(300) }}
 | 
					 | 
				
			||||||
                      direction="Column"
 | 
					 | 
				
			||||||
                      gap="200"
 | 
					 | 
				
			||||||
                      alignItems="Center"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <Text size="H4" align="Center">
 | 
					 | 
				
			||||||
                        No Threads
 | 
					 | 
				
			||||||
                      </Text>
 | 
					 | 
				
			||||||
                      <Text size="T400" align="Center">
 | 
					 | 
				
			||||||
                        Threads you are participating in will appear here.
 | 
					 | 
				
			||||||
                      </Text>
 | 
					 | 
				
			||||||
                    </Box>
 | 
					 | 
				
			||||||
                  </Box>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </Box>
 | 
					 | 
				
			||||||
            </Scroll>
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Menu>
 | 
					      </Menu>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										481
									
								
								src/app/features/room/threads-menu/ThreadsTimeline.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										481
									
								
								src/app/features/room/threads-menu/ThreadsTimeline.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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<string, string>;
 | 
				
			||||||
 | 
					  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 = () => (
 | 
				
			||||||
 | 
					    <Box shrink="No" gap="200" alignItems="Center">
 | 
				
			||||||
 | 
					      <Chip
 | 
				
			||||||
 | 
					        data-event-id={event.getId()}
 | 
				
			||||||
 | 
					        onClick={handleOpenClick}
 | 
				
			||||||
 | 
					        variant="Secondary"
 | 
				
			||||||
 | 
					        radii="Pill"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Text size="T200">Open</Text>
 | 
				
			||||||
 | 
					      </Chip>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 (
 | 
				
			||||||
 | 
					    <ModernLayout
 | 
				
			||||||
 | 
					      before={
 | 
				
			||||||
 | 
					        <AvatarBase>
 | 
				
			||||||
 | 
					          <Avatar size="300">
 | 
				
			||||||
 | 
					            <UserAvatar
 | 
				
			||||||
 | 
					              userId={sender}
 | 
				
			||||||
 | 
					              src={
 | 
				
			||||||
 | 
					                senderAvatarMxc
 | 
				
			||||||
 | 
					                  ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
 | 
				
			||||||
 | 
					                    undefined
 | 
				
			||||||
 | 
					                  : undefined
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              alt={displayName}
 | 
				
			||||||
 | 
					              renderFallback={() => <Icon size="200" src={Icons.User} filled />}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Avatar>
 | 
				
			||||||
 | 
					        </AvatarBase>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
 | 
				
			||||||
 | 
					        <Box gap="200" alignItems="Baseline">
 | 
				
			||||||
 | 
					          <Box alignItems="Center" gap="200">
 | 
				
			||||||
 | 
					            <Username style={{ color: usernameColor }}>
 | 
				
			||||||
 | 
					              <Text as="span" truncate>
 | 
				
			||||||
 | 
					                <UsernameBold>{displayName}</UsernameBold>
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </Username>
 | 
				
			||||||
 | 
					            {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					          <Time ts={event.getTs()} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        {renderOptions()}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					      {event.replyEventId && (
 | 
				
			||||||
 | 
					        <Reply
 | 
				
			||||||
 | 
					          room={room}
 | 
				
			||||||
 | 
					          replyEventId={event.replyEventId}
 | 
				
			||||||
 | 
					          threadRootId={event.threadRootId}
 | 
				
			||||||
 | 
					          onClick={handleOpenClick}
 | 
				
			||||||
 | 
					          getMemberPowerTag={getMemberPowerTag}
 | 
				
			||||||
 | 
					          accessibleTagColors={accessibleTagColors}
 | 
				
			||||||
 | 
					          legacyUsernameColor={legacyUsernameColor}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {renderContent(event.getType(), false, event, displayName, getContent)}
 | 
				
			||||||
 | 
					    </ModernLayout>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ThreadsTimelineProps = {
 | 
				
			||||||
 | 
					  timelines: EventTimeline[];
 | 
				
			||||||
 | 
					  requestClose: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProps) {
 | 
				
			||||||
 | 
					  const mx = useMatrixClient();
 | 
				
			||||||
 | 
					  const { navigateRoom } = useRoomNavigate();
 | 
				
			||||||
 | 
					  const alive = useAlive();
 | 
				
			||||||
 | 
					  const scrollRef = useRef<HTMLDivElement>(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<LinkifyOpts>(
 | 
				
			||||||
 | 
					    () => ({
 | 
				
			||||||
 | 
					      ...LINKIFY_OPTS,
 | 
				
			||||||
 | 
					      render: factoryRenderLinkifyWithMention((href) =>
 | 
				
			||||||
 | 
					        renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    [mx, room, mentionClickHandler]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      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 <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const threadDetail = getEventThreadDetail(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <RenderMessageContent
 | 
				
			||||||
 | 
					              displayName={displayName}
 | 
				
			||||||
 | 
					              msgType={event.getContent().msgtype ?? ''}
 | 
				
			||||||
 | 
					              ts={event.getTs()}
 | 
				
			||||||
 | 
					              getContent={getContent}
 | 
				
			||||||
 | 
					              edited={!!event.replacingEvent()}
 | 
				
			||||||
 | 
					              mediaAutoLoad={mediaAutoLoad}
 | 
				
			||||||
 | 
					              urlPreview={urlPreview}
 | 
				
			||||||
 | 
					              htmlReactParserOptions={htmlReactParserOptions}
 | 
				
			||||||
 | 
					              linkifyOpts={linkifyOpts}
 | 
				
			||||||
 | 
					              outlineAttachment
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            {threadDetail && (
 | 
				
			||||||
 | 
					              <ThreadSelectorContainer>
 | 
				
			||||||
 | 
					                <ThreadSelector
 | 
				
			||||||
 | 
					                  room={room}
 | 
				
			||||||
 | 
					                  threadDetail={threadDetail}
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  hour24Clock={hour24Clock}
 | 
				
			||||||
 | 
					                  dateFormatString={dateFormatString}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ThreadSelectorContainer>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [MessageEvent.RoomMessageEncrypted]: (mEvent, displayName) => {
 | 
				
			||||||
 | 
					        const eventId = mEvent.getId()!;
 | 
				
			||||||
 | 
					        const evtTimeline = room.getTimelineForEvent(eventId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <EncryptedContent mEvent={mEvent}>
 | 
				
			||||||
 | 
					            {() => {
 | 
				
			||||||
 | 
					              if (mEvent.isRedacted()) return <RedactedContent />;
 | 
				
			||||||
 | 
					              if (mEvent.getType() === MessageEvent.Sticker)
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                  <MSticker
 | 
				
			||||||
 | 
					                    content={mEvent.getContent()}
 | 
				
			||||||
 | 
					                    renderImageContent={(props) => (
 | 
				
			||||||
 | 
					                      <ImageContent
 | 
				
			||||||
 | 
					                        {...props}
 | 
				
			||||||
 | 
					                        autoPlay={mediaAutoLoad}
 | 
				
			||||||
 | 
					                        renderImage={(p) => <Image {...p} loading="lazy" />}
 | 
				
			||||||
 | 
					                        renderViewer={(p) => <ImageViewer {...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 (
 | 
				
			||||||
 | 
					                  <RenderMessageContent
 | 
				
			||||||
 | 
					                    displayName={displayName}
 | 
				
			||||||
 | 
					                    msgType={mEvent.getContent().msgtype ?? ''}
 | 
				
			||||||
 | 
					                    ts={mEvent.getTs()}
 | 
				
			||||||
 | 
					                    edited={!!editedEvent || !!mEvent.replacingEvent()}
 | 
				
			||||||
 | 
					                    getContent={getContent}
 | 
				
			||||||
 | 
					                    mediaAutoLoad={mediaAutoLoad}
 | 
				
			||||||
 | 
					                    urlPreview={urlPreview}
 | 
				
			||||||
 | 
					                    htmlReactParserOptions={htmlReactParserOptions}
 | 
				
			||||||
 | 
					                    linkifyOpts={linkifyOpts}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                  <Text>
 | 
				
			||||||
 | 
					                    <MessageNotDecryptedContent />
 | 
				
			||||||
 | 
					                  </Text>
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                <Text>
 | 
				
			||||||
 | 
					                  <MessageUnsupportedContent />
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          </EncryptedContent>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [MessageEvent.Sticker]: (event, displayName, getContent) => {
 | 
				
			||||||
 | 
					        if (event.isRedacted()) {
 | 
				
			||||||
 | 
					          return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const threadDetail = getEventThreadDetail(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <MSticker
 | 
				
			||||||
 | 
					              content={getContent()}
 | 
				
			||||||
 | 
					              renderImageContent={(props) => (
 | 
				
			||||||
 | 
					                <ImageContent
 | 
				
			||||||
 | 
					                  {...props}
 | 
				
			||||||
 | 
					                  autoPlay={mediaAutoLoad}
 | 
				
			||||||
 | 
					                  renderImage={(p) => <Image {...p} loading="lazy" />}
 | 
				
			||||||
 | 
					                  renderViewer={(p) => <ImageViewer {...p} />}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            {threadDetail && (
 | 
				
			||||||
 | 
					              <ThreadSelectorContainer>
 | 
				
			||||||
 | 
					                <ThreadSelector
 | 
				
			||||||
 | 
					                  room={room}
 | 
				
			||||||
 | 
					                  threadDetail={threadDetail}
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  hour24Clock={hour24Clock}
 | 
				
			||||||
 | 
					                  dateFormatString={dateFormatString}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ThreadSelectorContainer>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    undefined,
 | 
				
			||||||
 | 
					    (event) => {
 | 
				
			||||||
 | 
					      if (event.isRedacted()) {
 | 
				
			||||||
 | 
					        return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Box grow="Yes" direction="Column">
 | 
				
			||||||
 | 
					          <Text size="T400" priority="300">
 | 
				
			||||||
 | 
					            <code className={customHtmlCss.Code}>{event.getType()}</code>
 | 
				
			||||||
 | 
					            {' event'}
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 (
 | 
				
			||||||
 | 
					    <Scroll ref={scrollRef} size="300" hideTrack visibility="Hover">
 | 
				
			||||||
 | 
					      <Box className={css.ThreadsMenuContent} direction="Column" gap="100">
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            position: 'relative',
 | 
				
			||||||
 | 
					            height: virtualizer.getTotalSize(),
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {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 (
 | 
				
			||||||
 | 
					              <VirtualTile
 | 
				
			||||||
 | 
					                virtualItem={vItem}
 | 
				
			||||||
 | 
					                style={{ paddingBottom: config.space.S200 }}
 | 
				
			||||||
 | 
					                ref={virtualizer.measureElement}
 | 
				
			||||||
 | 
					                key={vItem.index}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <SequenceCard
 | 
				
			||||||
 | 
					                  key={event.getId()}
 | 
				
			||||||
 | 
					                  style={{ padding: config.space.S400, borderRadius: config.radii.R300 }}
 | 
				
			||||||
 | 
					                  variant="SurfaceVariant"
 | 
				
			||||||
 | 
					                  direction="Column"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <ThreadMessage
 | 
				
			||||||
 | 
					                    room={room}
 | 
				
			||||||
 | 
					                    event={event}
 | 
				
			||||||
 | 
					                    renderContent={renderMatrixEvent}
 | 
				
			||||||
 | 
					                    onOpen={handleOpen}
 | 
				
			||||||
 | 
					                    getMemberPowerTag={getMemberPowerTag}
 | 
				
			||||||
 | 
					                    accessibleTagColors={accessibleTagColors}
 | 
				
			||||||
 | 
					                    legacyUsernameColor={legacyUsernameColor || direct}
 | 
				
			||||||
 | 
					                    hour24Clock={hour24Clock}
 | 
				
			||||||
 | 
					                    dateFormatString={dateFormatString}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </SequenceCard>
 | 
				
			||||||
 | 
					              </VirtualTile>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {paginationToken && <ThreadsLoading />}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Scroll>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,29 +1,20 @@
 | 
				
			||||||
import { useCallback } from 'react';
 | 
					import { useCallback } from 'react';
 | 
				
			||||||
import { Direction, MatrixEvent, Room, ThreadFilterType } from 'matrix-js-sdk';
 | 
					import { EventTimelineSet, Room } from 'matrix-js-sdk';
 | 
				
			||||||
import { useMatrixClient } from './useMatrixClient';
 | 
					import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
 | 
				
			||||||
import { AsyncStatus, useAsyncCallbackValue } from './useAsyncCallback';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useRoomMyThreads = (room: Room): MatrixEvent[] | undefined => {
 | 
					export const useRoomMyThreads = (room: Room): AsyncState<EventTimelineSet, Error> => {
 | 
				
			||||||
  const mx = useMatrixClient();
 | 
					  const [threadsState] = useAsyncCallbackValue<EventTimelineSet, Error>(
 | 
				
			||||||
 | 
					    useCallback(async () => {
 | 
				
			||||||
 | 
					      await room.createThreadsTimelineSets();
 | 
				
			||||||
 | 
					      await room.fetchRoomThreads();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [fetchState] = useAsyncCallbackValue(
 | 
					      const timelineSet = room.threadsTimelineSets[0];
 | 
				
			||||||
    useCallback(
 | 
					      if (timelineSet === undefined) {
 | 
				
			||||||
      () =>
 | 
					        throw new Error('Failed to fetch My Threads!');
 | 
				
			||||||
        mx.createThreadListMessagesRequest(
 | 
					      }
 | 
				
			||||||
          room.roomId,
 | 
					      return timelineSet;
 | 
				
			||||||
          null,
 | 
					    }, [room])
 | 
				
			||||||
          30,
 | 
					 | 
				
			||||||
          Direction.Backward,
 | 
					 | 
				
			||||||
          ThreadFilterType.All
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      [mx, room]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (fetchState.status === AsyncStatus.Success) {
 | 
					  return threadsState;
 | 
				
			||||||
    const roomEvents = fetchState.data.chunk;
 | 
					 | 
				
			||||||
    const mEvents = roomEvents.map((event) => new MatrixEvent(event)).reverse();
 | 
					 | 
				
			||||||
    return mEvents;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return undefined;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,7 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
 | 
				
			||||||
export const startClient = async (mx: MatrixClient) => {
 | 
					export const startClient = async (mx: MatrixClient) => {
 | 
				
			||||||
  await mx.startClient({
 | 
					  await mx.startClient({
 | 
				
			||||||
    lazyLoadMembers: true,
 | 
					    lazyLoadMembers: true,
 | 
				
			||||||
 | 
					    threadSupport: true,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue