From 0f97d655d2a6a5345958d1231c54ef0718c458a6 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 17 Sep 2023 17:48:03 -0400 Subject: [PATCH 1/2] Add a prompt to launch Element X on mobile This shows a bottom sheet on mobile asking the user whether they want to open the call in Element X, as soon as the page is loaded. --- public/locales/en-GB/app.json | 4 ++ src/UrlParams.ts | 16 ++++++ src/room/AppSelectionModal.module.css | 33 ++++++++++++ src/room/AppSelectionModal.tsx | 77 +++++++++++++++++++++++++++ src/room/RoomPage.tsx | 53 ++++++++++-------- 5 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 src/room/AppSelectionModal.module.css create mode 100644 src/room/AppSelectionModal.tsx diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index f6a7ed65..d9132962 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -27,6 +27,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", @@ -70,9 +71,11 @@ "Not encrypted": "Not encrypted", "Not now, return to home screen": "Not now, return to home screen", "Not registered yet? <2>Create an account": "Not registered yet? <2>Create an account", + "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 +85,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…", diff --git a/src/UrlParams.ts b/src/UrlParams.ts index e6f0cd05..a4b67299 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -94,6 +94,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/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..7d5a1a47 --- /dev/null +++ b/src/room/AppSelectionModal.tsx @@ -0,0 +1,77 @@ +/* +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 "../NewModal"; +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 + 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?")} + + + + + ); +}; diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 50156e70..b9e9a8a5 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 { @@ -86,30 +88,37 @@ 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 && ( + + )} + ); }; From 662a85c16a60a0ac230b7beec211039ac903a690 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 18 Sep 2023 11:46:16 -0400 Subject: [PATCH 2/2] Add a clarifying comment --- src/room/AppSelectionModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx index 7d5a1a47..feeb009d 100644 --- a/src/room/AppSelectionModal.tsx +++ b/src/room/AppSelectionModal.tsx @@ -50,7 +50,10 @@ export const AppSelectionModal: FC = ({ roomId }) => { ? window.location.href : getRoomUrl(roomId, roomSharedKey ?? undefined) ); - // Edit the URL so that it opens in embedded mode + // 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;