From 1c55850296ecd14c2f19d5227baf89a9dac8a9cf Mon Sep 17 00:00:00 2001
From: Florian Raith <37345813+FlorianRaith@users.noreply.github.com>
Date: Sun, 3 Sep 2023 12:37:48 +0200
Subject: [PATCH] Add setting to mute everyone (#79)

* Add setting to mute everyone

* Format code
---
 src/components/Browser.vue         |  1 -
 src/composables/channel/browser.ts |  4 --
 src/composables/channel/channel.ts | 81 ++++++++++++++++++++++++++++--
 src/composables/channel/webcam.ts  | 16 +++++-
 src/views/RoomView.vue             | 58 ++++++++++++++++++++-
 5 files changed, 149 insertions(+), 11 deletions(-)

diff --git a/src/components/Browser.vue b/src/components/Browser.vue
index a597d76..5f9f666 100644
--- a/src/components/Browser.vue
+++ b/src/components/Browser.vue
@@ -291,7 +291,6 @@
   watch(
     () => channel.browser.browserStream,
     (stream) => {
-      console.log(stream);
       if (stream) {
         isStreaming.value = true;
         browserVideo.value!.srcObject = stream;
diff --git a/src/composables/channel/browser.ts b/src/composables/channel/browser.ts
index 4732a13..22a593d 100644
--- a/src/composables/channel/browser.ts
+++ b/src/composables/channel/browser.ts
@@ -16,11 +16,9 @@ export const useBrowser = () => {
   const browserPeer: Peer = new Peer();
 
   browserPeer.on('call', (call) => {
-    console.log('calling');
     call.answer();
 
     call.on('stream', (stream) => {
-      console.log('streaming');
       browserStream.value = stream;
     });
   });
@@ -33,8 +31,6 @@ export const useBrowser = () => {
     socket = newSocket;
 
     socket.on('open-website', (newPeerId: string) => {
-      console.log('connecting to peer', newPeerId);
-
       peerId.value = newPeerId;
       loadBrowserStream();
     });
diff --git a/src/composables/channel/channel.ts b/src/composables/channel/channel.ts
index b5b6ec6..aabafc5 100644
--- a/src/composables/channel/channel.ts
+++ b/src/composables/channel/channel.ts
@@ -59,11 +59,12 @@ export interface Teacher extends ChannelUser {
  * @property students - The list of students that are connected to the room
  */
 export interface JoinRoomResult {
-  room: Room;
+  room: Room & { channelId: string };
   browserPeerId: string;
   browserUrl: string;
   teacher: Teacher;
   students: Student[];
+  settings: Settings;
 }
 
 /**
@@ -88,6 +89,15 @@ export interface ChannelState {
   hasName: boolean;
   notes: Notes | null;
   whiteboard: Whiteboard | null;
+  settings: Settings;
+}
+
+/**
+ * The settings object that is used to represent the settings of the channel.
+ * @property globalMute - Whether all students are muted or not
+ */
+export interface Settings {
+  globalMute: boolean;
 }
 
 /**
@@ -112,6 +122,9 @@ export const useChannel = defineStore('channel', () => {
     hasName: false,
     notes: null,
     whiteboard: null,
+    settings: {
+      globalMute: false,
+    },
   } as ChannelState);
 
   /**
@@ -191,6 +204,13 @@ export const useChannel = defineStore('channel', () => {
     });
   }
 
+  function toggleGlobalMute(): void {
+    socket?.emit('update-settings', {
+      ...state.settings,
+      globalMute: !state.settings.globalMute,
+    });
+  }
+
   /**
    * Retrieves a user (either teacher or student) by their ID.
    * @param id - The ID of the user to retrieve.
@@ -281,7 +301,7 @@ export const useChannel = defineStore('channel', () => {
       roomId: room.id,
     };
 
-    socket?.emit('open-room', payload, async (result: any) => {
+    socket?.emit('open-room', payload, async (result: JoinRoomResult) => {
       state.connected = true;
       state.channelId = result.room.channelId;
       state.clientId = socket?.id || '';
@@ -295,6 +315,7 @@ export const useChannel = defineStore('channel', () => {
       };
       state.hasName = true;
       state.whiteboard = new Whiteboard(socket!, result.room.whiteboardCanvas);
+      state.settings = result.settings;
 
       browser.peerId.value = '';
       browser.setUrl('www.google.com');
@@ -332,11 +353,15 @@ export const useChannel = defineStore('channel', () => {
    */
   async function joinAsStudent(id: string, password?: string): Promise<void> {
     await connect();
-    return join(id, 'join-room-as-student', {
+    await join(id, 'join-room-as-student', {
       channelId: id,
       name: 'Verbinden...',
       password,
     });
+
+    if (state.settings.globalMute && currentUser().audio) {
+      webcam.toggleAudio();
+    }
   }
 
   /**
@@ -364,6 +389,7 @@ export const useChannel = defineStore('channel', () => {
         state.room = data.room;
         state.hasName = false;
         state.whiteboard = new Whiteboard(socket!, data.room.whiteboardCanvas);
+        state.settings = data.settings;
 
         browser.peerId.value = data.browserPeerId;
         browser.setUrl(data.browserUrl);
@@ -442,6 +468,9 @@ export const useChannel = defineStore('channel', () => {
       state.students = [];
       state.teacher = null;
       state.hasName = false;
+      state.settings = {
+        globalMute: false,
+      };
 
       if (router.currentRoute.value.name === 'room') {
         if (auth.isLoggedIn) {
@@ -541,6 +570,51 @@ export const useChannel = defineStore('channel', () => {
         }
       },
     );
+
+    socket.on('disable-audio-for', (id: string) => {
+      const student = studentById(id);
+
+      if (student) {
+        student.audio = false;
+      }
+    });
+
+    socket.on('update-settings', (settings: Settings) => {
+      const oldSettings = { ...state.settings };
+      state.settings = settings;
+
+      if (!oldSettings.globalMute && settings.globalMute) {
+        state.students
+          .filter((s) => s.audio)
+          .forEach((s) => webcam.disableAudioFor(s));
+
+        if (isStudent(currentUser())) {
+          alerts.primary(
+            'Stummschaltung',
+            'Der Lehrer hat alle Mikrofone stummgeschaltet.',
+          );
+        } else {
+          alerts.primary(
+            'Stummschaltung',
+            'Sie haben alle Mikrofone stummgeschaltet.',
+          );
+        }
+      }
+
+      if (oldSettings.globalMute && !settings.globalMute) {
+        if (isStudent(currentUser())) {
+          alerts.primary(
+            'Stummschaltung',
+            'Der Lehrer hat erlaubt alle Mikrofone wieder zu aktivieren.',
+          );
+        } else {
+          alerts.primary(
+            'Stummschaltung',
+            'Sie haben erlaubt alle Mikrofone wieder zu aktivieren.',
+          );
+        }
+      }
+    });
   }
 
   return {
@@ -564,5 +638,6 @@ export const useChannel = defineStore('channel', () => {
     loadNotes,
     toggleHandSignal,
     updatePermission,
+    toggleGlobalMute,
   };
 });
diff --git a/src/composables/channel/webcam.ts b/src/composables/channel/webcam.ts
index 9c77cf7..f28fda4 100644
--- a/src/composables/channel/webcam.ts
+++ b/src/composables/channel/webcam.ts
@@ -1,7 +1,7 @@
 import Peer from 'peerjs';
 import { reactive } from 'vue';
 import { Socket } from 'socket.io-client';
-import { ChannelUser } from '@/composables/channel/channel';
+import { ChannelUser, Student } from '@/composables/channel/channel';
 
 export const useWebcam = () => {
   let webcamsLoaded = false;
@@ -121,6 +121,19 @@ export const useWebcam = () => {
     });
   }
 
+  function disableAudioFor(student: Student): void {
+    const stream = streams[student.id];
+
+    if (!student.audio) {
+      return;
+    }
+
+    student.audio = false;
+
+    stream?.getAudioTracks().forEach((track) => (track.enabled = false));
+    socket?.emit('disable-audio-for', { userId: student.id });
+  }
+
   /**
    * Stops the webcam streaming of the current user and removes all other users' streams.
    */
@@ -145,6 +158,7 @@ export const useWebcam = () => {
     getStream,
     toggleVideo,
     toggleAudio,
+    disableAudioFor,
     stop,
   };
 };
diff --git a/src/views/RoomView.vue b/src/views/RoomView.vue
index 8a3b82a..c12367d 100644
--- a/src/views/RoomView.vue
+++ b/src/views/RoomView.vue
@@ -82,7 +82,39 @@
               <i class="fa fa-link me-1"></i>
               Teilen
             </button>
+            <button
+              v-if="
+                channel.state.teacher && channel.isSelf(channel.state.teacher)
+              "
+              class="btn btn-outline-primary btn-sm"
+              data-bs-toggle="dropdown"
+              aria-expanded="false"
+            >
+              <i class="fa fa-gear"></i>
+            </button>
+
+            <ul class="dropdown-menu">
+              <li>
+                <button
+                  class="dropdown-item"
+                  @click="channel.toggleGlobalMute()"
+                  v-text="
+                    !channel.state.settings.globalMute
+                      ? 'Schüler stummschalten'
+                      : 'Stummschaltung aufheben'
+                  "
+                ></button>
+              </li>
+            </ul>
           </div>
+
+          <small
+            v-if="channel.state.settings.globalMute"
+            class="text-warning mt-2"
+          >
+            <i class="fa fa-circle-exclamation me-1"></i>
+            Alle Schüler sind stummgeschaltet
+          </small>
         </div>
 
         <div
@@ -222,7 +254,13 @@
 
             <button
               type="button"
-              class="btn text-primary w-100"
+              class="btn w-100"
+              :class="
+                channel.state.settings.globalMute &&
+                channel.isStudent(channel.currentUser())
+                  ? 'text-danger'
+                  : 'text-primary'
+              "
               @click="toggleAudio()"
             >
               <i
@@ -264,7 +302,9 @@
   import BrowserComponent from '@/components/Browser.vue';
   import { ask } from '@/composables/prompt';
   import { useRouter } from 'vue-router';
+  import { useAlerts } from '@/composables/alerts';
 
+  const alerts = useAlerts();
   const auth = useAuth();
   const channel = useChannel();
   const router = useRouter();
@@ -335,8 +375,19 @@
   /**
    * Function that toggles the audio transmission status through the channel using 'channel.toggleAudio()'.
    */
-
   function toggleAudio() {
+    if (
+      channel.state.settings.globalMute &&
+      channel.isStudent(channel.currentUser())
+    ) {
+      alerts.danger(
+        'Du kannst dein Mikrofon nicht aktivieren',
+        'Der Lehrer hat alle Mikrofone deaktiviert.',
+      );
+
+      return;
+    }
+
     channel.webcam.toggleAudio();
   }
 
@@ -490,14 +541,17 @@
   #webcam-wrapper {
     scrollbar-color: #999 #333;
   }
+
   #webcam-wrapper::-webkit-scrollbar {
     width: 10px;
   }
+
   #webcam-wrapper::-webkit-scrollbar-thumb {
     background: #999;
     border-radius: 12px;
     margin: 1rem;
   }
+
   #webcam-wrapper::-webkit-scrollbar-track {
     background: #333;
     border-radius: 12px;
-- 
GitLab