redesign thread selector

This commit is contained in:
Ajay Bura 2025-09-25 12:17:44 +05:30
parent 67c6785bf3
commit a6a3ac3b24
8 changed files with 154 additions and 115 deletions

View file

@ -1,90 +0,0 @@
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
import { ContainerColor } from '../../styles/ContainerColor.css';
import * as css from './styles.css';
import { UserAvatar } from '../user-avatar';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
return <Box className={css.ThreadSelectorContainer}>{children}</Box>;
}
type ThreadSelectorProps = {
room: Room;
senderId: string;
threadDetail: IThreadBundledRelationship;
outlined?: boolean;
};
export function ThreadSelector({ room, senderId, threadDetail, outlined }: ThreadSelectorProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
const latestEvent = threadDetail.latest_event;
const latestSenderId = latestEvent.sender;
const latestSenderAvatarMxc = getMemberAvatarMxc(room, latestSenderId);
const latestDisplayName =
getMemberDisplayName(room, latestSenderId) ??
getMxIdLocalPart(latestSenderId) ??
latestSenderId;
const latestEventTs = latestEvent.origin_server_ts;
return (
<Box
className={classNames(
css.ThreadSelector,
outlined && css.ThreadSectorOutlined,
ContainerColor({ variant: 'SurfaceVariant' })
)}
alignItems="Center"
gap="200"
>
<Box gap="100" alignItems="Inherit">
<Avatar size="200" radii="300">
<UserAvatar
userId={senderId}
src={
senderAvatarMxc
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined
}
alt={senderId}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
{latestSenderId && (
<Avatar size="200" radii="300">
<UserAvatar
userId={latestSenderId}
src={
latestSenderAvatarMxc
? mxcUrlToHttp(mx, latestSenderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
undefined
: undefined
}
alt={senderId}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
)}
</Box>
<Box gap="200" alignItems="Inherit">
<Text className={css.ThreadRepliesCount} size="L400">
{threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
</Text>
<Text size="T200" truncate>
{/* TODO: date */}
Last Reply by <b>{latestDisplayName}</b> at {new Date(latestEventTs).getTime()}
</Text>
<Icon size="100" src={Icons.ChevronRight} />
</Box>
</Box>
);
}

View file

@ -1,20 +0,0 @@
import { style } from '@vanilla-extract/css';
import { color, config } from 'folds';
export const ThreadSelectorContainer = style({
paddingTop: config.space.S100,
});
export const ThreadSelector = style({
padding: config.space.S200,
borderRadius: config.radii.R400,
});
export const ThreadSectorOutlined = style({
borderWidth: config.borderWidth.B300,
});
export const ThreadRepliesCount = style({
color: color.Primary.Main,
flexShrink: 0,
});

View file

@ -127,7 +127,7 @@ import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/u
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { ThreadSelector, ThreadSelectorContainer } from '../../components/thread-selector'; import { ThreadSelector, ThreadSelectorContainer } from './message/thread-selector';
const TimelineFloat = as<'div', css.TimelineFloatVariants>( const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => ( ({ position, className, ...props }, ref) => (
@ -1113,7 +1113,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
{threadDetail && ( {threadDetail && (
<ThreadSelectorContainer> <ThreadSelectorContainer>
<ThreadSelector room={room} senderId={senderId} threadDetail={threadDetail} /> <ThreadSelector
room={room}
threadDetail={threadDetail}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
outlined={messageLayout === MessageLayout.Bubble}
/>
</ThreadSelectorContainer> </ThreadSelectorContainer>
)} )}
</Message> </Message>

View file

@ -0,0 +1,105 @@
import { Avatar, Box, Icon, Icons, Line, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
import * as css from './styles.css';
import { UserAvatar } from '../../../../components/user-avatar';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../../../utils/room';
import { useMatrixClient } from '../../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../../hooks/useMediaAuthentication';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../../utils/matrix';
import { Time } from '../../../../components/message';
export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
return <Box className={css.ThreadSelectorContainer}>{children}</Box>;
}
type ThreadSelectorProps = {
room: Room;
threadDetail: IThreadBundledRelationship;
outlined?: boolean;
hour24Clock: boolean;
dateFormatString: string;
};
export function ThreadSelector({
room,
threadDetail,
outlined,
hour24Clock,
dateFormatString,
}: ThreadSelectorProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const latestEvent = threadDetail.latest_event;
const latestSenderId = latestEvent.sender;
const latestSenderAvatarMxc = getMemberAvatarMxc(room, latestSenderId);
const latestDisplayName =
getMemberDisplayName(room, latestSenderId) ??
getMxIdLocalPart(latestSenderId) ??
latestSenderId;
const latestEventTs = latestEvent.origin_server_ts;
return (
<Box
as="button"
type="button"
className={classNames(css.ThreadSelector, outlined && css.ThreadSectorOutlined)}
alignItems="Center"
gap="300"
>
<Box className={css.ThreadRepliesCount} shrink="No" alignItems="Center" gap="100">
<Icon size="100" src={Icons.Thread} filled />
<Text size="L400">
{threadDetail.count} {threadDetail.count === 1 ? 'Thread Reply' : 'Thread Replies'}
</Text>
</Box>
{latestSenderId && (
<>
<Line
className={css.ThreadSelectorDivider}
direction="Vertical"
variant="SurfaceVariant"
/>
<Box gap="200" alignItems="Inherit">
<Box gap="100" alignItems="Inherit">
<Avatar size="200" radii="400">
<UserAvatar
userId={latestSenderId}
src={
latestSenderAvatarMxc
? mxcUrlToHttp(
mx,
latestSenderAvatarMxc,
useAuthentication,
48,
48,
'crop'
) ?? undefined
: undefined
}
alt={latestSenderId}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
</Box>
<Text size="T200" truncate>
<span>
Latest by <strong>{latestDisplayName}</strong> at{' '}
</span>
<Time
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
ts={latestEventTs}
/>
</Text>
<Icon size="100" src={Icons.ChevronRight} />
</Box>
</>
)}
</Box>
);
}

View file

@ -0,0 +1,37 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
import { ContainerColor } from '../../../../styles/ContainerColor.css';
export const ThreadSelectorContainer = style({
marginTop: config.space.S200,
});
export const ThreadSelector = style([
ContainerColor({ variant: 'SurfaceVariant' }),
{
padding: `${config.space.S200} ${config.space.S300}`,
borderRadius: config.radii.R400,
cursor: 'pointer',
selectors: {
'&:hover, &:focus-visible': {
backgroundColor: color.SurfaceVariant.ContainerHover,
},
'&:active': {
backgroundColor: color.SurfaceVariant.ContainerActive,
},
},
},
]);
export const ThreadSectorOutlined = style({
borderWidth: config.borderWidth.B300,
});
export const ThreadSelectorDivider = style({
height: toRem(16),
});
export const ThreadRepliesCount = style({
color: color.Primary.Main,
});

View file

@ -80,7 +80,7 @@ import {
} from '../../../hooks/useMemberPowerTag'; } from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag'; import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { useRoomMyThreads } from '../../../hooks/useRoomThreads'; import { useRoomMyThreads } from '../../../hooks/useRoomThreads';
import { ThreadSelector, ThreadSelectorContainer } from '../../../components/thread-selector'; import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
type ThreadMessageProps = { type ThreadMessageProps = {
room: Room; room: Room;
@ -287,9 +287,10 @@ export const ThreadsMenu = forwardRef<HTMLDivElement, ThreadsMenuProps>(
<ThreadSelectorContainer> <ThreadSelectorContainer>
<ThreadSelector <ThreadSelector
room={room} room={room}
senderId={event.getSender()!}
threadDetail={threadDetail} threadDetail={threadDetail}
outlined outlined
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/> />
</ThreadSelectorContainer> </ThreadSelectorContainer>
)} )}

View file

@ -14,7 +14,7 @@ export const useRoomMyThreads = (room: Room): MatrixEvent[] | undefined => {
null, null,
30, 30,
Direction.Backward, Direction.Backward,
ThreadFilterType.My ThreadFilterType.All
), ),
[mx, room] [mx, room]
) )