fix: emoji picker works + silence 429 presence rate-limit errors

Emoji bug root cause: EmojiBoard wraps itself in a FocusTrap with
clickOutsideDeactivates:true. When the picker was rendered inside
Input's 'after' prop, the FocusTrap treated clicks on the emoji items
as outside-clicks and deactivated (calling requestClose) before the
onEmojiSelect callback fired. Fixed by moving the emoji PopOut to be
a direct sibling of Input in the form row instead of nesting it inside
Input.after — matching the established pattern used in MessageEditor.

429 rate-limit: mx.setPresence() calls in handleClear and the
auto-clear timer effect had no rejection handling, causing unhandled
promise rejections logged to Sentry when Synapse rate-limits presence
updates. Added .catch(() => undefined) to both call sites. Sentry
issue JAVASCRIPT-REACT-E resolved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 15:20:42 -04:00
parent 90cb70c128
commit 60cbfec951
+41 -43
View File
@@ -365,17 +365,16 @@ function ProfileStatus() {
useEffect(() => {
if (!expiryTs) return undefined;
const remaining = expiryTs - Date.now();
if (remaining <= 0) {
const clearStatus = () => {
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
mx.setPresence({ presence: 'online', status_msg: '' }).catch(() => undefined);
};
if (remaining <= 0) {
clearStatus();
return undefined;
}
const timer = window.setTimeout(() => {
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
}, remaining);
const timer = window.setTimeout(clearStatus, remaining);
return () => clearTimeout(timer);
}, [expiryTs, userId, mx]);
@@ -421,7 +420,7 @@ function ProfileStatus() {
setStatusMsg('');
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
setExpiryTs(0);
mx.setPresence({ presence: 'online', status_msg: '' });
mx.setPresence({ presence: 'online', status_msg: '' }).catch(() => undefined);
};
const hasChanges = statusMsg !== (presence?.status ?? '');
@@ -440,7 +439,7 @@ function ProfileStatus() {
}
>
<Box direction="Column" grow="Yes" gap="100">
<Box as="form" onSubmit={handleSubmit} gap="200" aria-disabled={saving}>
<Box as="form" onSubmit={handleSubmit} gap="200" alignItems="Center" aria-disabled={saving}>
<Box grow="Yes" direction="Column">
<Input
name="statusMsgInput"
@@ -450,43 +449,42 @@ function ProfileStatus() {
placeholder="What's on your mind?"
variant="Secondary"
radii="300"
style={{ paddingRight: config.space.S200 }}
readOnly={saving}
after={
<PopOut
anchor={emojiAnchor}
position="Top"
align="End"
content={
<Suspense fallback={<Spinner size="100" />}>
<EmojiBoard
imagePackRooms={[]}
returnFocusOnDeactivate={false}
onEmojiSelect={handleEmojiSelect}
requestClose={() => setEmojiAnchor(undefined)}
/>
</Suspense>
}
>
<IconButton
type="button"
size="300"
radii="300"
variant="Secondary"
aria-label="Insert emoji"
aria-expanded={!!emojiAnchor}
aria-haspopup="dialog"
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
const rect = evt.currentTarget.getBoundingClientRect();
setEmojiAnchor((prev) => (prev ? undefined : rect));
}}
>
<Icon src={Icons.Smile} size="100" />
</IconButton>
</PopOut>
}
/>
</Box>
<PopOut
anchor={emojiAnchor}
position="Top"
align="End"
content={
<Suspense fallback={<Spinner size="100" />}>
<EmojiBoard
imagePackRooms={[]}
returnFocusOnDeactivate={false}
onEmojiSelect={handleEmojiSelect}
requestClose={() => setEmojiAnchor(undefined)}
/>
</Suspense>
}
>
<IconButton
type="button"
size="400"
radii="400"
variant="Surface"
fill="Soft"
outlined
aria-label="Insert emoji"
aria-expanded={!!emojiAnchor}
aria-haspopup="dialog"
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
const rect = evt.currentTarget.getBoundingClientRect();
setEmojiAnchor((prev) => (prev ? undefined : rect));
}}
>
<Icon src={Icons.Smile} size="400" />
</IconButton>
</PopOut>
<Button
size="400"
variant={hasChanges ? 'Success' : 'Secondary'}