mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
5 Commits
v0.23.0
...
hs/state-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce2b4c3cca | ||
|
|
8f05331703 | ||
|
|
04ff724246 | ||
|
|
2c889f9b45 | ||
|
|
543f656548 |
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user