ff7c2ed941
- BUG-16: Fixed pagination deadlock (fetching flag stuck on error path) - BUG-17: Fixed absoluteIndex===0 falsy check skipping unread jump - BUG-19: Fixed mEvt.getRoomId()! non-null assertion crash - BUG-20: Wrapped getSettings()/setSettings() in try/catch for corrupt localStorage - SEC: Replaced randomStr() Math.random() with crypto.getRandomValues() CSPRNG - SEC: Fixed afterLoginRedirectPath open redirect validation - SEC: Narrowed OSM iframe sandbox to scripts-only (removed allow-same-origin) - Perf-2: Memoized selectAtom in useSetting (prevented new atom ref per render) - Perf-4: Fixed typingMembers setTimeout leak (tracked timers per user/room) - Perf-8: Memoized getChatBg() result in RoomView (not inline in JSX) - Perf-12: Replaced body.class * font-family with body.class (inherited) - Perf-15: Memoized typingNames array chain in RoomViewTyping - Perf-9: Added blob URL cleanup useEffect in AudioContent - BUG: Fixed forEach(async) -> Promise.all in useCommands join handler - BUG: Fixed useCompositionEndTracking missing dependency array - A11y: Fixed spoiler button aria-pressed + keyboard handler - A11y: Added aria-label to Send message button - Build: Set sourcemap:false, removed netlify.toml from copyFiles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { Box, Icon, IconButton, Icons, Text, as } from 'folds';
|
|
import { Room } from 'matrix-js-sdk';
|
|
import classNames from 'classnames';
|
|
import { useSetAtom } from 'jotai';
|
|
import { roomIdToTypingMembersAtom } from '../../state/typingMembers';
|
|
import { TypingIndicator } from '../../components/typing-indicator';
|
|
import { getMemberDisplayName } from '../../utils/room';
|
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
|
import * as css from './RoomViewTyping.css';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
|
|
|
export type RoomViewTypingProps = {
|
|
room: Room;
|
|
};
|
|
export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
|
({ className, room, ...props }, ref) => {
|
|
const setTypingMembers = useSetAtom(roomIdToTypingMembersAtom);
|
|
const mx = useMatrixClient();
|
|
const typingMembers = useRoomTypingMember(room.roomId);
|
|
|
|
const myUserId = mx.getUserId();
|
|
const typingNames = useMemo(
|
|
() =>
|
|
typingMembers
|
|
.filter((receipt) => receipt.userId !== myUserId)
|
|
.map(
|
|
(receipt) =>
|
|
getMemberDisplayName(room, receipt.userId) ?? getMxIdLocalPart(receipt.userId)
|
|
)
|
|
.reverse(),
|
|
[typingMembers, myUserId, room]
|
|
);
|
|
|
|
if (typingNames.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const handleDropAll = () => {
|
|
// some homeserver does not timeout typing status
|
|
// we have given option so user can drop their typing status
|
|
typingMembers.forEach((receipt) =>
|
|
setTypingMembers({
|
|
type: 'DELETE',
|
|
roomId: room.roomId,
|
|
userId: receipt.userId,
|
|
})
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{ position: 'relative' }} aria-live="polite" aria-atomic="false">
|
|
<Box
|
|
className={classNames(css.RoomViewTyping, className)}
|
|
alignItems="Center"
|
|
gap="400"
|
|
{...props}
|
|
ref={ref}
|
|
>
|
|
<TypingIndicator />
|
|
<Text className={css.TypingText} size="T300" truncate>
|
|
{typingNames.length === 1 && (
|
|
<>
|
|
<b>{typingNames[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' is typing...'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{typingNames.length === 2 && (
|
|
<>
|
|
<b>{typingNames[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{typingNames[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are typing...'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{typingNames.length === 3 && (
|
|
<>
|
|
<b>{typingNames[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{typingNames[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{typingNames[2]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are typing...'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
{typingNames.length > 3 && (
|
|
<>
|
|
<b>{typingNames[0]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{typingNames[1]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{', '}
|
|
</Text>
|
|
<b>{typingNames[2]}</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' and '}
|
|
</Text>
|
|
<b>{typingNames.length - 3} others</b>
|
|
<Text as="span" size="Inherit" priority="300">
|
|
{' are typing...'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Text>
|
|
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
|
|
<Icon size="50" src={Icons.Cross} />
|
|
</IconButton>
|
|
</Box>
|
|
</div>
|
|
);
|
|
}
|
|
);
|