Compare commits

...

5 Commits

Author SHA1 Message Date
Will Hunt
ce2b4c3cca Add support for specifying custom sounds via state events. 2024-11-11 14:50:26 +00:00
Will Hunt
8f05331703 Remove useless list 2024-11-11 14:39:06 +00:00
Will Hunt
04ff724246 lint 2024-11-11 14:23:56 +00:00
Will Hunt
2c889f9b45 Add expander for reactions view 2024-11-11 12:53:59 +00:00
Will Hunt
543f656548 Replace search strings 2024-11-11 12:53:59 +00:00
6 changed files with 55 additions and 105 deletions

View File

@@ -4,19 +4,19 @@
},
"action": {
"close": "Close",
"close_search": "Close search",
"copy_link": "Copy link",
"edit": "Edit",
"go": "Go",
"invite": "Invite",
"lower_hand": "Lower hand",
"no": "No",
"open_search": "Open search",
"pick_reaction": "Pick reaction",
"raise_hand": "Raise hand",
"raise_hand_or_send_reaction": "Raise hand or send reaction",
"register": "Register",
"remove": "Remove",
"show_less": "Show less",
"show_more": "Show more",
"sign_in": "Sign in",
"sign_out": "Sign out",
"submit": "Submit",
@@ -62,7 +62,6 @@
"preferences": "Preferences",
"profile": "Profile",
"reaction": "Reaction",
"search": "Search",
"settings": "Settings",
"something_went_wrong": "Something went wrong",
"unencrypted": "Not encrypted",
@@ -128,7 +127,6 @@
"rageshake_sending": "Sending…",
"rageshake_sending_logs": "Sending debug logs…",
"rageshake_sent": "Thanks!",
"reaction_search": "Search reactions…",
"recaptcha_caption": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
"recaptcha_dismissed": "Recaptcha dismissed",
"recaptcha_not_loaded": "Recaptcha not loaded",

View File

@@ -22,6 +22,8 @@
display: flex;
flex-wrap: wrap;
gap: var(--cpd-separator-spacing);
max-height: 10em;
max-width: 22.5em;
}
.reactionPopupMenu section {
@@ -63,18 +65,6 @@
margin-left: var(--cpd-separator-spacing);
margin-right: var(--cpd-separator-spacing);
}
.searchForm {
display: flex;
flex-direction: row;
gap: var(--cpd-separator-spacing);
margin-bottom: var(--cpd-space-3x);
}
.searchForm > label {
flex: auto;
}
.alert {
margin-bottom: var(--cpd-space-3x);
animation: grow-in 200ms;

View File

@@ -5,24 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { Button as CpdButton, Tooltip, Alert } from "@vector-im/compound-web";
import {
Button as CpdButton,
Tooltip,
Search,
Form,
Alert,
} from "@vector-im/compound-web";
import {
SearchIcon,
CloseIcon,
RaisedHandSolidIcon,
ReactionIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import {
ChangeEventHandler,
ComponentPropsWithoutRef,
FC,
KeyboardEventHandler,
ReactNode,
useCallback,
useEffect,
@@ -41,7 +33,6 @@ import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMembership
import styles from "./ReactionToggleButton.module.css";
import {
ReactionOption,
ReactionSet,
ElementCallReactionEventType,
} from "../reactions";
import { Modal } from "../Modal";
@@ -84,44 +75,14 @@ export function ReactionPopupMenu({
canReact: boolean;
}): 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());
}, []);
const [isFullyExpanded, setExpanded] = useState(false);
const { allReactions } = useReactions();
const filteredReactionSet = useMemo(
() =>
ReactionSet.filter(
(reaction) =>
!isSearching ||
(!!searchText &&
(reaction.name.startsWith(searchText) ||
reaction.alias?.some((a) => a.startsWith(searchText)))),
).slice(0, 6),
[searchText, isSearching],
() => (isFullyExpanded ? allReactions : allReactions.slice(0, 6)),
[isFullyExpanded],
);
const onSearchKeyDown = useCallback<KeyboardEventHandler<never>>(
(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
if (!canReact) {
return;
}
if (filteredReactionSet.length !== 1) {
return;
}
sendReaction(filteredReactionSet[0]);
setIsSearching(false);
} else if (ev.key === "Escape") {
ev.preventDefault();
setIsSearching(false);
}
},
[sendReaction, filteredReactionSet, canReact, setIsSearching],
);
const label = isHandRaised ? t("action.lower_hand") : t("action.raise_hand");
return (
<>
@@ -149,31 +110,6 @@ export function ReactionPopupMenu({
</section>
<div className={styles.verticalSeperator} />
<section>
{isSearching ? (
<>
<Form.Root className={styles.searchForm}>
<Search
required
value={searchText}
name="reactionSearch"
placeholder={t("reaction_search")}
onChange={onSearch}
onKeyDown={onSearchKeyDown}
// This is a reasonable use of autofocus, we are focusing when
// the search button is clicked (which matches the Element Web reaction picker)
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
<CpdButton
Icon={CloseIcon}
aria-label={t("action.close_search")}
size="sm"
kind="destructive"
onClick={() => setIsSearching(false)}
/>
</Form.Root>
</>
) : null}
<menu className={styles.reactionsMenu}>
{filteredReactionSet.map((reaction) => (
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
@@ -191,21 +127,25 @@ export function ReactionPopupMenu({
))}
</menu>
</section>
{!isSearching ? (
<section style={{ marginLeft: "var(--cpd-separator-spacing)" }}>
<li key="search" className={styles.reactionPopupMenuItem}>
<Tooltip label={t("common.search")}>
<CpdButton
iconOnly
aria-label={t("action.open_search")}
Icon={SearchIcon}
kind="tertiary"
onClick={() => setIsSearching(true)}
/>
</Tooltip>
</li>
</section>
) : null}
<section style={{ marginLeft: "var(--cpd-separator-spacing)" }}>
<Tooltip
label={
isFullyExpanded ? t("action.show_less") : t("action.show_more")
}
>
<CpdButton
iconOnly
aria-label={
isFullyExpanded
? t("action.show_less")
: t("action.show_more")
}
Icon={isFullyExpanded ? ChevronUpIcon : ChevronDownIcon}
kind="tertiary"
onClick={() => setExpanded(!isFullyExpanded)}
/>
</Tooltip>
</section>
</div>
</>
);

View File

@@ -64,6 +64,10 @@ export interface ECallReactionEventContent {
name: string;
}
export interface ECallReactionSoundsEventContent {
reactions: ReactionOption[],
}
export const GenericReaction: ReactionOption = {
name: "generic",
emoji: "", // Filled in by user

View File

@@ -13,10 +13,10 @@ import {
soundEffectVolumeSetting as effectSoundVolumeSetting,
useSetting,
} from "../settings/settings";
import { GenericReaction, ReactionSet } from "../reactions";
import { GenericReaction } from "../reactions";
export function ReactionsAudioRenderer(): ReactNode {
const { reactions } = useReactions();
const { reactions, allReactions } = useReactions();
const [shouldPlay] = useSetting(playReactionsSound);
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting);
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
@@ -52,7 +52,7 @@ export function ReactionsAudioRenderer(): ReactNode {
// be delayed.
return (
<>
{[GenericReaction, ...ReactionSet].map(
{[GenericReaction, ...allReactions].map(
(r) =>
r.sound && (
<audio

View File

@@ -11,6 +11,7 @@ import {
RelationType,
RoomEvent as MatrixRoomEvent,
MatrixEventEvent,
Direction,
} from "matrix-js-sdk/src/matrix";
import { ReactionEventContent } from "matrix-js-sdk/src/types";
import {
@@ -29,7 +30,9 @@ import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships
import { useClientState } from "./ClientContext";
import {
ECallReactionEventContent,
ECallReactionSoundsEventContent,
ElementCallReactionEventType,
ElementCallReactionsEventType,
GenericReaction,
ReactionOption,
ReactionSet,
@@ -37,6 +40,7 @@ import {
import { useLatest } from "./useLatest";
interface ReactionsContextType {
allReactions: ReactionOption[],
raisedHands: Record<string, Date>;
supportsReactions: boolean;
reactions: Record<string, ReactionOption>;
@@ -96,6 +100,19 @@ export const ReactionsProvider = ({
{},
);
// TODO: Refetch if the timeline changes.
const allReactions = useMemo(() => {
const result = room.getLiveTimeline().getState(Direction.Forward)?.getStateEvents(ElementCallReactionsEventType, '');
if (!result) {
return ReactionSet;
}
const content = result.getContent() as ECallReactionSoundsEventContent;
if (!content.reactions || !Array.isArray(content.reactions)) {
logger.warn(`Reactions event ${result.event.event_id} was not correctly formatted, ignoring`);
}
return [...ReactionSet, ...content.reactions];
}, [room])
// Reduce the data down for the consumers.
const resultRaisedHands = useMemo(
() =>
@@ -345,6 +362,7 @@ export const ReactionsProvider = ({
supportsReactions,
reactions,
lowerHand,
allReactions,
}}
>
{children}