Add error for failured to send reaction.

This commit is contained in:
Half-Shot
2024-11-08 12:52:05 +00:00
parent afe64cfe20
commit e98f84eb8f
3 changed files with 133 additions and 87 deletions

View File

@@ -4,11 +4,14 @@
}, },
"action": { "action": {
"close": "Close", "close": "Close",
"close_search": "Close search",
"copy_link": "Copy link", "copy_link": "Copy link",
"edit": "Edit", "edit": "Edit",
"go": "Go", "go": "Go",
"invite": "Invite", "invite": "Invite",
"no": "No", "no": "No",
"open_search": "Open search",
"pick_reaction": "Pick reaction",
"raise_hand_or_send_reaction": "Raise hand or send reaction", "raise_hand_or_send_reaction": "Raise hand or send reaction",
"register": "Register", "register": "Register",
"remove": "Remove", "remove": "Remove",
@@ -58,6 +61,7 @@
"preferences": "Preferences", "preferences": "Preferences",
"profile": "Profile", "profile": "Profile",
"raise_hand": "Raise hand", "raise_hand": "Raise hand",
"search": "Search",
"settings": "Settings", "settings": "Settings",
"unencrypted": "Not encrypted", "unencrypted": "Not encrypted",
"username": "Username", "username": "Username",
@@ -128,6 +132,7 @@
"rageshake_sending": "Sending…", "rageshake_sending": "Sending…",
"rageshake_sending_logs": "Sending debug logs…", "rageshake_sending_logs": "Sending debug logs…",
"rageshake_sent": "Thanks!", "rageshake_sent": "Thanks!",
"reaction_search": "Search reactions…",
"recaptcha_caption": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>", "recaptcha_caption": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
"recaptcha_dismissed": "Recaptcha dismissed", "recaptcha_dismissed": "Recaptcha dismissed",
"recaptcha_not_loaded": "Recaptcha not loaded", "recaptcha_not_loaded": "Recaptcha not loaded",

View File

@@ -64,3 +64,19 @@
.searchForm > label { .searchForm > label {
flex: auto; flex: auto;
} }
.alert {
margin-bottom: var(--cpd-space-3x);
animation: grow-in 200ms;
height: 2.5em;
}
@keyframes grow-in {
from {
height: 0;
}
to {
height: 2.5em;
}
}

View File

@@ -11,6 +11,7 @@ import {
Separator, Separator,
Search, Search,
Form, Form,
Alert,
} from "@vector-im/compound-web"; } from "@vector-im/compound-web";
import { import {
SearchIcon, SearchIcon,
@@ -25,6 +26,7 @@ import {
KeyboardEventHandler, KeyboardEventHandler,
ReactNode, ReactNode,
useCallback, useCallback,
useEffect,
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
@@ -72,9 +74,11 @@ export function ReactionPopupMenu({
toggleRaisedHand, toggleRaisedHand,
isHandRaised, isHandRaised,
canReact, canReact,
errorText,
}: { }: {
sendReaction: (reaction: ReactionOption) => void; sendReaction: (reaction: ReactionOption) => void;
toggleRaisedHand: () => void; toggleRaisedHand: () => void;
errorText?: string;
isHandRaised: boolean; isHandRaised: boolean;
canReact: boolean; canReact: boolean;
}): ReactNode { }): ReactNode {
@@ -119,80 +123,91 @@ export function ReactionPopupMenu({
); );
const label = isHandRaised ? t("common.raise_hand") : t("common.lower_hand"); const label = isHandRaised ? t("common.raise_hand") : t("common.lower_hand");
return ( return (
<div className={styles.reactionPopupMenu}> <>
<section className={styles.handRaiseSection}> {errorText && (
<Tooltip label={label}> <Alert
<CpdButton className={styles.alert}
kind={isHandRaised ? "primary" : "secondary"} type="critical"
aria-pressed={isHandRaised} title="Something went wrong"
aria-label={label} >
onClick={() => toggleRaisedHand()} {errorText}
iconOnly </Alert>
Icon={RaisedHandSolidIcon} )}
/> <div className={styles.reactionPopupMenu}>
</Tooltip> <section className={styles.handRaiseSection}>
</section> <Tooltip label={label}>
<div className={styles.verticalSeperator} /> <CpdButton
<section> kind={isHandRaised ? "primary" : "secondary"}
{isSearching ? ( aria-pressed={isHandRaised}
<> aria-label={label}
<Form.Root className={styles.searchForm}> onClick={() => toggleRaisedHand()}
<Search iconOnly
required Icon={RaisedHandSolidIcon}
value={searchText} />
name="reactionSearch" </Tooltip>
placeholder="Search reactions…" </section>
onChange={onSearch} <div className={styles.verticalSeperator} />
onKeyDown={onSearchKeyDown} <section>
// This is a reasonable use of autofocus, we are focusing when {isSearching ? (
// the search button is clicked (which matches the Element Web reaction picker) <>
// eslint-disable-next-line jsx-a11y/no-autofocus <Form.Root className={styles.searchForm}>
autoFocus <Search
/> required
<CpdButton value={searchText}
Icon={CloseIcon} name="reactionSearch"
aria-label="close search" placeholder={t("reaction_search")}
size="sm" onChange={onSearch}
kind="destructive" onKeyDown={onSearchKeyDown}
onClick={() => setIsSearching(false)} // This is a reasonable use of autofocus, we are focusing when
/> // the search button is clicked (which matches the Element Web reaction picker)
</Form.Root> // eslint-disable-next-line jsx-a11y/no-autofocus
<Separator /> autoFocus
</> />
) : null}
<menu>
{filteredReactionSet.map((reaction) => (
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
<Tooltip label={reaction.name}>
<CpdButton <CpdButton
kind="secondary" Icon={CloseIcon}
className={styles.reactionButton} aria-label={t("action.close_search")}
disabled={!canReact} size="sm"
onClick={() => sendReaction(reaction)} kind="destructive"
> onClick={() => setIsSearching(false)}
{reaction.emoji} />
</CpdButton> </Form.Root>
<Separator />
</>
) : null}
<menu>
{filteredReactionSet.map((reaction) => (
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
<Tooltip label={reaction.name}>
<CpdButton
kind="secondary"
className={styles.reactionButton}
disabled={!canReact}
onClick={() => sendReaction(reaction)}
>
{reaction.emoji}
</CpdButton>
</Tooltip>
</li>
))}
</menu>
</section>
{!isSearching ? (
<section>
<li key="search" className={styles.reactionPopupMenuItem}>
<Tooltip label={t("common.search")}>
<CpdButton
iconOnly
aria-label={t("action.open_search")}
Icon={SearchIcon}
kind="tertiary"
onClick={() => setIsSearching(true)}
/>
</Tooltip> </Tooltip>
</li> </li>
))} </section>
</menu> ) : null}
</section> </div>
{!isSearching ? ( </>
<section>
<li key="search" className={styles.reactionPopupMenuItem}>
<Tooltip label="Search">
<CpdButton
iconOnly
aria-label="Open reactions search"
Icon={SearchIcon}
kind="tertiary"
onClick={() => setIsSearching(true)}
/>
</Tooltip>
</li>
</section>
) : null}
</div>
); );
} }
@@ -205,24 +220,30 @@ export function ReactionToggleButton({
client, client,
rtcSession, rtcSession,
}: ReactionToggleButtonProps): ReactNode { }: ReactionToggleButtonProps): ReactNode {
const { t } = useTranslation();
const { raisedHands, lowerHand, reactions } = useReactions(); const { raisedHands, lowerHand, reactions } = useReactions();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const userId = client.getUserId()!; const userId = client.getUserId()!;
const isHandRaised = !!raisedHands[userId]; const isHandRaised = !!raisedHands[userId];
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const [showReactionsMenu, setShowReactionsMenu] = useState(false); const [showReactionsMenu, setShowReactionsMenu] = useState(false);
const [errorText, setErrorText] = useState<string>();
useEffect(() => {
// Clear whenever the reactions menu state changes.
setErrorText(undefined);
}, [showReactionsMenu]);
const canReact = !reactions[userId]; const canReact = !reactions[userId];
const sendRelation = useCallback( const sendRelation = useCallback(
async (reaction: ReactionOption) => { async (reaction: ReactionOption) => {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
logger.error("Cannot find own membership event");
return;
}
const parentEventId = myMembership.eventId;
try { try {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
throw new Error("Cannot find own membership event");
}
const parentEventId = myMembership.eventId;
setBusy(true); setBusy(true);
await client.sendEvent( await client.sendEvent(
rtcSession.room.roomId, rtcSession.room.roomId,
@@ -236,12 +257,14 @@ export function ReactionToggleButton({
name: reaction.name, name: reaction.name,
}, },
); );
setErrorText(undefined);
setShowReactionsMenu(false);
// Do NOT close the menu after this. // Do NOT close the menu after this.
} catch (ex) { } catch (ex) {
setErrorText(ex instanceof Error ? ex.message : "Unknown error");
logger.error("Failed to send reaction", ex); logger.error("Failed to send reaction", ex);
} finally { } finally {
setBusy(false); setBusy(false);
setShowReactionsMenu(false);
} }
}, },
[memberships, client, userId, rtcSession], [memberships, client, userId, rtcSession],
@@ -258,13 +281,12 @@ export function ReactionToggleButton({
setBusy(false); setBusy(false);
} }
} else { } else {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
logger.error("Cannot find own membership event");
return;
}
const parentEventId = myMembership.eventId;
try { try {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
throw new Error("Cannot find own membership event");
}
const parentEventId = myMembership.eventId;
setBusy(true); setBusy(true);
const reaction = await client.sendEvent( const reaction = await client.sendEvent(
rtcSession.room.roomId, rtcSession.room.roomId,
@@ -278,11 +300,13 @@ export function ReactionToggleButton({
}, },
); );
logger.debug("Sent raise hand event", reaction.event_id); logger.debug("Sent raise hand event", reaction.event_id);
setErrorText(undefined);
setShowReactionsMenu(false);
} catch (ex) { } catch (ex) {
logger.error("Failed to send reaction event", ex); setErrorText(ex instanceof Error ? ex.message : "Unknown error");
logger.error("Failed to raise hand", ex);
} finally { } finally {
setBusy(false); setBusy(false);
setShowReactionsMenu(false);
} }
} }
}; };
@@ -307,12 +331,13 @@ export function ReactionToggleButton({
/> />
<Modal <Modal
open={showReactionsMenu} open={showReactionsMenu}
title="Pick reaction" title={t("action.pick_reaction")}
hideHeader hideHeader
classNameModal={styles.reactionPopupMenuModal} classNameModal={styles.reactionPopupMenuModal}
onDismiss={() => setShowReactionsMenu(false)} onDismiss={() => setShowReactionsMenu(false)}
> >
<ReactionPopupMenu <ReactionPopupMenu
errorText={errorText}
isHandRaised={isHandRaised} isHandRaised={isHandRaised}
canReact={canReact} canReact={canReact}
sendReaction={(reaction) => void sendRelation(reaction)} sendReaction={(reaction) => void sendRelation(reaction)}