mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
First PoC for reactions
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"send_reaction": "Send reaction",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"sign_out": "Sign out",
|
"sign_out": "Sign out",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
|||||||
40
src/button/RaisedHandToggleButton.module.css
Normal file
40
src/button/RaisedHandToggleButton.module.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.reactionPopupMenu {
|
||||||
|
padding: 1em;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
background: var(--cpd-color-bg-canvas-default);
|
||||||
|
top: -8em;
|
||||||
|
border-radius: var(--cpd-space-4x);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionPopupMenu menu {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionPopupMenu section {
|
||||||
|
height: fit-content;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionPopupMenuItem {
|
||||||
|
list-style: none;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionButton {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
border-radius: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verticalSeperator {
|
||||||
|
background-color: var(--cpd-color-gray-400);
|
||||||
|
width: 1px;
|
||||||
|
height: auto;
|
||||||
|
margin-left: var(--cpd-separator-spacing);
|
||||||
|
margin-right: var(--cpd-separator-spacing);
|
||||||
|
}
|
||||||
@@ -5,12 +5,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";
|
|
||||||
import {
|
import {
|
||||||
|
Button as CpdButton,
|
||||||
|
Tooltip,
|
||||||
|
Separator,
|
||||||
|
Search,
|
||||||
|
Form,
|
||||||
|
} from "@vector-im/compound-web";
|
||||||
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
FC,
|
FC,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -21,6 +29,12 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
|||||||
|
|
||||||
import { useReactions } from "../useReactions";
|
import { useReactions } from "../useReactions";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
import styles from "./RaisedHandToggleButton.module.css";
|
||||||
|
import {
|
||||||
|
ECallReactionEventContent,
|
||||||
|
ReactionOption,
|
||||||
|
ReactionSet,
|
||||||
|
} from "../reactions";
|
||||||
|
|
||||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
raised: boolean;
|
raised: boolean;
|
||||||
@@ -30,7 +44,7 @@ const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={t("common.raise_hand")}>
|
<Tooltip label={t("action.send_reaction")}>
|
||||||
<CpdButton
|
<CpdButton
|
||||||
kind={raised ? "primary" : "secondary"}
|
kind={raised ? "primary" : "secondary"}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -53,6 +67,79 @@ const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function ReactionPopupMenu({
|
||||||
|
sendRelation,
|
||||||
|
toggleRaisedHand,
|
||||||
|
isHandRaised,
|
||||||
|
canReact,
|
||||||
|
}: {
|
||||||
|
sendRelation: (reaction: ReactionOption) => void;
|
||||||
|
toggleRaisedHand: () => void;
|
||||||
|
isHandRaised: boolean;
|
||||||
|
canReact: boolean;
|
||||||
|
}): ReactNode {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const onSearch = useCallback<ChangeEventHandler<HTMLInputElement>>((ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setSearchText(ev.target.value.trim().toLocaleLowerCase());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredReactionSet = useMemo(
|
||||||
|
() =>
|
||||||
|
ReactionSet.filter(
|
||||||
|
(reaction) =>
|
||||||
|
reaction.name.startsWith(searchText) ||
|
||||||
|
reaction.alias?.some((a) => a.startsWith(searchText)),
|
||||||
|
).slice(0, 6),
|
||||||
|
[searchText],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className={styles.reactionPopupMenu}>
|
||||||
|
<section className={styles.handRaiseSection}>
|
||||||
|
<Tooltip label={t("common.raise_hand")}>
|
||||||
|
<CpdButton
|
||||||
|
kind={isHandRaised ? "primary" : "secondary"}
|
||||||
|
className={styles.reactionButton}
|
||||||
|
key={"raise-hand"}
|
||||||
|
onClick={() => toggleRaisedHand()}
|
||||||
|
>
|
||||||
|
🖐️
|
||||||
|
</CpdButton>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
<div className={styles.verticalSeperator}></div>
|
||||||
|
<section>
|
||||||
|
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<Search
|
||||||
|
value={searchText}
|
||||||
|
name="reactionSearch"
|
||||||
|
onChange={onSearch}
|
||||||
|
/>
|
||||||
|
</Form.Root>
|
||||||
|
<Separator />
|
||||||
|
<menu>
|
||||||
|
{filteredReactionSet.map((reaction) => (
|
||||||
|
<li className={styles.reactionPopupMenuItem}>
|
||||||
|
<Tooltip label={reaction.name}>
|
||||||
|
<CpdButton
|
||||||
|
kind="secondary"
|
||||||
|
className={styles.reactionButton}
|
||||||
|
key={reaction.name}
|
||||||
|
disabled={!canReact}
|
||||||
|
onClick={() => sendRelation(reaction)}
|
||||||
|
>
|
||||||
|
{reaction.emoji}
|
||||||
|
</CpdButton>
|
||||||
|
</Tooltip>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</menu>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface RaisedHandToggleButtonProps {
|
interface RaisedHandToggleButtonProps {
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
@@ -62,11 +149,49 @@ export function RaiseHandToggleButton({
|
|||||||
client,
|
client,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
}: RaisedHandToggleButtonProps): ReactNode {
|
}: RaisedHandToggleButtonProps): ReactNode {
|
||||||
const { raisedHands, myReactionId } = useReactions();
|
const { raisedHands, myReactionId, 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(true);
|
||||||
|
|
||||||
|
const canReact = !reactions[userId];
|
||||||
|
|
||||||
|
const sendRelation = useCallback(
|
||||||
|
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 {
|
||||||
|
setBusy(true);
|
||||||
|
// XXX: Trying to send a unspec'd event seems to miss the 3rd overload, need to come back to this.
|
||||||
|
// @ts-expect-error
|
||||||
|
await client.sendEvent(
|
||||||
|
rtcSession.room.roomId,
|
||||||
|
null,
|
||||||
|
"io.element.call.reaction",
|
||||||
|
{
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: RelationType.Reference,
|
||||||
|
event_id: parentEventId,
|
||||||
|
},
|
||||||
|
emoji: reaction.emoji,
|
||||||
|
name: reaction.name,
|
||||||
|
} as ECallReactionEventContent,
|
||||||
|
);
|
||||||
|
setShowReactionsMenu(false);
|
||||||
|
} catch (ex) {
|
||||||
|
logger.error("Failed to send reaction", ex);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[memberships, client],
|
||||||
|
);
|
||||||
|
|
||||||
const toggleRaisedHand = useCallback(() => {
|
const toggleRaisedHand = useCallback(() => {
|
||||||
const raiseHand = async (): Promise<void> => {
|
const raiseHand = async (): Promise<void> => {
|
||||||
@@ -124,10 +249,20 @@ export function RaiseHandToggleButton({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InnerButton
|
<>
|
||||||
disabled={busy}
|
<InnerButton
|
||||||
onClick={toggleRaisedHand}
|
disabled={busy}
|
||||||
raised={isHandRaised}
|
onClick={() => setShowReactionsMenu((show) => !show)}
|
||||||
/>
|
raised={isHandRaised}
|
||||||
|
/>
|
||||||
|
{showReactionsMenu && (
|
||||||
|
<ReactionPopupMenu
|
||||||
|
isHandRaised={isHandRaised}
|
||||||
|
canReact={canReact}
|
||||||
|
sendRelation={sendRelation}
|
||||||
|
toggleRaisedHand={toggleRaisedHand}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/reactions/index.ts
Normal file
74
src/reactions/index.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import dogSoundOgg from "../sound/reactions/dog.ogg?url";
|
||||||
|
import dogSoundMp3 from "../sound/reactions/dog.mp3?url";
|
||||||
|
import { RelationType } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
|
export interface ReactionOption {
|
||||||
|
emoji: string;
|
||||||
|
name: string;
|
||||||
|
alias?: string[];
|
||||||
|
sound?: {
|
||||||
|
mp3?: string;
|
||||||
|
ogg: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ECallReactionEventContent {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: RelationType.Reference;
|
||||||
|
event_id: string;
|
||||||
|
};
|
||||||
|
emoji: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenericReaction: ReactionOption = {
|
||||||
|
name: "generic",
|
||||||
|
emoji: "", // Filled in by user
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReactionSet: ReactionOption[] = [
|
||||||
|
{
|
||||||
|
emoji: "🐶",
|
||||||
|
name: "dog",
|
||||||
|
alias: ["doggo", "pupper", "woofer"],
|
||||||
|
sound: {
|
||||||
|
ogg: dogSoundOgg,
|
||||||
|
mp3: dogSoundMp3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "👍",
|
||||||
|
name: "thumbsup",
|
||||||
|
alias: ["+1", "yes", "thumbs up"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "👎",
|
||||||
|
name: "thumbsdown",
|
||||||
|
alias: ["-1", "no", "thumbs no"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "🎉",
|
||||||
|
name: "party",
|
||||||
|
alias: ["hurray", "success"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "🦗",
|
||||||
|
name: "crickets",
|
||||||
|
alias: ["awkward", "silence"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "🐱",
|
||||||
|
name: "cat",
|
||||||
|
alias: ["meow", "kitty"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "😵💫",
|
||||||
|
name: "dizzy",
|
||||||
|
alias: ["dazed", "confused"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emoji: "👌",
|
||||||
|
name: "ok",
|
||||||
|
alias: ["okay", "cool"],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -175,13 +175,23 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
connState,
|
connState,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { supportsReactions, raisedHands } = useReactions();
|
const { supportsReactions, raisedHands, reactions } = useReactions();
|
||||||
const raisedHandCount = useMemo(
|
const raisedHandCount = useMemo(
|
||||||
() => Object.keys(raisedHands).length,
|
() => Object.keys(raisedHands).length,
|
||||||
[raisedHands],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
const previousRaisedHandCount = useDeferredValue(raisedHandCount);
|
const previousRaisedHandCount = useDeferredValue(raisedHandCount);
|
||||||
|
|
||||||
|
// TODO: This may need to ensure we don't change the value if a duplicate reaction comes down.
|
||||||
|
const reactionsSet = useMemo(
|
||||||
|
() => [...new Set([...Object.values(reactions)])],
|
||||||
|
[reactions],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Got reaction change", reactionsSet);
|
||||||
|
}, [reactionsSet]);
|
||||||
|
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -634,6 +644,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
<source src={handSoundOgg} type="audio/ogg; codecs=vorbis" />
|
<source src={handSoundOgg} type="audio/ogg; codecs=vorbis" />
|
||||||
<source src={handSoundMp3} type="audio/mpeg" />
|
<source src={handSoundMp3} type="audio/mpeg" />
|
||||||
</audio>
|
</audio>
|
||||||
|
{reactionsSet.map(
|
||||||
|
(r) =>
|
||||||
|
r.sound && (
|
||||||
|
<audio key={r.name} autoPlay hidden>
|
||||||
|
<source src={r.sound.ogg} type="audio/ogg; codecs=vorbis" />
|
||||||
|
{r.sound.mp3 ? (
|
||||||
|
<source src={r.sound.mp3} type="audio/mpeg" />
|
||||||
|
) : null}
|
||||||
|
</audio>
|
||||||
|
),
|
||||||
|
)}
|
||||||
{footer}
|
{footer}
|
||||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
|
|||||||
BIN
src/sound/reactions/dog.mp3
Normal file
BIN
src/sound/reactions/dog.mp3
Normal file
Binary file not shown.
BIN
src/sound/reactions/dog.ogg
Normal file
BIN
src/sound/reactions/dog.ogg
Normal file
Binary file not shown.
@@ -45,6 +45,7 @@ import { Slider } from "../Slider";
|
|||||||
import { MediaView } from "./MediaView";
|
import { MediaView } from "./MediaView";
|
||||||
import { useLatest } from "../useLatest";
|
import { useLatest } from "../useLatest";
|
||||||
import { useReactions } from "../useReactions";
|
import { useReactions } from "../useReactions";
|
||||||
|
import { ReactionOption } from "../reactions";
|
||||||
|
|
||||||
interface TileProps {
|
interface TileProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -91,7 +92,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
},
|
},
|
||||||
[vm],
|
[vm],
|
||||||
);
|
);
|
||||||
const { raisedHands } = useReactions();
|
const { raisedHands, reactions } = useReactions();
|
||||||
|
|
||||||
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
||||||
|
|
||||||
@@ -110,6 +111,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""];
|
const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""];
|
||||||
|
const currentReaction: ReactionOption | undefined =
|
||||||
|
reactions[vm.member?.userId ?? ""];
|
||||||
|
|
||||||
const showSpeaking = showSpeakingIndicators && speaking;
|
const showSpeaking = showSpeakingIndicators && speaking;
|
||||||
|
|
||||||
@@ -152,6 +155,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
raisedHandTime={handRaised}
|
raisedHandTime={handRaised}
|
||||||
|
currentReaction={currentReaction}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import styles from "./MediaView.module.css";
|
|||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator";
|
import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator";
|
||||||
import { showHandRaisedTimer, useSetting } from "../settings/settings";
|
import { showHandRaisedTimer, useSetting } from "../settings/settings";
|
||||||
|
import { ReactionOption } from "../reactions";
|
||||||
|
|
||||||
interface Props extends ComponentProps<typeof animated.div> {
|
interface Props extends ComponentProps<typeof animated.div> {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -35,6 +36,7 @@ interface Props extends ComponentProps<typeof animated.div> {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
primaryButton?: ReactNode;
|
primaryButton?: ReactNode;
|
||||||
raisedHandTime?: Date;
|
raisedHandTime?: Date;
|
||||||
|
currentReaction?: ReactionOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||||
@@ -54,6 +56,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
displayName,
|
displayName,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
raisedHandTime,
|
raisedHandTime,
|
||||||
|
currentReaction,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -101,7 +104,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
<div className={styles.nameTag}>
|
<div className={styles.nameTag}>
|
||||||
{nameTagLeadingIcon}
|
{nameTagLeadingIcon}
|
||||||
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
||||||
{displayName}
|
{displayName} {currentReaction?.emoji ?? ""}
|
||||||
</Text>
|
</Text>
|
||||||
{unencryptedWarning && (
|
{unencryptedWarning && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -26,11 +26,18 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
|
|
||||||
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
||||||
import { useClientState } from "./ClientContext";
|
import { useClientState } from "./ClientContext";
|
||||||
|
import {
|
||||||
|
ECallReactionEventContent,
|
||||||
|
GenericReaction,
|
||||||
|
ReactionOption,
|
||||||
|
ReactionSet,
|
||||||
|
} from "./reactions";
|
||||||
|
|
||||||
interface ReactionsContextType {
|
interface ReactionsContextType {
|
||||||
raisedHands: Record<string, Date>;
|
raisedHands: Record<string, Date>;
|
||||||
supportsReactions: boolean;
|
supportsReactions: boolean;
|
||||||
myReactionId: string | null;
|
myReactionId: string | null;
|
||||||
|
reactions: Record<string, ReactionOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
||||||
@@ -80,6 +87,10 @@ export const ReactionsProvider = ({
|
|||||||
const room = rtcSession.room;
|
const room = rtcSession.room;
|
||||||
const myUserId = room.client.getUserId();
|
const myUserId = room.client.getUserId();
|
||||||
|
|
||||||
|
const [reactions, setReactions] = useState<Record<string, ReactionOption>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate our own reaction event.
|
// Calculate our own reaction event.
|
||||||
const myReactionId = useMemo(
|
const myReactionId = useMemo(
|
||||||
(): string | null =>
|
(): string | null =>
|
||||||
@@ -184,7 +195,44 @@ export const ReactionsProvider = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() === EventType.Reaction) {
|
if (event.getType() === "io.element.call.reaction") {
|
||||||
|
// TODO: Validate content.
|
||||||
|
const content: ECallReactionEventContent = event.getContent();
|
||||||
|
|
||||||
|
const membershipEventId = content["m.relates_to"].event_id;
|
||||||
|
// Check to see if this reaction was made to a membership event (and the
|
||||||
|
// sender of the reaction matches the membership)
|
||||||
|
if (
|
||||||
|
!memberships.some(
|
||||||
|
(e) => e.eventId === membershipEventId && e.sender === sender,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`Reaction target was not a membership event for ${sender}, ignoring`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One of our custom reactions
|
||||||
|
const reaction = ReactionSet.find((r) => r.name === content.name) ?? {
|
||||||
|
...GenericReaction,
|
||||||
|
emoji: content.emoji,
|
||||||
|
};
|
||||||
|
setReactions((reactions) => {
|
||||||
|
if (reactions[sender]) {
|
||||||
|
// We've still got a reaction from this user, ignore it to prevent spamming
|
||||||
|
return reactions;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
// Clear the reaction after some time.
|
||||||
|
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
||||||
|
}, 3000);
|
||||||
|
return {
|
||||||
|
...reactions,
|
||||||
|
[sender]: reaction,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (event.getType() === EventType.Reaction) {
|
||||||
const content = event.getContent() as ReactionEventContent;
|
const content = event.getContent() as ReactionEventContent;
|
||||||
const membershipEventId = content["m.relates_to"].event_id;
|
const membershipEventId = content["m.relates_to"].event_id;
|
||||||
|
|
||||||
@@ -241,6 +289,7 @@ export const ReactionsProvider = ({
|
|||||||
raisedHands: resultRaisedHands,
|
raisedHands: resultRaisedHands,
|
||||||
supportsReactions,
|
supportsReactions,
|
||||||
myReactionId,
|
myReactionId,
|
||||||
|
reactions,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user