diff --git a/src/App.jsx b/src/App.jsx index 882b372..65dc0c0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -25,14 +25,17 @@ import { Link, Redirect, } from "react-router-dom"; -import { ConferenceCall } from "./ConferenceCall"; +import { + useConferenceCallManager, + useVideoRoom, +} from "./ConferenceCallManagerHooks"; export default function App() { const { protocol, host } = window.location; // Assume homeserver is hosted on same domain (proxied in development by vite) const homeserverUrl = `${protocol}//${host}`; - const { loading, authenticated, error, client, login, register } = - useClient(homeserverUrl); + const { loading, authenticated, error, manager, login, register } = + useConferenceCallManager(homeserverUrl); return ( @@ -44,7 +47,7 @@ export default function App() { {authenticated ? ( - + ) : ( <>
@@ -56,7 +59,11 @@ export default function App() { )} - {!authenticated ? : } + {!authenticated ? ( + + ) : ( + + )} )} @@ -65,166 +72,6 @@ export default function App() { ); } -function waitForSync(client) { - return new Promise((resolve, reject) => { - const onSync = (state) => { - if (state === "PREPARED") { - resolve(); - client.removeListener("sync", onSync); - } - }; - client.on("sync", onSync); - }); -} - -function useClient(homeserverUrl) { - const [{ loading, authenticated, client, error }, setState] = useState({ - loading: true, - authenticated: false, - client: undefined, - error: undefined, - }); - - useEffect(() => { - async function restoreClient() { - try { - const authStore = localStorage.getItem("matrix-auth-store"); - - if (authStore) { - const { user_id, device_id, access_token } = JSON.parse(authStore); - - const client = matrixcs.createClient({ - baseUrl: homeserverUrl, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); - - await client.startClient(); - - await waitForSync(client); - - setState({ - client, - loading: false, - authenticated: true, - error: undefined, - }); - } else { - setState({ - client: undefined, - loading: false, - authenticated: false, - error: undefined, - }); - } - } catch (err) { - console.error(err); - localStorage.removeItem("matrix-auth-store"); - setState({ - client: undefined, - loading: false, - authenticated: false, - error: err, - }); - } - } - - restoreClient(); - }, []); - - const login = useCallback(async (username, password) => { - try { - setState((prevState) => ({ - ...prevState, - authenticated: false, - error: undefined, - })); - - const registrationClient = matrixcs.createClient(homeserverUrl); - - const { user_id, device_id, access_token } = - await registrationClient.loginWithPassword(username, password); - - const client = matrixcs.createClient({ - baseUrl: homeserverUrl, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); - - await client.startClient(); - - localStorage.setItem( - "matrix-auth-store", - JSON.stringify({ user_id, device_id, access_token }) - ); - setState({ - client, - loading: false, - authenticated: true, - error: undefined, - }); - } catch (err) { - console.error(err); - localStorage.removeItem("matrix-auth-store"); - setState({ - client: undefined, - loading: false, - authenticated: false, - error: err, - }); - } - }, []); - - const register = useCallback(async (username, password) => { - try { - setState((prevState) => ({ - ...prevState, - authenticated: false, - error: undefined, - })); - - const registrationClient = matrixcs.createClient(homeserverUrl); - - const { user_id, device_id, access_token } = - await registrationClient.register(username, password, null, { - type: "m.login.dummy", - }); - - const client = matrixcs.createClient({ - baseUrl: homeserverUrl, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); - - await client.startClient(); - - localStorage.setItem( - "matrix-auth-store", - JSON.stringify({ user_id, device_id, access_token }) - ); - setState({ - client, - loading: false, - authenticated: true, - error: undefined, - }); - } catch (err) { - localStorage.removeItem("matrix-auth-store"); - setState({ - client: undefined, - loading: false, - authenticated: false, - error: err, - }); - } - }, []); - - return { loading, authenticated, client, error, login, register }; -} - function Register({ onRegister }) { const usernameRef = useRef(); const passwordRef = useRef(); @@ -267,7 +114,7 @@ function Login({ onLogin }) { ); } -function JoinOrCreateRoom({ client }) { +function JoinOrCreateRoom({ manager }) { const history = useHistory(); const roomNameRef = useRef(); const roomIdRef = useRef(); @@ -277,15 +124,15 @@ function JoinOrCreateRoom({ client }) { useEffect(() => { function updateRooms() { - setRooms(client.getRooms()); + setRooms(manager.client.getRooms()); } updateRooms(); - client.on("Room", updateRooms); + manager.client.on("Room", updateRooms); return () => { - client.removeListener("Room", updateRooms); + manager.client.removeListener("Room", updateRooms); }; }, []); @@ -294,7 +141,7 @@ function JoinOrCreateRoom({ client }) { e.preventDefault(); setCreateRoomError(undefined); - client + manager.client .createRoom({ visibility: "private", preset: "public_chat", @@ -305,7 +152,7 @@ function JoinOrCreateRoom({ client }) { }) .catch(setCreateRoomError); }, - [client] + [manager] ); const onJoinRoom = useCallback( @@ -313,14 +160,14 @@ function JoinOrCreateRoom({ client }) { e.preventDefault(); setJoinRoomError(undefined); - client + manager.client .joinRoom(roomIdRef.current.value) .then(({ roomId }) => { history.push(`/room/${roomId}`); }) .catch(setJoinRoomError); }, - [client] + [manager] ); return ( @@ -366,117 +213,10 @@ function JoinOrCreateRoom({ client }) { ); } -function useVideoRoom(client, roomId, timeout = 5000) { - const [ - { loading, joined, room, conferenceCall, participants, error }, - setState, - ] = useState({ - loading: true, - joined: false, - room: undefined, - participants: [], - error: undefined, - conferenceCall: null, - }); - - useEffect(() => { - const conferenceCall = new ConferenceCall(client, roomId); - - setState((prevState) => ({ - ...prevState, - conferenceCall, - loading: true, - room: undefined, - error: undefined, - })); - - client.joinRoom(roomId).catch((err) => { - setState((prevState) => ({ ...prevState, loading: false, error: err })); - }); - - let initialRoom = client.getRoom(roomId); - - if (initialRoom) { - setState((prevState) => ({ - ...prevState, - loading: false, - room: initialRoom, - error: undefined, - })); - return; - } - - let timeoutId; - - function roomCallback(room) { - if (room && room.roomId === roomId) { - clearTimeout(timeoutId); - client.removeListener("Room", roomCallback); - setState((prevState) => ({ - ...prevState, - loading: false, - room, - error: undefined, - })); - } - } - - client.on("Room", roomCallback); - - timeoutId = setTimeout(() => { - setState((prevState) => ({ - ...prevState, - loading: false, - room: undefined, - error: new Error("Room could not be found."), - })); - client.removeListener("Room", roomCallback); - }, timeout); - - return () => { - client.removeListener("Room", roomCallback); - clearTimeout(timeoutId); - }; - }, [roomId]); - - const joinCall = useCallback(() => { - const onParticipantsChanged = () => { - setState((prevState) => ({ - ...prevState, - participants: conferenceCall.participants, - })); - }; - - conferenceCall.on("participants_changed", onParticipantsChanged); - - conferenceCall.join(); - - setState((prevState) => ({ - ...prevState, - joined: true, - })); - - return () => { - conferenceCall.removeListener( - "participants_changed", - onParticipantsChanged - ); - - setState((prevState) => ({ - ...prevState, - joined: false, - participants: [], - })); - }; - }, [client, conferenceCall, roomId]); - - return { loading, joined, room, participants, error, joinCall }; -} - -function Room({ client }) { +function Room({ manager }) { const { roomId } = useParams(); const { loading, joined, room, participants, error, joinCall } = useVideoRoom( - client, + manager, roomId ); @@ -486,7 +226,7 @@ function Room({ client }) {

{room.name}

-
{client.getUserId()}
+
{manager.client.getUserId()}
)} diff --git a/src/ConferenceCall.js b/src/ConferenceCallManager.js similarity index 70% rename from src/ConferenceCall.js rename to src/ConferenceCallManager.js index 447e41e..c047731 100644 --- a/src/ConferenceCall.js +++ b/src/ConferenceCallManager.js @@ -4,11 +4,118 @@ const CONF_ROOM = "me.robertlong.conf"; const CONF_PARTICIPANT = "me.robertlong.conf.participant"; const PARTICIPANT_TIMEOUT = 1000 * 5; -export class ConferenceCall extends EventEmitter { - constructor(client, roomId) { +function waitForSync(client) { + return new Promise((resolve, reject) => { + const onSync = (state) => { + if (state === "PREPARED") { + resolve(); + client.removeListener("sync", onSync); + } + }; + client.on("sync", onSync); + }); +} + +export class ConferenceCallManager extends EventEmitter { + static async restore(homeserverUrl) { + try { + const authStore = localStorage.getItem("matrix-auth-store"); + + if (authStore) { + const { user_id, device_id, access_token } = JSON.parse(authStore); + + const client = matrixcs.createClient({ + baseUrl: homeserverUrl, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); + + const manager = new ConferenceCallManager(client); + + await client.startClient(); + + await waitForSync(client); + + return manager; + } + } catch (err) { + localStorage.removeItem("matrix-auth-store"); + throw err; + } + } + + static async login(homeserverUrl, username, password) { + try { + const registrationClient = matrixcs.createClient(homeserverUrl); + + const { user_id, device_id, access_token } = + await registrationClient.loginWithPassword(username, password); + + const client = matrixcs.createClient({ + baseUrl: homeserverUrl, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); + + localStorage.setItem( + "matrix-auth-store", + JSON.stringify({ user_id, device_id, access_token }) + ); + + const manager = new ConferenceCallManager(client); + + await client.startClient(); + + await waitForSync(client); + + return manager; + } catch (err) { + localStorage.removeItem("matrix-auth-store"); + + throw err; + } + } + + static async register(homeserverUrl, username, password) { + try { + const registrationClient = matrixcs.createClient(homeserverUrl); + + const { user_id, device_id, access_token } = + await registrationClient.register(username, password, null, { + type: "m.login.dummy", + }); + + const client = matrixcs.createClient({ + baseUrl: homeserverUrl, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); + + localStorage.setItem( + "matrix-auth-store", + JSON.stringify({ user_id, device_id, access_token }) + ); + + const manager = new ConferenceCallManager(client); + + await client.startClient(); + + await waitForSync(client); + + return manager; + } catch (err) { + localStorage.removeItem("matrix-auth-store"); + + throw err; + } + } + + constructor(client) { super(); this.client = client; - this.roomId = roomId; this.joined = false; this.room = null; this.localParticipant = { @@ -25,7 +132,7 @@ export class ConferenceCall extends EventEmitter { this.client.on("Call.incoming", this._onIncomingCall); } - join() { + join(roomId) { console.debug( "join", `Local user ${this.client.getUserId()} joining room ${this.roomId}` @@ -33,6 +140,7 @@ export class ConferenceCall extends EventEmitter { this.joined = true; + this.roomId = roomId; this.room = this.client.getRoom(this.roomId); const activeConf = this.room.currentState diff --git a/src/ConferenceCallManagerHooks.js b/src/ConferenceCallManagerHooks.js new file mode 100644 index 0000000..bf13d0b --- /dev/null +++ b/src/ConferenceCallManagerHooks.js @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useState } from "react"; +import { ConferenceCallManager } from "./ConferenceCallManager"; + +export function useConferenceCallManager(homeserverUrl) { + const [{ loading, authenticated, manager, error }, setState] = useState({ + loading: true, + authenticated: false, + manager: undefined, + error: undefined, + }); + + useEffect(() => { + ConferenceCallManager.restore(homeserverUrl) + .then((manager) => { + console.log(manager); + setState({ + manager, + loading: false, + authenticated: !!manager, + error: undefined, + }); + }) + .catch((err) => { + console.error(err); + + setState({ + manager: undefined, + loading: false, + authenticated: false, + error: err, + }); + }); + }, []); + + const login = useCallback(async (username, password) => { + setState((prevState) => ({ + ...prevState, + authenticated: false, + error: undefined, + })); + + ConferenceCallManager.login(homeserverUrl, username, password) + .then((manager) => { + setState({ + manager, + loading: false, + authenticated: true, + error: undefined, + }); + }) + .catch((err) => { + console.error(err); + + setState({ + manager: undefined, + loading: false, + authenticated: false, + error: err, + }); + }); + }, []); + + const register = useCallback(async (username, password) => { + setState((prevState) => ({ + ...prevState, + authenticated: false, + error: undefined, + })); + + ConferenceCallManager.register(homeserverUrl, username, password) + .then((manager) => { + setState({ + manager, + loading: false, + authenticated: true, + error: undefined, + }); + }) + .catch((err) => { + console.error(err); + + setState({ + manager: undefined, + loading: false, + authenticated: false, + error: err, + }); + }); + }, []); + + return { loading, authenticated, manager, error, login, register }; +} + +export function useVideoRoom(manager, roomId, timeout = 5000) { + const [{ loading, joined, room, participants, error }, setState] = useState({ + loading: true, + joined: false, + room: undefined, + participants: [], + error: undefined, + }); + + useEffect(() => { + setState((prevState) => ({ + ...prevState, + loading: true, + room: undefined, + error: undefined, + })); + + manager.client.joinRoom(roomId).catch((err) => { + setState((prevState) => ({ ...prevState, loading: false, error: err })); + }); + + let initialRoom = manager.client.getRoom(roomId); + + if (initialRoom) { + setState((prevState) => ({ + ...prevState, + loading: false, + room: initialRoom, + error: undefined, + })); + return; + } + + let timeoutId; + + function roomCallback(room) { + if (room && room.roomId === roomId) { + clearTimeout(timeoutId); + manager.client.removeListener("Room", roomCallback); + setState((prevState) => ({ + ...prevState, + loading: false, + room, + error: undefined, + })); + } + } + + manager.client.on("Room", roomCallback); + + timeoutId = setTimeout(() => { + setState((prevState) => ({ + ...prevState, + loading: false, + room: undefined, + error: new Error("Room could not be found."), + })); + manager.client.removeListener("Room", roomCallback); + }, timeout); + + return () => { + manager.client.removeListener("Room", roomCallback); + clearTimeout(timeoutId); + }; + }, [roomId]); + + const joinCall = useCallback(() => { + const onParticipantsChanged = () => { + setState((prevState) => ({ + ...prevState, + participants: manager.participants, + })); + }; + + manager.on("participants_changed", onParticipantsChanged); + + manager.join(roomId); + + setState((prevState) => ({ + ...prevState, + joined: true, + })); + + return () => { + manager.removeListener("participants_changed", onParticipantsChanged); + + setState((prevState) => ({ + ...prevState, + joined: false, + participants: [], + })); + }; + }, [manager, roomId]); + + return { loading, joined, room, participants, error, joinCall }; +}