mirror of
https://github.com/vector-im/element-call.git
synced 2026-04-03 07:10:26 +00:00
First PoC for reactions
This commit is contained in:
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.
|
||||
*/
|
||||
|
||||
import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";
|
||||
import {
|
||||
Button as CpdButton,
|
||||
Tooltip,
|
||||
Separator,
|
||||
Search,
|
||||
Form,
|
||||
} from "@vector-im/compound-web";
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
ComponentPropsWithoutRef,
|
||||
FC,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -21,6 +29,12 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import { useReactions } from "../useReactions";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
import styles from "./RaisedHandToggleButton.module.css";
|
||||
import {
|
||||
ECallReactionEventContent,
|
||||
ReactionOption,
|
||||
ReactionSet,
|
||||
} from "../reactions";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
raised: boolean;
|
||||
@@ -30,7 +44,7 @@ const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip label={t("common.raise_hand")}>
|
||||
<Tooltip label={t("action.send_reaction")}>
|
||||
<CpdButton
|
||||
kind={raised ? "primary" : "secondary"}
|
||||
{...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 {
|
||||
rtcSession: MatrixRTCSession;
|
||||
client: MatrixClient;
|
||||
@@ -62,11 +149,49 @@ export function RaiseHandToggleButton({
|
||||
client,
|
||||
rtcSession,
|
||||
}: RaisedHandToggleButtonProps): ReactNode {
|
||||
const { raisedHands, myReactionId } = useReactions();
|
||||
const { raisedHands, myReactionId, reactions } = useReactions();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const userId = client.getUserId()!;
|
||||
const isHandRaised = !!raisedHands[userId];
|
||||
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 raiseHand = async (): Promise<void> => {
|
||||
@@ -124,10 +249,20 @@ export function RaiseHandToggleButton({
|
||||
]);
|
||||
|
||||
return (
|
||||
<InnerButton
|
||||
disabled={busy}
|
||||
onClick={toggleRaisedHand}
|
||||
raised={isHandRaised}
|
||||
/>
|
||||
<>
|
||||
<InnerButton
|
||||
disabled={busy}
|
||||
onClick={() => setShowReactionsMenu((show) => !show)}
|
||||
raised={isHandRaised}
|
||||
/>
|
||||
{showReactionsMenu && (
|
||||
<ReactionPopupMenu
|
||||
isHandRaised={isHandRaised}
|
||||
canReact={canReact}
|
||||
sendRelation={sendRelation}
|
||||
toggleRaisedHand={toggleRaisedHand}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user