diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 38b2e70e..41498fd7 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -61,14 +61,15 @@ "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", "full_screen_view_h1": "<0>Oops, something's gone wrong.", "group_call_loader": { - "banned_body": "You got banned from the room.", + "banned_body": "You have been banned from the room.", "banned_heading": "Banned", - "call_ended_body": "You got removed from the call.", - "call_ended_heading": "Call Ended", + "call_ended_body": "You have been removed from the call.", + "call_ended_heading": "Call ended", "failed_heading": "Call not found", "failed_text": "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.", "knock_reject_body": "The room members declined your request to join.", - "knock_reject_heading": "Not allowed to join" + "knock_reject_heading": "Not allowed to join", + "reason": "Reason" }, "hangup_button_label": "End call", "header_label": "Element Call Home", @@ -101,6 +102,7 @@ "mute_microphone_button_label": "Mute microphone", "participant_count_one": "{{count, number}}", "participant_count_other": "{{count, number}}", + "participant_count_unkonwn": "unknown", "rageshake_button_error_caption": "Retry sending logs", "rageshake_request_modal": { "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.", diff --git a/src/Header.tsx b/src/Header.tsx index 7ca8929d..db922fae 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -117,7 +117,7 @@ interface RoomHeaderInfoProps { name: string; avatarUrl: string | null; encrypted: boolean; - participantCount: number; + participantCount: number | null; } export const RoomHeaderInfo: FC = ({ @@ -150,7 +150,7 @@ export const RoomHeaderInfo: FC = ({ - {participantCount > 0 && ( + {(participantCount ?? 0) > 0 && (
= ({ aria-label={t("header_participants_label")} /> - {t("participant_count", { count: participantCount })} + {participantCount + ? t("participant_count", { count: participantCount }) + : t("participant_count_unkonwn")}
)} diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 547393bf..3977b3df 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -16,12 +16,12 @@ limitations under the License. import { useMemo } from "react"; import { useLocation } from "react-router-dom"; +import { logger } from "matrix-js-sdk/src/logger"; import { Config } from "./config/Config"; +import { EncryptionSystem } from "./e2ee/sharedKeyManagement"; +import { E2eeType } from "./e2ee/e2eeType"; -export const PASSWORD_STRING = "password="; -export const PER_PARTICIPANT_STRING = "perParticipantE2EE="; -export const VIA_SERVERS_STRING = "viaServers="; interface RoomIdentifier { roomAlias: string | null; roomId: string | null; @@ -329,3 +329,32 @@ export const useRoomIdentifier = (): RoomIdentifier => { [pathname, search, hash], ); }; + +export function generateUrlSearchParams( + roomId: string, + encryptionSystem: EncryptionSystem, + viaServers?: string[], +): URLSearchParams { + const params = new URLSearchParams(); + // The password shouldn't need URL encoding here (we generate URL-safe ones) but encode + // it in case it came from another client that generated a non url-safe one + switch (encryptionSystem?.kind) { + case E2eeType.SHARED_KEY: { + const encodedPassword = encodeURIComponent(encryptionSystem.secret); + if (encodedPassword !== encryptionSystem.secret) { + logger.info( + "Encoded call password used non URL-safe chars: buggy client?", + ); + } + params.set("password", encodedPassword); + break; + } + case E2eeType.PER_PARTICIPANT: + params.set("perParticipantE2EE", "true"); + break; + } + params.set("roomId", roomId); + viaServers?.forEach((s) => params.set("viaServers", s)); + + return params; +} diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index 967ab014..6f826cfe 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -79,25 +79,25 @@ export function useRoomEncryptionSystem(roomId: string): EncryptionSystem { // (and we still need to take the value it returns because // the effect won't run in time for it to save to localstorage in // time for us to read it out again). - const [urlRoomId, passwordFormUrl] = useKeyFromUrl(); + const [urlRoomId, passwordFromUrl] = useKeyFromUrl(); const storedPassword = useInternalRoomSharedKey(roomId); const room = client?.getRoom(roomId); - const e2eeSystem = useMemo(() => { - if (!room) return { kind: E2eeType.NONE } as Unencrypted; + const e2eeSystem = useMemo(() => { + if (!room) return { kind: E2eeType.NONE }; if (storedPassword) return { kind: E2eeType.SHARED_KEY, secret: storedPassword, - } as SharedSecret; + }; if (urlRoomId === roomId) return { kind: E2eeType.SHARED_KEY, - secret: passwordFormUrl, - } as SharedSecret; + secret: passwordFromUrl, + }; if (room.hasEncryptionStateEvent()) { - return { kind: E2eeType.PER_PARTICIPANT } as PerParticipantE2EE; + return { kind: E2eeType.PER_PARTICIPANT }; } - return { kind: E2eeType.NONE } as EncryptionSystem; - }, [passwordFormUrl, room, roomId, storedPassword, urlRoomId]); + return { kind: E2eeType.NONE }; + }, [passwordFromUrl, room, roomId, storedPassword, urlRoomId]); return e2eeSystem; } diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index d5a26012..d2880795 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -93,7 +93,7 @@ const roomIsJoinable = (room: Room): boolean => { return joinRule === JoinRule.Knock || joinRule === JoinRule.Public; }; -const roomIsJoinedWithCall = (room: Room): boolean => { +const roomHasCallMembershipEvents = (room: Room): boolean => { const roomStateEvents = room .getLiveTimeline() .getState(EventTimeline.FORWARDS)?.events; @@ -108,13 +108,13 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { useEffect(() => { function updateRooms(): void { + // We want to show all rooms that historically had a call and which we are (can become) part of. const rooms = client .getRooms() - .filter(roomIsJoinedWithCall) + .filter(roomHasCallMembershipEvents) .filter(roomIsJoinable); const sortedRooms = sortRooms(client, rooms); const items = sortedRooms.map((room) => { - // const groupCall = client.getGroupCallForRoom(room.roomId)!; const session = client.matrixRTC.getRoomSession(room); session.memberships; return { diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 5540592b..c93580b1 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -29,12 +29,7 @@ import { secureRandomBase64Url } from "matrix-js-sdk/src/randomstring"; import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { Room } from "matrix-js-sdk/src/models/room"; import IndexedDBWorker from "./IndexedDBWorker?worker"; -import { - getUrlParams, - PASSWORD_STRING, - PER_PARTICIPANT_STRING, - VIA_SERVERS_STRING, -} from "./UrlParams"; +import { generateUrlSearchParams, getUrlParams } from "./UrlParams"; import { loadOlm } from "./olm"; import { Config } from "./config/Config"; import { E2eeType } from "./e2ee/e2eeType"; @@ -339,8 +334,6 @@ export async function createRoom( const result = await createPromise; - logger.log(`Creating group call in ${result.room_id}`); - let password; if (e2ee == E2eeType.SHARED_KEY) { password = secureRandomBase64Url(16); @@ -383,33 +376,10 @@ export function getRelativeRoomUrl( roomName?: string, viaServers?: string[], ): string { - // The password shouldn't need URL encoding here (we generate URL-safe ones) but encode - // it in case it came from another client that generated a non url-safe one - const encryptionPart = ((): string => { - switch (encryptionSystem?.kind) { - case E2eeType.SHARED_KEY: { - const encodedPassword = encodeURIComponent(encryptionSystem.secret); - if (encodedPassword !== encryptionSystem.secret) { - logger.info( - "Encoded call password used non URL-safe chars: buggy client?", - ); - } - return "&" + PASSWORD_STRING + encodedPassword; - } - case E2eeType.PER_PARTICIPANT: - return "&" + PER_PARTICIPANT_STRING + "true"; - case E2eeType.NONE: - return ""; - } - })(); - - const roomIdPart = `roomId=${roomId}`; - const viaServersPart = viaServers - ? viaServers.map((s) => "&" + VIA_SERVERS_STRING + s).join("") + const roomPart = roomName + ? "/" + roomAliasLocalpartFromRoomName(roomName) : ""; - return `/room/#${ - roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : "" - }?${roomIdPart}${encryptionPart}${viaServersPart}`; + return `/room/#${roomPart}?${generateUrlSearchParams(roomId, encryptionSystem, viaServers).toString()}`; } export function getAvatarUrl( diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index 11a6b94b..3ccc7b49 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode, useCallback } from "react"; +import { useCallback } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { MatrixError } from "matrix-js-sdk"; @@ -24,7 +24,7 @@ import { Heading, Link, Text } from "@vector-im/compound-web"; import { useLoadGroupCall, GroupCallStatus, - CustomMessage, + CallTerminatedMessage, } from "./useLoadGroupCall"; import { ErrorView, FullScreenView } from "../FullScreenView"; @@ -32,7 +32,7 @@ interface Props { client: MatrixClient; roomIdOrAlias: string; viaServers: string[]; - children: (groupCallState: GroupCallStatus) => ReactNode; + children: (groupCallState: GroupCallStatus) => JSX.Element; } export function GroupCallLoader({ @@ -57,7 +57,7 @@ export function GroupCallLoader({ case "loaded": case "waitForInvite": case "canKnock": - return <>{children(groupCallState)}; + return children(groupCallState); case "loading": return ( @@ -77,14 +77,14 @@ export function GroupCallLoader({ ); - } else if (groupCallState.error instanceof CustomMessage) { + } else if (groupCallState.error instanceof CallTerminatedMessage) { return ( {groupCallState.error.message} {groupCallState.error.messageBody} {groupCallState.error.reason && ( <> - Reason: + {t("group_call_loader.reason")}: "{groupCallState.error.reason}" )} diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 911ed8ba..a5d756af 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -83,12 +83,12 @@ export const GroupCallView: FC = ({ // The mute state reactively gets updated once the participant count reaches the threshold. // The user then still is able to unmute again. // The more common case is that the user is muted from the start (participant count is already over the threshold). - const [autoMuteHappened, setAutoMuteHappened] = useState(false); + const autoMuteHappened = useRef(false); useEffect(() => { - if (autoMuteHappened) return; + if (autoMuteHappened.current) return; if (memberships.length >= MUTE_PARTICIPANT_COUNT) { muteStates.audio.setEnabled?.(false); - setAutoMuteHappened(true); + autoMuteHappened.current = true; } }, [autoMuteHappened, memberships, muteStates.audio]); @@ -293,7 +293,7 @@ export const GroupCallView: FC = ({ if (e2eeSystem.kind === E2eeType.NONE && !widget) { // the url wants encryption, but we don't have a encryption system. (e.g. when joining a call without password) - // TODO: we need to figure out what we do with encryption goning forward. + // TODO: we need to figure out what we do with encryption going forward. // In the SPA a room is unencrypted but uses a shared secret for the call (since we distribute via url that works) // In embedded mode unencrypted rooms would make most sense to also call unencrypted. // A call where one person joins with the spa and another person with the widget would be a problem. @@ -301,7 +301,7 @@ export const GroupCallView: FC = ({ diff --git a/src/room/LobbyView.module.css b/src/room/LobbyView.module.css index 1aa54c60..8a3d2a2c 100644 --- a/src/room/LobbyView.module.css +++ b/src/room/LobbyView.module.css @@ -24,9 +24,17 @@ limitations under the License. overflow: hidden; height: 100%; } + .wait { - color: black !important; - background-color: rgba(255, 255, 255, 0.67) !important; + color: var(--cpd-color-text-primary) !important; + background-color: var(--cpd-color-bg-canvas-default) !important; + /* relative colors are only supported on chromium based browsers */ + background-color: rgb( + from var(--cpd-color-bg-canvas-default) r g b / 0.5 + ) !important; +} +.wait > svg { + color: var(--cpd-color-theme-primary) !important; } @media (max-width: 500px) { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 370c3c59..14ff4bc1 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -43,10 +43,10 @@ interface Props { matrixInfo: MatrixInfo; muteStates: MuteStates; onEnter: () => void; - enterLabel?: JSX.Element; + enterLabel?: JSX.Element | string; confineToRoom: boolean; hideHeader: boolean; - participantCount: number; + participantCount: number | null; onShareClick: (() => void) | null; waitingForInvite?: boolean; } @@ -121,11 +121,9 @@ export const LobbyView: FC = ({