diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json
index 84798e1e..f769c7e0 100644
--- a/public/locales/en-GB/app.json
+++ b/public/locales/en-GB/app.json
@@ -26,6 +26,7 @@
"Close": "Close",
"Confirm password": "Confirm password",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
+ "Continue in browser": "Continue in browser",
"Copied!": "Copied!",
"Copy": "Copy",
"Copy and share this call link": "Copy and share this call link",
@@ -65,14 +66,17 @@
"Microphone off": "Microphone off",
"Microphone on": "Microphone on",
"More": "More",
+ "Mute microphone": "Mute microphone",
"Name of call": "Name of call",
"No": "No",
"Not encrypted": "Not encrypted",
"Not now, return to home screen": "Not now, return to home screen",
"Not registered yet? <2>Create an account2>": "Not registered yet? <2>Create an account2>",
+ "Open in the app": "Open in the app",
"Password": "Password",
"Passwords must match": "Passwords must match",
"Profile": "Profile",
+ "Ready to join?": "Ready to join?",
"Recaptcha dismissed": "Recaptcha dismissed",
"Recaptcha not loaded": "Recaptcha not loaded",
"Reconnect": "Reconnect",
@@ -82,6 +86,7 @@
"Retry sending logs": "Retry sending logs",
"Return to home screen": "Return to home screen",
"Select an option": "Select an option",
+ "Select app": "Select app",
"Send debug logs": "Send debug logs",
"Sending debug logs…": "Sending debug logs…",
"Sending…": "Sending…",
@@ -97,6 +102,8 @@
"Speaker": "Speaker",
"Spotlight": "Spotlight",
"Start new call": "Start new call",
+ "Start video": "Start video",
+ "Stop video": "Stop video",
"Submit": "Submit",
"Submit feedback": "Submit feedback",
"Submitting…": "Submitting…",
@@ -105,12 +112,11 @@
"Thanks!": "Thanks!",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy2> and <6>Terms of Service6> apply.<9>9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy2> and <6>Terms of Service6> apply.<9>9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)12>",
+ "Unmute microphone": "Unmute microphone",
"User menu": "User menu",
"Username": "Username",
"Version: {{version}}": "Version: {{version}}",
"Video": "Video",
- "Video off": "Video off",
- "Video on": "Video on",
"Waiting for other participants…": "Waiting for other participants…",
"Yes, join call": "Yes, join call",
"You": "You",
diff --git a/src/UrlParams.ts b/src/UrlParams.ts
index 6a2ea501..4aa6ace4 100644
--- a/src/UrlParams.ts
+++ b/src/UrlParams.ts
@@ -90,6 +90,22 @@ interface UrlParams {
password: string | null;
}
+export function editFragmentQuery(
+ hash: string,
+ edit: (params: URLSearchParams) => URLSearchParams
+): string {
+ const fragmentQueryStart = hash.indexOf("?");
+ const fragmentParams = edit(
+ new URLSearchParams(
+ fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
+ )
+ );
+ return `${hash.substring(
+ 0,
+ fragmentQueryStart
+ )}?${fragmentParams.toString()}`;
+}
+
/**
* Gets the app parameters for the current URL.
* @param ignoreRoomAlias If true, does not try to parse a room alias from the URL
diff --git a/src/button/Button.tsx b/src/button/Button.tsx
index 2b8049e5..3d269dc2 100644
--- a/src/button/Button.tsx
+++ b/src/button/Button.tsx
@@ -145,11 +145,11 @@ export function MicButton({
}) {
const { t } = useTranslation();
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
- const label = muted ? t("Microphone off") : t("Microphone on");
+ const label = muted ? t("Unmute microphone") : t("Mute microphone");
return (
-
@@ -166,11 +166,11 @@ export function VideoButton({
}) {
const { t } = useTranslation();
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
- const label = muted ? t("Video off") : t("Video on");
+ const label = muted ? t("Start video") : t("Stop video");
return (
-
+
diff --git a/src/room/AppSelectionModal.module.css b/src/room/AppSelectionModal.module.css
new file mode 100644
index 00000000..773df4fd
--- /dev/null
+++ b/src/room/AppSelectionModal.module.css
@@ -0,0 +1,33 @@
+/*
+Copyright 2023 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.
+*/
+
+.modal p {
+ text-align: center;
+ margin-block-end: var(--cpd-space-8x);
+}
+
+.modal button,
+.modal a {
+ width: 100%;
+}
+
+.modal button {
+ margin-block-end: var(--cpd-space-6x);
+}
+
+.modal a {
+ box-sizing: border-box;
+}
diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx
new file mode 100644
index 00000000..8d4a05f8
--- /dev/null
+++ b/src/room/AppSelectionModal.tsx
@@ -0,0 +1,80 @@
+/*
+Copyright 2023 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 { FC, MouseEvent, useCallback, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Button, Text } from "@vector-im/compound-web";
+import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
+
+import { Modal } from "../Modal";
+import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
+import { getRoomUrl } from "../matrix-utils";
+import styles from "./AppSelectionModal.module.css";
+import { editFragmentQuery } from "../UrlParams";
+
+interface Props {
+ roomId: string | null;
+}
+
+export const AppSelectionModal: FC = ({ roomId }) => {
+ const { t } = useTranslation();
+
+ const [open, setOpen] = useState(true);
+ const onBrowserClick = useCallback(
+ (e: MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setOpen(false);
+ },
+ [setOpen]
+ );
+
+ const roomSharedKey = useRoomSharedKey(roomId ?? "");
+ const appUrl = useMemo(() => {
+ // If the room ID is not known, fall back to the URL of the current page
+ const url = new URL(
+ roomId === null
+ ? window.location.href
+ : getRoomUrl(roomId, roomSharedKey ?? undefined)
+ );
+ // Edit the URL so that it opens in embedded mode. We do this for two
+ // reasons: It causes the mobile app to limit the user to only visiting the
+ // room in question, and it prevents this app selection prompt from being
+ // shown a second time.
+ url.hash = editFragmentQuery(url.hash, (params) => {
+ params.set("isEmbedded", "");
+ return params;
+ });
+
+ const result = new URL("element://call");
+ result.searchParams.set("url", url.toString());
+ return result.toString();
+ }, [roomId, roomSharedKey]);
+
+ return (
+
+
+ {t("Ready to join?")}
+
+
+ {t("Continue in browser")}
+
+
+ {t("Open in the app")}
+
+
+ );
+};
diff --git a/src/room/CallEndedView.module.css b/src/room/CallEndedView.module.css
index 9952a784..12409d4e 100644
--- a/src/room/CallEndedView.module.css
+++ b/src/room/CallEndedView.module.css
@@ -17,7 +17,8 @@ limitations under the License.
.headline {
text-align: center;
margin-bottom: 60px;
- white-space: pre;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
}
.callEndedContent {
@@ -66,6 +67,7 @@ limitations under the License.
flex: 1;
flex-direction: column;
align-items: center;
+ padding-inline: var(--inline-content-inset);
}
.logo {
diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx
index c7eed871..35cdda27 100644
--- a/src/room/RoomPage.tsx
+++ b/src/room/RoomPage.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { FC, useEffect, useState, useCallback } from "react";
+import { FC, useEffect, useState, useCallback, ReactNode } from "react";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useClientLegacy } from "../ClientContext";
@@ -26,6 +26,8 @@ import { useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { useOptInAnalytics } from "../settings/useSetting";
import { HomePage } from "../home/HomePage";
+import { platform } from "../Platform";
+import { AppSelectionModal } from "./AppSelectionModal";
export const RoomPage: FC = () => {
const {
@@ -85,29 +87,36 @@ export const RoomPage: FC = () => {
[client, passwordlessUser, isEmbedded, preload, hideHeader]
);
+ let content: ReactNode;
if (loading || isRegistering) {
- return ;
- }
-
- if (error) {
- return ;
- }
-
- if (!client) {
- return ;
- }
-
- if (!roomIdOrAlias) {
- return ;
+ content = ;
+ } else if (error) {
+ content = ;
+ } else if (!client) {
+ content = ;
+ } else if (!roomIdOrAlias) {
+ // TODO: This doesn't belong here, the app routes need to be reworked
+ content = ;
+ } else {
+ content = (
+
+ {groupCallView}
+
+ );
}
return (
-
- {groupCallView}
-
+ <>
+ {content}
+ {/* On mobile, show a prompt to launch the mobile app. If in embedded mode,
+ that means we *are* in the mobile app and should show no further prompt. */}
+ {(platform === "android" || platform === "ios") && !isEmbedded && (
+
+ )}
+ >
);
};
diff --git a/yarn.lock b/yarn.lock
index 6ab5f6cf..6ed7f185 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4564,11 +4564,16 @@
"@types/tough-cookie" "*"
parse5 "^7.0.0"
-"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
+"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.8":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
+"@types/json-schema@^7.0.5":
+ version "7.0.13"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
+ integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==
+
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -4615,9 +4620,9 @@
integrity sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==
"@types/node@^18.13.0":
- version "18.13.0"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
- integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
+ version "18.17.17"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.17.tgz#53cc07ce582c9d7c5850702a3c2cb0af0d7b0ca1"
+ integrity sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@@ -4650,9 +4655,9 @@
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/q@^1.5.1":
- version "1.5.5"
- resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
- integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
+ version "1.5.6"
+ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.6.tgz#a6edffe8283910e46dc7a573621f928e6b47fa56"
+ integrity sha512-IKjZ8RjTSwD4/YG+2gtj7BPFRB/lNbWKTiSj3M7U/TD2B7HfYCxvp2Zz6xA2WIY7pAuL1QOUPw8gQRbUrrq4fQ==
"@types/qs@^6.9.5":
version "6.9.8"
@@ -4660,9 +4665,9 @@
integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==
"@types/react-dom@^18.0.0":
- version "18.0.7"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.7.tgz#ee7cf8ec4e6977e3f0a7b1d38bd89c75aa2aec28"
- integrity sha512-HaXc+BbqAZE1RdsK3tC8SbkFy6UL2xF76lT9rQs5JkPrJg3rWA3Ou/Lhw3YJQzEDkBpmJ79nBsfnd05WrBd2QQ==
+ version "18.2.7"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63"
+ integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==
dependencies:
"@types/react" "*"
@@ -4905,17 +4910,17 @@
"@typescript-eslint/types" "6.1.0"
eslint-visitor-keys "^3.4.1"
-"@use-gesture/core@10.2.27":
- version "10.2.27"
- resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.2.27.tgz#0f24b17c036cd828ba07e3451ff45e2df959c6f5"
- integrity sha512-V4XV7hn9GAD2MYu8yBBVi5iuWBsAMfjPRMsEVzoTNGYH72tf0kFP+OKqGKc8YJFQIJx6yj+AOqxmEHOmx2/MEA==
+"@use-gesture/core@10.3.0":
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.3.0.tgz#9afd3777a45b2a08990a5dcfcf8d9ddd55b00db9"
+ integrity sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==
"@use-gesture/react@^10.2.11":
- version "10.2.27"
- resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.2.27.tgz#7fbd50d14449ec5bc49c9b6cfef8a2845f5e0608"
- integrity sha512-7E5vnWCxeslWlxwZ8uKIcnUZVMTRMZ8cvSnLLKF1NkyNb3PnNiAzoXM4G1vTKJKRhgOTeI6wK1YsEpwo9ABV5w==
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.3.0.tgz#180534c821fd635c2853cbcfa813f92c94f27e3f"
+ integrity sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==
dependencies:
- "@use-gesture/core" "10.2.27"
+ "@use-gesture/core" "10.3.0"
"@vector-im/compound-design-tokens@^0.0.5":
version "0.0.5"
@@ -5789,7 +5794,7 @@ babel-jest@^29.7.0:
graceful-fs "^4.2.9"
slash "^3.0.0"
-babel-loader@^8.0.0:
+babel-loader@^8.0.0, babel-loader@^8.2.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8"
integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==
@@ -5799,16 +5804,6 @@ babel-loader@^8.0.0:
make-dir "^3.1.0"
schema-utils "^2.6.5"
-babel-loader@^8.2.3:
- version "8.2.5"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e"
- integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==
- dependencies:
- find-cache-dir "^3.3.1"
- loader-utils "^2.0.0"
- make-dir "^3.1.0"
- schema-utils "^2.6.5"
-
babel-plugin-add-react-displayname@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5"