First PoC for reactions

This commit is contained in:
Half-Shot
2024-11-01 14:11:36 +00:00
parent f54e1e2046
commit 373a12a3b5
10 changed files with 339 additions and 12 deletions

View 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);
}

View File

@@ -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}
/>
)}
</>
);
}