Merge remote-tracking branch 'origin/livekit' into hs/add-buttons-for-reactions

This commit is contained in:
Half-Shot 2024-11-15 16:21:32 +00:00
commit 547f74718b
13 changed files with 151 additions and 294 deletions

View File

@ -8,7 +8,7 @@ server {
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate"; add_header Cache-Control "public, max-age=30, stale-while-revalidate=30";
} }
# assets can be cached because they have hashed filenames # assets can be cached because they have hashed filenames

View File

@ -163,8 +163,8 @@
"preferences_tab": { "preferences_tab": {
"reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet", "reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet",
"reactions_play_sound_label": "Reaktionstöne abspielen", "reactions_play_sound_label": "Reaktionstöne abspielen",
"reactions_show_description": "Reaktionen anzeigen", "reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.",
"reactions_show_label": "Zeige eine Animation, wenn jemand eine Reaktion sendet.", "reactions_show_label": "Reaktionen anzeigen",
"reactions_title": "Reaktionen" "reactions_title": "Reaktionen"
}, },
"preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden",

View File

@ -4,19 +4,19 @@
}, },
"action": { "action": {
"close": "Close", "close": "Close",
"close_search": "Close search",
"copy_link": "Copy link", "copy_link": "Copy link",
"edit": "Edit", "edit": "Edit",
"go": "Go", "go": "Go",
"invite": "Invite", "invite": "Invite",
"lower_hand": "Lower hand ({{keyboardShortcut}})", "lower_hand": "Lower hand ({{keyboardShortcut}})",
"no": "No", "no": "No",
"open_search": "Open search",
"pick_reaction": "Pick reaction", "pick_reaction": "Pick reaction",
"raise_hand": "Raise hand ({{keyboardShortcut}})", "raise_hand": "Raise hand ({{keyboardShortcut}})",
"raise_hand_or_send_reaction": "Raise hand or send reaction", "raise_hand_or_send_reaction": "Raise hand or send reaction",
"register": "Register", "register": "Register",
"remove": "Remove", "remove": "Remove",
"show_less": "Show less",
"show_more": "Show more",
"sign_in": "Sign in", "sign_in": "Sign in",
"sign_out": "Sign out", "sign_out": "Sign out",
"submit": "Submit", "submit": "Submit",
@ -62,7 +62,6 @@
"preferences": "Preferences", "preferences": "Preferences",
"profile": "Profile", "profile": "Profile",
"reaction": "Reaction", "reaction": "Reaction",
"search": "Search",
"settings": "Settings", "settings": "Settings",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
"unencrypted": "Not encrypted", "unencrypted": "Not encrypted",
@ -128,7 +127,6 @@
"rageshake_sending": "Sending…", "rageshake_sending": "Sending…",
"rageshake_sending_logs": "Sending debug logs…", "rageshake_sending_logs": "Sending debug logs…",
"rageshake_sent": "Thanks!", "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_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_dismissed": "Recaptcha dismissed",
"recaptcha_not_loaded": "Recaptcha not loaded", "recaptcha_not_loaded": "Recaptcha not loaded",
@ -163,8 +161,8 @@
"preferences_tab": { "preferences_tab": {
"reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.", "reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.",
"reactions_play_sound_label": "Play reaction sounds", "reactions_play_sound_label": "Play reaction sounds",
"reactions_show_description": "Show reactions", "reactions_show_description": "Show an animation when anyone sends a reaction.",
"reactions_show_label": "Show an animation when anyone sends a reaction.", "reactions_show_label": "Show reactions",
"reactions_title": "Reactions" "reactions_title": "Reactions"
}, },
"preferences_tab_body": "Here you can configure extra options for an improved experience", "preferences_tab_body": "Here you can configure extra options for an improved experience",

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.png" /> <link rel="icon" type="image/svg+xml" href="favicon.png" />
<link rel="preload" href="/config.json" as="fetch" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"

View File

@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
.modal { .modal {
--inset-inline: 520px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -35,7 +36,7 @@ Please see LICENSE in the repository root for full details.
.drawer { .drawer {
background: var(--cpd-color-bg-canvas-default); background: var(--cpd-color-bg-canvas-default);
inset-block-end: 0; inset-block-end: 0;
inset-inline: max(0px, calc((100% - 520px) / 2)); inset-inline: max(0px, calc((100% - var(--inset-inline)) / 2));
max-block-size: 90%; max-block-size: 90%;
border-start-start-radius: var(--border-radius); border-start-start-radius: var(--border-radius);
border-start-end-radius: var(--border-radius); border-start-end-radius: var(--border-radius);

View File

@ -42,8 +42,9 @@ Please see LICENSE in the repository root for full details.
} }
.overlay.animate { .overlay.animate {
--overlay-top: 50%;
left: 50%; left: 50%;
top: 50%; top: var(--overlay-top);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }

View File

@ -3,78 +3,99 @@
} }
.reactionPopupMenu { .reactionPopupMenu {
--reaction-button-padding: 10px;
--reaction-button-fontsize: 20px;
--reaction-button-gap: var(--cpd-separator-spacing);
display: flex; display: flex;
width: fit-content;
} }
.reactionPopupMenuModal { @media (max-width: 420px) {
width: fit-content !important; .reactionPopupMenu {
top: 82vh !important; --reaction-button-padding: 8px;
--reaction-button-fontsize: 16px;
--reaction-button-gap: 6px;
}
} }
.reactionPopupMenuModal > div > div { div.reactionPopupMenuRoot.reactionPopupMenuModal {
padding-inline: var(--cpd-space-6x) !important; --overlay-top: 82vh;
padding-block: var(--cpd-space-6x) var(--cpd-space-8x) !important; width: fit-content;
} }
.reactionPopupMenu menu { div.reactionPopupMenuRoot {
margin: 0; /* Center the drawer */
padding: 0; --inset-inline: 30em;
display: flex; }
flex-wrap: wrap;
gap: var(--cpd-separator-spacing); .reactionPopupMenuRoot > div {
width: fit-content;
max-width: 100vw;
}
div.reactionPopupMenuRoot.reactionPopupMenuModal > div > div {
padding-inline: var(--cpd-space-6x);
padding-block: var(--cpd-space-6x);
} }
.reactionPopupMenu section { .reactionPopupMenu section {
height: fit-content; height: fit-content;
margin-top: auto; flex: 1;
margin-bottom: auto; max-width: fit-content;
} }
.reactionPopupMenuItem { .reactionPopupMenu section.reactionsMenuSection {
list-style: none; margin: auto 0;
flex: auto;
} }
.reactionsMenu { .reactionsMenu {
min-height: 3em; margin: 0;
padding: 0;
flex-grow: 1;
gap: var(--reaction-button-gap);
/* Height of 3 rows plus padding. */
max-height: calc(
((var(--reaction-button-fontsize) + var(--cpd-separator-spacing)) * 2) * 3
);
max-width: calc(
((var(--reaction-button-fontsize) + var(--cpd-separator-spacing)) * 2) * 5
);
overflow-x: hidden;
overflow-y: auto;
list-style: none;
flex-wrap: wrap;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: start;
align-items: auto;
align-content: start;
width: fit-content;
}
.reactionsMenu > * {
flex: 0 0 auto;
} }
.reactionButton { .reactionButton {
padding: 1em; padding: var(--reaction-button-padding);
font-size: 1.6em;
width: 1.4em;
height: 1.4em;
border-radius: var(--cpd-radius-pill-effect); border-radius: var(--cpd-radius-pill-effect);
} font-size: var(--reaction-button-fontsize);
min-block-size: unset;
@media (max-width: 800px) { border: none;
.reactionButton { aspect-ratio: 1 / 1;
padding: 1em; height: 100%;
font-size: 1em;
width: 1em;
height: 1em;
min-block-size: unset;
}
} }
.verticalSeperator { .verticalSeperator {
background-color: var(--cpd-color-gray-800); background-color: var(--cpd-color-gray-800);
width: 1px; width: 2px;
height: auto; height: auto;
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);
margin-bottom: var(--cpd-space-3x);
}
.searchForm > label {
flex: auto;
}
.alert { .alert {
margin-bottom: var(--cpd-space-3x); margin-bottom: var(--cpd-space-3x);
animation: grow-in 200ms; animation: grow-in 200ms;

View File

@ -5,8 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { fireEvent, render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { act } from "react";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { userEvent } from "@testing-library/user-event"; import { userEvent } from "@testing-library/user-event";
@ -116,7 +115,7 @@ test("Can react with emoji", async () => {
]); ]);
}); });
test("Can search for and send emoji", async () => { test("Can fully expand emoji picker", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
@ -124,9 +123,7 @@ test("Can search for and send emoji", async () => {
<TestComponent rtcSession={rtcSession} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("action.raise_hand_or_send_reaction")); await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search")); await user.click(getByLabelText("action.show_more"));
// Search should autofocus.
await user.keyboard("crickets");
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
await user.click(getByText("🦗")); await user.click(getByText("🦗"));
@ -146,39 +143,7 @@ test("Can search for and send emoji", async () => {
]); ]);
}); });
test("Can search for and send emoji with the keyboard", async () => { test("Can close reaction dialog", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, getByPlaceholderText, container } = render(
<TestComponent rtcSession={rtcSession} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
const searchField = getByPlaceholderText("reaction_search");
// Search should autofocus.
await user.keyboard("crickets");
expect(container).toMatchSnapshot();
act(() => {
fireEvent.keyDown(searchField, { key: "Enter" });
});
expect(room.testSentEvents).toEqual([
[
undefined,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: memberEventAlice,
rel_type: "m.reference",
},
name: "crickets",
emoji: "🦗",
},
],
]);
});
test("Can close search", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
@ -186,23 +151,7 @@ test("Can close search", async () => {
<TestComponent rtcSession={rtcSession} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("action.raise_hand_or_send_reaction")); await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search")); await user.click(getByLabelText("action.show_more"));
await user.click(getByLabelText("action.close_search")); await user.click(getByLabelText("action.show_less"));
expect(container).toMatchSnapshot();
});
test("Can close search with the escape key", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container, getByPlaceholderText } = render(
<TestComponent rtcSession={rtcSession} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
const searchField = getByPlaceholderText("reaction_search");
act(() => {
fireEvent.keyDown(searchField, { key: "Escape" });
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });

View File

@ -5,24 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { Button as CpdButton, Tooltip, Alert } from "@vector-im/compound-web";
import { import {
Button as CpdButton,
Tooltip,
Search,
Form,
Alert,
} from "@vector-im/compound-web";
import {
SearchIcon,
CloseIcon,
RaisedHandSolidIcon, RaisedHandSolidIcon,
ReactionIcon, ReactionIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { import {
ChangeEventHandler,
ComponentPropsWithoutRef, ComponentPropsWithoutRef,
FC, FC,
KeyboardEventHandler,
ReactNode, ReactNode,
useCallback, useCallback,
useEffect, useEffect,
@ -76,43 +68,11 @@ export function ReactionPopupMenu({
canReact: boolean; canReact: boolean;
}): ReactNode { }): ReactNode {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchText, setSearchText] = useState(""); const [isFullyExpanded, setExpanded] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const onSearch = useCallback<ChangeEventHandler<HTMLInputElement>>((ev) => {
ev.preventDefault();
setSearchText(ev.target.value.trim().toLocaleLowerCase());
}, []);
const filteredReactionSet = useMemo( const filteredReactionSet = useMemo(
() => () => (isFullyExpanded ? ReactionSet : ReactionSet.slice(0, 5)),
ReactionSet.filter( [isFullyExpanded],
(reaction) =>
!isSearching ||
(!!searchText &&
(reaction.name.startsWith(searchText) ||
reaction.alias?.some((a) => a.startsWith(searchText)))),
).slice(0, ReactionsRowSize),
[searchText, isSearching],
);
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 const label = isHandRaised
? t("action.lower_hand", { keyboardShortcut: "H" }) ? t("action.lower_hand", { keyboardShortcut: "H" })
@ -143,36 +103,15 @@ export function ReactionPopupMenu({
</Tooltip> </Tooltip>
</section> </section>
<div className={styles.verticalSeperator} /> <div className={styles.verticalSeperator} />
<section> <section className={styles.reactionsMenuSection}>
{isSearching ? ( <menu
<> className={classNames(
<Form.Root className={styles.searchForm}> isFullyExpanded && styles.reactionsMenuExpanded,
<Search styles.reactionsMenu,
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, index) => ( {filteredReactionSet.map((reaction, index) => (
<li className={styles.reactionPopupMenuItem} key={reaction.name}> <li key={reaction.name}>
{/* Show the keyboard key assigned to the reaction */}
<Tooltip <Tooltip
label={ label={
index < ReactionsRowSize index < ReactionsRowSize
@ -198,21 +137,23 @@ export function ReactionPopupMenu({
))} ))}
</menu> </menu>
</section> </section>
{!isSearching ? ( <section style={{ marginLeft: "var(--cpd-separator-spacing)" }}>
<section style={{ marginLeft: "var(--cpd-separator-spacing)" }}> <Tooltip
<li key="search" className={styles.reactionPopupMenuItem}> label={
<Tooltip label={t("common.search")}> isFullyExpanded ? t("action.show_less") : t("action.show_more")
<CpdButton }
iconOnly >
aria-label={t("action.open_search")} <CpdButton
Icon={SearchIcon} iconOnly
kind="tertiary" aria-label={
onClick={() => setIsSearching(true)} isFullyExpanded ? t("action.show_less") : t("action.show_more")
/> }
</Tooltip> Icon={isFullyExpanded ? ChevronUpIcon : ChevronDownIcon}
</li> kind="tertiary"
</section> onClick={() => setExpanded(!isFullyExpanded)}
) : null} />
</Tooltip>
</section>
</div> </div>
</> </>
); );
@ -289,12 +230,13 @@ export function ReactionToggleButton({
title={t("action.pick_reaction")} title={t("action.pick_reaction")}
hideHeader hideHeader
classNameModal={styles.reactionPopupMenuModal} classNameModal={styles.reactionPopupMenuModal}
className={styles.reactionPopupMenuRoot}
onDismiss={() => setShowReactionsMenu(false)} onDismiss={() => setShowReactionsMenu(false)}
> >
<ReactionPopupMenu <ReactionPopupMenu
errorText={errorText} errorText={errorText}
isHandRaised={isHandRaised} isHandRaised={isHandRaised}
canReact={canReact} canReact={!busy && canReact}
sendReaction={(reaction) => void sendRelation(reaction)} sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={wrappedToggleRaisedHand} toggleRaisedHand={wrappedToggleRaisedHand}
/> />

View File

@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Can close search 1`] = ` exports[`Can close reaction dialog 1`] = `
<div <div
aria-hidden="true" aria-hidden="true"
data-aria-hidden="true" data-aria-hidden="true"
@ -10,7 +10,7 @@ exports[`Can close search 1`] = `
aria-expanded="true" aria-expanded="true"
aria-haspopup="true" aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction" aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":rec:" aria-labelledby=":r9l:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary" data-kind="primary"
data-size="lg" data-size="lg"
@ -36,16 +36,19 @@ exports[`Can close search 1`] = `
</div> </div>
`; `;
exports[`Can close search with the escape key 1`] = ` exports[`Can fully expand emoji picker 1`] = `
<div> <div
aria-hidden="true"
data-aria-hidden="true"
>
<button <button
aria-disabled="false" aria-disabled="false"
aria-expanded="false" aria-expanded="true"
aria-haspopup="true" aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction" aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":rhh:" aria-labelledby=":r6c:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary" data-kind="primary"
data-size="lg" data-size="lg"
role="button" role="button"
tabindex="0" tabindex="0"
@ -76,7 +79,7 @@ exports[`Can lower hand 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction" aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r3i:" aria-labelledby=":r36:"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary" data-kind="primary"
data-size="lg" data-size="lg"
@ -142,7 +145,7 @@ exports[`Can raise hand 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction" aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r1p:" aria-labelledby=":r1j:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary" data-kind="secondary"
data-size="lg" data-size="lg"
@ -167,75 +170,3 @@ exports[`Can raise hand 1`] = `
</button> </button>
</div> </div>
`; `;
exports[`Can search for and send emoji 1`] = `
<div
aria-hidden="true"
data-aria-hidden="true"
>
<button
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r74:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.536 15.536a1 1 0 0 0-1.415-1.415 2.987 2.987 0 0 1-2.12.879 2.988 2.988 0 0 1-2.122-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
/>
</svg>
</button>
</div>
`;
exports[`Can search for and send emoji with the keyboard 1`] = `
<div
aria-hidden="true"
data-aria-hidden="true"
>
<button
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":ra3:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.536 15.536a1 1 0 0 0-1.415-1.415 2.987 2.987 0 0 1-2.12.879 2.988 2.988 0 0 1-2.122-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
/>
</svg>
</button>
</div>
`;

View File

@ -28,11 +28,11 @@ export class Config {
const internalInstance = new Config(); const internalInstance = new Config();
Config.internalInstance = internalInstance; Config.internalInstance = internalInstance;
Config.internalInstance.initPromise = downloadConfig( Config.internalInstance.initPromise = downloadConfig("/config.json").then(
"../config.json", (config) => {
).then((config) => { internalInstance.config = merge({}, DEFAULT_CONFIG, config);
internalInstance.config = merge({}, DEFAULT_CONFIG, config); },
}); );
} }
return Config.internalInstance.initPromise; return Config.internalInstance.initPromise;
} }
@ -74,11 +74,7 @@ async function downloadConfig(
configJsonFilename: string, configJsonFilename: string,
): Promise<ConfigOptions> { ): Promise<ConfigOptions> {
const url = new URL(configJsonFilename, window.location.href); const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString()); const res = await fetch(url);
const res = await fetch(url, {
cache: "no-cache",
method: "GET",
});
if (!res.ok || res.status === 404 || res.status === 0) { if (!res.ok || res.status === 404 || res.status === 0) {
// Lack of a config isn't an error, we should just use the defaults. // Lack of a config isn't an error, we should just use the defaults.

View File

@ -8,7 +8,8 @@ Please see LICENSE in the repository root for full details.
.content { .content {
width: 100%; width: 100%;
max-width: 350px; max-width: 350px;
align-self: center; margin-left: auto;
margin-right: auto;
} }
.avatarFieldRow { .avatarFieldRow {

16
src/useIsTouchscreen.ts Normal file
View File

@ -0,0 +1,16 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { useMediaQuery } from "./useMediaQuery";
/**
* @returns Whether the device is a touchscreen device.
*/
// Empirically, Chrome on Android can end up not matching (hover: none), but
// still matching (pointer: coarse) :/
export const useIsTouchscreen = (): boolean =>
useMediaQuery("(hover: none) or (pointer: coarse)");