diff --git a/package.json b/package.json index 6a5a47a1..7cf8eb1d 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.2.2", "jest-environment-jsdom": "^29.3.1", + "jest-mock": "^29.5.0", "prettier": "^2.6.2", "sass": "^1.42.1", "storybook-builder-vite": "^0.1.12", diff --git a/src/App.tsx b/src/App.tsx index 0ef5bcd6..71cc9710 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,6 @@ import { HomePage } from "./home/HomePage"; import { LoginPage } from "./auth/LoginPage"; import { RegisterPage } from "./auth/RegisterPage"; import { RoomPage } from "./room/RoomPage"; -import { RoomRedirect } from "./room/RoomRedirect"; import { ClientProvider } from "./ClientContext"; import { usePageFocusStyle } from "./usePageFocusStyle"; import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage"; @@ -71,14 +70,11 @@ export default function App({ history }: AppProps) { - - - - + diff --git a/src/UrlParams.ts b/src/UrlParams.ts index eb2b1795..f41e3cfa 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 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. @@ -17,6 +17,8 @@ limitations under the License. import { useMemo } from "react"; import { useLocation } from "react-router-dom"; +import { Config } from "./config/Config"; + interface UrlParams { roomAlias: string | null; roomId: string | null; @@ -93,14 +95,39 @@ interface UrlParams { * @returns The app parameters encoded in the URL */ export const getUrlParams = ( - query: string = window.location.search, - fragment: string = window.location.hash + ignoreRoomAlias?: boolean, + search = window.location.search, + pathname = window.location.pathname, + hash = window.location.hash ): UrlParams => { - const fragmentQueryStart = fragment.indexOf("?"); + let roomAlias: string | undefined; + if (!ignoreRoomAlias) { + if (hash === "") { + roomAlias = pathname.substring(1); // Strip the "/" + + // Delete "/room/" and "?", if present + if (roomAlias.startsWith("room/")) { + roomAlias = roomAlias.substring("room/".length); + } + // Add "#", if not present + if (!roomAlias.startsWith("#")) { + roomAlias = `#${roomAlias}`; + } + } else { + roomAlias = hash; + } + + // Add server part, if not present + if (!roomAlias.includes(":")) { + roomAlias = `${roomAlias}:${Config.defaultServerName()}`; + } + } + + const fragmentQueryStart = hash.indexOf("?"); const fragmentParams = new URLSearchParams( - fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart) + fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) ); - const queryParams = new URLSearchParams(query); + const queryParams = new URLSearchParams(search); // Normally, URL params should be encoded in the fragment so as to avoid // leaking them to the server. However, we also check the normal query @@ -114,16 +141,10 @@ export const getUrlParams = ( ...queryParams.getAll(name), ]; - // The part of the fragment before the ? - const fragmentRoute = - fragmentQueryStart === -1 - ? fragment - : fragment.substring(0, fragmentQueryStart); - const fontScale = parseFloat(getParam("fontScale") ?? ""); return { - roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null, + roomAlias: !roomAlias || roomAlias.includes("!") ? null : roomAlias, roomId: getParam("roomId"), viaServers: getAllParams("via"), isEmbedded: hasParam("embed"), @@ -149,6 +170,9 @@ export const getUrlParams = ( * @returns The app parameters for the current URL */ export const useUrlParams = (): UrlParams => { - const { hash, search } = useLocation(); - return useMemo(() => getUrlParams(search, hash), [search, hash]); + const { search, pathname, hash } = useLocation(); + return useMemo( + () => getUrlParams(false, search, pathname, hash), + [search, pathname, hash] + ); }; diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index 545dfade..c3d5a6ae 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -35,13 +35,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) { return ( <>
- {rooms.map(({ roomId, roomName, avatarUrl, participants }) => ( + {rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => ( @@ -59,7 +59,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) { interface CallTileProps { name: string; avatarUrl: string; - roomId: string; + roomAlias: string; participants: RoomMember[]; client: MatrixClient; disableFacepile?: boolean; @@ -67,14 +67,17 @@ interface CallTileProps { function CallTile({ name, avatarUrl, - roomId, + roomAlias, participants, client, disableFacepile, }: CallTileProps) { return (
- + {name} - {getRoomUrl(roomId)} + {getRoomUrl(roomAlias)} {participants && !disableFacepile && (
); diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 54f52491..76da921d 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -71,8 +71,9 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { setLoading(true); const [roomAlias] = await createRoom(client, roomName, ptt); + if (roomAlias) { - history.push(`/room/${roomAlias}`); + history.push(`/${roomAlias.substring(1).split(":")[0]}`); } } diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 554d8de6..f479901f 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -93,8 +93,7 @@ export const UnauthenticatedView: FC = () => { setOnFinished(() => { setClient({ client, session }); const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName); - const [, serverName] = client.getUserId()!.split(":"); - history.push(`/room/#${aliasLocalpart}:${serverName}`); + history.push(`/${aliasLocalpart}`); }); setLoading(false); @@ -111,7 +110,7 @@ export const UnauthenticatedView: FC = () => { } setClient({ client, session }); - history.push(`/room/${roomAlias}`); + history.push(`/${roomAlias.substring(1).split(":")[0]}`); } submit().catch((error) => { diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index 463a1bb3..0f5d8ef9 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -22,7 +22,7 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv import { useState, useEffect } from "react"; export interface GroupCallRoom { - roomId: string; + roomAlias: string; roomName: string; avatarUrl: string; room: Room; @@ -89,12 +89,13 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { const groupCalls = client.groupCallEventHandler.groupCalls.values(); const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room); - const sortedRooms = sortRooms(client, rooms); + const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias + const sortedRooms = sortRooms(client, filteredRooms); const items = sortedRooms.map((room) => { const groupCall = client.getGroupCallForRoom(room.roomId)!; return { - roomId: room.getCanonicalAlias() || room.roomId, + roomAlias: room.getCanonicalAlias(), roomName: room.name, avatarUrl: room.getMxcAvatarUrl()!, room, @@ -103,7 +104,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { }; }); - setRooms(items); + setRooms(items as GroupCallRoom[]); } updateRooms(); diff --git a/src/initializer.tsx b/src/initializer.tsx index f1f37966..850de125 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -55,7 +55,7 @@ export class Initializer { languageDetector.addDetector({ name: "urlFragment", // Look for a language code in the URL's fragment - lookup: () => getUrlParams().lang ?? undefined, + lookup: () => getUrlParams(true).lang ?? undefined, }); i18n @@ -140,7 +140,7 @@ export class Initializer { } // Custom fonts - const { fonts, fontScale } = getUrlParams(); + const { fonts, fontScale } = getUrlParams(true); if (fontScale !== null) { document.documentElement.style.setProperty( "--font-scale", diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index ffbdf4e6..6dff2de3 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -345,15 +345,11 @@ export async function createRoom( // Returns a URL to that will load Element Call with the given room export function getRoomUrl(roomIdOrAlias: string): string { if (roomIdOrAlias.startsWith("#")) { - const [localPart, host] = roomIdOrAlias.replace("#", "").split(":"); - - if (host !== Config.defaultServerName()) { - return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`; - } else { - return `${window.location.protocol}//${window.location.host}/${localPart}`; - } + return `${window.location.protocol}//${window.location.host}/${ + roomIdOrAlias.substring(1).split(":")[0] + }`; } else { - return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`; + return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`; } } diff --git a/src/room/RoomRedirect.tsx b/src/room/RoomRedirect.tsx deleted file mode 100644 index 3d45086d..00000000 --- a/src/room/RoomRedirect.tsx +++ /dev/null @@ -1,44 +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 { useEffect } from "react"; -import { useLocation, useHistory } from "react-router-dom"; - -import { Config } from "../config/Config"; -import { LoadingView } from "../FullScreenView"; - -// A component that, when loaded, redirects the client to a full room URL -// based on the current URL being an abbreviated room URL -export function RoomRedirect() { - const { pathname } = useLocation(); - const history = useHistory(); - - useEffect(() => { - let roomId = pathname; - - if (pathname.startsWith("/")) { - roomId = roomId.substring(1, roomId.length); - } - - if (!roomId.startsWith("#") && !roomId.startsWith("!")) { - roomId = `#${roomId}:${Config.defaultServerName()}`; - } - - history.replace(`/room/${roomId.toLowerCase()}`); - }, [pathname, history]); - - return ; -} diff --git a/test/UrlParams-test.ts b/test/UrlParams-test.ts new file mode 100644 index 00000000..9d1e5198 --- /dev/null +++ b/test/UrlParams-test.ts @@ -0,0 +1,98 @@ +/* +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 { mocked } from "jest-mock"; +import { getUrlParams } from "../src/UrlParams"; +import { Config } from "../src/config/Config"; + +const ROOM_NAME = "roomNameHere"; +const ROOM_ID = "d45f138fsd"; +const ORIGIN = "https://call.element.io"; +const HOMESERVER = "call.ems.host"; + +jest.mock("../src/config/Config"); + +describe("UrlParams", () => { + beforeAll(() => { + mocked(Config.defaultServerName).mockReturnValue("call.ems.host"); + }); + + describe("handles URL with /room/", () => { + it("and nothing else", () => { + expect(getUrlParams(false, "", `/room/${ROOM_NAME}`, "").roomAlias).toBe( + `#${ROOM_NAME}:${HOMESERVER}` + ); + }); + + it("and #", () => { + expect( + getUrlParams(false, "", `${ORIGIN}/room/`, `#${ROOM_NAME}`).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and # and server part", () => { + expect( + getUrlParams(false, "", `/room/`, `#${ROOM_NAME}:${HOMESERVER}`) + .roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and server part", () => { + expect( + getUrlParams(false, "", `/room/${ROOM_NAME}:${HOMESERVER}`, "") + .roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + }); + + describe("handles URL without /room/", () => { + it("and nothing else", () => { + expect(getUrlParams(false, "", `/${ROOM_NAME}`, "").roomAlias).toBe( + `#${ROOM_NAME}:${HOMESERVER}` + ); + }); + + it("and with #", () => { + expect(getUrlParams(false, "", "", `#${ROOM_NAME}`).roomAlias).toBe( + `#${ROOM_NAME}:${HOMESERVER}` + ); + }); + + it("and with # and server part", () => { + expect( + getUrlParams(false, "", "", `#${ROOM_NAME}:${HOMESERVER}`).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and with server part", () => { + expect( + getUrlParams(false, "", `/${ROOM_NAME}:${HOMESERVER}`, "").roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + }); + + describe("handles search params", () => { + it("(roomId)", () => { + expect(getUrlParams(true, `?roomId=${ROOM_ID}`).roomId).toBe(ROOM_ID); + }); + }); + + it("ignores room alias", () => { + expect( + getUrlParams(true, "", `/room/${ROOM_NAME}:${HOMESERVER}`).roomAlias + ).toBeFalsy(); + }); +}); diff --git a/test/home/CallList-test.tsx b/test/home/CallList-test.tsx index 7b3925d4..a161960f 100644 --- a/test/home/CallList-test.tsx +++ b/test/home/CallList-test.tsx @@ -34,7 +34,7 @@ describe("CallList", () => { it("should show room", async () => { const rooms = [ - { roomName: "Room #1", roomId: "!roomId" }, + { roomName: "Room #1", roomAlias: "#room-name:server.org" }, ] as GroupCallRoom[]; const result = renderComponent(rooms); diff --git a/yarn.lock b/yarn.lock index d8207984..e35ce34b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2014,6 +2014,13 @@ dependencies: "@sinclair/typebox" "^0.24.1" +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + "@jest/source-map@^29.2.0": version "29.2.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" @@ -2088,6 +2095,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@joshwooding/vite-plugin-react-docgen-typescript@0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.0.2.tgz#e0ae8c94f468da3a273a7b0acf23ba3565f86cbc" @@ -3098,6 +3117,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== +"@sinclair/typebox@^0.25.16": + version "0.25.24" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -10309,6 +10333,15 @@ jest-mock@^29.3.1: "@types/node" "*" jest-util "^29.3.1" +jest-mock@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-util "^29.5.0" + jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" @@ -10451,6 +10484,18 @@ jest-util@^29.3.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^29.2.2: version "29.2.2" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.2.2.tgz#e43ce1931292dfc052562a11bc681af3805eadce"