mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-08 20:09:19 +00:00
Add error for failured to send reaction.
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user