Set up translation with i18next

This commit is contained in:
Robin Townsend
2022-10-10 09:19:10 -04:00
parent eca598e28f
commit 8524b9ecd6
55 changed files with 1470 additions and 326 deletions

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React from "react";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import styles from "./AudioPreview.module.css";
import { SelectInput } from "../input/SelectInput";
@@ -43,24 +44,26 @@ export function AudioPreview({
audioOutputs,
setAudioOutput,
}: Props) {
const { t } = useTranslation();
return (
<>
<h1>{`${roomName} - Walkie-talkie call`}</h1>
<h1>{t("{{roomName}} - Walkie-talkie call", { roomName })}</h1>
<div className={styles.preview}>
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Microphone permissions needed to join the call.
{t("Microphone permissions needed to join the call.")}
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Accept microphone permissions to join the call.
{t("Accept microphone permissions to join the call.")}
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (
<>
<SelectInput
label="Microphone"
label={t("Microphone")}
selectedKey={audioInput}
onSelectionChange={setAudioInput}
className={styles.inputField}
@@ -69,13 +72,13 @@ export function AudioPreview({
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Microphone ${index + 1}`}
: t("Microphone {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
label={t("Speaker")}
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
className={styles.inputField}
@@ -84,7 +87,7 @@ export function AudioPreview({
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Speaker ${index + 1}`}
: t("Speaker {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";
@@ -24,6 +25,7 @@ import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
export function CallEndedView({ client }: { client: MatrixClient }) {
const { t } = useTranslation();
const { displayName } = useProfile(client);
return (
@@ -37,29 +39,31 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
<div className={styles.container}>
<main className={styles.main}>
<Headline className={styles.headline}>
{displayName}, your call is now ended
{t("{{displayName}}, your call is now ended", { displayName })}
</Headline>
<div className={styles.callEndedContent}>
<Subtitle>
Why not finish by setting up a password to keep your account?
</Subtitle>
<Subtitle>
You'll be able to keep your name and set an avatar for use on
future calls
</Subtitle>
<Trans>
<Subtitle>
Why not finish by setting up a password to keep your account?
</Subtitle>
<Subtitle>
You'll be able to keep your name and set an avatar for use on
future calls
</Subtitle>
</Trans>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
Create account
{t("Create account")}
</LinkButton>
</div>
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">
Not now, return to home screen
{t("Not now, return to home screen")}
</Link>
</Body>
</div>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { useCallback, useEffect } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
@@ -25,6 +26,7 @@ import {
useRageshakeRequest,
} from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
interface Props {
inCall: boolean;
roomId: string;
@@ -32,7 +34,9 @@ interface Props {
// TODO: add all props for for <Modal>
[index: string]: unknown;
}
export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest();
@@ -67,15 +71,20 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
}, [sent, onClose]);
return (
<Modal title="Submit Feedback" isDismissable onClose={onClose} {...rest}>
<Modal
title={t("Submit feedback")}
isDismissable
onClose={onClose}
{...rest}
>
<ModalContent>
<Body>Having trouble? Help us fix it.</Body>
<Body>{t("Having trouble? Help us fix it.")}</Body>
<form onSubmit={onSubmitFeedback}>
<FieldRow>
<InputField
id="description"
name="description"
label="Description (optional)"
label={t("Description (optional)")}
type="textarea"
/>
</FieldRow>
@@ -83,19 +92,19 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
<InputField
id="sendLogs"
name="sendLogs"
label="Include Debug Logs"
label={t("Include debug logs")}
type="checkbox"
defaultChecked
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={sending}>
{sending ? "Submitting feedback..." : "Submit Feedback"}
{sending ? t("Submitting feedback…") : t("Submit feedback")}
</Button>
</FieldRow>
</form>

View File

@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
@@ -27,28 +28,33 @@ import { Menu } from "../Menu";
import { TooltipTrigger } from "../Tooltip";
export type Layout = "freedom" | "spotlight";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
}
export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]);
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={() => "Layout Type"}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
<Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key="spotlight" textValue="Spotlight">
<Item key="spotlight" textValue={t("Spotlight")}>
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { ReactNode } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useTranslation } from "react-i18next";
import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
@@ -37,6 +38,7 @@ export function GroupCallLoader({
children,
createPtt,
}: Props): JSX.Element {
const { t } = useTranslation();
const { loading, error, groupCall } = useLoadGroupCall(
client,
roomIdOrAlias,
@@ -44,12 +46,12 @@ export function GroupCallLoader({
createPtt
);
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
if (loading) {
return (
<FullScreenView>
<h1>Loading room...</h1>
<h1>{t("Loading room…")}</h1>
</FullScreenView>
);
}

View File

@@ -19,6 +19,7 @@ import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
@@ -81,8 +82,8 @@ export function GroupCallView({
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
const { t } = useTranslation();
const { setAudioInput, setVideoInput } = useMediaHandler();
const avatarUrl = useRoomAvatar(groupCall.room);
useEffect(() => {
@@ -240,7 +241,7 @@ export function GroupCallView({
} else if (state === GroupCallState.Entering) {
return (
<FullScreenView>
<h1>Entering room...</h1>
<h1>{t("Entering room…")}</h1>
</FullScreenView>
);
} else if (left) {
@@ -257,7 +258,7 @@ export function GroupCallView({
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>Loading room...</h1>
<h1>{t("Loading room…")}</h1>
</FullScreenView>
);
} else {

View File

@@ -23,6 +23,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import type { IWidgetApiRequest } from "matrix-widget-api";
import styles from "./InCallView.module.css";
@@ -112,6 +113,7 @@ export function InCallView({
unencryptedEventsFromUsers,
hideHeader,
}: Props) {
const { t } = useTranslation();
usePreventScroll();
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
@@ -247,7 +249,7 @@ export function InCallView({
if (items.length === 0) {
return (
<div className={styles.centerMessage}>
<p>Waiting for other participants...</p>
<p>{t("Waiting for other participants…")}</p>
</div>
);
}

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
@@ -25,19 +26,23 @@ interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string;
}
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
<Modal
title="Invite People"
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
const { t } = useTranslation();
return (
<Modal
title={t("Invite people")}
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -19,6 +19,7 @@ import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { PressEvent } from "@react-types/shared";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next";
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
@@ -66,6 +67,7 @@ export function LobbyView({
isEmbedded,
hideHeader,
}: Props) {
const { t } = useTranslation();
const { stream } = useCallFeed(localCallFeed);
const {
audioInput,
@@ -142,15 +144,15 @@ export function LobbyView({
variant="secondaryCopy"
value={getRoomUrl(roomIdOrAlias)}
className={styles.copyButton}
copiedMessage="Call link copied"
copiedMessage={t("Call link copied")}
>
Copy call link and join later
{t("Copy call link and join later")}
</CopyButton>
</div>
{!isEmbedded && (
<Body className={styles.joinRoomFooter}>
<Link color="primary" to="/">
Take me Home
{t("Take me Home")}
</Link>
</Body>
)}

View File

@@ -18,6 +18,7 @@ import React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { OverlayTriggerState } from "@react-stately/overlays";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { Menu } from "../Menu";
@@ -31,6 +32,7 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
interface Props {
roomIdOrAlias: string;
inCall: boolean;
@@ -42,6 +44,7 @@ interface Props {
onClose: () => void;
};
}
export function OverflowMenu({
roomIdOrAlias,
inCall,
@@ -50,6 +53,8 @@ export function OverflowMenu({
feedbackModalState,
feedbackModalProps,
}: Props) {
const { t } = useTranslation();
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
@@ -90,29 +95,31 @@ export function OverflowMenu({
[feedbackModalState, inviteModalState, settingsModalState]
);
const tooltip = useCallback(() => t("More"), [t]);
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger tooltip={() => "More"} placement="top">
<TooltipTrigger tooltip={tooltip} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="more menu" onAction={onAction}>
<Menu {...props} label={t("More menu")} onAction={onAction}>
{showInvite && (
<Item key="invite" textValue="Invite people">
<Item key="invite" textValue={t("Invite people")}>
<AddUserIcon />
<span>Invite people</span>
<span>{t("Invite people")}</span>
</Item>
)}
<Item key="settings" textValue="Settings">
<Item key="settings" textValue={t("Settings")}>
<SettingsIcon />
<span>Settings</span>
<span>{t("Settings")}</span>
</Item>
<Item key="feedback" textValue="Submit Feedback">
<Item key="feedback" textValue={t("Submit feedback")}>
<FeedbackIcon />
<span>Submit Feedback</span>
<span>{t("Submit feedback")}</span>
</Item>
</Menu>
)}

View File

@@ -17,10 +17,12 @@ limitations under the License.
import React, { useEffect } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import i18n from "i18next";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next";
import { useDelayedState } from "../useDelayedState";
import { useModalTriggerState } from "../Modal";
@@ -50,40 +52,45 @@ function getPromptText(
talkOverEnabled: boolean,
activeSpeakerUserId: string,
activeSpeakerDisplayName: string,
connected: boolean
connected: boolean,
t: typeof i18n.t
): string {
if (!connected) return "Connection lost";
if (!connected) return t("Connection lost");
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
if (networkWaiting) {
return "Waiting for network";
return t("Waiting for network");
}
if (showTalkOverError) {
return "You can't talk at the same time";
return t("You can't talk at the same time");
}
if (pttButtonHeld && activeSpeakerIsLocalUser) {
if (isTouchScreen) {
return "Release to stop";
return t("Release to stop");
} else {
return "Release spacebar key to stop";
return t("Release spacebar key to stop");
}
}
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
if (isTouchScreen) {
return `Press and hold to talk over ${activeSpeakerDisplayName}`;
return t("Press and hold to talk over {{name}}", {
name: activeSpeakerDisplayName,
});
} else {
return `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`;
return t("Press and hold spacebar to talk over {{name}}", {
name: activeSpeakerDisplayName,
});
}
}
if (isTouchScreen) {
return "Press and hold to talk";
return t("Press and hold to talk");
} else {
return "Press and hold spacebar to talk";
return t("Press and hold spacebar to talk");
}
}
@@ -112,6 +119,7 @@ export const PTTCallView: React.FC<Props> = ({
isEmbedded,
hideHeader,
}) => {
const { t } = useTranslation();
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
@@ -195,9 +203,11 @@ export const PTTCallView: React.FC<Props> = ({
{showControls && (
<>
<div className={styles.participants}>
<p>{`${participants.length} ${
participants.length > 1 ? "people" : "person"
} connected`}</p>
<p>
{t("{{count}} people connected", {
count: participants.length,
})}
</p>
<Facepile
size={facepileSize}
max={8}
@@ -230,8 +240,10 @@ export const PTTCallView: React.FC<Props> = ({
<AudioIcon className={styles.speakerIcon} />
)}
{activeSpeakerIsLocalUser
? "Talking..."
: `${activeSpeakerDisplayName} is talking...`}
? t("Talking…")
: t("{{name}} is talking…", {
name: activeSpeakerDisplayName,
})}
</h2>
<Timer value={activeSpeakerUserId} />
</div>
@@ -263,7 +275,8 @@ export const PTTCallView: React.FC<Props> = ({
talkOverEnabled,
activeSpeakerUserId,
activeSpeakerDisplayName,
connected
connected,
t
)}
</p>
)}
@@ -278,7 +291,7 @@ export const PTTCallView: React.FC<Props> = ({
<Toggle
isSelected={talkOverEnabled}
onChange={setTalkOverEnabled}
label="Talk over speaker"
label={t("Talk over speaker")}
id="talkOverEnabled"
/>
)}

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Button } from "../button";
@@ -33,6 +34,7 @@ export const RageshakeRequestModal: FC<Props> = ({
roomIdOrAlias,
...rest
}) => {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
@@ -42,11 +44,12 @@ export const RageshakeRequestModal: FC<Props> = ({
}, [sent, rest]);
return (
<Modal title="Debug Log Request" isDismissable {...rest}>
<Modal title={t("Debug log request")} isDismissable {...rest}>
<ModalContent>
<Body>
Another user on this call is having an issue. In order to better
diagnose these issues we'd like to collect a debug log.
{t(
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
)}
</Body>
<FieldRow>
<Button
@@ -59,12 +62,12 @@ export const RageshakeRequestModal: FC<Props> = ({
}
disabled={sending}
>
{sending ? "Sending debug log..." : "Send debug log"}
{sending ? t("Sending debug log…") : t("Send debug log")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
</ModalContent>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";
@@ -50,6 +51,7 @@ export function RoomAuthView() {
[registerPasswordlessUser]
);
const { t } = useTranslation();
const location = useLocation();
return (
@@ -64,42 +66,46 @@ export function RoomAuthView() {
</Header>
<div className={styles.container}>
<main className={styles.main}>
<Headline className={styles.headline}>Join Call</Headline>
<Headline className={styles.headline}>{t("Join call")}</Headline>
<Form className={styles.form} onSubmit={onSubmit}>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
label={t("Display name")}
placeholder={t("Display name")}
type="text"
required
autoComplete="off"
/>
</FieldRow>
<Caption>
By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Trans>
By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Trans>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
{loading ? "Loading..." : "Join call now"}
{loading ? t("Loading…") : t("Join call now")}
</Button>
<div id={recaptchaId} />
</Form>
</main>
<Body className={styles.footer}>
{"Not registered yet? "}
<Link
color="primary"
to={{ pathname: "/login", state: { from: location } }}
>
Create an account
</Link>
<Trans>
{"Not registered yet? "}
<Link
color="primary"
to={{ pathname: "/login", state: { from: location } }}
>
Create an account
</Link>
</Trans>
</Body>
</div>
</>

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClient } from "../ClientContext";
@@ -22,11 +23,13 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
import { useRoomParams } from "./useRoomParams";
import { useUrlParams } from "../UrlParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { translatedError } from "../TranslatedError";
export const RoomPage: FC = () => {
const { t } = useTranslation();
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
@@ -39,9 +42,9 @@ export const RoomPage: FC = () => {
hideHeader,
isPtt,
displayName,
} = useRoomParams();
} = useUrlParams();
const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) throw new Error("No room specified");
if (!roomIdOrAlias) throw translatedError("No room specified", t);
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false);

View File

@@ -19,6 +19,7 @@ import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { MicButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream";
@@ -40,6 +41,7 @@ interface Props {
audioOutput: string;
stream: MediaStream;
}
export function VideoPreview({
client,
state,
@@ -51,6 +53,7 @@ export function VideoPreview({
audioOutput,
stream,
}: Props) {
const { t } = useTranslation();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
@@ -64,12 +67,12 @@ export function VideoPreview({
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
Camera/microphone permissions needed to join the call.
{t("Camera/microphone permissions needed to join the call.")}
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
Accept camera/microphone permissions to join the call.
{t("Accept camera/microphone permissions to join the call.")}
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (

View File

@@ -26,8 +26,10 @@ import {
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next";
import { usePageUnload } from "./usePageUnload";
import { TranslatedError, translatedError } from "../TranslatedError";
export interface UseGroupCallReturnType {
state: GroupCallState;
@@ -37,7 +39,7 @@ export interface UseGroupCallReturnType {
userMediaFeeds: CallFeed[];
microphoneMuted: boolean;
localVideoMuted: boolean;
error: Error;
error: TranslatedError | null;
initLocalCallFeed: () => void;
enter: () => void;
leave: () => void;
@@ -60,7 +62,7 @@ interface State {
localCallFeed: CallFeed;
activeSpeaker: string;
userMediaFeeds: CallFeed[];
error: Error;
error: TranslatedError | null;
microphoneMuted: boolean;
localVideoMuted: boolean;
screenshareFeeds: CallFeed[];
@@ -309,15 +311,18 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
});
}, [groupCall]);
const { t } = useTranslation();
useEffect(() => {
if (window.RTCPeerConnection === undefined) {
const error = new Error(
"WebRTC is not supported or is being blocked in this browser."
const error = translatedError(
"WebRTC is not supported or is being blocked in this browser.",
t
);
console.error(error);
updateState({ error });
}
}, []);
}, [t]);
return {
state,

View File

@@ -24,10 +24,12 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync";
import { useTranslation } from "react-i18next";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
import { translatedError } from "../TranslatedError";
export interface GroupCallLoadState {
loading: boolean;
@@ -41,6 +43,7 @@ export const useLoadGroupCall = (
viaServers: string[],
createPtt: boolean
): GroupCallLoadState => {
const { t } = useTranslation();
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
useEffect(() => {
@@ -122,7 +125,7 @@ export const useLoadGroupCall = (
const timeout = setTimeout(() => {
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
reject(new Error("Fetching group call timed out."));
reject(translatedError("Fetching group call timed out.", t));
}, 30000);
});
};
@@ -153,7 +156,7 @@ export const useLoadGroupCall = (
.catch((error) =>
setState((prevState) => ({ ...prevState, loading: false, error }))
);
}, [client, roomIdOrAlias, viaServers, createPtt]);
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
return state;
};

View File

@@ -1,100 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
export interface RoomParams {
roomAlias: string | null;
roomId: string | null;
viaServers: string[];
// Whether the app is running in embedded mode, and should keep the user
// confined to the current room
isEmbedded: boolean;
// Whether the app should pause before joining the call until it sees an
// io.element.join widget action, allowing it to be preloaded
preload: boolean;
// Whether to hide the room header when in a call
hideHeader: boolean;
// Whether to start a walkie-talkie call instead of a video call
isPtt: boolean;
// Whether to use end-to-end encryption
e2eEnabled: boolean;
// The user's ID (only used in Matroska mode)
userId: string | null;
// The display name to use for auto-registration
displayName: string | null;
// The device's ID (only used in Matroska mode)
deviceId: string | null;
}
/**
* Gets the room parameters for the current URL.
* @param {string} query The URL query string
* @param {string} fragment The URL fragment string
* @returns {RoomParams} The room parameters encoded in the URL
*/
export const getRoomParams = (
query: string = window.location.search,
fragment: string = window.location.hash
): RoomParams => {
const fragmentQueryStart = fragment.indexOf("?");
const fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
);
const queryParams = new URLSearchParams(query);
// Normally, room params should be encoded in the fragment so as to avoid
// leaking them to the server. However, we also check the normal query
// string for backwards compatibility with versions that only used that.
const hasParam = (name: string): boolean =>
fragmentParams.has(name) || queryParams.has(name);
const getParam = (name: string): string | null =>
fragmentParams.get(name) ?? queryParams.get(name);
const getAllParams = (name: string): string[] => [
...fragmentParams.getAll(name),
...queryParams.getAll(name),
];
// The part of the fragment before the ?
const fragmentRoute =
fragmentQueryStart === -1
? fragment
: fragment.substring(0, fragmentQueryStart);
return {
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
roomId: getParam("roomId"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),
hideHeader: hasParam("hideHeader"),
isPtt: hasParam("ptt"),
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
userId: getParam("userId"),
displayName: getParam("displayName"),
deviceId: getParam("deviceId"),
};
};
/**
* Hook to simplify use of getRoomParams.
* @returns {RoomParams} The room parameters for the current URL
*/
export const useRoomParams = (): RoomParams => {
const { hash, search } = useLocation();
return useMemo(() => getRoomParams(search, hash), [search, hash]);
};