diff --git a/src/App.jsx b/src/App.jsx
index 4808a67d..5dea73e2 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -23,7 +23,7 @@ import { LoginPage } from "./auth/LoginPage";
import { RegisterPage } from "./auth/RegisterPage";
import { RoomPage } from "./room/RoomPage";
import { RoomRedirect } from "./room/RoomRedirect";
-import { ClientProvider } from "./ConferenceCallManagerHooks";
+import { ClientProvider } from "./ClientContext";
import { usePageFocusStyle } from "./usePageFocusStyle";
const SentryRoute = Sentry.withSentryRouting(Route);
diff --git a/src/ClientContext.jsx b/src/ClientContext.jsx
new file mode 100644
index 00000000..7023aa4a
--- /dev/null
+++ b/src/ClientContext.jsx
@@ -0,0 +1,204 @@
+/*
+Copyright 2021 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 React, {
+ useCallback,
+ useEffect,
+ useState,
+ createContext,
+ useMemo,
+ useContext,
+} from "react";
+import { useHistory } from "react-router-dom";
+import { initClient } from "./matrix-utils";
+
+const ClientContext = createContext();
+
+export function ClientProvider({ children }) {
+ const history = useHistory();
+ const [
+ { loading, isAuthenticated, isPasswordlessUser, client, userName },
+ setState,
+ ] = useState({
+ loading: true,
+ isAuthenticated: false,
+ isPasswordlessUser: false,
+ client: undefined,
+ userName: null,
+ });
+
+ useEffect(() => {
+ async function restore() {
+ try {
+ const authStore = localStorage.getItem("matrix-auth-store");
+
+ if (authStore) {
+ const {
+ user_id,
+ device_id,
+ access_token,
+ passwordlessUser,
+ tempPassword,
+ } = JSON.parse(authStore);
+
+ const client = await initClient({
+ baseUrl: defaultHomeserver,
+ accessToken: access_token,
+ userId: user_id,
+ deviceId: device_id,
+ });
+
+ localStorage.setItem(
+ "matrix-auth-store",
+ JSON.stringify({
+ user_id,
+ device_id,
+ access_token,
+
+ passwordlessUser,
+ tempPassword,
+ })
+ );
+
+ return { client, passwordlessUser };
+ }
+
+ return { client: undefined };
+ } catch (err) {
+ localStorage.removeItem("matrix-auth-store");
+ throw err;
+ }
+ }
+
+ restore()
+ .then(({ client, passwordlessUser }) => {
+ setState({
+ client,
+ loading: false,
+ isAuthenticated: !!client,
+ isPasswordlessUser: !!passwordlessUser,
+ userName: client?.getUserIdLocalpart(),
+ });
+ })
+ .catch(() => {
+ setState({
+ client: undefined,
+ loading: false,
+ isAuthenticated: false,
+ isPasswordlessUser: false,
+ userName: null,
+ });
+ });
+ }, []);
+
+ const changePassword = useCallback(
+ async (password) => {
+ const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
+ localStorage.getItem("matrix-auth-store")
+ );
+
+ await client.setPassword(
+ {
+ type: "m.login.password",
+ identifier: {
+ type: "m.id.user",
+ user: existingSession.user_id,
+ },
+ user: existingSession.user_id,
+ password: tempPassword,
+ },
+ password
+ );
+
+ localStorage.setItem(
+ "matrix-auth-store",
+ JSON.stringify({
+ ...existingSession,
+ passwordlessUser: false,
+ })
+ );
+
+ setState({
+ client,
+ loading: false,
+ isAuthenticated: true,
+ isPasswordlessUser: false,
+ userName: client.getUserIdLocalpart(),
+ });
+ },
+ [client]
+ );
+
+ const setClient = useCallback((client, session) => {
+ if (client) {
+ localStorage.setItem("matrix-auth-store", JSON.stringify(session));
+
+ setState({
+ client,
+ loading: false,
+ isAuthenticated: true,
+ isPasswordlessUser: !!session.passwordlessUser,
+ userName: client.getUserIdLocalpart(),
+ });
+ } else {
+ localStorage.removeItem("matrix-auth-store");
+
+ setState({
+ client: undefined,
+ loading: false,
+ isAuthenticated: false,
+ isPasswordlessUser: false,
+ userName: null,
+ });
+ }
+ }, []);
+
+ const logout = useCallback(() => {
+ localStorage.removeItem("matrix-auth-store");
+ window.location = "/";
+ }, [history]);
+
+ const context = useMemo(
+ () => ({
+ loading,
+ isAuthenticated,
+ isPasswordlessUser,
+ client,
+ changePassword,
+ logout,
+ userName,
+ setClient,
+ }),
+ [
+ loading,
+ isAuthenticated,
+ isPasswordlessUser,
+ client,
+ changePassword,
+ logout,
+ userName,
+ setClient,
+ ]
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useClient() {
+ return useContext(ClientContext);
+}
diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx
deleted file mode 100644
index 3af8725f..00000000
--- a/src/ConferenceCallManagerHooks.jsx
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
-Copyright 2021 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 React, {
- useCallback,
- useEffect,
- useState,
- createContext,
- useMemo,
- useContext,
-} from "react";
-import matrix from "matrix-js-sdk/src/browser-index";
-import {
- GroupCallIntent,
- GroupCallType,
-} from "matrix-js-sdk/src/browser-index";
-import { useHistory } from "react-router-dom";
-
-export const defaultHomeserver =
- import.meta.env.VITE_DEFAULT_HOMESERVER ||
- `${window.location.protocol}//${window.location.host}`;
-
-export const defaultHomeserverHost = new URL(defaultHomeserver).host;
-
-const ClientContext = createContext();
-
-function waitForSync(client) {
- return new Promise((resolve, reject) => {
- const onSync = (state, _old, data) => {
- if (state === "PREPARED") {
- resolve();
- client.removeListener("sync", onSync);
- } else if (state === "ERROR") {
- reject(data?.error);
- client.removeListener("sync", onSync);
- }
- };
- client.on("sync", onSync);
- });
-}
-
-export async function initClient(clientOptions) {
- const client = matrix.createClient(clientOptions);
-
- await client.startClient({
- // dirty hack to reduce chance of gappy syncs
- // should be fixed by spotting gaps and backpaginating
- initialSyncLimit: 50,
- });
-
- await waitForSync(client);
-
- return client;
-}
-
-export function ClientProvider({ children }) {
- const history = useHistory();
- const [
- { loading, isAuthenticated, isPasswordlessUser, client, userName },
- setState,
- ] = useState({
- loading: true,
- isAuthenticated: false,
- isPasswordlessUser: false,
- client: undefined,
- userName: null,
- });
-
- useEffect(() => {
- async function restore() {
- try {
- const authStore = localStorage.getItem("matrix-auth-store");
-
- if (authStore) {
- const {
- user_id,
- device_id,
- access_token,
- passwordlessUser,
- tempPassword,
- } = JSON.parse(authStore);
-
- const client = await initClient({
- baseUrl: defaultHomeserver,
- accessToken: access_token,
- userId: user_id,
- deviceId: device_id,
- });
-
- localStorage.setItem(
- "matrix-auth-store",
- JSON.stringify({
- user_id,
- device_id,
- access_token,
-
- passwordlessUser,
- tempPassword,
- })
- );
-
- return { client, passwordlessUser };
- }
-
- return { client: undefined };
- } catch (err) {
- localStorage.removeItem("matrix-auth-store");
- throw err;
- }
- }
-
- restore()
- .then(({ client, passwordlessUser }) => {
- setState({
- client,
- loading: false,
- isAuthenticated: !!client,
- isPasswordlessUser: !!passwordlessUser,
- userName: client?.getUserIdLocalpart(),
- });
- })
- .catch(() => {
- setState({
- client: undefined,
- loading: false,
- isAuthenticated: false,
- isPasswordlessUser: false,
- userName: null,
- });
- });
- }, []);
-
- const changePassword = useCallback(
- async (password) => {
- const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
- localStorage.getItem("matrix-auth-store")
- );
-
- await client.setPassword(
- {
- type: "m.login.password",
- identifier: {
- type: "m.id.user",
- user: existingSession.user_id,
- },
- user: existingSession.user_id,
- password: tempPassword,
- },
- password
- );
-
- localStorage.setItem(
- "matrix-auth-store",
- JSON.stringify({
- ...existingSession,
- passwordlessUser: false,
- })
- );
-
- setState({
- client,
- loading: false,
- isAuthenticated: true,
- isPasswordlessUser: false,
- userName: client.getUserIdLocalpart(),
- });
- },
- [client]
- );
-
- const setClient = useCallback((client, session) => {
- if (client) {
- localStorage.setItem("matrix-auth-store", JSON.stringify(session));
-
- setState({
- client,
- loading: false,
- isAuthenticated: true,
- isPasswordlessUser: !!session.passwordlessUser,
- userName: client.getUserIdLocalpart(),
- });
- } else {
- localStorage.removeItem("matrix-auth-store");
-
- setState({
- client: undefined,
- loading: false,
- isAuthenticated: false,
- isPasswordlessUser: false,
- userName: null,
- });
- }
- }, []);
-
- const logout = useCallback(() => {
- localStorage.removeItem("matrix-auth-store");
- window.location = "/";
- }, [history]);
-
- const context = useMemo(
- () => ({
- loading,
- isAuthenticated,
- isPasswordlessUser,
- client,
- changePassword,
- logout,
- userName,
- setClient,
- }),
- [
- loading,
- isAuthenticated,
- isPasswordlessUser,
- client,
- changePassword,
- logout,
- userName,
- setClient,
- ]
- );
-
- return (
- {children}
- );
-}
-
-export function useClient() {
- return useContext(ClientContext);
-}
-
-export function roomAliasFromRoomName(roomName) {
- return roomName
- .trim()
- .replace(/\s/g, "-")
- .replace(/[^\w-]/g, "")
- .toLowerCase();
-}
-
-export async function createRoom(client, name) {
- const { room_id, room_alias } = await client.createRoom({
- visibility: "private",
- preset: "public_chat",
- name,
- room_alias_name: roomAliasFromRoomName(name),
- power_level_content_override: {
- invite: 100,
- kick: 100,
- ban: 100,
- redact: 50,
- state_default: 0,
- events_default: 0,
- users_default: 0,
- events: {
- "m.room.power_levels": 100,
- "m.room.history_visibility": 100,
- "m.room.tombstone": 100,
- "m.room.encryption": 100,
- "m.room.name": 50,
- "m.room.message": 0,
- "m.room.encrypted": 50,
- "m.sticker": 50,
- "org.matrix.msc3401.call.member": 0,
- },
- users: {
- [client.getUserId()]: 100,
- },
- },
- });
-
- await client.createGroupCall(
- room_id,
- GroupCallType.Video,
- GroupCallIntent.Prompt
- );
-
- return room_alias || room_id;
-}
-
-const tsCache = {};
-
-function getLastTs(client, r) {
- if (tsCache[r.roomId]) {
- return tsCache[r.roomId];
- }
-
- if (!r || !r.timeline) {
- const ts = Number.MAX_SAFE_INTEGER;
- tsCache[r.roomId] = ts;
- return ts;
- }
-
- const myUserId = client.getUserId();
-
- if (r.getMyMembership() !== "join") {
- const membershipEvent = r.currentState.getStateEvents(
- "m.room.member",
- myUserId
- );
-
- if (membershipEvent && !Array.isArray(membershipEvent)) {
- const ts = membershipEvent.getTs();
- tsCache[r.roomId] = ts;
- return ts;
- }
- }
-
- for (let i = r.timeline.length - 1; i >= 0; --i) {
- const ev = r.timeline[i];
- const ts = ev.getTs();
-
- if (ts) {
- tsCache[r.roomId] = ts;
- return ts;
- }
- }
-
- const ts = Number.MAX_SAFE_INTEGER;
- tsCache[r.roomId] = ts;
- return ts;
-}
-
-function sortRooms(client, rooms) {
- return rooms.sort((a, b) => {
- return getLastTs(client, b) - getLastTs(client, a);
- });
-}
-
-export function useGroupCallRooms(client) {
- const [rooms, setRooms] = useState([]);
-
- useEffect(() => {
- function updateRooms() {
- const groupCalls = client.groupCallEventHandler.groupCalls.values();
- const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
- const sortedRooms = sortRooms(client, rooms);
- const items = sortedRooms.map((room) => {
- const groupCall = client.getGroupCallForRoom(room.roomId);
-
- return {
- roomId: room.getCanonicalAlias() || room.roomId,
- roomName: room.name,
- avatarUrl: null,
- room,
- groupCall,
- participants: [...groupCall.participants],
- };
- });
- setRooms(items);
- }
-
- updateRooms();
-
- client.on("GroupCall.incoming", updateRooms);
- client.on("GroupCall.participants", updateRooms);
-
- return () => {
- client.removeListener("GroupCall.incoming", updateRooms);
- client.removeListener("GroupCall.participants", updateRooms);
- };
- }, []);
-
- return rooms;
-}
-
-export function getRoomUrl(roomId) {
- if (roomId.startsWith("#")) {
- const [localPart, host] = roomId.replace("#", "").split(":");
-
- if (host !== defaultHomeserverHost) {
- return `${window.location.host}/room/${roomId}`;
- } else {
- return `${window.location.host}/${localPart}`;
- }
- } else {
- return `${window.location.host}/room/${roomId}`;
- }
-}
-
-export function getAvatarUrl(client, mxcUrl, avatarSize = 96) {
- const width = Math.floor(avatarSize * window.devicePixelRatio);
- const height = Math.floor(avatarSize * window.devicePixelRatio);
- return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
-}
-
-export function useProfile(client) {
- const [{ loading, displayName, avatarUrl, error, success }, setState] =
- useState(() => {
- const user = client?.getUser(client.getUserId());
-
- return {
- success: false,
- loading: false,
- displayName: user?.displayName,
- avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
- error: null,
- };
- });
-
- useEffect(() => {
- const onChangeUser = (_event, { displayName, avatarUrl }) => {
- setState({
- success: false,
- loading: false,
- displayName,
- avatarUrl: getAvatarUrl(client, avatarUrl),
- error: null,
- });
- };
-
- let user;
-
- if (client) {
- const userId = client.getUserId();
- user = client.getUser(userId);
- user.on("User.displayName", onChangeUser);
- user.on("User.avatarUrl", onChangeUser);
- }
-
- return () => {
- if (user) {
- user.removeListener("User.displayName", onChangeUser);
- user.removeListener("User.avatarUrl", onChangeUser);
- }
- };
- }, [client]);
-
- const saveProfile = useCallback(
- async ({ displayName, avatar }) => {
- if (client) {
- setState((prev) => ({
- ...prev,
- loading: true,
- error: null,
- success: false,
- }));
-
- try {
- await client.setDisplayName(displayName);
-
- let mxcAvatarUrl;
-
- if (avatar) {
- mxcAvatarUrl = await client.uploadContent(avatar);
- await client.setAvatarUrl(mxcAvatarUrl);
- }
-
- setState((prev) => ({
- ...prev,
- displayName,
- avatarUrl: mxcAvatarUrl
- ? getAvatarUrl(client, mxcAvatarUrl)
- : prev.avatarUrl,
- loading: false,
- success: true,
- }));
- } catch (error) {
- setState((prev) => ({
- ...prev,
- loading: false,
- error,
- success: false,
- }));
- }
- } else {
- console.error("Client not initialized before calling saveProfile");
- }
- },
- [client]
- );
-
- return { loading, error, displayName, avatarUrl, saveProfile, success };
-}
diff --git a/src/Facepile.jsx b/src/Facepile.jsx
index bc084e4d..53b0b948 100644
--- a/src/Facepile.jsx
+++ b/src/Facepile.jsx
@@ -2,7 +2,7 @@ import React from "react";
import styles from "./Facepile.module.css";
import classNames from "classnames";
import { Avatar } from "./Avatar";
-import { getAvatarUrl } from "./ConferenceCallManagerHooks";
+import { getAvatarUrl } from "./matrix-utils";
export function Facepile({ className, client, participants, ...rest }) {
return (
diff --git a/src/ProfileModal.jsx b/src/ProfileModal.jsx
index 6378f59d..5b7e47f7 100644
--- a/src/ProfileModal.jsx
+++ b/src/ProfileModal.jsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { Button } from "./button";
-import { useProfile } from "./ConferenceCallManagerHooks";
+import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Modal, ModalContent } from "./Modal";
diff --git a/src/UserMenuContainer.jsx b/src/UserMenuContainer.jsx
index d70cb256..4229f69c 100644
--- a/src/UserMenuContainer.jsx
+++ b/src/UserMenuContainer.jsx
@@ -1,6 +1,7 @@
import React, { useCallback } from "react";
import { useHistory, useLocation } from "react-router-dom";
-import { useClient, useProfile } from "./ConferenceCallManagerHooks";
+import { useClient } from "./ClientContext";
+import { useProfile } from "./useProfile";
import { useModalTriggerState } from "./Modal";
import { ProfileModal } from "./ProfileModal";
import { UserMenu } from "./UserMenu";
diff --git a/src/auth/LoginPage.jsx b/src/auth/LoginPage.jsx
index e1df8dc6..5a8a4d26 100644
--- a/src/auth/LoginPage.jsx
+++ b/src/auth/LoginPage.jsx
@@ -19,10 +19,7 @@ import { useHistory, useLocation, Link } from "react-router-dom";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
import { FieldRow, InputField, ErrorMessage } from "../Input";
import { Button } from "../button";
-import {
- defaultHomeserver,
- defaultHomeserverHost,
-} from "../ConferenceCallManagerHooks";
+import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
import styles from "./LoginPage.module.css";
import { useInteractiveLogin } from "./useInteractiveLogin";
diff --git a/src/auth/RegisterPage.jsx b/src/auth/RegisterPage.jsx
index f584bc24..cd05b8b0 100644
--- a/src/auth/RegisterPage.jsx
+++ b/src/auth/RegisterPage.jsx
@@ -18,10 +18,8 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "../Input";
import { Button } from "../button";
-import {
- useClient,
- defaultHomeserverHost,
-} from "../ConferenceCallManagerHooks";
+import { useClient } from "../ClientContext";
+import { defaultHomeserverHost } from "../matrix-utils";
import { useInteractiveRegistration } from "./useInteractiveRegistration";
import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
diff --git a/src/auth/useInteractiveLogin.js b/src/auth/useInteractiveLogin.js
index 64e42298..4b1bc540 100644
--- a/src/auth/useInteractiveLogin.js
+++ b/src/auth/useInteractiveLogin.js
@@ -1,10 +1,7 @@
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useCallback } from "react";
-import {
- useClient,
- initClient,
- defaultHomeserver,
-} from "../ConferenceCallManagerHooks";
+import { useClient } from "../ClientContext";
+import { initClient, defaultHomeserver } from "../matrix-utils";
export function useInteractiveLogin() {
const { setClient } = useClient();
diff --git a/src/auth/useInteractiveRegistration.js b/src/auth/useInteractiveRegistration.js
index 350f2fa1..57e21787 100644
--- a/src/auth/useInteractiveRegistration.js
+++ b/src/auth/useInteractiveRegistration.js
@@ -1,10 +1,7 @@
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useEffect, useCallback, useRef } from "react";
-import {
- useClient,
- initClient,
- defaultHomeserver,
-} from "../ConferenceCallManagerHooks";
+import { useClient } from "../ClientContext";
+import { initClient, defaultHomeserver } from "../matrix-utils";
export function useInteractiveRegistration() {
const { setClient } = useClient();
diff --git a/src/home/CallList.jsx b/src/home/CallList.jsx
index de8e4c03..35b4da61 100644
--- a/src/home/CallList.jsx
+++ b/src/home/CallList.jsx
@@ -5,7 +5,7 @@ import { Facepile } from "../Facepile";
import { Avatar } from "../Avatar";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import styles from "./CallList.module.css";
-import { getRoomUrl } from "../ConferenceCallManagerHooks";
+import { getRoomUrl } from "../matrix-utils";
import { Body, Caption } from "../typography/Typography";
export function CallList({ rooms, client }) {
diff --git a/src/home/HomePage.jsx b/src/home/HomePage.jsx
index 0c5a8113..daed4709 100644
--- a/src/home/HomePage.jsx
+++ b/src/home/HomePage.jsx
@@ -15,7 +15,7 @@ limitations under the License.
*/
import React from "react";
-import { useClient } from "../ConferenceCallManagerHooks";
+import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView";
diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.jsx
index aad1a82e..1c73177b 100644
--- a/src/home/RegisteredView.jsx
+++ b/src/home/RegisteredView.jsx
@@ -1,9 +1,6 @@
import React, { useState, useCallback } from "react";
-import {
- createRoom,
- useGroupCallRooms,
- roomAliasFromRoomName,
-} from "../ConferenceCallManagerHooks";
+import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
+import { useGroupCallRooms } from "./useGroupCallRooms";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import commonStyles from "./common.module.css";
import styles from "./RegisteredView.module.css";
diff --git a/src/home/UnauthenticatedView.jsx b/src/home/UnauthenticatedView.jsx
index 608d37de..c4eeca79 100644
--- a/src/home/UnauthenticatedView.jsx
+++ b/src/home/UnauthenticatedView.jsx
@@ -5,10 +5,7 @@ import { useHistory } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "../Input";
import { Button } from "../button";
import { randomString } from "matrix-js-sdk/src/randomstring";
-import {
- createRoom,
- roomAliasFromRoomName,
-} from "../ConferenceCallManagerHooks";
+import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
diff --git a/src/home/useGroupCallRooms.js b/src/home/useGroupCallRooms.js
new file mode 100644
index 00000000..ea075f85
--- /dev/null
+++ b/src/home/useGroupCallRooms.js
@@ -0,0 +1,87 @@
+import { useState, useEffect } from "react";
+
+const tsCache = {};
+
+function getLastTs(client, r) {
+ if (tsCache[r.roomId]) {
+ return tsCache[r.roomId];
+ }
+
+ if (!r || !r.timeline) {
+ const ts = Number.MAX_SAFE_INTEGER;
+ tsCache[r.roomId] = ts;
+ return ts;
+ }
+
+ const myUserId = client.getUserId();
+
+ if (r.getMyMembership() !== "join") {
+ const membershipEvent = r.currentState.getStateEvents(
+ "m.room.member",
+ myUserId
+ );
+
+ if (membershipEvent && !Array.isArray(membershipEvent)) {
+ const ts = membershipEvent.getTs();
+ tsCache[r.roomId] = ts;
+ return ts;
+ }
+ }
+
+ for (let i = r.timeline.length - 1; i >= 0; --i) {
+ const ev = r.timeline[i];
+ const ts = ev.getTs();
+
+ if (ts) {
+ tsCache[r.roomId] = ts;
+ return ts;
+ }
+ }
+
+ const ts = Number.MAX_SAFE_INTEGER;
+ tsCache[r.roomId] = ts;
+ return ts;
+}
+
+function sortRooms(client, rooms) {
+ return rooms.sort((a, b) => {
+ return getLastTs(client, b) - getLastTs(client, a);
+ });
+}
+
+export function useGroupCallRooms(client) {
+ const [rooms, setRooms] = useState([]);
+
+ useEffect(() => {
+ function updateRooms() {
+ const groupCalls = client.groupCallEventHandler.groupCalls.values();
+ const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
+ const sortedRooms = sortRooms(client, rooms);
+ const items = sortedRooms.map((room) => {
+ const groupCall = client.getGroupCallForRoom(room.roomId);
+
+ return {
+ roomId: room.getCanonicalAlias() || room.roomId,
+ roomName: room.name,
+ avatarUrl: null,
+ room,
+ groupCall,
+ participants: [...groupCall.participants],
+ };
+ });
+ setRooms(items);
+ }
+
+ updateRooms();
+
+ client.on("GroupCall.incoming", updateRooms);
+ client.on("GroupCall.participants", updateRooms);
+
+ return () => {
+ client.removeListener("GroupCall.incoming", updateRooms);
+ client.removeListener("GroupCall.participants", updateRooms);
+ };
+ }, []);
+
+ return rooms;
+}
diff --git a/src/matrix-utils.js b/src/matrix-utils.js
new file mode 100644
index 00000000..f2f4ec0e
--- /dev/null
+++ b/src/matrix-utils.js
@@ -0,0 +1,108 @@
+import matrix from "matrix-js-sdk/src/browser-index";
+import {
+ GroupCallIntent,
+ GroupCallType,
+} from "matrix-js-sdk/src/browser-index";
+
+export const defaultHomeserver =
+ import.meta.env.VITE_DEFAULT_HOMESERVER ||
+ `${window.location.protocol}//${window.location.host}`;
+
+export const defaultHomeserverHost = new URL(defaultHomeserver).host;
+
+function waitForSync(client) {
+ return new Promise((resolve, reject) => {
+ const onSync = (state, _old, data) => {
+ if (state === "PREPARED") {
+ resolve();
+ client.removeListener("sync", onSync);
+ } else if (state === "ERROR") {
+ reject(data?.error);
+ client.removeListener("sync", onSync);
+ }
+ };
+ client.on("sync", onSync);
+ });
+}
+
+export async function initClient(clientOptions) {
+ const client = matrix.createClient(clientOptions);
+
+ await client.startClient({
+ // dirty hack to reduce chance of gappy syncs
+ // should be fixed by spotting gaps and backpaginating
+ initialSyncLimit: 50,
+ });
+
+ await waitForSync(client);
+
+ return client;
+}
+
+export function roomAliasFromRoomName(roomName) {
+ return roomName
+ .trim()
+ .replace(/\s/g, "-")
+ .replace(/[^\w-]/g, "")
+ .toLowerCase();
+}
+
+export async function createRoom(client, name) {
+ const { room_id, room_alias } = await client.createRoom({
+ visibility: "private",
+ preset: "public_chat",
+ name,
+ room_alias_name: roomAliasFromRoomName(name),
+ power_level_content_override: {
+ invite: 100,
+ kick: 100,
+ ban: 100,
+ redact: 50,
+ state_default: 0,
+ events_default: 0,
+ users_default: 0,
+ events: {
+ "m.room.power_levels": 100,
+ "m.room.history_visibility": 100,
+ "m.room.tombstone": 100,
+ "m.room.encryption": 100,
+ "m.room.name": 50,
+ "m.room.message": 0,
+ "m.room.encrypted": 50,
+ "m.sticker": 50,
+ "org.matrix.msc3401.call.member": 0,
+ },
+ users: {
+ [client.getUserId()]: 100,
+ },
+ },
+ });
+
+ await client.createGroupCall(
+ room_id,
+ GroupCallType.Video,
+ GroupCallIntent.Prompt
+ );
+
+ return room_alias || room_id;
+}
+
+export function getRoomUrl(roomId) {
+ if (roomId.startsWith("#")) {
+ const [localPart, host] = roomId.replace("#", "").split(":");
+
+ if (host !== defaultHomeserverHost) {
+ return `${window.location.host}/room/${roomId}`;
+ } else {
+ return `${window.location.host}/${localPart}`;
+ }
+ } else {
+ return `${window.location.host}/room/${roomId}`;
+ }
+}
+
+export function getAvatarUrl(client, mxcUrl, avatarSize = 96) {
+ const width = Math.floor(avatarSize * window.devicePixelRatio);
+ const height = Math.floor(avatarSize * window.devicePixelRatio);
+ return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
+}
diff --git a/src/room/CallEndedView.jsx b/src/room/CallEndedView.jsx
index 77d085d8..54f7c916 100644
--- a/src/room/CallEndedView.jsx
+++ b/src/room/CallEndedView.jsx
@@ -1,7 +1,7 @@
import React from "react";
import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";
-import { useProfile } from "../ConferenceCallManagerHooks";
+import { useProfile } from "../useProfile";
import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx
index e3b7db13..54246e81 100644
--- a/src/room/InCallView.jsx
+++ b/src/room/InCallView.jsx
@@ -12,7 +12,7 @@ import VideoGrid, {
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
-import { getAvatarUrl } from "../ConferenceCallManagerHooks";
+import { getAvatarUrl } from "../matrix-utils";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
diff --git a/src/room/InviteModal.jsx b/src/room/InviteModal.jsx
index a1c73a6f..1036f517 100644
--- a/src/room/InviteModal.jsx
+++ b/src/room/InviteModal.jsx
@@ -1,7 +1,7 @@
import React from "react";
import { Modal, ModalContent } from "../Modal";
import { CopyButton } from "../button";
-import { getRoomUrl } from "../ConferenceCallManagerHooks";
+import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
export function InviteModal({ roomId, ...rest }) {
diff --git a/src/room/LobbyView.jsx b/src/room/LobbyView.jsx
index f13c9752..714c2096 100644
--- a/src/room/LobbyView.jsx
+++ b/src/room/LobbyView.jsx
@@ -5,7 +5,7 @@ import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
-import { getRoomUrl } from "../ConferenceCallManagerHooks";
+import { getRoomUrl } from "../matrix-utils";
import { OverflowMenu } from "./OverflowMenu";
import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography";
diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.jsx
index ec76e9a3..30bdc4cd 100644
--- a/src/room/RoomPage.jsx
+++ b/src/room/RoomPage.jsx
@@ -16,7 +16,7 @@ limitations under the License.
import React, { useMemo } from "react";
import { useLocation, useParams } from "react-router-dom";
-import { useClient } from "../ConferenceCallManagerHooks";
+import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
diff --git a/src/room/RoomRedirect.jsx b/src/room/RoomRedirect.jsx
index 1725a74f..3c733fc1 100644
--- a/src/room/RoomRedirect.jsx
+++ b/src/room/RoomRedirect.jsx
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
import { useLocation, useHistory } from "react-router-dom";
-import { defaultHomeserverHost } from "../ConferenceCallManagerHooks";
+import { defaultHomeserverHost } from "../matrix-utils";
import { LoadingView } from "../FullScreenView";
export function RoomRedirect() {
diff --git a/src/useProfile.js b/src/useProfile.js
new file mode 100644
index 00000000..0f164f7c
--- /dev/null
+++ b/src/useProfile.js
@@ -0,0 +1,91 @@
+import { useState, useCallback, useEffect } from "react";
+import { getAvatarUrl } from "./matrix-utils";
+
+export function useProfile(client) {
+ const [{ loading, displayName, avatarUrl, error, success }, setState] =
+ useState(() => {
+ const user = client?.getUser(client.getUserId());
+
+ return {
+ success: false,
+ loading: false,
+ displayName: user?.displayName,
+ avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
+ error: null,
+ };
+ });
+
+ useEffect(() => {
+ const onChangeUser = (_event, { displayName, avatarUrl }) => {
+ setState({
+ success: false,
+ loading: false,
+ displayName,
+ avatarUrl: getAvatarUrl(client, avatarUrl),
+ error: null,
+ });
+ };
+
+ let user;
+
+ if (client) {
+ const userId = client.getUserId();
+ user = client.getUser(userId);
+ user.on("User.displayName", onChangeUser);
+ user.on("User.avatarUrl", onChangeUser);
+ }
+
+ return () => {
+ if (user) {
+ user.removeListener("User.displayName", onChangeUser);
+ user.removeListener("User.avatarUrl", onChangeUser);
+ }
+ };
+ }, [client]);
+
+ const saveProfile = useCallback(
+ async ({ displayName, avatar }) => {
+ if (client) {
+ setState((prev) => ({
+ ...prev,
+ loading: true,
+ error: null,
+ success: false,
+ }));
+
+ try {
+ await client.setDisplayName(displayName);
+
+ let mxcAvatarUrl;
+
+ if (avatar) {
+ mxcAvatarUrl = await client.uploadContent(avatar);
+ await client.setAvatarUrl(mxcAvatarUrl);
+ }
+
+ setState((prev) => ({
+ ...prev,
+ displayName,
+ avatarUrl: mxcAvatarUrl
+ ? getAvatarUrl(client, mxcAvatarUrl)
+ : prev.avatarUrl,
+ loading: false,
+ success: true,
+ }));
+ } catch (error) {
+ setState((prev) => ({
+ ...prev,
+ loading: false,
+ error,
+ success: false,
+ }));
+ }
+ } else {
+ console.error("Client not initialized before calling saveProfile");
+ }
+ },
+ [client]
+ );
+
+ return { loading, error, displayName, avatarUrl, saveProfile, success };
+}