Moving to matrix-js-sdk
This commit is contained in:
		
					parent
					
						
							
								4d7e5583eb
							
						
					
				
			
			
				commit
				
					
						d813509541
					
				
			
		
					 13 changed files with 821 additions and 2519 deletions
				
			
		| 
						 | 
				
			
			@ -8,7 +8,6 @@
 | 
			
		|||
    <script>
 | 
			
		||||
      window.global = window;
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src="/browser-matrix.js"></script>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1110
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1110
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
    "color-hash": "^2.0.1",
 | 
			
		||||
    "events": "^3.3.0",
 | 
			
		||||
    "lodash-move": "^1.1.1",
 | 
			
		||||
    "matrix-js-sdk": "^12.0.1",
 | 
			
		||||
    "matrix-js-sdk": "file:../matrix-js-sdk",
 | 
			
		||||
    "re-resizable": "^6.9.0",
 | 
			
		||||
    "react": "^17.0.0",
 | 
			
		||||
    "react-dom": "^17.0.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								src/App.jsx
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								src/App.jsx
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -22,13 +22,14 @@ import {
 | 
			
		|||
  Redirect,
 | 
			
		||||
  useLocation,
 | 
			
		||||
} from "react-router-dom";
 | 
			
		||||
import { useConferenceCallManager } from "./ConferenceCallManagerHooks";
 | 
			
		||||
import { useClient } from "./ConferenceCallManagerHooks";
 | 
			
		||||
import { Home } from "./Home";
 | 
			
		||||
import { Room, RoomAuth } from "./Room";
 | 
			
		||||
import { Room } from "./Room";
 | 
			
		||||
import { GridDemo } from "./GridDemo";
 | 
			
		||||
import { RegisterPage } from "./RegisterPage";
 | 
			
		||||
import { LoginPage } from "./LoginPage";
 | 
			
		||||
import { Center } from "./Layout";
 | 
			
		||||
import { GuestAuthPage } from "./GuestAuthPage";
 | 
			
		||||
 | 
			
		||||
export default function App() {
 | 
			
		||||
  const { protocol, host } = window.location;
 | 
			
		||||
| 
						 | 
				
			
			@ -37,12 +38,12 @@ export default function App() {
 | 
			
		|||
  const {
 | 
			
		||||
    loading,
 | 
			
		||||
    authenticated,
 | 
			
		||||
    error,
 | 
			
		||||
    manager,
 | 
			
		||||
    client,
 | 
			
		||||
    login,
 | 
			
		||||
    loginAsGuest,
 | 
			
		||||
    logout,
 | 
			
		||||
    registerGuest,
 | 
			
		||||
    register,
 | 
			
		||||
  } = useConferenceCallManager(homeserverUrl);
 | 
			
		||||
  } = useClient(homeserverUrl);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Router>
 | 
			
		||||
| 
						 | 
				
			
			@ -54,19 +55,19 @@ export default function App() {
 | 
			
		|||
        ) : (
 | 
			
		||||
          <Switch>
 | 
			
		||||
            <AuthenticatedRoute authenticated={authenticated} exact path="/">
 | 
			
		||||
              <Home manager={manager} error={error} />
 | 
			
		||||
              <Home client={client} onLogout={logout} />
 | 
			
		||||
            </AuthenticatedRoute>
 | 
			
		||||
            <Route exact path="/login">
 | 
			
		||||
              <LoginPage onLogin={login} error={error} />
 | 
			
		||||
              <LoginPage onLogin={login} />
 | 
			
		||||
            </Route>
 | 
			
		||||
            <Route exact path="/register">
 | 
			
		||||
              <RegisterPage onRegister={register} error={error} />
 | 
			
		||||
              <RegisterPage onRegister={register} />
 | 
			
		||||
            </Route>
 | 
			
		||||
            <Route path="/room/:roomId">
 | 
			
		||||
              {authenticated ? (
 | 
			
		||||
                <Room manager={manager} error={error} />
 | 
			
		||||
                <Room client={client} />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <RoomAuth error={error} onLoginAsGuest={loginAsGuest} />
 | 
			
		||||
                <GuestAuthPage onRegisterGuest={registerGuest} />
 | 
			
		||||
              )}
 | 
			
		||||
            </Route>
 | 
			
		||||
            <Route exact path="/grid">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,981 +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 EventEmitter from "events";
 | 
			
		||||
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
 | 
			
		||||
import { randomString } from "./randomstring";
 | 
			
		||||
 | 
			
		||||
const CONF_ROOM = "me.robertlong.conf";
 | 
			
		||||
const CONF_PARTICIPANT = "me.robertlong.conf.participant";
 | 
			
		||||
const PARTICIPANT_TIMEOUT = 1000 * 15;
 | 
			
		||||
const SPEAKING_THRESHOLD = -80;
 | 
			
		||||
const ACTIVE_SPEAKER_INTERVAL = 1000;
 | 
			
		||||
const ACTIVE_SPEAKER_SAMPLES = 8;
 | 
			
		||||
 | 
			
		||||
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({
 | 
			
		||||
          // dirty hack to reduce chance of gappy syncs
 | 
			
		||||
          // should be fixed by spotting gaps and backpaginating
 | 
			
		||||
          initialSyncLimit: 50,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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({
 | 
			
		||||
        // dirty hack to reduce chance of gappy syncs
 | 
			
		||||
        // should be fixed by spotting gaps and backpaginating
 | 
			
		||||
        initialSyncLimit: 50,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await waitForSync(client);
 | 
			
		||||
 | 
			
		||||
      return manager;
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async loginAsGuest(homeserverUrl, displayName) {
 | 
			
		||||
    const registrationClient = matrixcs.createClient(homeserverUrl);
 | 
			
		||||
 | 
			
		||||
    const { user_id, device_id, access_token } =
 | 
			
		||||
      await registrationClient.registerGuest();
 | 
			
		||||
 | 
			
		||||
    const client = matrixcs.createClient({
 | 
			
		||||
      baseUrl: homeserverUrl,
 | 
			
		||||
      accessToken: access_token,
 | 
			
		||||
      userId: user_id,
 | 
			
		||||
      deviceId: device_id,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await client.setDisplayName(displayName);
 | 
			
		||||
 | 
			
		||||
    client.setGuest(true);
 | 
			
		||||
 | 
			
		||||
    const manager = new ConferenceCallManager(client);
 | 
			
		||||
 | 
			
		||||
    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 manager;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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({
 | 
			
		||||
        // dirty hack to reduce chance of gappy syncs
 | 
			
		||||
        // should be fixed by spotting gaps and backpaginating
 | 
			
		||||
        initialSyncLimit: 50,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await waitForSync(client);
 | 
			
		||||
 | 
			
		||||
      return manager;
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(client) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    this.client = client;
 | 
			
		||||
 | 
			
		||||
    this.room = null;
 | 
			
		||||
 | 
			
		||||
    // The session id is used to re-initiate calls if the user's participant
 | 
			
		||||
    // session id has changed
 | 
			
		||||
    this.sessionId = randomString(16);
 | 
			
		||||
 | 
			
		||||
    this._memberParticipantStateTimeout = null;
 | 
			
		||||
 | 
			
		||||
    // Whether or not we have entered the conference call.
 | 
			
		||||
    this.entered = false;
 | 
			
		||||
 | 
			
		||||
    this._left = false;
 | 
			
		||||
 | 
			
		||||
    // The MatrixCalls that were picked up by the Call.incoming listener,
 | 
			
		||||
    // before the user entered the conference call.
 | 
			
		||||
    this._incomingCallQueue = [];
 | 
			
		||||
 | 
			
		||||
    // A representation of the conference call data for each room member
 | 
			
		||||
    // that has entered the call.
 | 
			
		||||
    this.participants = [];
 | 
			
		||||
 | 
			
		||||
    this.localVideoStream = null;
 | 
			
		||||
    this.localParticipant = null;
 | 
			
		||||
    this.localCallFeed = null;
 | 
			
		||||
 | 
			
		||||
    this.audioMuted = false;
 | 
			
		||||
    this.videoMuted = false;
 | 
			
		||||
 | 
			
		||||
    this.activeSpeaker = null;
 | 
			
		||||
    this._speakerMap = new Map();
 | 
			
		||||
    this._activeSpeakerLoopTimeout = null;
 | 
			
		||||
 | 
			
		||||
    this.client.on("RoomState.members", this._onRoomStateMembers);
 | 
			
		||||
    this.client.on("Call.incoming", this._onIncomingCall);
 | 
			
		||||
    this.callDebugger = new ConferenceCallDebugger(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async enter(roomId, timeout = 30000) {
 | 
			
		||||
    this._left = false;
 | 
			
		||||
 | 
			
		||||
    // Ensure that we have joined the provided room.
 | 
			
		||||
    await this.client.joinRoom(roomId);
 | 
			
		||||
 | 
			
		||||
    // Get the room info, wait if it hasn't been fetched yet.
 | 
			
		||||
    // Timeout after 30 seconds or the provided duration.
 | 
			
		||||
    const room = await new Promise((resolve, reject) => {
 | 
			
		||||
      const initialRoom = this.client.getRoom(roomId);
 | 
			
		||||
 | 
			
		||||
      if (initialRoom) {
 | 
			
		||||
        resolve(initialRoom);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const roomTimeout = setTimeout(() => {
 | 
			
		||||
        reject(new Error("Room could not be found."));
 | 
			
		||||
      }, timeout);
 | 
			
		||||
 | 
			
		||||
      const roomCallback = (room) => {
 | 
			
		||||
        if (room && room.roomId === roomId) {
 | 
			
		||||
          this.client.removeListener("Room", roomCallback);
 | 
			
		||||
          clearTimeout(roomTimeout);
 | 
			
		||||
          resolve(room);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.client.on("Room", roomCallback);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Ensure that this room is marked as a conference room so clients can react appropriately
 | 
			
		||||
    const activeConf = room.currentState
 | 
			
		||||
      .getStateEvents(CONF_ROOM, "")
 | 
			
		||||
      ?.getContent()?.active;
 | 
			
		||||
 | 
			
		||||
    if (!activeConf) {
 | 
			
		||||
      this._sendStateEventWithRetry(
 | 
			
		||||
        room.roomId,
 | 
			
		||||
        CONF_ROOM,
 | 
			
		||||
        { active: true },
 | 
			
		||||
        ""
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Request permissions and get the user's webcam/mic stream if we haven't yet.
 | 
			
		||||
    const userId = this.client.getUserId();
 | 
			
		||||
    const stream = await this.getLocalVideoStream();
 | 
			
		||||
 | 
			
		||||
    // It's possible to navigate to another page while the microphone permission prompt is
 | 
			
		||||
    // open, so check to see if we've left the call.
 | 
			
		||||
    // Only set class variables below this check so that leaveRoom properly handles
 | 
			
		||||
    // state cleanup.
 | 
			
		||||
    if (this._left) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.room = room;
 | 
			
		||||
 | 
			
		||||
    this.localParticipant = {
 | 
			
		||||
      local: true,
 | 
			
		||||
      userId,
 | 
			
		||||
      displayName: this.client.getUser(this.client.getUserId()).rawDisplayName,
 | 
			
		||||
      sessionId: this.sessionId,
 | 
			
		||||
      call: null,
 | 
			
		||||
      stream,
 | 
			
		||||
      audioMuted: this.audioMuted,
 | 
			
		||||
      videoMuted: this.videoMuted,
 | 
			
		||||
      speaking: false,
 | 
			
		||||
      activeSpeaker: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.activeSpeaker = this.localParticipant;
 | 
			
		||||
 | 
			
		||||
    this.participants.push(this.localParticipant);
 | 
			
		||||
    this.emit("debugstate", userId, null, "you");
 | 
			
		||||
 | 
			
		||||
    this.localCallFeed = new matrixcs.CallFeed(
 | 
			
		||||
      stream,
 | 
			
		||||
      this.localParticipant.userId,
 | 
			
		||||
      "m.usermedia",
 | 
			
		||||
      this.client,
 | 
			
		||||
      this.room.roomId,
 | 
			
		||||
      this.audioMuted,
 | 
			
		||||
      this.videoMuted
 | 
			
		||||
    );
 | 
			
		||||
    this.localCallFeed.on("mute_state_changed", () =>
 | 
			
		||||
      this._onCallFeedMuteStateChanged(
 | 
			
		||||
        this.localParticipant,
 | 
			
		||||
        this.localCallFeed
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
    this.localCallFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
 | 
			
		||||
    this.localCallFeed.measureVolumeActivity(true);
 | 
			
		||||
    this.localCallFeed.on("speaking", (speaking) => {
 | 
			
		||||
      this._onCallFeedSpeaking(this.localParticipant, speaking);
 | 
			
		||||
    });
 | 
			
		||||
    this.localCallFeed.on("volume_changed", (maxVolume) =>
 | 
			
		||||
      this._onCallFeedVolumeChange(this.localParticipant, maxVolume)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Announce to the other room members that we have entered the room.
 | 
			
		||||
    // Continue doing so every PARTICIPANT_TIMEOUT ms
 | 
			
		||||
    this._updateMemberParticipantState();
 | 
			
		||||
 | 
			
		||||
    this.entered = true;
 | 
			
		||||
 | 
			
		||||
    // Answer any pending incoming calls.
 | 
			
		||||
    const incomingCallCount = this._incomingCallQueue.length;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < incomingCallCount; i++) {
 | 
			
		||||
      const call = this._incomingCallQueue.pop();
 | 
			
		||||
      this._onIncomingCall(call);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set up participants for the members currently in the room.
 | 
			
		||||
    // Other members will be picked up by the RoomState.members event.
 | 
			
		||||
    const initialMembers = room.getMembers();
 | 
			
		||||
 | 
			
		||||
    for (const member of initialMembers) {
 | 
			
		||||
      this._onMemberChanged(member);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("entered");
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
    this._onActiveSpeakerLoop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  leaveCall() {
 | 
			
		||||
    if (!this.entered) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userId = this.client.getUserId();
 | 
			
		||||
    const currentMemberState = this.room.currentState.getStateEvents(
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this._sendStateEventWithRetry(
 | 
			
		||||
      this.room.roomId,
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      {
 | 
			
		||||
        ...currentMemberState.getContent(),
 | 
			
		||||
        [CONF_PARTICIPANT]: null,
 | 
			
		||||
      },
 | 
			
		||||
      userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (const participant of this.participants) {
 | 
			
		||||
      if (!participant.local && participant.call) {
 | 
			
		||||
        participant.call.hangup("user_hangup", false);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.client.stopLocalMediaStream();
 | 
			
		||||
    this.localVideoStream = null;
 | 
			
		||||
    this.localCallFeed.dispose();
 | 
			
		||||
    this.localCallFeed = null;
 | 
			
		||||
 | 
			
		||||
    this.room = null;
 | 
			
		||||
    this.entered = false;
 | 
			
		||||
    this._left = true;
 | 
			
		||||
    this.participants = [];
 | 
			
		||||
    this.localParticipant = null;
 | 
			
		||||
    this.activeSpeaker = null;
 | 
			
		||||
    this.audioMuted = false;
 | 
			
		||||
    this.videoMuted = false;
 | 
			
		||||
    clearTimeout(this._memberParticipantStateTimeout);
 | 
			
		||||
    clearTimeout(this._activeSpeakerLoopTimeout);
 | 
			
		||||
    this._speakerMap.clear();
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
    this.emit("left");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getLocalVideoStream() {
 | 
			
		||||
    if (this.localVideoStream) {
 | 
			
		||||
      return this.localVideoStream;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const stream = await this.client.getLocalVideoStream();
 | 
			
		||||
 | 
			
		||||
    this.localVideoStream = stream;
 | 
			
		||||
 | 
			
		||||
    return stream;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setAudioMuted(muted) {
 | 
			
		||||
    this.audioMuted = muted;
 | 
			
		||||
 | 
			
		||||
    if (this.localCallFeed) {
 | 
			
		||||
      this.localCallFeed.setAudioMuted(muted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const localStream = this.localVideoStream;
 | 
			
		||||
 | 
			
		||||
    if (localStream) {
 | 
			
		||||
      for (const track of localStream.getTracks()) {
 | 
			
		||||
        if (track.kind === "audio") {
 | 
			
		||||
          track.enabled = !this.audioMuted;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let participant of this.participants) {
 | 
			
		||||
      const call = participant.call;
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        call &&
 | 
			
		||||
        call.localUsermediaStream &&
 | 
			
		||||
        call.isMicrophoneMuted() !== this.audioMuted
 | 
			
		||||
      ) {
 | 
			
		||||
        call.setMicrophoneMuted(this.audioMuted);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setVideoMuted(muted) {
 | 
			
		||||
    this.videoMuted = muted;
 | 
			
		||||
 | 
			
		||||
    if (this.localCallFeed) {
 | 
			
		||||
      this.localCallFeed.setVideoMuted(muted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const localStream = this.localVideoStream;
 | 
			
		||||
 | 
			
		||||
    if (localStream) {
 | 
			
		||||
      for (const track of localStream.getTracks()) {
 | 
			
		||||
        if (track.kind === "video") {
 | 
			
		||||
          track.enabled = !this.videoMuted;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let participant of this.participants) {
 | 
			
		||||
      const call = participant.call;
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        call &&
 | 
			
		||||
        call.localUsermediaStream &&
 | 
			
		||||
        call.isLocalVideoMuted() !== this.videoMuted
 | 
			
		||||
      ) {
 | 
			
		||||
        call.setLocalVideoMuted(this.videoMuted);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  logout() {
 | 
			
		||||
    localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Call presence
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  _updateMemberParticipantState = () => {
 | 
			
		||||
    const userId = this.client.getUserId();
 | 
			
		||||
    const currentMemberState = this.room.currentState.getStateEvents(
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this._sendStateEventWithRetry(
 | 
			
		||||
      this.room.roomId,
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      {
 | 
			
		||||
        ...currentMemberState.getContent(),
 | 
			
		||||
        [CONF_PARTICIPANT]: {
 | 
			
		||||
          sessionId: this.sessionId,
 | 
			
		||||
          expiresAt: new Date().getTime() + PARTICIPANT_TIMEOUT * 2,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const now = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
    for (const participant of this.participants) {
 | 
			
		||||
      if (participant.local) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const memberStateEvent = this.room.currentState.getStateEvents(
 | 
			
		||||
        "m.room.member",
 | 
			
		||||
        participant.userId
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const memberStateContent = memberStateEvent.getContent();
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        !memberStateContent ||
 | 
			
		||||
        !memberStateContent[CONF_PARTICIPANT] ||
 | 
			
		||||
        typeof memberStateContent[CONF_PARTICIPANT] !== "object" ||
 | 
			
		||||
        (memberStateContent[CONF_PARTICIPANT].expiresAt &&
 | 
			
		||||
          memberStateContent[CONF_PARTICIPANT].expiresAt < now)
 | 
			
		||||
      ) {
 | 
			
		||||
        this.emit("debugstate", participant.userId, null, "inactive");
 | 
			
		||||
 | 
			
		||||
        if (participant.call) {
 | 
			
		||||
          // NOTE: This should remove the participant on the next tick
 | 
			
		||||
          // since matrix-js-sdk awaits a promise before firing user_hangup
 | 
			
		||||
          participant.call.hangup("user_hangup", false);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._memberParticipantStateTimeout = setTimeout(
 | 
			
		||||
      this._updateMemberParticipantState,
 | 
			
		||||
      PARTICIPANT_TIMEOUT
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Call Setup
 | 
			
		||||
   *
 | 
			
		||||
   * There are two different paths for calls to be created:
 | 
			
		||||
   * 1. Incoming calls triggered by the Call.incoming event.
 | 
			
		||||
   * 2. Outgoing calls to the initial members of a room or new members
 | 
			
		||||
   *    as they are observed by the RoomState.members event.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  _onIncomingCall = (call) => {
 | 
			
		||||
    // If we haven't entered yet, add the call to a queue which we'll use later.
 | 
			
		||||
    if (!this.entered) {
 | 
			
		||||
      this._incomingCallQueue.push(call);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The incoming calls may be for another room, which we will ignore.
 | 
			
		||||
    if (call.roomId !== this.room.roomId) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (call.state !== "ringing") {
 | 
			
		||||
      console.warn("Incoming call no longer in ringing state. Ignoring.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the remote video stream if it exists.
 | 
			
		||||
    const remoteFeed = call.getRemoteFeeds()[0];
 | 
			
		||||
    const stream = remoteFeed && remoteFeed.stream;
 | 
			
		||||
    const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
 | 
			
		||||
    const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
 | 
			
		||||
 | 
			
		||||
    const userId = call.opponentMember.userId;
 | 
			
		||||
 | 
			
		||||
    const memberStateEvent = this.room.currentState.getStateEvents(
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const memberStateContent = memberStateEvent.getContent();
 | 
			
		||||
 | 
			
		||||
    if (!memberStateContent || !memberStateContent[CONF_PARTICIPANT]) {
 | 
			
		||||
      call.reject();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { sessionId } = memberStateContent[CONF_PARTICIPANT];
 | 
			
		||||
 | 
			
		||||
    // Check if the user calling has an existing participant and use this call instead.
 | 
			
		||||
    const existingParticipant = this.participants.find(
 | 
			
		||||
      (p) => p.userId === userId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let participant;
 | 
			
		||||
 | 
			
		||||
    console.log(call.opponentMember);
 | 
			
		||||
 | 
			
		||||
    if (existingParticipant) {
 | 
			
		||||
      participant = existingParticipant;
 | 
			
		||||
      // This also fires the hangup event and triggers those side-effects
 | 
			
		||||
      existingParticipant.call.hangup("replaced", false);
 | 
			
		||||
      existingParticipant.call = call;
 | 
			
		||||
      existingParticipant.stream = stream;
 | 
			
		||||
      existingParticipant.audioMuted = audioMuted;
 | 
			
		||||
      existingParticipant.videoMuted = videoMuted;
 | 
			
		||||
      existingParticipant.speaking = false;
 | 
			
		||||
      existingParticipant.activeSpeaker = false;
 | 
			
		||||
      existingParticipant.sessionId = sessionId;
 | 
			
		||||
    } else {
 | 
			
		||||
      participant = {
 | 
			
		||||
        local: false,
 | 
			
		||||
        userId,
 | 
			
		||||
        displayName: call.opponentMember.rawDisplayName,
 | 
			
		||||
        sessionId,
 | 
			
		||||
        call,
 | 
			
		||||
        stream,
 | 
			
		||||
        audioMuted,
 | 
			
		||||
        videoMuted,
 | 
			
		||||
        speaking: false,
 | 
			
		||||
        activeSpeaker: false,
 | 
			
		||||
      };
 | 
			
		||||
      this.participants.push(participant);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (remoteFeed) {
 | 
			
		||||
      remoteFeed.on("mute_state_changed", () =>
 | 
			
		||||
        this._onCallFeedMuteStateChanged(participant, remoteFeed)
 | 
			
		||||
      );
 | 
			
		||||
      remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
 | 
			
		||||
      remoteFeed.measureVolumeActivity(true);
 | 
			
		||||
      remoteFeed.on("speaking", (speaking) => {
 | 
			
		||||
        this._onCallFeedSpeaking(participant, speaking);
 | 
			
		||||
      });
 | 
			
		||||
      remoteFeed.on("volume_changed", (maxVolume) =>
 | 
			
		||||
        this._onCallFeedVolumeChange(participant, maxVolume)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    call.on("state", (state) =>
 | 
			
		||||
      this._onCallStateChanged(participant, call, state)
 | 
			
		||||
    );
 | 
			
		||||
    call.on("feeds_changed", () => this._onCallFeedsChanged(participant, call));
 | 
			
		||||
    call.on("replaced", (newCall) =>
 | 
			
		||||
      this._onCallReplaced(participant, call, newCall)
 | 
			
		||||
    );
 | 
			
		||||
    call.on("hangup", () => this._onCallHangup(participant, call));
 | 
			
		||||
    call.answer();
 | 
			
		||||
 | 
			
		||||
    this.emit("call", call);
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onRoomStateMembers = (_event, _state, member) => {
 | 
			
		||||
    this._onMemberChanged(member);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onMemberChanged = (member) => {
 | 
			
		||||
    // Don't process new members until we've entered the conference call.
 | 
			
		||||
    if (!this.entered) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The member events may be received for another room, which we will ignore.
 | 
			
		||||
    if (member.roomId !== this.room.roomId) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Don't process your own member.
 | 
			
		||||
    const localUserId = this.client.getUserId();
 | 
			
		||||
 | 
			
		||||
    if (member.userId === localUserId) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the latest member participant state event.
 | 
			
		||||
    const memberStateEvent = this.room.currentState.getStateEvents(
 | 
			
		||||
      "m.room.member",
 | 
			
		||||
      member.userId
 | 
			
		||||
    );
 | 
			
		||||
    const memberStateContent = memberStateEvent.getContent();
 | 
			
		||||
 | 
			
		||||
    if (!memberStateContent) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const participantInfo = memberStateContent[CONF_PARTICIPANT];
 | 
			
		||||
 | 
			
		||||
    if (!participantInfo || typeof participantInfo !== "object") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { expiresAt, sessionId } = participantInfo;
 | 
			
		||||
 | 
			
		||||
    // If the participant state has expired, ignore this user.
 | 
			
		||||
    const now = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
    if (expiresAt < now) {
 | 
			
		||||
      this.emit("debugstate", member.userId, null, "inactive");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If there is an existing participant for this member check the session id.
 | 
			
		||||
    // If the session id changed then we can hang up the old call and start a new one.
 | 
			
		||||
    // Otherwise, ignore the member change event because we already have an active participant.
 | 
			
		||||
    let participant = this.participants.find((p) => p.userId === member.userId);
 | 
			
		||||
 | 
			
		||||
    if (participant) {
 | 
			
		||||
      if (participant.sessionId !== sessionId) {
 | 
			
		||||
        this.emit("debugstate", member.userId, null, "inactive");
 | 
			
		||||
        participant.call.hangup("replaced", false);
 | 
			
		||||
      } else {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Only initiate a call with a user who has a userId that is lexicographically
 | 
			
		||||
    // less than your own. Otherwise, that user will call you.
 | 
			
		||||
    if (member.userId < localUserId) {
 | 
			
		||||
      this.emit("debugstate", member.userId, null, "waiting for invite");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const call = this.client.createCall(this.room.roomId, member.userId);
 | 
			
		||||
 | 
			
		||||
    if (participant) {
 | 
			
		||||
      participant.sessionId = sessionId;
 | 
			
		||||
      participant.call = call;
 | 
			
		||||
      participant.stream = null;
 | 
			
		||||
      participant.audioMuted = false;
 | 
			
		||||
      participant.videoMuted = false;
 | 
			
		||||
      participant.speaking = false;
 | 
			
		||||
      participant.activeSpeaker = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      participant = {
 | 
			
		||||
        local: false,
 | 
			
		||||
        userId: member.userId,
 | 
			
		||||
        displayName: member.rawDisplayName,
 | 
			
		||||
        sessionId,
 | 
			
		||||
        call,
 | 
			
		||||
        stream: null,
 | 
			
		||||
        audioMuted: false,
 | 
			
		||||
        videoMuted: false,
 | 
			
		||||
        speaking: false,
 | 
			
		||||
        activeSpeaker: false,
 | 
			
		||||
      };
 | 
			
		||||
      // TODO: Should we wait until the call has been answered to push the participant?
 | 
			
		||||
      // Or do we hide the participant until their stream is live?
 | 
			
		||||
      // Does hiding a participant without a stream present a privacy problem because
 | 
			
		||||
      // a participant without a stream can still listen in on other user's streams?
 | 
			
		||||
      this.participants.push(participant);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    call.on("state", (state) =>
 | 
			
		||||
      this._onCallStateChanged(participant, call, state)
 | 
			
		||||
    );
 | 
			
		||||
    call.on("feeds_changed", () => this._onCallFeedsChanged(participant, call));
 | 
			
		||||
    call.on("replaced", (newCall) =>
 | 
			
		||||
      this._onCallReplaced(participant, call, newCall)
 | 
			
		||||
    );
 | 
			
		||||
    call.on("hangup", () => this._onCallHangup(participant, call));
 | 
			
		||||
 | 
			
		||||
    call.placeVideoCall().then(() => {
 | 
			
		||||
      this.emit("call", call);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Call Event Handlers
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  _onCallStateChanged = (participant, call, state) => {
 | 
			
		||||
    if (
 | 
			
		||||
      call.localUsermediaStream &&
 | 
			
		||||
      call.isMicrophoneMuted() !== this.audioMuted
 | 
			
		||||
    ) {
 | 
			
		||||
      call.setMicrophoneMuted(this.audioMuted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      call.localUsermediaStream &&
 | 
			
		||||
      call.isLocalVideoMuted() !== this.videoMuted
 | 
			
		||||
    ) {
 | 
			
		||||
      call.setLocalVideoMuted(this.videoMuted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("debugstate", participant.userId, call.callId, state);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallFeedsChanged = (participant, call) => {
 | 
			
		||||
    const remoteFeed = call.getRemoteFeeds()[0];
 | 
			
		||||
    const stream = remoteFeed && remoteFeed.stream;
 | 
			
		||||
    const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
 | 
			
		||||
    const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
 | 
			
		||||
 | 
			
		||||
    if (remoteFeed && participant.stream !== stream) {
 | 
			
		||||
      participant.stream = stream;
 | 
			
		||||
      participant.audioMuted = audioMuted;
 | 
			
		||||
      participant.videoMuted = videoMuted;
 | 
			
		||||
      remoteFeed.on("mute_state_changed", () =>
 | 
			
		||||
        this._onCallFeedMuteStateChanged(participant, remoteFeed)
 | 
			
		||||
      );
 | 
			
		||||
      remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
 | 
			
		||||
      remoteFeed.measureVolumeActivity(true);
 | 
			
		||||
      remoteFeed.on("speaking", (speaking) => {
 | 
			
		||||
        this._onCallFeedSpeaking(participant, speaking);
 | 
			
		||||
      });
 | 
			
		||||
      remoteFeed.on("volume_changed", (maxVolume) =>
 | 
			
		||||
        this._onCallFeedVolumeChange(participant, maxVolume)
 | 
			
		||||
      );
 | 
			
		||||
      this._onCallFeedMuteStateChanged(participant, remoteFeed);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallFeedMuteStateChanged = (participant, feed) => {
 | 
			
		||||
    participant.audioMuted = feed.isAudioMuted();
 | 
			
		||||
    participant.videoMuted = feed.isVideoMuted();
 | 
			
		||||
 | 
			
		||||
    if (participant.audioMuted) {
 | 
			
		||||
      this._speakerMap.set(
 | 
			
		||||
        participant.userId,
 | 
			
		||||
        Array(ACTIVE_SPEAKER_SAMPLES).fill(-Infinity)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallFeedSpeaking = (participant, speaking) => {
 | 
			
		||||
    participant.speaking = speaking;
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallFeedVolumeChange = (participant, maxVolume) => {
 | 
			
		||||
    if (!this._speakerMap.has(participant.userId)) {
 | 
			
		||||
      this._speakerMap.set(
 | 
			
		||||
        participant.userId,
 | 
			
		||||
        Array(ACTIVE_SPEAKER_SAMPLES).fill(-Infinity)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const volumeArr = this._speakerMap.get(participant.userId);
 | 
			
		||||
    volumeArr.shift();
 | 
			
		||||
    volumeArr.push(maxVolume);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onActiveSpeakerLoop = () => {
 | 
			
		||||
    let topAvg;
 | 
			
		||||
    let activeSpeakerId;
 | 
			
		||||
 | 
			
		||||
    for (const [userId, volumeArr] of this._speakerMap) {
 | 
			
		||||
      let total = 0;
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < volumeArr.length; i++) {
 | 
			
		||||
        const volume = volumeArr[i];
 | 
			
		||||
        total += Math.max(volume, SPEAKING_THRESHOLD);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const avg = total / ACTIVE_SPEAKER_SAMPLES;
 | 
			
		||||
 | 
			
		||||
      if (!topAvg) {
 | 
			
		||||
        topAvg = avg;
 | 
			
		||||
        activeSpeakerId = userId;
 | 
			
		||||
      } else if (avg > topAvg) {
 | 
			
		||||
        topAvg = avg;
 | 
			
		||||
        activeSpeakerId = userId;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (activeSpeakerId && topAvg > SPEAKING_THRESHOLD) {
 | 
			
		||||
      const nextActiveSpeaker = this.participants.find(
 | 
			
		||||
        (p) => p.userId === activeSpeakerId
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker) {
 | 
			
		||||
        this.activeSpeaker.activeSpeaker = false;
 | 
			
		||||
        nextActiveSpeaker.activeSpeaker = true;
 | 
			
		||||
        this.activeSpeaker = nextActiveSpeaker;
 | 
			
		||||
        this.emit("participants_changed");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._activeSpeakerLoopTimeout = setTimeout(
 | 
			
		||||
      this._onActiveSpeakerLoop,
 | 
			
		||||
      ACTIVE_SPEAKER_INTERVAL
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallReplaced = (participant, call, newCall) => {
 | 
			
		||||
    participant.call = newCall;
 | 
			
		||||
 | 
			
		||||
    newCall.on("state", (state) =>
 | 
			
		||||
      this._onCallStateChanged(participant, newCall, state)
 | 
			
		||||
    );
 | 
			
		||||
    newCall.on("feeds_changed", () =>
 | 
			
		||||
      this._onCallFeedsChanged(participant, newCall)
 | 
			
		||||
    );
 | 
			
		||||
    newCall.on("replaced", (nextCall) =>
 | 
			
		||||
      this._onCallReplaced(participant, newCall, nextCall)
 | 
			
		||||
    );
 | 
			
		||||
    newCall.on("hangup", () => this._onCallHangup(participant, newCall));
 | 
			
		||||
 | 
			
		||||
    const remoteFeed = newCall.getRemoteFeeds()[0];
 | 
			
		||||
    participant.stream = remoteFeed ? remoteFeed.stream : null;
 | 
			
		||||
    participant.audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
 | 
			
		||||
    participant.videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
 | 
			
		||||
 | 
			
		||||
    if (remoteFeed) {
 | 
			
		||||
      remoteFeed.on("mute_state_changed", () =>
 | 
			
		||||
        this._onCallFeedMuteStateChanged(participant, remoteFeed)
 | 
			
		||||
      );
 | 
			
		||||
      remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
 | 
			
		||||
      remoteFeed.measureVolumeActivity(true);
 | 
			
		||||
      remoteFeed.on("speaking", (speaking) => {
 | 
			
		||||
        this._onCallFeedSpeaking(participant, speaking);
 | 
			
		||||
      });
 | 
			
		||||
      remoteFeed.on("volume_changed", (maxVolume) =>
 | 
			
		||||
        this._onCallFeedVolumeChange(participant, maxVolume)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("call", newCall);
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _onCallHangup = (participant, call) => {
 | 
			
		||||
    if (call.hangupReason === "replaced") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const participantIndex = this.participants.indexOf(participant);
 | 
			
		||||
 | 
			
		||||
    if (participantIndex === -1) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.participants.splice(participantIndex, 1);
 | 
			
		||||
 | 
			
		||||
    if (this.activeSpeaker === participant && this.participants.length > 0) {
 | 
			
		||||
      this.activeSpeaker = this.participants[0];
 | 
			
		||||
      this.activeSpeaker.activeSpeaker = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._speakerMap.delete(participant.userId);
 | 
			
		||||
 | 
			
		||||
    this.emit("participants_changed");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Utils
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  _sendStateEventWithRetry(
 | 
			
		||||
    roomId,
 | 
			
		||||
    eventType,
 | 
			
		||||
    content,
 | 
			
		||||
    stateKey,
 | 
			
		||||
    callback,
 | 
			
		||||
    maxAttempts = 5
 | 
			
		||||
  ) {
 | 
			
		||||
    const sendStateEventWithRetry = async (attempt = 0) => {
 | 
			
		||||
      try {
 | 
			
		||||
        return await this.client.sendStateEvent(
 | 
			
		||||
          roomId,
 | 
			
		||||
          eventType,
 | 
			
		||||
          content,
 | 
			
		||||
          stateKey,
 | 
			
		||||
          callback
 | 
			
		||||
        );
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (attempt >= maxAttempts) {
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await new Promise((resolve) => setTimeout(resolve(), 5));
 | 
			
		||||
 | 
			
		||||
        return sendStateEventWithRetry(attempt + 1);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return sendStateEventWithRetry();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import { useCallback, useEffect, useState } from "react";
 | 
			
		||||
import { ConferenceCallManager } from "./ConferenceCallManager";
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import matrix from "matrix-js-sdk";
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/9039885
 | 
			
		||||
function isIOS() {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,289 +33,230 @@ function isIOS() {
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useConferenceCallManager(homeserverUrl) {
 | 
			
		||||
  const [{ loading, authenticated, manager, error }, setState] = useState({
 | 
			
		||||
function waitForSync(client) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const onSync = (state) => {
 | 
			
		||||
      if (state === "PREPARED") {
 | 
			
		||||
        resolve();
 | 
			
		||||
        client.removeListener("sync", onSync);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    client.on("sync", onSync);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function initClient(clientOptions, guest) {
 | 
			
		||||
  const client = matrix.createClient(clientOptions);
 | 
			
		||||
 | 
			
		||||
  if (guest) {
 | 
			
		||||
    client.setGuest(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 async function fetchRoom(client, roomId, join, timeout = 5000) {
 | 
			
		||||
  let room = client.getRoom(roomId);
 | 
			
		||||
 | 
			
		||||
  if (room) {
 | 
			
		||||
    return room;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (join) {
 | 
			
		||||
    room = await client.joinRoom(roomId);
 | 
			
		||||
 | 
			
		||||
    if (room) {
 | 
			
		||||
      return room;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let timeoutId;
 | 
			
		||||
 | 
			
		||||
    function onRoom(room) {
 | 
			
		||||
      if (room && room.roomId === roomId) {
 | 
			
		||||
        clearTimeout(timeoutId);
 | 
			
		||||
        client.removeListener("Room", onRoom);
 | 
			
		||||
        resolve(room);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const room = client.getRoom(roomId);
 | 
			
		||||
 | 
			
		||||
    if (room) {
 | 
			
		||||
      resolve(room);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    client.on("Room", onRoom);
 | 
			
		||||
 | 
			
		||||
    if (timeout) {
 | 
			
		||||
      timeoutId = setTimeout(() => {
 | 
			
		||||
        client.removeListener("Room", onRoom);
 | 
			
		||||
        reject(new Error("Fetching room timed out."));
 | 
			
		||||
      }, timeout);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useClient(homeserverUrl) {
 | 
			
		||||
  const [{ loading, authenticated, client }, setState] = useState({
 | 
			
		||||
    loading: true,
 | 
			
		||||
    authenticated: false,
 | 
			
		||||
    manager: undefined,
 | 
			
		||||
    error: undefined,
 | 
			
		||||
    client: undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    ConferenceCallManager.restore(homeserverUrl)
 | 
			
		||||
      .then((manager) => {
 | 
			
		||||
        setState({
 | 
			
		||||
          manager,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: !!manager,
 | 
			
		||||
          error: undefined,
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
    async function restore() {
 | 
			
		||||
      try {
 | 
			
		||||
        const authStore = localStorage.getItem("matrix-auth-store");
 | 
			
		||||
 | 
			
		||||
        setState({
 | 
			
		||||
          manager: undefined,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: false,
 | 
			
		||||
          error: err,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
        if (authStore) {
 | 
			
		||||
          const { user_id, device_id, access_token } = JSON.parse(authStore);
 | 
			
		||||
 | 
			
		||||
  const login = useCallback(async (username, password, cb) => {
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      authenticated: false,
 | 
			
		||||
      error: undefined,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    ConferenceCallManager.login(homeserverUrl, username, password)
 | 
			
		||||
      .then((manager) => {
 | 
			
		||||
        setState({
 | 
			
		||||
          manager,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: true,
 | 
			
		||||
          error: undefined,
 | 
			
		||||
          const client = await initClient({
 | 
			
		||||
            baseUrl: homeserverUrl,
 | 
			
		||||
            accessToken: access_token,
 | 
			
		||||
            userId: user_id,
 | 
			
		||||
            deviceId: device_id,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
        if (cb) {
 | 
			
		||||
          cb();
 | 
			
		||||
          localStorage.setItem(
 | 
			
		||||
            "matrix-auth-store",
 | 
			
		||||
            JSON.stringify({ user_id, device_id, access_token })
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          return client;
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
        throw err;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    restore()
 | 
			
		||||
      .then((client) => {
 | 
			
		||||
        if (client) {
 | 
			
		||||
          setState({ client, loading: false, authenticated: true });
 | 
			
		||||
        } else {
 | 
			
		||||
          setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
 | 
			
		||||
        setState({
 | 
			
		||||
          manager: undefined,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: false,
 | 
			
		||||
          error: err,
 | 
			
		||||
        });
 | 
			
		||||
      .catch(() => {
 | 
			
		||||
        setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const loginAsGuest = useCallback(async (displayName) => {
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      authenticated: false,
 | 
			
		||||
      error: undefined,
 | 
			
		||||
    }));
 | 
			
		||||
  const login = useCallback(async (username, password) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const registrationClient = matrix.createClient(homeserverUrl);
 | 
			
		||||
 | 
			
		||||
    ConferenceCallManager.loginAsGuest(homeserverUrl, displayName)
 | 
			
		||||
      .then((manager) => {
 | 
			
		||||
        setState({
 | 
			
		||||
          manager,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: true,
 | 
			
		||||
          error: undefined,
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
      const { user_id, device_id, access_token } =
 | 
			
		||||
        await registrationClient.loginWithPassword(username, password);
 | 
			
		||||
 | 
			
		||||
        setState({
 | 
			
		||||
          manager: undefined,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: false,
 | 
			
		||||
          error: err,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const register = useCallback(async (username, password, cb) => {
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      authenticated: false,
 | 
			
		||||
      error: undefined,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    ConferenceCallManager.register(homeserverUrl, username, password)
 | 
			
		||||
      .then((manager) => {
 | 
			
		||||
        setState({
 | 
			
		||||
          manager,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: true,
 | 
			
		||||
          error: undefined,
 | 
			
		||||
      const client = await initClient({
 | 
			
		||||
        baseUrl: homeserverUrl,
 | 
			
		||||
        accessToken: access_token,
 | 
			
		||||
        userId: user_id,
 | 
			
		||||
        deviceId: device_id,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
        if (cb) {
 | 
			
		||||
          cb();
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        "matrix-auth-store",
 | 
			
		||||
        JSON.stringify({ user_id, device_id, access_token })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      setState({ client, loading: false, authenticated: true });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
      setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
 | 
			
		||||
        setState({
 | 
			
		||||
          manager: undefined,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          authenticated: false,
 | 
			
		||||
          error: err,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.confManager = manager;
 | 
			
		||||
  const registerGuest = useCallback(async (displayName) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const registrationClient = matrix.createClient(homeserverUrl);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.confManager = undefined;
 | 
			
		||||
    };
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
      const { user_id, device_id, access_token } =
 | 
			
		||||
        await registrationClient.registerGuest({});
 | 
			
		||||
 | 
			
		||||
      const client = await initClient(
 | 
			
		||||
        {
 | 
			
		||||
          baseUrl: homeserverUrl,
 | 
			
		||||
          accessToken: access_token,
 | 
			
		||||
          userId: user_id,
 | 
			
		||||
          deviceId: device_id,
 | 
			
		||||
        },
 | 
			
		||||
        true
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        "matrix-auth-store",
 | 
			
		||||
        JSON.stringify({ user_id, device_id, access_token })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      setState({ client, loading: false, authenticated: true });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
      setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const register = useCallback(async (username, password) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const registrationClient = matrix.createClient(homeserverUrl);
 | 
			
		||||
 | 
			
		||||
      const { user_id, device_id, access_token } =
 | 
			
		||||
        await registrationClient.register(username, password, null, {
 | 
			
		||||
          type: "m.login.dummy",
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      const client = await initClient({
 | 
			
		||||
        baseUrl: homeserverUrl,
 | 
			
		||||
        accessToken: access_token,
 | 
			
		||||
        userId: user_id,
 | 
			
		||||
        deviceId: device_id,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        "matrix-auth-store",
 | 
			
		||||
        JSON.stringify({ user_id, device_id, access_token })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      setState({ client, loading: false, authenticated: true });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
      setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const logout = useCallback(() => {
 | 
			
		||||
    localStorage.removeItem("matrix-auth-store");
 | 
			
		||||
    setState({ client: undefined, loading: false, authenticated: false });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    loading,
 | 
			
		||||
    authenticated,
 | 
			
		||||
    manager,
 | 
			
		||||
    error,
 | 
			
		||||
    client,
 | 
			
		||||
    login,
 | 
			
		||||
    loginAsGuest,
 | 
			
		||||
    registerGuest,
 | 
			
		||||
    register,
 | 
			
		||||
    logout,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useVideoRoom(manager, roomId, timeout = 5000) {
 | 
			
		||||
  const [
 | 
			
		||||
    {
 | 
			
		||||
      loading,
 | 
			
		||||
      joined,
 | 
			
		||||
      joining,
 | 
			
		||||
      room,
 | 
			
		||||
      participants,
 | 
			
		||||
      error,
 | 
			
		||||
      videoMuted,
 | 
			
		||||
      audioMuted,
 | 
			
		||||
    },
 | 
			
		||||
    setState,
 | 
			
		||||
  ] = useState({
 | 
			
		||||
    loading: true,
 | 
			
		||||
    joining: false,
 | 
			
		||||
    joined: false,
 | 
			
		||||
    room: undefined,
 | 
			
		||||
    participants: [],
 | 
			
		||||
    error: undefined,
 | 
			
		||||
    videoMuted: false,
 | 
			
		||||
    audioMuted: false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      loading: true,
 | 
			
		||||
      room: undefined,
 | 
			
		||||
      error: undefined,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const onParticipantsChanged = () => {
 | 
			
		||||
      setState((prevState) => ({
 | 
			
		||||
        ...prevState,
 | 
			
		||||
        participants: [...manager.participants],
 | 
			
		||||
      }));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    manager.on("participants_changed", onParticipantsChanged);
 | 
			
		||||
 | 
			
		||||
    manager.client.joinRoom(roomId).catch((err) => {
 | 
			
		||||
      setState((prevState) => ({ ...prevState, loading: false, error: err }));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
        }));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let initialRoom = manager.client.getRoom(roomId);
 | 
			
		||||
 | 
			
		||||
    if (initialRoom) {
 | 
			
		||||
      setState((prevState) => ({
 | 
			
		||||
        ...prevState,
 | 
			
		||||
        loading: false,
 | 
			
		||||
        room: initialRoom,
 | 
			
		||||
        error: undefined,
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onLeaveCall() {
 | 
			
		||||
      setState((prevState) => ({
 | 
			
		||||
        ...prevState,
 | 
			
		||||
        videoMuted: manager.videoMuted,
 | 
			
		||||
        audioMuted: manager.audioMuted,
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    manager.on("left", onLeaveCall);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      manager.client.removeListener("Room", roomCallback);
 | 
			
		||||
      manager.removeListener("participants_changed", onParticipantsChanged);
 | 
			
		||||
      clearTimeout(timeoutId);
 | 
			
		||||
      manager.leaveCall();
 | 
			
		||||
      manager.removeListener("left", onLeaveCall);
 | 
			
		||||
    };
 | 
			
		||||
  }, [manager, roomId]);
 | 
			
		||||
 | 
			
		||||
  const joinCall = useCallback(() => {
 | 
			
		||||
    if (joining || joined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      joining: true,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    manager
 | 
			
		||||
      .enter(roomId)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        setState((prevState) => ({
 | 
			
		||||
          ...prevState,
 | 
			
		||||
          joining: false,
 | 
			
		||||
          joined: true,
 | 
			
		||||
        }));
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        setState((prevState) => ({
 | 
			
		||||
          ...prevState,
 | 
			
		||||
          joining: false,
 | 
			
		||||
          joined: false,
 | 
			
		||||
          error,
 | 
			
		||||
        }));
 | 
			
		||||
      });
 | 
			
		||||
  }, [manager, roomId, joining, joined]);
 | 
			
		||||
 | 
			
		||||
  const leaveCall = useCallback(() => {
 | 
			
		||||
    manager.leaveCall();
 | 
			
		||||
 | 
			
		||||
    setState((prevState) => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      participants: [...manager.participants],
 | 
			
		||||
      joined: false,
 | 
			
		||||
      joining: false,
 | 
			
		||||
    }));
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
 | 
			
		||||
function usePageUnload(callback) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let pageVisibilityTimeout;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -327,25 +268,11 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
 | 
			
		|||
          // Wait 5 seconds before closing the page to avoid accidentally leaving
 | 
			
		||||
          // TODO: Make this configurable?
 | 
			
		||||
          pageVisibilityTimeout = setTimeout(() => {
 | 
			
		||||
            manager.leaveCall();
 | 
			
		||||
 | 
			
		||||
            setState((prevState) => ({
 | 
			
		||||
              ...prevState,
 | 
			
		||||
              participants: [...manager.participants],
 | 
			
		||||
              joined: false,
 | 
			
		||||
              joining: false,
 | 
			
		||||
            }));
 | 
			
		||||
            callback();
 | 
			
		||||
          }, 5000);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        manager.leaveCall();
 | 
			
		||||
 | 
			
		||||
        setState((prevState) => ({
 | 
			
		||||
          ...prevState,
 | 
			
		||||
          participants: [...manager.participants],
 | 
			
		||||
          joined: false,
 | 
			
		||||
          joining: false,
 | 
			
		||||
        }));
 | 
			
		||||
        callback();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -363,31 +290,174 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
 | 
			
		|||
      window.removeEventListener("beforeunload", onBeforeUnload);
 | 
			
		||||
      clearTimeout(pageVisibilityTimeout);
 | 
			
		||||
    };
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
  }, []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  const toggleMuteAudio = useCallback(() => {
 | 
			
		||||
    manager.setAudioMuted(!manager.audioMuted);
 | 
			
		||||
    setState((prevState) => ({ ...prevState, audioMuted: manager.audioMuted }));
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
function getParticipants(groupCall) {
 | 
			
		||||
  return [...groupCall.participants];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  const toggleMuteVideo = useCallback(() => {
 | 
			
		||||
    manager.setVideoMuted(!manager.videoMuted);
 | 
			
		||||
    setState((prevState) => ({ ...prevState, videoMuted: manager.videoMuted }));
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
export function useGroupCall(client, roomId) {
 | 
			
		||||
  const groupCallRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
  const [
 | 
			
		||||
    {
 | 
			
		||||
      loading,
 | 
			
		||||
    joined,
 | 
			
		||||
    joining,
 | 
			
		||||
      entered,
 | 
			
		||||
      entering,
 | 
			
		||||
      room,
 | 
			
		||||
      participants,
 | 
			
		||||
      error,
 | 
			
		||||
    joinCall,
 | 
			
		||||
    leaveCall,
 | 
			
		||||
    toggleMuteVideo,
 | 
			
		||||
    toggleMuteAudio,
 | 
			
		||||
    videoMuted,
 | 
			
		||||
    audioMuted,
 | 
			
		||||
      microphoneMuted,
 | 
			
		||||
      localVideoMuted,
 | 
			
		||||
    },
 | 
			
		||||
    setState,
 | 
			
		||||
  ] = useState({
 | 
			
		||||
    loading: true,
 | 
			
		||||
    entered: false,
 | 
			
		||||
    entering: false,
 | 
			
		||||
    room: null,
 | 
			
		||||
    participants: [],
 | 
			
		||||
    error: null,
 | 
			
		||||
    microphoneMuted: false,
 | 
			
		||||
    localVideoMuted: false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const updateState = (state) =>
 | 
			
		||||
    setState((prevState) => ({ ...prevState, ...state }));
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function onParticipantsChanged(...args) {
 | 
			
		||||
      console.log(...args);
 | 
			
		||||
      updateState({ participants: getParticipants(groupCallRef.current) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
 | 
			
		||||
      updateState({
 | 
			
		||||
        microphoneMuted,
 | 
			
		||||
        localVideoMuted,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function init() {
 | 
			
		||||
      const room = await fetchRoom(client, roomId, true);
 | 
			
		||||
 | 
			
		||||
      const groupCall = client.createGroupCall(roomId, "video");
 | 
			
		||||
      groupCallRef.current = groupCall;
 | 
			
		||||
      groupCall.on("active_speaker_changed", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("participants_changed", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("speaking", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("mute_state_changed", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("participant_call_replaced", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("participant_call_feeds_changed", onParticipantsChanged);
 | 
			
		||||
      groupCall.on("local_mute_state_changed", onLocalMuteStateChanged);
 | 
			
		||||
 | 
			
		||||
      updateState({
 | 
			
		||||
        room,
 | 
			
		||||
        loading: false,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init().catch((error) => {
 | 
			
		||||
      if (groupCallRef.current) {
 | 
			
		||||
        const groupCall = groupCallRef.current;
 | 
			
		||||
        groupCall.removeListener(
 | 
			
		||||
          "active_speaker_changed",
 | 
			
		||||
          onParticipantsChanged
 | 
			
		||||
        );
 | 
			
		||||
        groupCall.removeListener("participants_changed", onParticipantsChanged);
 | 
			
		||||
        groupCall.removeListener("speaking", onParticipantsChanged);
 | 
			
		||||
        groupCall.removeListener("mute_state_changed", onParticipantsChanged);
 | 
			
		||||
        groupCall.removeListener(
 | 
			
		||||
          "participant_call_replaced",
 | 
			
		||||
          onParticipantsChanged
 | 
			
		||||
        );
 | 
			
		||||
        groupCall.removeListener(
 | 
			
		||||
          "participant_call_feeds_changed",
 | 
			
		||||
          onParticipantsChanged
 | 
			
		||||
        );
 | 
			
		||||
        groupCall.removeListener(
 | 
			
		||||
          "local_mute_state_changed",
 | 
			
		||||
          onLocalMuteStateChanged
 | 
			
		||||
        );
 | 
			
		||||
        groupCall.leave();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      updateState({ error, loading: false });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (groupCallRef.current) {
 | 
			
		||||
        groupCallRef.current.leave();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, [client, roomId]);
 | 
			
		||||
 | 
			
		||||
  usePageUnload(() => {
 | 
			
		||||
    if (groupCallRef.current) {
 | 
			
		||||
      groupCallRef.current.leave();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const initLocalParticipant = useCallback(
 | 
			
		||||
    () => groupCallRef.current.initLocalParticipant(),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const enter = useCallback(() => {
 | 
			
		||||
    updateState({ entering: true });
 | 
			
		||||
 | 
			
		||||
    groupCallRef.current
 | 
			
		||||
      .enter()
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        updateState({
 | 
			
		||||
          entered: true,
 | 
			
		||||
          entering: false,
 | 
			
		||||
          participants: getParticipants(groupCallRef.current),
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        updateState({ error, entering: false });
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const leave = useCallback(() => {
 | 
			
		||||
    groupCallRef.current.leave();
 | 
			
		||||
    updateState({
 | 
			
		||||
      entered: false,
 | 
			
		||||
      participants: [],
 | 
			
		||||
      microphoneMuted: false,
 | 
			
		||||
      localVideoMuted: false,
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const toggleLocalVideoMuted = useCallback(() => {
 | 
			
		||||
    groupCallRef.current.setLocalVideoMuted(
 | 
			
		||||
      !groupCallRef.current.isLocalVideoMuted()
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const toggleMicrophoneMuted = useCallback(() => {
 | 
			
		||||
    groupCallRef.current.setMicrophoneMuted(
 | 
			
		||||
      !groupCallRef.current.isMicrophoneMuted()
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    loading,
 | 
			
		||||
    entered,
 | 
			
		||||
    entering,
 | 
			
		||||
    roomName: room ? room.name : null,
 | 
			
		||||
    participants,
 | 
			
		||||
    groupCall: groupCallRef.current,
 | 
			
		||||
    microphoneMuted,
 | 
			
		||||
    localVideoMuted,
 | 
			
		||||
    error,
 | 
			
		||||
    initLocalParticipant,
 | 
			
		||||
    enter,
 | 
			
		||||
    leave,
 | 
			
		||||
    toggleLocalVideoMuted,
 | 
			
		||||
    toggleMicrophoneMuted,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -440,22 +510,22 @@ function sortRooms(client, rooms) {
 | 
			
		|||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useRooms(manager) {
 | 
			
		||||
export function useRooms(client) {
 | 
			
		||||
  const [rooms, setRooms] = useState([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function updateRooms() {
 | 
			
		||||
      const visibleRooms = manager.client.getVisibleRooms();
 | 
			
		||||
      const sortedRooms = sortRooms(manager.client, visibleRooms);
 | 
			
		||||
      const visibleRooms = client.getVisibleRooms();
 | 
			
		||||
      const sortedRooms = sortRooms(client, visibleRooms);
 | 
			
		||||
      setRooms(sortedRooms);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateRooms();
 | 
			
		||||
 | 
			
		||||
    manager.client.on("Room", updateRooms);
 | 
			
		||||
    client.on("Room", updateRooms);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      manager.client.removeListener("Room", updateRooms);
 | 
			
		||||
      client.removeListener("Room", updateRooms);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										91
									
								
								src/GuestAuthPage.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/GuestAuthPage.jsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,91 @@
 | 
			
		|||
/*
 | 
			
		||||
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, { useState, useRef, useCallback } from "react";
 | 
			
		||||
import styles from "./GuestAuthPage.module.css";
 | 
			
		||||
import { useLocation, useHistory, Link } from "react-router-dom";
 | 
			
		||||
import { Header, LeftNav } from "./Header";
 | 
			
		||||
import { Button, FieldRow, InputField, ErrorMessage } from "./Input";
 | 
			
		||||
import { Center, Content, Info, Modal } from "./Layout";
 | 
			
		||||
 | 
			
		||||
export function GuestAuthPage({ onLoginAsGuest }) {
 | 
			
		||||
  const displayNameRef = useRef();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const [error, setError] = useState();
 | 
			
		||||
 | 
			
		||||
  const onSubmitLoginForm = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onLoginAsGuest(displayNameRef.current.value).catch(setError);
 | 
			
		||||
    },
 | 
			
		||||
    [onLoginAsGuest, location, history]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.guestAuthPage}>
 | 
			
		||||
      <Header>
 | 
			
		||||
        <LeftNav />
 | 
			
		||||
      </Header>
 | 
			
		||||
      <Content>
 | 
			
		||||
        <Center>
 | 
			
		||||
          <Modal>
 | 
			
		||||
            <h2>Login As Guest</h2>
 | 
			
		||||
            <form onSubmit={onSubmitLoginForm}>
 | 
			
		||||
              <FieldRow>
 | 
			
		||||
                <InputField
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  ref={displayNameRef}
 | 
			
		||||
                  placeholder="Display Name"
 | 
			
		||||
                  label="Display Name"
 | 
			
		||||
                  autoCorrect="off"
 | 
			
		||||
                  autoCapitalize="none"
 | 
			
		||||
                />
 | 
			
		||||
              </FieldRow>
 | 
			
		||||
              {error && (
 | 
			
		||||
                <FieldRow>
 | 
			
		||||
                  <ErrorMessage>{error.message}</ErrorMessage>
 | 
			
		||||
                </FieldRow>
 | 
			
		||||
              )}
 | 
			
		||||
              <FieldRow rightAlign>
 | 
			
		||||
                <Button type="submit">Login as guest</Button>
 | 
			
		||||
              </FieldRow>
 | 
			
		||||
            </form>
 | 
			
		||||
            <Info>
 | 
			
		||||
              <Link
 | 
			
		||||
                to={{
 | 
			
		||||
                  pathname: "/login",
 | 
			
		||||
                  state: location.state,
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Sign in
 | 
			
		||||
              </Link>
 | 
			
		||||
              {" or "}
 | 
			
		||||
              <Link
 | 
			
		||||
                to={{
 | 
			
		||||
                  pathname: "/register",
 | 
			
		||||
                  state: location.state,
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Create account
 | 
			
		||||
              </Link>
 | 
			
		||||
            </Info>
 | 
			
		||||
          </Modal>
 | 
			
		||||
        </Center>
 | 
			
		||||
      </Content>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/GuestAuthPage.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/GuestAuthPage.module.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
.guestAuthPage {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/Home.jsx
									
										
									
									
									
								
							
							
						
						
									
										27
									
								
								src/Home.jsx
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -25,12 +25,12 @@ import { Center, Content, Modal } from "./Layout";
 | 
			
		|||
 | 
			
		||||
const colorHash = new ColorHash({ lightness: 0.3 });
 | 
			
		||||
 | 
			
		||||
export function Home({ manager }) {
 | 
			
		||||
export function Home({ client, onLogout }) {
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const roomNameRef = useRef();
 | 
			
		||||
  const guestAccessRef = useRef();
 | 
			
		||||
  const [createRoomError, setCreateRoomError] = useState();
 | 
			
		||||
  const rooms = useRooms(manager);
 | 
			
		||||
  const rooms = useRooms(client);
 | 
			
		||||
 | 
			
		||||
  const onCreateRoom = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ export function Home({ manager }) {
 | 
			
		|||
      setCreateRoomError(undefined);
 | 
			
		||||
 | 
			
		||||
      async function createRoom(name, guestAccess) {
 | 
			
		||||
        const { room_id } = await manager.client.createRoom({
 | 
			
		||||
        const { room_id } = await client.createRoom({
 | 
			
		||||
          visibility: "private",
 | 
			
		||||
          preset: "public_chat",
 | 
			
		||||
          name,
 | 
			
		||||
| 
						 | 
				
			
			@ -62,14 +62,14 @@ export function Home({ manager }) {
 | 
			
		|||
                  "m.sticker": 50,
 | 
			
		||||
                },
 | 
			
		||||
                users: {
 | 
			
		||||
                  [manager.client.getUserId()]: 100,
 | 
			
		||||
                  [client.getUserId()]: 100,
 | 
			
		||||
                },
 | 
			
		||||
              }
 | 
			
		||||
            : undefined,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (guestAccess) {
 | 
			
		||||
          await manager.client.setGuestAccess(room_id, {
 | 
			
		||||
          await client.setGuestAccess(room_id, {
 | 
			
		||||
            allowJoin: true,
 | 
			
		||||
            allowRead: true,
 | 
			
		||||
          });
 | 
			
		||||
| 
						 | 
				
			
			@ -83,27 +83,14 @@ export function Home({ manager }) {
 | 
			
		|||
        guestAccessRef.current.checked
 | 
			
		||||
      ).catch(setCreateRoomError);
 | 
			
		||||
    },
 | 
			
		||||
    [manager]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const onLogout = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      manager.logout();
 | 
			
		||||
      location.reload();
 | 
			
		||||
    },
 | 
			
		||||
    [manager]
 | 
			
		||||
    [client]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Header>
 | 
			
		||||
        <LeftNav />
 | 
			
		||||
        <UserNav
 | 
			
		||||
          signedIn={manager.client}
 | 
			
		||||
          userName={manager.client.getUserId()}
 | 
			
		||||
          onLogout={onLogout}
 | 
			
		||||
        />
 | 
			
		||||
        <UserNav signedIn userName={client.getUserId()} onLogout={onLogout} />
 | 
			
		||||
      </Header>
 | 
			
		||||
      <Content>
 | 
			
		||||
        <Center>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,26 +20,25 @@ import { Header, LeftNav } from "./Header";
 | 
			
		|||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
 | 
			
		||||
import { Center, Content, Info, Modal } from "./Layout";
 | 
			
		||||
 | 
			
		||||
export function LoginPage({ onLogin, error }) {
 | 
			
		||||
export function LoginPage({ onLogin }) {
 | 
			
		||||
  const loginUsernameRef = useRef();
 | 
			
		||||
  const loginPasswordRef = useRef();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const [error, setError] = useState();
 | 
			
		||||
 | 
			
		||||
  const onSubmitLoginForm = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onLogin(
 | 
			
		||||
        loginUsernameRef.current.value,
 | 
			
		||||
        loginPasswordRef.current.value,
 | 
			
		||||
        () => {
 | 
			
		||||
      onLogin(loginUsernameRef.current.value, loginPasswordRef.current.value)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          if (location.state && location.state.from) {
 | 
			
		||||
            history.replace(location.state.from);
 | 
			
		||||
          } else {
 | 
			
		||||
            history.replace("/");
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
        })
 | 
			
		||||
        .catch(setError);
 | 
			
		||||
    },
 | 
			
		||||
    [onLogin, location, history]
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,32 +14,34 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { useCallback, useRef } from "react";
 | 
			
		||||
import React, { useCallback, useRef, useState } from "react";
 | 
			
		||||
import { useHistory, useLocation, Link } from "react-router-dom";
 | 
			
		||||
import { Header, LeftNav } from "./Header";
 | 
			
		||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
 | 
			
		||||
import { Center, Content, Info, Modal } from "./Layout";
 | 
			
		||||
 | 
			
		||||
export function RegisterPage({ onRegister, error }) {
 | 
			
		||||
export function RegisterPage({ onRegister }) {
 | 
			
		||||
  const registerUsernameRef = useRef();
 | 
			
		||||
  const registerPasswordRef = useRef();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const [error, setError] = useState();
 | 
			
		||||
 | 
			
		||||
  const onSubmitRegisterForm = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onRegister(
 | 
			
		||||
        registerUsernameRef.current.value,
 | 
			
		||||
        registerPasswordRef.current.value,
 | 
			
		||||
        () => {
 | 
			
		||||
        registerPasswordRef.current.value
 | 
			
		||||
      )
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          if (location.state && location.state.from) {
 | 
			
		||||
            history.replace(location.state.from);
 | 
			
		||||
          } else {
 | 
			
		||||
            history.replace("/");
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
        })
 | 
			
		||||
        .catch(setError);
 | 
			
		||||
    },
 | 
			
		||||
    [onRegister, location, history]
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										376
									
								
								src/Room.jsx
									
										
									
									
									
								
							
							
						
						
									
										376
									
								
								src/Room.jsx
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -23,7 +23,7 @@ import React, {
 | 
			
		|||
} from "react";
 | 
			
		||||
import styles from "./Room.module.css";
 | 
			
		||||
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
 | 
			
		||||
import { useVideoRoom } from "./ConferenceCallManagerHooks";
 | 
			
		||||
import { useGroupCall } from "./ConferenceCallManagerHooks";
 | 
			
		||||
import { DevTools } from "./DevTools";
 | 
			
		||||
import { VideoGrid } from "./VideoGrid";
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,25 +42,16 @@ function useQuery() {
 | 
			
		|||
  return useMemo(() => new URLSearchParams(location.search), [location.search]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Room({ manager }) {
 | 
			
		||||
  const { roomId } = useParams();
 | 
			
		||||
function useDebugMode() {
 | 
			
		||||
  const query = useQuery();
 | 
			
		||||
  const {
 | 
			
		||||
    loading,
 | 
			
		||||
    joined,
 | 
			
		||||
    joining,
 | 
			
		||||
    room,
 | 
			
		||||
    participants,
 | 
			
		||||
    error,
 | 
			
		||||
    joinCall,
 | 
			
		||||
    leaveCall,
 | 
			
		||||
    toggleMuteVideo,
 | 
			
		||||
    toggleMuteAudio,
 | 
			
		||||
    videoMuted,
 | 
			
		||||
    audioMuted,
 | 
			
		||||
  } = useVideoRoom(manager, roomId);
 | 
			
		||||
  const debugStr = query.get("debug");
 | 
			
		||||
  const [debug, setDebug] = useState(debugStr === "" || debugStr === "true");
 | 
			
		||||
  const [debugMode, setDebugMode] = useState(
 | 
			
		||||
    debugStr === "" || debugStr === "true"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const toggleDebugMode = useCallback(() => {
 | 
			
		||||
    setDebugMode((prevDebugMode) => !prevDebugMode);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function onKeyDown(event) {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +59,7 @@ export function Room({ manager }) {
 | 
			
		|||
        document.activeElement.tagName !== "input" &&
 | 
			
		||||
        event.code === "Backquote"
 | 
			
		||||
      ) {
 | 
			
		||||
        setDebug((prevDebug) => !prevDebug);
 | 
			
		||||
        toggleDebugMode();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,190 +70,237 @@ export function Room({ manager }) {
 | 
			
		|||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [debugMode, toggleDebugMode];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useRoomLayout() {
 | 
			
		||||
  const [layout, setLayout] = useState("gallery");
 | 
			
		||||
 | 
			
		||||
  const toggleLayout = useCallback(() => {
 | 
			
		||||
    setLayout(layout === "spotlight" ? "gallery" : "spotlight");
 | 
			
		||||
  }, [layout]);
 | 
			
		||||
 | 
			
		||||
  return [layout, toggleLayout];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Room({ client }) {
 | 
			
		||||
  const { roomId } = useParams();
 | 
			
		||||
  const {
 | 
			
		||||
    loading,
 | 
			
		||||
    entered,
 | 
			
		||||
    entering,
 | 
			
		||||
    roomName,
 | 
			
		||||
    participants,
 | 
			
		||||
    groupCall,
 | 
			
		||||
    microphoneMuted,
 | 
			
		||||
    localVideoMuted,
 | 
			
		||||
    error,
 | 
			
		||||
    initLocalParticipant,
 | 
			
		||||
    enter,
 | 
			
		||||
    leave,
 | 
			
		||||
    toggleLocalVideoMuted,
 | 
			
		||||
    toggleMicrophoneMuted,
 | 
			
		||||
  } = useGroupCall(client, roomId);
 | 
			
		||||
 | 
			
		||||
  const content = () => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return <LoadingErrorView error={error} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (loading) {
 | 
			
		||||
      return <LoadingRoomView />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (entering) {
 | 
			
		||||
      return <EnteringRoomView />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!entered) {
 | 
			
		||||
      return (
 | 
			
		||||
    <div className={styles.room}>
 | 
			
		||||
      {!loading && room && (
 | 
			
		||||
        <Header>
 | 
			
		||||
          <LeftNav />
 | 
			
		||||
          <CenterNav>
 | 
			
		||||
            <h3>{room.name}</h3>
 | 
			
		||||
          </CenterNav>
 | 
			
		||||
          <RightNav>
 | 
			
		||||
            {!loading && room && joined && (
 | 
			
		||||
              <LayoutToggleButton
 | 
			
		||||
                title={layout === "spotlight" ? "Spotlight" : "Gallery"}
 | 
			
		||||
                layout={layout}
 | 
			
		||||
                onClick={toggleLayout}
 | 
			
		||||
        <RoomSetupView
 | 
			
		||||
          roomName={roomName}
 | 
			
		||||
          onInitLocalParticipant={initLocalParticipant}
 | 
			
		||||
          onEnter={enter}
 | 
			
		||||
          microphoneMuted={microphoneMuted}
 | 
			
		||||
          localVideoMuted={localVideoMuted}
 | 
			
		||||
          toggleLocalVideoMuted={toggleLocalVideoMuted}
 | 
			
		||||
          toggleMicrophoneMuted={toggleMicrophoneMuted}
 | 
			
		||||
        />
 | 
			
		||||
            )}
 | 
			
		||||
            <SettingsButton
 | 
			
		||||
              title={debug ? "Disable DevTools" : "Enable DevTools"}
 | 
			
		||||
              on={debug}
 | 
			
		||||
              onClick={() => setDebug((debug) => !debug)}
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return (
 | 
			
		||||
        <InRoomView
 | 
			
		||||
          roomName={roomName}
 | 
			
		||||
          microphoneMuted={microphoneMuted}
 | 
			
		||||
          localVideoMuted={localVideoMuted}
 | 
			
		||||
          toggleLocalVideoMuted={toggleLocalVideoMuted}
 | 
			
		||||
          toggleMicrophoneMuted={toggleMicrophoneMuted}
 | 
			
		||||
          participants={participants}
 | 
			
		||||
          onLeave={leave}
 | 
			
		||||
          groupCall={groupCall}
 | 
			
		||||
        />
 | 
			
		||||
          </RightNav>
 | 
			
		||||
        </Header>
 | 
			
		||||
      )}
 | 
			
		||||
      {loading && (
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return <div className={styles.room}>{content()}</div>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function LoadingRoomView() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className={styles.centerMessage}>
 | 
			
		||||
        <p>Loading room...</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {error && <div className={styles.centerMessage}>{error.message}</div>}
 | 
			
		||||
      {!loading && room && !joined && (
 | 
			
		||||
        <JoinRoom
 | 
			
		||||
          manager={manager}
 | 
			
		||||
          joining={joining}
 | 
			
		||||
          joinCall={joinCall}
 | 
			
		||||
          toggleMuteVideo={toggleMuteVideo}
 | 
			
		||||
          toggleMuteAudio={toggleMuteAudio}
 | 
			
		||||
          videoMuted={videoMuted}
 | 
			
		||||
          audioMuted={audioMuted}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {!loading && room && joined && participants.length === 0 && (
 | 
			
		||||
        <div className={styles.centerMessage}>
 | 
			
		||||
          <p>Waiting for other participants...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {!loading && room && joined && participants.length > 0 && (
 | 
			
		||||
        <VideoGrid participants={participants} layout={layout} />
 | 
			
		||||
      )}
 | 
			
		||||
      {!loading && room && joined && (
 | 
			
		||||
        <div className={styles.footer}>
 | 
			
		||||
          <MicButton muted={audioMuted} onClick={toggleMuteAudio} />
 | 
			
		||||
          <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
 | 
			
		||||
          <HangupButton onClick={leaveCall} />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {debug && <DevTools manager={manager} />}
 | 
			
		||||
    </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function RoomAuth({ onLoginAsGuest, error }) {
 | 
			
		||||
  const displayNameRef = useRef();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
 | 
			
		||||
  const onSubmitLoginForm = useCallback(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onLoginAsGuest(displayNameRef.current.value);
 | 
			
		||||
    },
 | 
			
		||||
    [onLoginAsGuest, location, history]
 | 
			
		||||
export function EnteringRoomView() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className={styles.centerMessage}>
 | 
			
		||||
        <p>Entering room...</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function LoadingErrorView({ error }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className={styles.centerMessage}>
 | 
			
		||||
        <ErrorMessage>{error.message}</ErrorMessage>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PermissionState = {
 | 
			
		||||
  Waiting: "waiting",
 | 
			
		||||
  Granted: "granted",
 | 
			
		||||
  Denied: "denied",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function RoomSetupView({
 | 
			
		||||
  roomName,
 | 
			
		||||
  onInitLocalParticipant,
 | 
			
		||||
  onEnter,
 | 
			
		||||
  microphoneMuted,
 | 
			
		||||
  localVideoMuted,
 | 
			
		||||
  toggleLocalVideoMuted,
 | 
			
		||||
  toggleMicrophoneMuted,
 | 
			
		||||
  groupCall,
 | 
			
		||||
}) {
 | 
			
		||||
  const videoRef = useRef();
 | 
			
		||||
  const [permissionState, setPermissionState] = useState(
 | 
			
		||||
    PermissionState.Waiting
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    onInitLocalParticipant()
 | 
			
		||||
      .then((localParticipant) => {
 | 
			
		||||
        if (videoRef.current) {
 | 
			
		||||
          videoRef.current.srcObject = localParticipant.usermediaStream;
 | 
			
		||||
          videoRef.current.play();
 | 
			
		||||
          setPermissionState(PermissionState.Granted);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {
 | 
			
		||||
        if (videoRef.current) {
 | 
			
		||||
          setPermissionState(PermissionState.Denied);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }, [onInitLocalParticipant]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Header>
 | 
			
		||||
        <LeftNav />
 | 
			
		||||
        <CenterNav>
 | 
			
		||||
          <h3>{roomName}</h3>
 | 
			
		||||
        </CenterNav>
 | 
			
		||||
      </Header>
 | 
			
		||||
      <Content>
 | 
			
		||||
        <Center>
 | 
			
		||||
          <Modal>
 | 
			
		||||
            <h2>Login As Guest</h2>
 | 
			
		||||
            <form onSubmit={onSubmitLoginForm}>
 | 
			
		||||
              <FieldRow>
 | 
			
		||||
                <InputField
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  ref={displayNameRef}
 | 
			
		||||
                  placeholder="Display Name"
 | 
			
		||||
                  label="Display Name"
 | 
			
		||||
                  autoCorrect="off"
 | 
			
		||||
                  autoCapitalize="none"
 | 
			
		||||
                />
 | 
			
		||||
              </FieldRow>
 | 
			
		||||
              {error && (
 | 
			
		||||
                <FieldRow>
 | 
			
		||||
                  <ErrorMessage>{error.message}</ErrorMessage>
 | 
			
		||||
                </FieldRow>
 | 
			
		||||
              )}
 | 
			
		||||
              <FieldRow rightAlign>
 | 
			
		||||
                <Button type="submit">Login as guest</Button>
 | 
			
		||||
              </FieldRow>
 | 
			
		||||
            </form>
 | 
			
		||||
            <Info>
 | 
			
		||||
              <Link
 | 
			
		||||
                to={{
 | 
			
		||||
                  pathname: "/login",
 | 
			
		||||
                  state: location.state,
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Sign in
 | 
			
		||||
              </Link>
 | 
			
		||||
              {" or "}
 | 
			
		||||
              <Link
 | 
			
		||||
                to={{
 | 
			
		||||
                  pathname: "/register",
 | 
			
		||||
                  state: location.state,
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Create account
 | 
			
		||||
              </Link>
 | 
			
		||||
            </Info>
 | 
			
		||||
          </Modal>
 | 
			
		||||
        </Center>
 | 
			
		||||
      </Content>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function JoinRoom({
 | 
			
		||||
  joining,
 | 
			
		||||
  joinCall,
 | 
			
		||||
  manager,
 | 
			
		||||
  toggleMuteVideo,
 | 
			
		||||
  toggleMuteAudio,
 | 
			
		||||
  videoMuted,
 | 
			
		||||
  audioMuted,
 | 
			
		||||
}) {
 | 
			
		||||
  const videoRef = useRef();
 | 
			
		||||
  const [hasPermissions, setHasPermissions] = useState(false);
 | 
			
		||||
  const [needsPermissions, setNeedsPermissions] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    manager
 | 
			
		||||
      .getLocalVideoStream()
 | 
			
		||||
      .then((stream) => {
 | 
			
		||||
        if (videoRef.current) {
 | 
			
		||||
          videoRef.current.srcObject = stream;
 | 
			
		||||
          videoRef.current.play();
 | 
			
		||||
          setHasPermissions(true);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {
 | 
			
		||||
        if (videoRef.current) {
 | 
			
		||||
          setNeedsPermissions(true);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }, [manager]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
      <div className={styles.joinRoom}>
 | 
			
		||||
        <div className={styles.preview}>
 | 
			
		||||
        {needsPermissions && (
 | 
			
		||||
          {permissionState === PermissionState.Denied && (
 | 
			
		||||
            <p className={styles.webcamPermissions}>
 | 
			
		||||
              Webcam permissions needed to join the call.
 | 
			
		||||
            </p>
 | 
			
		||||
          )}
 | 
			
		||||
          <video ref={videoRef} muted playsInline disablePictureInPicture />
 | 
			
		||||
        </div>
 | 
			
		||||
      {hasPermissions && (
 | 
			
		||||
        {permissionState === PermissionState.Granted && (
 | 
			
		||||
          <div className={styles.previewButtons}>
 | 
			
		||||
          <MicButton muted={audioMuted} onClick={toggleMuteAudio} />
 | 
			
		||||
          <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
 | 
			
		||||
            <MicButton
 | 
			
		||||
              muted={microphoneMuted}
 | 
			
		||||
              onClick={toggleMicrophoneMuted}
 | 
			
		||||
            />
 | 
			
		||||
            <VideoButton
 | 
			
		||||
              enabled={localVideoMuted}
 | 
			
		||||
              onClick={toggleLocalVideoMuted}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      <Button disabled={!hasPermissions || joining} onClick={joinCall}>
 | 
			
		||||
        Join Call
 | 
			
		||||
        <Button
 | 
			
		||||
          disabled={permissionState !== PermissionState.Granted}
 | 
			
		||||
          onClick={onEnter}
 | 
			
		||||
        >
 | 
			
		||||
          Enter Call
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InRoomView({
 | 
			
		||||
  roomName,
 | 
			
		||||
  microphoneMuted,
 | 
			
		||||
  localVideoMuted,
 | 
			
		||||
  toggleLocalVideoMuted,
 | 
			
		||||
  toggleMicrophoneMuted,
 | 
			
		||||
  participants,
 | 
			
		||||
  onLeave,
 | 
			
		||||
  groupCall,
 | 
			
		||||
}) {
 | 
			
		||||
  const [debugMode, toggleDebugMode] = useDebugMode();
 | 
			
		||||
  const [roomLayout, toggleRoomLayout] = useRoomLayout();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Header>
 | 
			
		||||
        <LeftNav />
 | 
			
		||||
        <CenterNav>
 | 
			
		||||
          <h3>{roomName}</h3>
 | 
			
		||||
        </CenterNav>
 | 
			
		||||
        <RightNav>
 | 
			
		||||
          <LayoutToggleButton
 | 
			
		||||
            title={roomLayout === "spotlight" ? "Spotlight" : "Gallery"}
 | 
			
		||||
            layout={roomLayout}
 | 
			
		||||
            onClick={toggleRoomLayout}
 | 
			
		||||
          />
 | 
			
		||||
          <SettingsButton
 | 
			
		||||
            title={debugMode ? "Disable DevTools" : "Enable DevTools"}
 | 
			
		||||
            onClick={toggleDebugMode}
 | 
			
		||||
          />
 | 
			
		||||
        </RightNav>
 | 
			
		||||
      </Header>
 | 
			
		||||
      {participants.length === 0 ? (
 | 
			
		||||
        <div className={styles.centerMessage}>
 | 
			
		||||
          <p>Waiting for other participants...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <VideoGrid participants={participants} layout={roomLayout} />
 | 
			
		||||
      )}
 | 
			
		||||
      <div className={styles.footer}>
 | 
			
		||||
        <MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} />
 | 
			
		||||
        <VideoButton
 | 
			
		||||
          enabled={localVideoMuted}
 | 
			
		||||
          onClick={toggleLocalVideoMuted}
 | 
			
		||||
        />
 | 
			
		||||
        <HangupButton onClick={onLeave} />
 | 
			
		||||
      </div>
 | 
			
		||||
      {debugMode && <DevTools groupCall={groupCall} />}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -422,7 +422,7 @@ export function VideoGrid({ participants, layout }) {
 | 
			
		|||
 | 
			
		||||
      for (const tile of tiles) {
 | 
			
		||||
        let participant = participants.find(
 | 
			
		||||
          (participant) => participant.userId === tile.key
 | 
			
		||||
          (participant) => participant.member.userId === tile.key
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let remove = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -442,7 +442,7 @@ export function VideoGrid({ participants, layout }) {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        newTiles.push({
 | 
			
		||||
          key: participant.userId,
 | 
			
		||||
          key: participant.member.userId,
 | 
			
		||||
          participant,
 | 
			
		||||
          remove,
 | 
			
		||||
          presenter,
 | 
			
		||||
| 
						 | 
				
			
			@ -450,13 +450,13 @@ export function VideoGrid({ participants, layout }) {
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      for (const participant of participants) {
 | 
			
		||||
        if (newTiles.some(({ key }) => participant.userId === key)) {
 | 
			
		||||
        if (newTiles.some(({ key }) => participant.member.userId === key)) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Added tiles
 | 
			
		||||
        newTiles.push({
 | 
			
		||||
          key: participant.userId,
 | 
			
		||||
          key: participant.member.userId,
 | 
			
		||||
          participant,
 | 
			
		||||
          remove: false,
 | 
			
		||||
          presenter: layout === "spotlight" && participant.activeSpeaker,
 | 
			
		||||
| 
						 | 
				
			
			@ -719,17 +719,19 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
 | 
			
		|||
  const videoRef = useRef();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (participant.stream) {
 | 
			
		||||
      if (participant.local) {
 | 
			
		||||
    if (participant.usermediaStream) {
 | 
			
		||||
      // Mute the local video
 | 
			
		||||
      // TODO: Should GroupCallParticipant have a local field?
 | 
			
		||||
      if (!participant.call) {
 | 
			
		||||
        videoRef.current.muted = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      videoRef.current.srcObject = participant.stream;
 | 
			
		||||
      videoRef.current.srcObject = participant.usermediaStream;
 | 
			
		||||
      videoRef.current.play();
 | 
			
		||||
    } else {
 | 
			
		||||
      videoRef.current.srcObject = null;
 | 
			
		||||
    }
 | 
			
		||||
  }, [participant.stream]);
 | 
			
		||||
  }, [participant.usermediaStream]);
 | 
			
		||||
 | 
			
		||||
  // Firefox doesn't respect the disablePictureInPicture attribute
 | 
			
		||||
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
 | 
			
		||||
| 
						 | 
				
			
			@ -738,15 +740,15 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
 | 
			
		|||
    <animated.div className={styles.participantTile} style={style} {...rest}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames(styles.participantName, {
 | 
			
		||||
          [styles.speaking]: participant.speaking,
 | 
			
		||||
          [styles.speaking]: participant.usermediaStream?.speaking,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        {participant.speaking ? (
 | 
			
		||||
        {participant.usermediaStream?.speaking ? (
 | 
			
		||||
          <MicIcon />
 | 
			
		||||
        ) : participant.audioMuted ? (
 | 
			
		||||
        ) : participant.isAudioMuted() ? (
 | 
			
		||||
          <MuteMicIcon className={styles.muteMicIcon} />
 | 
			
		||||
        ) : null}
 | 
			
		||||
        <span>{participant.displayName}</span>
 | 
			
		||||
        <span>{participant.member.rawDisplayName}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      {participant.videoMuted && (
 | 
			
		||||
        <DisableVideoIcon
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue