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