mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
Linting and general stability improvements.
This commit is contained in:
@@ -21,7 +21,11 @@
|
||||
|
||||
.reactionPopupMenuItem {
|
||||
list-style: none;
|
||||
gap: 1em;
|
||||
margin-left: var(--cpd-separator-spacing);
|
||||
}
|
||||
|
||||
.reactionPopupMenuItem:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.reactionButton {
|
||||
@@ -37,3 +41,13 @@
|
||||
margin-left: var(--cpd-separator-spacing);
|
||||
margin-right: var(--cpd-separator-spacing);
|
||||
}
|
||||
|
||||
.searchForm {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--cpd-separator-spacing);
|
||||
}
|
||||
|
||||
.searchForm > label {
|
||||
flex: auto;
|
||||
}
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
Search,
|
||||
Form,
|
||||
} from "@vector-im/compound-web";
|
||||
import { ReactionIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
SearchIcon,
|
||||
ReactionIcon,
|
||||
CloseIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
ComponentPropsWithoutRef,
|
||||
@@ -35,6 +39,7 @@ import {
|
||||
ECallReactionEventContent,
|
||||
ReactionOption,
|
||||
ReactionSet,
|
||||
ElementCallReactionEventType,
|
||||
} from "../reactions";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
@@ -69,6 +74,7 @@ export function ReactionPopupMenu({
|
||||
}): ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const onSearch = useCallback<ChangeEventHandler<HTMLInputElement>>((ev) => {
|
||||
ev.preventDefault();
|
||||
setSearchText(ev.target.value.trim().toLocaleLowerCase());
|
||||
@@ -78,11 +84,14 @@ export function ReactionPopupMenu({
|
||||
() =>
|
||||
ReactionSet.filter(
|
||||
(reaction) =>
|
||||
reaction.name.startsWith(searchText) ||
|
||||
reaction.alias?.some((a) => a.startsWith(searchText)),
|
||||
!isSearching ||
|
||||
(!!searchText &&
|
||||
(reaction.name.startsWith(searchText) ||
|
||||
reaction.alias?.some((a) => a.startsWith(searchText)))),
|
||||
).slice(0, 6),
|
||||
[searchText],
|
||||
[searchText, isSearching],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.reactionPopupMenu}>
|
||||
<section className={styles.handRaiseSection}>
|
||||
@@ -99,14 +108,28 @@ export function ReactionPopupMenu({
|
||||
</section>
|
||||
<div className={styles.verticalSeperator} />
|
||||
<section>
|
||||
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
||||
<Search
|
||||
value={searchText}
|
||||
name="reactionSearch"
|
||||
onChange={onSearch}
|
||||
/>
|
||||
</Form.Root>
|
||||
<Separator />
|
||||
{isSearching ? (
|
||||
<>
|
||||
<Form.Root
|
||||
className={styles.searchForm}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<Search
|
||||
value={searchText}
|
||||
name="reactionSearch"
|
||||
placeholder="Search reactions…"
|
||||
onChange={onSearch}
|
||||
/>
|
||||
<CpdButton
|
||||
Icon={CloseIcon}
|
||||
size="sm"
|
||||
kind="destructive"
|
||||
onClick={() => setIsSearching(false)}
|
||||
/>
|
||||
</Form.Root>
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
<menu>
|
||||
{filteredReactionSet.map((reaction) => (
|
||||
<li className={styles.reactionPopupMenuItem}>
|
||||
@@ -123,21 +146,33 @@ export function ReactionPopupMenu({
|
||||
</Tooltip>
|
||||
</li>
|
||||
))}
|
||||
{!isSearching ? (
|
||||
<li key="search" className={styles.reactionPopupMenuItem}>
|
||||
<Tooltip label="Search">
|
||||
<CpdButton
|
||||
iconOnly
|
||||
Icon={SearchIcon}
|
||||
kind="tertiary"
|
||||
onClick={() => setIsSearching(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</li>
|
||||
) : null}
|
||||
</menu>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RaisedHandToggleButtonProps {
|
||||
interface ReactionToggleButtonProps {
|
||||
rtcSession: MatrixRTCSession;
|
||||
client: MatrixClient;
|
||||
}
|
||||
|
||||
export function RaiseHandToggleButton({
|
||||
export function ReactionToggleButton({
|
||||
client,
|
||||
rtcSession,
|
||||
}: RaisedHandToggleButtonProps): ReactNode {
|
||||
}: ReactionToggleButtonProps): ReactNode {
|
||||
const { raisedHands, myReactionId, reactions } = useReactions();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const userId = client.getUserId()!;
|
||||
@@ -161,7 +196,7 @@ export function RaiseHandToggleButton({
|
||||
await client.sendEvent(
|
||||
rtcSession.room.roomId,
|
||||
null,
|
||||
"io.element.call.reaction",
|
||||
ElementCallReactionEventType,
|
||||
{
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
@@ -171,7 +206,7 @@ export function RaiseHandToggleButton({
|
||||
name: reaction.name,
|
||||
} as ECallReactionEventContent,
|
||||
);
|
||||
setShowReactionsMenu(false);
|
||||
// Do NOT close the menu after this.
|
||||
} catch (ex) {
|
||||
logger.error("Failed to send reaction", ex);
|
||||
} finally {
|
||||
@@ -222,6 +257,7 @@ export function RaiseHandToggleButton({
|
||||
logger.error("Failed to send reaction event", ex);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setShowReactionsMenu(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -7,4 +7,4 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
export * from "./Button";
|
||||
export * from "./LinkButton";
|
||||
export * from "./RaisedHandToggleButton";
|
||||
export * from "./ReactionToggleButton";
|
||||
|
||||
@@ -7,17 +7,22 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { RelationType } from "matrix-js-sdk/src/types";
|
||||
|
||||
import catSoundOgg from "../sound/reactions/cat.mp3?url";
|
||||
import catSoundOgg from "../sound/reactions/cat.ogg?url";
|
||||
import catSoundMp3 from "../sound/reactions/cat.mp3?url";
|
||||
import cricketsSoundOgg from "../sound/reactions/crickets.mp3?url";
|
||||
import clapSoundOgg from "../sound/reactions/clap.ogg?url";
|
||||
import clapSoundMp3 from "../sound/reactions/clap.mp3?url";
|
||||
import cricketsSoundOgg from "../sound/reactions/crickets.ogg?url";
|
||||
import cricketsSoundMp3 from "../sound/reactions/crickets.mp3?url";
|
||||
import dogSoundOgg from "../sound/reactions/dog.ogg?url";
|
||||
import dogSoundMp3 from "../sound/reactions/dog.mp3?url";
|
||||
import genericSoundOgg from "../sound/reactions/generic.mp3?url";
|
||||
import genericSoundOgg from "../sound/reactions/generic.ogg?url";
|
||||
import genericSoundMp3 from "../sound/reactions/generic.mp3?url";
|
||||
import lightbulbSoundOgg from "../sound/reactions/lightbulb.mp3?url";
|
||||
import lightbulbSoundOgg from "../sound/reactions/lightbulb.ogg?url";
|
||||
import lightbulbSoundMp3 from "../sound/reactions/lightbulb.mp3?url";
|
||||
import partySoundOgg from "../sound/reactions/party.ogg?url";
|
||||
import partySoundMp3 from "../sound/reactions/party.mp3?url";
|
||||
|
||||
export const ElementCallReactionEventType = "io.element.call.reaction";
|
||||
|
||||
export interface ReactionOption {
|
||||
emoji: string;
|
||||
@@ -47,21 +52,30 @@ export const GenericReaction: ReactionOption = {
|
||||
},
|
||||
};
|
||||
|
||||
// The first 6 reactions are always visible.
|
||||
export const ReactionSet: ReactionOption[] = [
|
||||
{
|
||||
emoji: "👍",
|
||||
name: "thumbsup",
|
||||
alias: ["+1", "yes", "thumbs up"],
|
||||
},
|
||||
{
|
||||
emoji: "👎",
|
||||
name: "thumbsdown",
|
||||
alias: ["-1", "no", "thumbs no"],
|
||||
},
|
||||
{
|
||||
emoji: "🎉",
|
||||
name: "party",
|
||||
alias: ["hurray", "success"],
|
||||
sound: {
|
||||
ogg: partySoundOgg,
|
||||
mp3: partySoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "👏",
|
||||
name: "clapping",
|
||||
alias: ["celebrate", "success"],
|
||||
sound: {
|
||||
ogg: clapSoundOgg,
|
||||
mp3: clapSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🐶",
|
||||
@@ -72,15 +86,6 @@ export const ReactionSet: ReactionOption[] = [
|
||||
mp3: dogSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🦗",
|
||||
name: "crickets",
|
||||
alias: ["awkward", "silence"],
|
||||
sound: {
|
||||
ogg: cricketsSoundOgg,
|
||||
mp3: cricketsSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🐱",
|
||||
name: "cat",
|
||||
@@ -99,6 +104,20 @@ export const ReactionSet: ReactionOption[] = [
|
||||
mp3: lightbulbSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🦗",
|
||||
name: "crickets",
|
||||
alias: ["awkward", "silence"],
|
||||
sound: {
|
||||
ogg: cricketsSoundOgg,
|
||||
mp3: cricketsSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "👎",
|
||||
name: "thumbsdown",
|
||||
alias: ["-1", "no", "thumbs no"],
|
||||
},
|
||||
{
|
||||
emoji: "😵💫",
|
||||
name: "dizzy",
|
||||
@@ -109,4 +128,14 @@ export const ReactionSet: ReactionOption[] = [
|
||||
name: "ok",
|
||||
alias: ["okay", "cool"],
|
||||
},
|
||||
{
|
||||
emoji: "🥰",
|
||||
name: "heart",
|
||||
alias: ["heart", "love", "smiling"],
|
||||
},
|
||||
{
|
||||
emoji: "😄",
|
||||
name: "laugh",
|
||||
alias: ["giggle", "joy", "smiling"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -147,11 +147,11 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
.floatingReaction {
|
||||
position: relative;
|
||||
display: inline;
|
||||
z-index: 2;
|
||||
font-size: 32pt;
|
||||
animation-duration: 5s;
|
||||
animation-duration: 4s;
|
||||
animation-name: reaction-up;
|
||||
bottom: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
VideoButton,
|
||||
ShareScreenButton,
|
||||
SettingsButton,
|
||||
RaiseHandToggleButton,
|
||||
ReactionToggleButton,
|
||||
} from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
@@ -83,6 +83,7 @@ import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
|
||||
import { ReactionsProvider, useReactions } from "../useReactions";
|
||||
import handSoundOgg from "../sound/raise_hand.ogg?url";
|
||||
import handSoundMp3 from "../sound/raise_hand.mp3?url";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -182,12 +183,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
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],
|
||||
);
|
||||
|
||||
const reactionsIcons = useMemo(
|
||||
() =>
|
||||
Object.entries(reactions).map(([sender, { emoji }]) => ({
|
||||
@@ -558,7 +553,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
}
|
||||
if (supportsReactions) {
|
||||
buttons.push(
|
||||
<RaiseHandToggleButton
|
||||
<ReactionToggleButton
|
||||
client={client}
|
||||
rtcSession={rtcSession}
|
||||
key="4"
|
||||
@@ -650,17 +645,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
<source src={handSoundOgg} type="audio/ogg; codecs=vorbis" />
|
||||
<source src={handSoundMp3} type="audio/mpeg" />
|
||||
</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>
|
||||
),
|
||||
)}
|
||||
<ReactionsAudioRenderer />
|
||||
{reactionsIcons.map(({ sender, emoji, startX }) => (
|
||||
<span
|
||||
style={{ left: `${startX}vw` }}
|
||||
|
||||
32
src/room/ReactionAudioRenderer.tsx
Normal file
32
src/room/ReactionAudioRenderer.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { useReactions } from "../useReactions";
|
||||
|
||||
export function ReactionsAudioRenderer(): ReactNode {
|
||||
const { reactions } = useReactions();
|
||||
|
||||
// TODO: This may need to ensure we don't change the value if a duplicate reaction comes down.
|
||||
const expectedReactions = [...new Set([...Object.values(reactions)])];
|
||||
return (
|
||||
<>
|
||||
{expectedReactions.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>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
BIN
src/sound/reactions/clap.mp3
Normal file
BIN
src/sound/reactions/clap.mp3
Normal file
Binary file not shown.
BIN
src/sound/reactions/clap.ogg
Normal file
BIN
src/sound/reactions/clap.ogg
Normal file
Binary file not shown.
BIN
src/sound/reactions/party.mp3
Normal file
BIN
src/sound/reactions/party.mp3
Normal file
Binary file not shown.
BIN
src/sound/reactions/party.ogg
Normal file
BIN
src/sound/reactions/party.ogg
Normal file
Binary file not shown.
Reference in New Issue
Block a user