diff --git a/src/app/components/image-pack-view/ImagePackView.tsx b/src/app/components/image-pack-view/ImagePackView.tsx
index ab81d5031..fb0e189be 100644
--- a/src/app/components/image-pack-view/ImagePackView.tsx
+++ b/src/app/components/image-pack-view/ImagePackView.tsx
@@ -29,7 +29,7 @@ export function ImagePackView({ address, requestClose }: ImagePackViewProps) {
-
+
diff --git a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
index 217491e68..74244dde1 100644
--- a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
+++ b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
@@ -68,7 +68,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
Leave Room
-
+
diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx
index 2ffc9ec45..542bd515e 100644
--- a/src/app/components/message/FileHeader.tsx
+++ b/src/app/components/message/FileHeader.tsx
@@ -48,6 +48,7 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
variant={hasError ? 'Critical' : 'SurfaceVariant'}
size="300"
radii="300"
+ aria-label={downloading ? 'Downloading...' : hasError ? 'Download failed, click to retry' : 'Download file'}
>
{downloading ? (
diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx
index 08019b1c7..34bc1583d 100644
--- a/src/app/components/message/content/AudioContent.tsx
+++ b/src/app/components/message/content/AudioContent.tsx
@@ -173,6 +173,7 @@ export function AudioContent({
size="300"
radii="Pill"
onClick={() => setMute(!mute)}
+ aria-label={mute ? 'Unmute' : 'Mute'}
aria-pressed={mute}
>
diff --git a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx
index 521287722..c6eae43f4 100644
--- a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx
+++ b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx
@@ -26,7 +26,7 @@ export const RoomTopicViewer = as<
{name}
-
+
diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx
index f8b1ee5ee..6a76c0737 100644
--- a/src/app/features/room/RoomInput.tsx
+++ b/src/app/features/room/RoomInput.tsx
@@ -94,6 +94,7 @@ import { getImageUrlBlob, loadImageElement } from '../../utils/dom';
import { safeFile } from '../../utils/mimeTypes';
import { fulfilledPromiseSettledResult } from '../../utils/common';
import { useSetting } from '../../state/hooks/settings';
+import { useAlive } from '../../hooks/useAlive';
import { settingsAtom } from '../../state/settings';
import {
getAudioMsgContent,
@@ -141,6 +142,7 @@ export const RoomInput = forwardRef(
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
+ const alive = useAlive();
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
const replyUserID = replyDraft?.userId;
@@ -250,9 +252,14 @@ export const RoomInput = forwardRef(
useCallback((width) => setHideStickerBtn(width < 500), [])
);
+ const didRestoreDraft = React.useRef(false);
useEffect(() => {
- Transforms.insertFragment(editor, msgDraft);
- }, [editor, msgDraft]);
+ if (didRestoreDraft.current) return;
+ didRestoreDraft.current = true;
+ if (msgDraft.length > 0) {
+ Transforms.insertFragment(editor, msgDraft);
+ }
+ }, [editor]);
useEffect(
() => () => {
@@ -490,6 +497,7 @@ export const RoomInput = forwardRef(
});
} catch (e) {
console.error('GIF send failed', e);
+ if (!alive()) return;
setGifError('Failed to send GIF. Please try again.');
setTimeout(() => setGifError(null), 4000);
}
diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx
index 6db36889b..b40c108b4 100644
--- a/src/app/features/room/RoomViewHeader.tsx
+++ b/src/app/features/room/RoomViewHeader.tsx
@@ -532,7 +532,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
}
>
{(triggerRef) => (
-
+
)}
diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx
index a716bb7f6..44fe99a28 100644
--- a/src/app/features/room/message/Message.tsx
+++ b/src/app/features/room/message/Message.tsx
@@ -727,7 +727,7 @@ export type MessageProps = {
hour24Clock: boolean;
dateFormatString: string;
};
-export const Message = as<'div', MessageProps>(
+export const Message = React.memo(as<'div', MessageProps>(
(
{
className,
@@ -984,6 +984,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant"
size="300"
radii="300"
+ aria-label="Add reaction"
aria-pressed={!!emojiBoardAnchor}
>
@@ -996,6 +997,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant"
size="300"
radii="300"
+ aria-label="Reply"
>
@@ -1006,6 +1008,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant"
size="300"
radii="300"
+ aria-label="Reply in thread"
>
@@ -1016,6 +1019,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant"
size="300"
radii="300"
+ aria-label="Edit message"
>
@@ -1201,7 +1205,8 @@ export const Message = as<'div', MessageProps>(
size="300"
radii="300"
onClick={handleOpenMenu}
- aria-pressed={!!menuAnchor}
+ aria-expanded={!!menuAnchor}
+ aria-haspopup="menu"
>
@@ -1232,7 +1237,7 @@ export const Message = as<'div', MessageProps>(
);
}
-);
+));
export type EventProps = {
room: Room;
@@ -1243,7 +1248,7 @@ export type EventProps = {
hideReadReceipts?: boolean;
showDeveloperTools?: boolean;
};
-export const Event = as<'div', EventProps>(
+export const Event = React.memo(as<'div', EventProps>(
(
{
className,
@@ -1370,7 +1375,8 @@ export const Event = as<'div', EventProps>(
size="300"
radii="300"
onClick={handleOpenMenu}
- aria-pressed={!!menuAnchor}
+ aria-expanded={!!menuAnchor}
+ aria-haspopup="menu"
>
@@ -1383,4 +1389,4 @@ export const Event = as<'div', EventProps>(
);
}
-);
+));
diff --git a/src/app/hooks/useTypingStatusUpdater.ts b/src/app/hooks/useTypingStatusUpdater.ts
index db8ceff1e..6dea1bcd8 100644
--- a/src/app/hooks/useTypingStatusUpdater.ts
+++ b/src/app/hooks/useTypingStatusUpdater.ts
@@ -6,6 +6,7 @@ type TypingStatusUpdater = (typing: boolean) => void;
export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => {
const statusSentTsRef = useRef(0);
+ const typingTimerRef = useRef | undefined>(undefined);
const sendTypingStatus: TypingStatusUpdater = useMemo(() => {
statusSentTsRef.current = 0;
@@ -19,9 +20,10 @@ export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): Typing
const sentTs = Date.now();
statusSentTsRef.current = sentTs;
- // Don't believe server will timeout typing status;
- // Clear typing status after timeout if already not;
- setTimeout(() => {
+ // Cancel any previous pending timeout before scheduling a new one
+ if (typingTimerRef.current !== undefined) clearTimeout(typingTimerRef.current);
+ typingTimerRef.current = setTimeout(() => {
+ typingTimerRef.current = undefined;
if (statusSentTsRef.current === sentTs) {
mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
statusSentTsRef.current = 0;
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 24faf680c..dc47e6913 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -206,6 +206,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
defaultValue={defaultUsername ?? defaultEmail}
style={{ paddingRight: config.space.S300 }}
name="usernameInput"
+ aria-label="Username or email"
variant="Background"
size="500"
required
@@ -227,7 +228,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
Password
-
+
{loginState.status === AsyncStatus.Error && (
<>