Set up translation with i18next
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
Reference in New Issue
Block a user