Browse Source

Merge branch 'master' into dev-vj

Vijayakrishnan Krishnan 5 years ago
parent
commit
45edad30ca

+ 10 - 2
app/Http/Controllers/MeetingCenterController.php

@@ -6,6 +6,8 @@ use App\Models\Meeting;
 use App\Models\MeetingParticipant;
 use App\Models\MeetingParticipant;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use App\Models\Pro;
 use App\Models\Pro;
+use App\Models\AppSession;
+use App\HttpModels\ProModel;
 
 
 class MeetingCenterController extends Controller
 class MeetingCenterController extends Controller
 {
 {
@@ -15,7 +17,13 @@ class MeetingCenterController extends Controller
         if($fragment) {
         if($fragment) {
             $page = '/' . $fragment;
             $page = '/' . $fragment;
         }
         }
-        return view('mc', compact('page'));
+
+        $sessionKey = $request->cookie("sessionKey");
+
+        $appSession = AppSession::where("session_key",$sessionKey)->first();
+        $user = new ProModel($appSession->pro);
+
+        return view('mc', compact('page','user'));
     }
     }
 
 
-}
+}

+ 23 - 0
app/HttpModels/LobbyModel.php

@@ -0,0 +1,23 @@
+<?php
+namespace App\HttpModels;
+
+use App\Models\Lobby;
+
+class LobbyModel {
+    public $uid;
+    public $name;
+    public $isStrangerAccessible;
+    public $isClientAccessible;
+    public $meetings = [];
+
+    public function __construct(Lobby $lobby)
+    {
+        $this->uid = $lobby->uid;
+        $this->name = $lobby->name;
+        $this->isStrangerAccessible = $lobby->is_stranger_accessible;
+        $this->isClientAccessible = $lobby->is_client_accessible;
+        foreach ($lobby->meetings()->where('meeting.is_active',true)->get() as $meeting) {
+            $this->meetings[] = new MeetingModel($meeting);
+        }
+    }
+}

+ 14 - 0
app/HttpModels/MeetingModel.php

@@ -0,0 +1,14 @@
+<?php
+namespace App\HttpModels;
+
+use App\Models\Meeting;
+
+class MeetingModel {
+    public $uid;
+    public $name;
+    
+    public function __construct(Meeting $meeting) {
+        $this->uid = $meeting->uid;
+        $this->name = $meeting->title;
+    }
+}

+ 25 - 0
app/HttpModels/ProModel.php

@@ -0,0 +1,25 @@
+<?php
+namespace App\HttpModels;
+
+use App\Models\Pro;
+
+class ProModel {
+    public $uid;
+    public $name;
+    public $type;
+    public $avatarFile;
+    public $is_active_and_visible;
+    public $lobbies = [];
+
+    public function __construct(Pro $pro)
+    {
+        $this->uid = $pro->uid;
+        $this->name = $pro->name_display;
+        $this->type = $pro->pro_type;
+        $this->avatarFile = $pro->profile_picture_base64;
+        $this->is_active_and_visible = $pro->is_currently_clocked_in;
+        foreach ($pro->lobbies()->where('lobby.is_active',true)->get() as $lobby) {
+            $this->lobbies[] = new LobbyModel($lobby);
+        }
+    }
+}

+ 5 - 2
app/Models/AppSession.php

@@ -9,5 +9,8 @@ class AppSession extends Model
 
 
     protected $table = "app_session";
     protected $table = "app_session";
 
 
-    //
-}
+    public function pro()
+    {
+        return $this->belongsTo(Pro::class);
+    }
+}

+ 16 - 0
app/Models/Lobby.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Lobby extends Model
+{
+    
+    protected $table = "lobby";
+
+    public function meetings()
+    {
+        return $this->hasMany(Meeting::class);
+    }
+}

+ 5 - 2
app/Models/Pro.php

@@ -9,5 +9,8 @@ class Pro extends Model
 
 
     protected $table = "pro";
     protected $table = "pro";
 
 
-    //
-}
+    public function lobbies()
+    {
+        return $this->belongsToMany(Lobby::class, 'lobby_pro', 'pro_id', 'lobby_id');
+    }
+}

+ 0 - 10
resources/js/app.js

@@ -32,16 +32,6 @@ files.forEach(file => {
     Vue.component(component, () => import(`${file}`))
     Vue.component(component, () => import(`${file}`))
 })
 })
 
 
-import VueSocketIO from "vue-socket.io"
-import SocketIO from "socket.io-client"
-
-Vue.use(
-    new VueSocketIO({
-        debug: false,
-        connection: SocketIO(process.env.MIX_SOCKET_SERVICE_URL)
-    })
-)
-
 let app = new Vue({
 let app = new Vue({
     el: '#meetingsApp',
     el: '#meetingsApp',
     store,
     store,

+ 117 - 29
resources/js/components/pages/MeetingsAppRoot.vue

@@ -1,46 +1,134 @@
 <template>
 <template>
     <v-app>
     <v-app>
         <v-expansion-panels v-model="active_panel" tile hover multiple>
         <v-expansion-panels v-model="active_panel" tile hover multiple>
-                <v-expansion-panel>
-                    <v-expansion-panel-header>Lobby List</v-expansion-panel-header>
-                        <v-expansion-panel-content>
-                            <lobby-list></lobby-list>
-                        </v-expansion-panel-content>
-                </v-expansion-panel>
-
-                <v-expansion-panel>
-                    <v-expansion-panel-header>Meeting Room</v-expansion-panel-header>
-                        <v-expansion-panel-content>
-                            <meeting-room></meeting-room>
-                        </v-expansion-panel-content>
-                </v-expansion-panel>
-
-                <v-expansion-panel>
-                    <v-expansion-panel-header>Messenger</v-expansion-panel-header>
-                        <v-expansion-panel-content>
-                            <messenger></messenger>
-                        </v-expansion-panel-content>
-                </v-expansion-panel>
+            <v-expansion-panel class="no-v-padding">
+                <v-expansion-panel-header>Lobby List</v-expansion-panel-header>
+                <v-expansion-panel-content>
+                    <lobby-list></lobby-list>
+                </v-expansion-panel-content>
+            </v-expansion-panel>
+
+            <v-expansion-panel>
+                <v-expansion-panel-header>Meeting Room</v-expansion-panel-header>
+                <v-expansion-panel-content>
+                    <meeting-room></meeting-room>
+                </v-expansion-panel-content>
+            </v-expansion-panel>
+
+            <v-expansion-panel>
+                <v-expansion-panel-header>Messenger</v-expansion-panel-header>
+                <v-expansion-panel-content>
+                    <messenger></messenger>
+                </v-expansion-panel-content>
+            </v-expansion-panel>
+
+            <v-expansion-panel>
+                <v-expansion-panel-header>Settings</v-expansion-panel-header>
+                <v-expansion-panel-content>
+                    <settings></settings>
+                </v-expansion-panel-content>
+            </v-expansion-panel>
         </v-expansion-panels>
         </v-expansion-panels>
+        <call-bubble :call="incomingCallDetails"></call-bubble>
     </v-app>
     </v-app>
 </template>
 </template>
 
 
 <script>
 <script>
+import { mapState } from "vuex";
+import VueSocketIO from "vue-socket.io";
+import SocketIO from "socket.io-client";
+
+Vue.use(
+    new VueSocketIO({
+        debug: true,
+        connection: SocketIO(process.env.MIX_SOCKET_SERVICE_URL)
+    })
+);
+
 export default {
 export default {
-    data(){
+    props: {
+        userProp: {
+            type: Object,
+            required: true
+        }
+    },
+    data() {
         return {
         return {
-            active_panel: [0,1,2]
+            active_panel: [0, 1, 2],
+            incomingCallDetails: null,
+            lobbies: [
+                {
+                    lobbyUid: "1",
+                    name: "Test Lobby",
+                    description: null,
+                    isStrangerAccessible: true,
+                    isClientAccessible: true
+                }
+            ]
+        };
+    },
+    computed: {
+        ...mapState(["user"])
+    },
+    sockets: {
+        incomingCall: function(data) {
+            let self = this;
+
+            if (!data.time_limit) {
+                data.time_limit = 10;
+            }
+
+            self.incomingCallDetails = data;
+
+            let timer = setInterval(() => {
+                if (self.incomingCallDetails.time_limit > 0) {
+                    self.incomingCallDetails.time_limit--;
+
+                    if (self.incomingCallDetails.time_limit == 0) {
+                        clearInterval(timer);
+                        console.log("TIME IS OUT!");
+                    }
+                }
+            }, 1000);
         }
         }
+    },
+    methods: {
+        //
+    },
+    mounted() {
+        let lobbies = this.userProp.lobbies;
+        delete this.userProp.lobbies;
+
+        this.$socket.emit("userData", { user: this.userProp })
+
+        lobbies.map(lobby => {
+            lobby.selected_meeting = null
+            lobby.meetings.map(meeting => {
+                meeting.active_members = []
+                meeting.pros_online = []
+            })
+        })
+
+        this.$store.commit("setInitialUser", this.userProp)
+        this.$store.commit("setLobbies", lobbies)
+
+        this.$socket.emit("lobbyDataRequest", lobbies)
+
+        this.sockets.subscribe("lobby-data", data => {
+            for (let meeting of data.meetings) {
+                this.$store.commit("setLobbyActivity", meeting)
+            }
+        })
     }
     }
-}
+};
 </script>
 </script>
 
 
 <style>
 <style>
-    .v-expansion-panel--active {
-        height: 100%;
-    }
+.v-expansion-panel--active {
+    height: 100%;
+}
 
 
-    .v-expansion-panel-content__wrap {
-        padding: 24px 16px;
-    }
+.v-expansion-panel-content__wrap {
+    padding: 24px 16px;
+}
 </style>
 </style>

+ 95 - 2
resources/js/components/partials/LobbyList.vue

@@ -1,11 +1,104 @@
 <template>
 <template>
   <div>
   <div>
-    This is a Lobby List
+    <v-card v-for="lobby in lobbies" flat :key="lobby.id" :loading="loading">
+
+    <div class="d-flex flex-row justify-content-between">
+        <div>
+            <v-card-title>{{lobby.name}}</v-card-title>
+
+            <v-card-text>
+                <div>{{lobby.description || 'No description available.'}}</div>
+            </v-card-text>
+        </div>
+    </div>
+
+    <v-divider class="mx-4"></v-divider>
+
+    <v-list dense v-if="meetingListFiltered(lobby.meetings).length">
+        <v-container class="overflow-y-auto px-0 pt-0">
+            <v-subheader class="d-flex flex-row justify-content-between">
+                <span>Meetings ({{meetingListFiltered(lobby.meetings).length}})</span>
+            </v-subheader>
+        </v-container>
+        <v-container style="height: 210px" class="overflow-y-auto">
+            <v-row>
+                <v-list-item-group class="w-100" v-model="lobby.selected_meeting" color="primary">
+                    <v-list-item v-for="(item, i) in meetingListFiltered(lobby.meetings)" :key="i" :value="item">
+                        <v-list-item-icon>
+                            <v-icon v-text="'mdi-clock'"></v-icon>
+                        </v-list-item-icon>
+                        <v-list-item-content>
+                            <v-list-item-title v-text="item.name"></v-list-item-title>
+                            <v-tooltip top v-if="item.pros_online.length > 0">
+                                <template v-slot:activator="{ on }">
+                                    <v-list-item-subtitle
+                                        v-on="on"
+                                        v-text="item.pros_online.length > 0 ? `PROs Connected (${item.pros_online.length})` : 'No PROs Connected'"
+                                    ></v-list-item-subtitle>
+                                </template>
+                                <span v-for="pro in item.pros_online" :key="pro.UID">
+                                    {{pro.name}}
+                                    <br />
+                                </span>
+                            </v-tooltip>
+                            <v-list-item-subtitle v-else v-text="item.pros_online.length > 0 ? `PROs Connected (${item.pros_online.length})` : 'No PROs Connected'"></v-list-item-subtitle>
+                        </v-list-item-content>
+                        <v-list-item-action>
+                            <v-list-item-action-text :class="{'status':true, 'is-active':item.active_members.length > 0}" v-text="'Online: ' + item.active_members.length"></v-list-item-action-text>
+                        </v-list-item-action>
+                    </v-list-item>
+                </v-list-item-group>
+            </v-row>
+        </v-container>
+    </v-list>
+
+    <v-list v-else>
+        <v-subheader class="d-flex flex-row justify-content-between">
+            <span>Meetings ({{meetingListFiltered(lobby.meetings).length}})</span>
+        </v-subheader>
+        <v-container style="height: 210px" class="overflow-y-auto">
+            <v-row>
+                <v-list-item-group class="w-100 text-center" color="primary">
+                    <v-list-item>
+                        <v-list-item-content>
+                            <v-list-item-title muted v-text="'No Meetings found for this lobby.'"></v-list-item-title>
+                        </v-list-item-content>
+                    </v-list-item>
+                </v-list-item-group>
+            </v-row>
+        </v-container>
+    </v-list>
+
+    <transition-expand>
+        <div v-if="meetingListFiltered(lobby.meetings).length">
+            <v-divider class="mx-4"></v-divider>
+
+            <v-card-actions>
+                <v-btn :disabled="lobby.selected_meeting !== 0 && !lobby.selected_meeting" color="deep-purple accent-4" text @click="joinMeeting(lobby)">Join</v-btn>
+                <v-btn :disabled="lobby.selected_meeting !== 0 && !lobby.selected_meeting" color="deep-purple accent-4" text @click="inviteToMeeting(lobby)">Invite</v-btn>
+            </v-card-actions>
+        </div>
+    </transition-expand>
+</v-card>
   </div>
   </div>
 </template>
 </template>
 
 
 <script>
 <script>
-export default {
+import { mapState } from "vuex";
 
 
+export default {
+  data(){
+    return {
+      loading: false
+    }
+  },
+  computed: {
+    ...mapState(["lobbies", "user"]),
+  },
+  methods: {
+    meetingListFiltered(meetings) {
+      return meetings.filter((x) => x.pros_online.length == 0 || x.pros_online.findIndex((y) => y.uid == this.user.uid) !== -1)
+    }
+  }
 }
 }
 </script>
 </script>

+ 514 - 2
resources/js/components/partials/MeetingRoom.vue

@@ -1,11 +1,523 @@
 <template>
 <template>
-  <div>
-      This is a meeting room
+  <div class="h-100" v-if="meeting.passwordRequired ? meeting.passwordProvided : true">
+    <div v-if="accessDialogShown">Please allow access to camera and microphone</div>
+      <div class="h-100" ref="videos">
+          <transition-group tag="div" class="video-wrapper" name="tiles">
+              <grid-layout
+                  :layout="videoGrid"
+                  :key="uniqueId"
+                  :col-num="maxCols"
+                  :max-rows="maxRows"
+                  :row-height="rowHeight"
+                  :is-draggable="true"
+                  :is-resizable="true"
+                  :verticalCompact="true"
+                  :margin="[gridPadding, gridPadding]"
+                  :use-css-transforms="true"
+              >
+                  <grid-item v-for="(video, index) in videoGrid" :key="video.id" :id="video.id" :x="video.x" :y="video.y" :w="video.w" :h="video.h" :i="video.i">
+                      <v-btn :color="'#282e38'" fab large dark class="v-btn--kick" @click="kickParcitipant(video)" v-show="!video.video.self && user.type == 'associate'">
+                          <v-icon>mdi-account-remove</v-icon>
+                      </v-btn>
+                  </grid-item>
+              </grid-layout>
+          </transition-group>
+          <!-- <div v-for="(video, index) in videos" :id="video.id" :key="video.id" @click="switchToFullSize(video, index)" :class="{'selected-high': video.id == bigMembersCountSelectedId, 'selected-low': lowMembersCountSelectedId == video.id}" class="video-container">
+          </div>-->
+      </div>
+      <button class="unscale" @click="bigMembersCountSelectedId = null" v-show="bigMembersCountSelectedId !== null">
+          <i class="mdi mdi-close"></i>
+      </button>
+      <div style="display:none" ref="otContainer">
+          <!-- Container to catch OpenTok Elements before reattaching -->
+      </div>
+      <!-- <modal-questions ref="initialModal" @questionsAnswered="getToken" @ready="ready"></modal-questions> -->
   </div>
   </div>
 </template>
 </template>
 
 
 <script>
 <script>
+import { mapState } from "vuex";
+
+import VueGridLayout from "vue-grid-layout";
+
 export default {
 export default {
+  components: {
+    GridLayout: VueGridLayout.GridLayout,
+    GridItem: VueGridLayout.GridItem
+  },
+  data() {
+    return {
+        readyForUse: false,
+        uniqueId: Math.floor(Math.random() * Math.floor(10000)),
+        counter: 1,
+        openTokSession: null,
+        publisher: null,
+        screenPublisher: null,
+        subscribers: [],
+        publisherReady: false,
+        accessDialogShown: false,
+        videos: [],
+        videoGrid: [],
+        bigMembersCountSelectedId: null,
+        lowMembersCountSelectedId: null,
+        maxCols: 12,
+        maxRows: 4,
+        rowHeight: 240,
+        gridPadding: 8,
+        loadingInProgress: false
+    };
+  },
+  computed: {
+    ...mapState(["meeting", "user", "session"])
+  },
+  methods: {
+    disconnect() {
+        if (!this.openTokSession) return;
+        this.openTokSession.disconnect();
+        this.openTokSession.off();
+        this.videos = [];
+        this.publisher.destroy();
+        this.publisher = null;
+        this.subscribers = [];
+        this.publisherReady = false;
+        this.$store.commit("setSessionConnectivityState", false);
+        this.openTokSession = null;
+        if (this.screenPublisher) {
+            this.screenPublisher.destroy();
+            this.screenPublisher = null;
+            this.$store.commit("setScreenShareState", null);
+        }
+    },
+    getToken(data, isAssociate) {
+        if (this.meeting.scheduledDate && !this.meeting.startedAt && this.meeting.scheduledDate > new Date() && (this.user.type === "guest" || !confirm("Meeting not started. Start it now?")))
+            return;
+        data.meetingId = this.meeting.id;
+        data.userUid = this.user.UID;
+        $.ajax({
+            url: isAssociate ? "/associate/token" : "/token",
+            method: "POST",
+            headers: {
+                "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
+            },
+            data,
+            success: data => {
+                this.$refs.initialModal.hide();
+                if (!isAssociate) {
+                    const user = Object.assign({}, this.user);
+                    user.id = data.participantId;
+                    user.name = data.participantName;
+                    user.pin = data.participantPin;
+                    this.$store.commit("setUser", user);
+                }
+                this.initializeOpenTok(data.apiKey, data.sessionId, data.token);
+            },
+            error: jXhr => {
+                if (jXhr.responseJSON && jXhr.responseJSON.errorCode) {
+                    switch (jXhr.responseJSON.errorCode) {
+                        case "LM-1":
+                            alert("Meeting not started yet.");
+                            break;
+                        case "LM-2":
+                            alert("You was kicked from this meeting.");
+                            window.location = "/";
+                            break;
+                        case "LM-3":
+                            alert("Wrong password");
+                            this.$refs.initialModal.disableWaiting();
+                            break;
+                    }
+                } else {
+                    this.$refs.initialModal.disableWaiting();
+                    alert(getSingleError(jXhr));
+                }
+            }
+        });
+    },
+    createVideoContainer() {
+        let videoCont = {};
+        videoCont.id = `${this.uniqueId}_video_${this.counter++}`;
+        if (this.videos.length == 1 && !this.lowMembersCountSelectedId) {
+            this.lowMembersCountSelectedId = videoCont.id;
+        }
+
+        this.videos.push(videoCont);
+        this.adjustVideoContainers();
+
+        return videoCont;
+    },
+    adjustVideoContainers() {
+        this.videoGrid = [];
+
+        let windowHeight = window.innerHeight;
+        this.rowHeight = (windowHeight - this.gridPadding * (this.maxRows + 1)) / this.maxRows;
+
+        let cols = Math.ceil(Math.sqrt(this.videos.length));
+        let rows = Math.ceil(this.videos.length / cols);
+
+        let elementsLastRow = this.videos.length % cols;
+        let lastNormalIndex = this.videos.length - elementsLastRow;
+
+        if (elementsLastRow) {
+            this.maxCols = cols * elementsLastRow;
+        } else {
+            this.maxCols = cols;
+        }
+
+        let colsPerElement = this.maxCols / cols;
+        let rowsPerElement = this.maxRows / rows;
+
+        let colsLastRow = Math.ceil(this.maxCols / elementsLastRow);
+
+        let cntX = 0;
+        let cntY = 0;
+
+        for (let [index, video] of this.videos.entries()) {
+            video.i = index;
+            video.x = cntX;
+            video.y = cntY;
+            video.h = rowsPerElement;
+            video.w = colsPerElement;
+            cntX += colsPerElement;
+
+            if (cntX >= this.maxCols) {
+                cntX = 0;
+                cntY += rowsPerElement;
+            }
+
+            let videoTemp = Object.assign({}, video);
+            videoTemp.video = video;
+            this.videoGrid.push(videoTemp);
+        }
+
+        /* OLD IMPLEMENTATION */
+
+        /* for (let [index, video] of this.videos.entries()) {
+            video.i = index;
+            if (index < lastNormalIndex) {
+                video.x = cntX;
+                video.y = cntY;
+                video.h = rowsPerElement;
+                video.w = colsPerElement;
+                cntX += colsPerElement;
+
+                if (cntX >= this.maxCols) {
+                    cntX = 0;
+                    cntY += rowsPerElement;
+                }
+            } else {
+                video.x = cntX;
+                video.y = cntY;
+                video.h = rowsPerElement;
+                video.w = colsLastRow;
+                cntX += colsLastRow;
+            }
+
+            let videoTemp = Object.assign({}, video);
+            this.videoGrid.push(videoTemp);
+        } */
+
+        /* let gridColumn, gridRows = ''
+
+        if(rows && cols){
+            gridColumn = `grid-template-columns: repeat(${cols}, minmax(320px, 1fr));`
+            gridRows = `grid-template-rows: repeat(${rows}, minmax(240px, 1fr));`
+        } */
+    },
+    initializeOpenTok(apiKey, sessionId, token) {
+      this.openTokSession = OT.initSession(apiKey, sessionId);
+
+      this.openTokSession.on({
+          sessionDisconnected: event => {
+              if (event.reason === "forceDisconnected") {
+                  alert("You were kicked.");
+                  if (this.user.type === "associate") {
+                      this.disconnect();
+                      this.$store.dispatch("leaveMeeting");
+                  } else {
+                      window.location = "/";
+                  }
+              }
+          }
+      });
+
+      // Create a publisher
+      this.publisher = OT.initPublisher(
+          this.$refs.otContainer,
+          {
+            insertMode: "append",
+            width: "100%",
+            height: "100%",
+            resolution: "1280x720",
+            frameRate: 30,
+            name: this.user.name,
+            style: {
+                nameDisplayMode: "on",
+                archiveStatusDisplayMode: "off"
+            }
+          },
+          error => {
+            if (error) {
+                alert(error.message);
+            } else {
+                this.publisherReady = true;
+                const cont = this.createVideoContainer();
+                this.$nextTick(() => {
+                    cont.el = $(`#${cont.id}`)[0];
+                    cont.el.appendChild(this.publisher.element);
+                    cont.obj = this.publisher;
+                    this.$set(cont, "self", true);
+                    //cont.self = true;
+                });
+            }
+          }
+      );
+
+      this.publisher.on({
+          accessDialogOpened: e => {
+            this.accessDialogShown = true;
+          },
+          accessDialogClosed: e => {
+            this.accessDialogShown = false;
+          }
+      });
+
+      this.openTokSession.on("streamCreated", event => {
+        //console.log("stream Created event");
+        let container;
+        if (event.stream.videoType === "screen") {
+          //TODO: This is
+        }
+        const subscriber = this.openTokSession.subscribe(
+          event.stream,
+          this.$refs.otContainer,
+          {
+            insertMode: "append",
+            width: "100%",
+            height: "100%",
+            style: { nameDisplayMode: "on" }
+          },
+          error => {
+              if (error) {
+                alert(error.message);
+              } else {
+                  const cont = this.createVideoContainer();
+                  this.$nextTick(() => {
+                    cont.el = $(`#${cont.id}`)[0];
+                    cont.el.appendChild(subscriber.element);
+                    cont.obj = subscriber;
+                    container = cont;
+                  });
+              }
+          }
+        );
+        subscriber.on({
+          destroyed: e => {
+            container.el.remove();
+            const index = this.videos.findIndex(v => v.id == container.id);
+            if (index >= 0) this.videos.splice(index, 1);
+            this.adjustVideoContainers();
+          }
+        });
+        this.subscribers.push(subscriber);
+      });
+
+    this.openTokSession.connect(token, error => {
+        // If the connection is successful, publish to the session
+        if (error) {
+            alert(error.message);
+        } else {
+            this.$store.commit("setSessionConnectivityState", true);
+        }
+      });
+      this.loadingInProgress = false;
+    },
+    publishToSession() {
+        this.openTokSession.publish(this.publisher, error => {
+          if (error) {
+            alert(error.message);
+          }
+        });
+    },
+    switchToFullSize(video, index) {
+        if (this.videos.length < 4) {
+          if (this.lowMembersCountSelectedId !== video.id && index !== 0) {
+            this.lowMembersCountSelectedId = video.id;
+          }
+        } else {
+          this.bigMembersCountSelectedId = video.id;
+        }
+    },
+    ready() {
+        this.$nextTick(function() {
+          this.readyForUse = true;
+          if (this.user.type === "associate") {
+            if (!this.meeting.id || this.active_menu_item.template !== "room") return;
+            this.getToken({}, true);
+            return;
+          }
+
+          if (this.meeting.scheduledDate && !this.meeting.startedAt && this.meeting.scheduledDate > new Date()) {
+            alert("Meeting not started.");
+            return;
+          }
+          let participantId = this.user.id;
+          if (!participantId) {
+            this.$refs.initialModal.show(this.meeting.passwordRequired);
+            return;
+          }
+          this.getToken({
+            participantId
+          });
+        });
+    },
+    shareScreen() {
+        if (this.screenPublisher) return;
+        this.screenPublisher = OT.initPublisher(
+            this.$refs.otContainer,
+            {
+              insertMode: "append",
+              width: "100%",
+              height: "100%",
+              resolution: "1280x720",
+              frameRate: 30,
+              videoSource: "screen",
+              name: this.user.name,
+              style: {
+                nameDisplayMode: "on",
+                archiveStatusDisplayMode: "off"
+              }
+            },
+            error => {
+                if (error) {
+                  console.error(error);
+                  if (error.code !== 1500) alert(error.message);
+                  this.screenPublisher = null;
+                } else {
+                  const cont = this.createVideoContainer();
+                  this.$nextTick(() => {
+                    cont.el = $(`#${cont.id}`)[0];
+                    cont.el.appendChild(this.screenPublisher.element);
+                    cont.obj = this.screenPublisher;
+                    // cont.self = true;
+
+                    this.$set(cont, "self", true);
+                  });
+                  this.screenPublisher.on("streamDestroyed", event => {
+                    cont.el.remove();
+                    const index = this.videos.findIndex(v => v.id == cont.id);
+                    if (index >= 0) this.videos.splice(index, 1);
+                    this.adjustVideoContainers();
+                    this.screenPublisher = null;
+                    this.$store.commit("setScreenShareState", false);
+                  });
+                  this.openTokSession.publish(this.screenPublisher, error => {
+                    if (error) {
+                      alert(error.message);
+                    }
+                  });
+                  this.$store.commit("setScreenShareState", true);
+                }
+            }
+        );
+    },
+    kickParcitipantContinue(participant) {
+        const connection = participant.video.obj.stream.connection;
+        const [type, id] = connection.data.split(":");
+        this.openTokSession.forceDisconnect(connection, error => {
+          if (error) {
+            console.error(error);
+          } else {
+            $.ajax({
+              url: `/associate/meeting/${this.meeting.id}/kick`,
+              method: "POST",
+              headers: {
+                "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
+              },
+              data: {
+                type,
+                id
+              },
+              error: jXhr => {
+                console.error(getSingleError(jXhr));
+              }
+            });
+          }
+        });
+    },
+    kickParcitipant(participant) {
+      if (confirm("Are you sure want to kick this person out of meeting?")) this.kickParcitipantContinue(participant);
+    }
+},
+watch: {
+    publisherReady(val) {
+      if (val && this.session.sessionConnected) this.publishToSession();
+    },
+    "session.sessionConnected"(val) {
+      if (val && this.publisherReady) this.publishToSession();
+    },
+    "user.is_active_and_visible"(val) {
+      if (!this.publisher || !this.publisher.publishVideo) return;
+      this.publisher.publishVideo(val);
+      this.publisher.publishAudio(val);
+    },
+    "meeting.id"(val) {
+      if (this.loadingInProgress) return;
+      this.loadingInProgress = true;
+      this.disconnect();
+
+      if (val) {
+          this.getToken({}, true);
+      } else {
+          this.loadingInProgress = false;
+      }
+    },
+    "active_menu_item.template"(val) {
+      if (this.loadingInProgress || val !== "room" || !this.meeting.id || this.openTokSession) return;
+      this.loadingInProgress = true;
+      this.getToken({}, true);
+    }
+  },
+  created() {
+    OT.checkScreenSharingCapability(response => {
+      this.$store.commit("setScreesharingAvailability", response.supported);
+    });
+  },
+  mounted() {
+    let self = this;
+
+    this.$eventBus.$on("screenShare", this.shareScreen);
+    this.$eventBus.$on("meetingRejoin", () => {
+      this.loadingInProgress = true;
+      this.disconnect();
+      this.getToken({}, true);
+    });
+
+    let width = $(window).width();
+    let height = $(window).height();
+
+    window.addEventListener("resize", function() {
+      let new_width = $(window).width();
+      let new_height = $(window).height();
+
+      if (width !== new_width || height !== new_height) {
+        self.adjustVideoContainers();
+      }
+    });
 
 
+    this.$eventBus.$on("leaveMeeting", () => {
+      $.ajax({
+        url: "/associate/meeting/leave",
+        method: "POST",
+        headers: {
+          "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
+        },
+        error: jXhr => {
+          console.error(getSingleError(jXhr));
+        }
+      });
+      this.$store.dispatch("leaveMeeting");
+      this.disconnect();
+    });
+  }
 }
 }
 </script>
 </script>

+ 34 - 0
resources/js/components/partials/Settings.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="d-flex flex-column">
+        <v-switch v-model="newSettings.newMeetingNotificationExpanded" @change="updateSettings('newMeetingNotificationExpanded')" :label="'Always expand new meeting notification'"></v-switch>
+    </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+
+export default {
+    data() {
+        return {
+            newSettings: null
+        }
+    },
+    computed: {
+        ...mapState(["settings"]),
+    },
+    methods: {
+        updateSettings(setting){
+            localStorage.setItem(setting, this.newSettings[setting])
+            this.$store.commit('updateSettings', this.newSettings)
+            console.log('here')
+        }
+    },
+    created(){
+        this.newSettings = Object.assign({}, this.settings)
+    }
+}
+</script>
+
+<style>
+
+</style>

+ 82 - 0
resources/js/components/utils/TransitionExpand.vue

@@ -0,0 +1,82 @@
+<template>
+  <transition
+    :name="animation_name || 'expand'"
+    @enter="enter"
+    @after-enter="afterEnter"
+    @leave="leave"
+  >
+    <slot/>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'TransitionExpand',
+  props: ['animation_name'],
+  methods: {
+    enter(element) {
+      let self = this
+      const width = getComputedStyle(element).width;
+
+      element.style.width = width;
+      element.style.position = 'absolute';
+      element.style.visibility = 'hidden';
+      element.style.height = 'auto';
+
+      const height = getComputedStyle(element).height;
+
+      element.style.width = null;
+      element.style.position = null;
+      element.style.visibility = null;
+      element.style.height = 0;
+
+      // Force repaint to make sure the
+      // animation is triggered correctly.
+      getComputedStyle(element).height;
+
+      // Trigger the animation.
+      // We use `setTimeout` because we need
+      // to make sure the browser has finished
+      // painting after setting the `height`
+      // to `0` in the line above.
+      setTimeout(() => {
+        element.style.height = height;
+      });
+
+      this.$emit('init')
+    },
+    afterEnter(element) {
+      element.style.height = 'auto';
+    },
+    leave(element) {
+      const height = getComputedStyle(element).height;
+
+      element.style.height = height;
+
+      // Force repaint to make sure the
+      // animation is triggered correctly.
+      getComputedStyle(element).height;
+
+      setTimeout(() => {
+        element.style.height = 0;
+        element.style.paddingTop = 0;
+        element.style.paddingBottom = 0;
+      });
+
+      this.$emit('init')
+    }
+  },
+  mounted() {
+    this.$emit('init')
+  }
+};
+</script>
+
+<style scoped>
+* {
+  will-change: height;
+  transform: translateZ(0);
+  backface-visibility: hidden;
+  perspective: 1000px;
+}
+</style>

+ 10 - 14
resources/js/components/vuex/index.js

@@ -9,14 +9,11 @@ export default () => new Vuex.Store({
     },
     },
     state: {
     state: {
         user: {
         user: {
-            id: null,
             UID: null,
             UID: null,
             name: 'Guest',
             name: 'Guest',
             type: 'guest',
             type: 'guest',
-            pin: null,
             avatarFile: null,
             avatarFile: null,
-            is_active_and_visible: true,
-            isAdmin: false
+            is_active_and_visible: true
         },
         },
         session: {
         session: {
             screenSharingAvailable: false,
             screenSharingAvailable: false,
@@ -29,8 +26,8 @@ export default () => new Vuex.Store({
             template: 'lobby-list'
             template: 'lobby-list'
         },
         },
         meeting: {
         meeting: {
-            id: "",
-            name: "",
+            id: '',
+            name: '',
             passwordRequired: false,
             passwordRequired: false,
             lobby: null,
             lobby: null,
             active_members: 0,
             active_members: 0,
@@ -38,7 +35,9 @@ export default () => new Vuex.Store({
             startedAt: null
             startedAt: null
         },
         },
         lobbies: [],
         lobbies: [],
-        chat_user_notification: false
+        settings: {
+            newMeetingNotificationExpanded: localStorage.getItem('newMeetingNotificationExpanded') == 'true' ? true : false
+        }
     },
     },
     mutations: {
     mutations: {
         setUsername: (state, data) => {
         setUsername: (state, data) => {
@@ -62,9 +61,6 @@ export default () => new Vuex.Store({
         setScreenShareState: (state, data) => {
         setScreenShareState: (state, data) => {
             state.session.screenSharingActive = data
             state.session.screenSharingActive = data
         },
         },
-        setMenuItem: (state, data) => {
-            state.active_menu_item = data
-        },
         setLobbies(state, data) {
         setLobbies(state, data) {
             state.lobbies = data
             state.lobbies = data
         },
         },
@@ -77,7 +73,7 @@ export default () => new Vuex.Store({
             if (lobby !== -1) {
             if (lobby !== -1) {
                 state.lobbies[lobby].meetings = data.meetings.map(m => {
                 state.lobbies[lobby].meetings = data.meetings.map(m => {
                     m.active_members = []
                     m.active_members = []
-                    m.associates_online = []
+                    m.pros_online = []
                     return m
                     return m
                 });
                 });
             }
             }
@@ -88,7 +84,7 @@ export default () => new Vuex.Store({
                 let meeting = lobby[0].meetings.filter((cur) => cur.id == data.meetingID)
                 let meeting = lobby[0].meetings.filter((cur) => cur.id == data.meetingID)
                 if (meeting.length) {
                 if (meeting.length) {
                     meeting[0].active_members = data.active_members
                     meeting[0].active_members = data.active_members
-                    meeting[0].associates_online = data.associates_online
+                    meeting[0].pros_online = data.pros_online
 
 
                     if (state.meeting.id && state.meeting.id == meeting[0].id) {
                     if (state.meeting.id && state.meeting.id == meeting[0].id) {
                         state.meeting.active_members = data.active_members
                         state.meeting.active_members = data.active_members
@@ -117,8 +113,8 @@ export default () => new Vuex.Store({
         setSessionConnectivityState(state, data) {
         setSessionConnectivityState(state, data) {
             state.session.sessionConnected = data
             state.session.sessionConnected = data
         },
         },
-        setChatUserNotification(state, data) {
-            state.chat_user_notification = data
+        updateSettings(state, data) {
+            state.settings = data
         }
         }
     },
     },
     actions: {
     actions: {

+ 178 - 0
resources/js/components/widgets/CallBubble.vue

@@ -0,0 +1,178 @@
+<template>
+  <v-btn
+        color="pink"
+        class="callBtn"
+        :class="{'showingAll': showCalleeDetails, 'active': call && call.time_limit > 0}"
+        dark
+        absolute
+        bottom
+        right
+        ripple
+        @click="shiftForm"
+        >
+        <div v-if="call">
+            <div class="d-flex flex-row align-items-center btnHeader">
+                <v-icon>mdi-video</v-icon>
+                <div class="ml-2 incomingCallMsg">Incoming Call {{!showCalleeDetails ? `(${call.time_limit})` : ''}}</div>
+            </div>
+            <div class="fullDetails mt-3">
+                <div class="d-flex flex-row justify-content-between">
+                    <span>Callee:</span>
+                    <span>{{call.user_type}}</span>
+                </div>
+
+                <div class="d-flex flex-row justify-content-between">
+                    <span>Lobby:</span>
+                    <span>{{call.lobby || 'None'}}</span>
+                </div>
+
+                <div class="d-flex flex-row justify-content-between mt-2">
+                    <span>{{call.name || 'Unknown'}}</span>
+                </div>
+
+                <div class="d-flex flex-row justify-content-center mt-5">
+                    <h2>{{call.time_limit}}</h2>
+                </div>
+
+                <div class="d-flex flex-row justify-content-between mt-5 ctrlBtns">
+                    <span @click="handleCall(true)">
+                        Accept
+                    </span>
+                    /
+                    <span @click="handleCall(false)">
+                        Reject
+                    </span>
+                </div>
+            </div>
+        </div>
+    </v-btn>
+</template>
+
+<script>
+import { mapState } from "vuex"
+
+export default {
+    props: {
+        call: {
+            type: Object
+        }
+    },
+    data(){
+        return {
+            showCalleeDetails: false
+        }
+    },
+    computed: {
+        ...mapState(["settings"]),
+    },
+    watch: {
+        call: {
+            deep: true,
+            handler(newVal){
+                if(newVal.time_limit == 0){
+                    let self = this
+
+                    setTimeout(() => {
+                        self.showCalleeDetails = false
+                    }, 750)
+                }
+            }
+        }
+    },
+    methods: {
+        handleCall(acceptCall) {
+            if(acceptCall){
+                console.log('Call accepted!')
+            } else {
+                console.log('Call Rejected!')
+            }
+        },
+        shiftForm(){
+            if(!this.settings.newMeetingNotificationExpanded){
+                this.showCalleeDetails = !this.showCalleeDetails
+            } else {
+                this.showCalleeDetails = true
+            }
+        }
+    },
+    created(){
+        if(this.settings.newMeetingNotificationExpanded){
+            this.showCalleeDetails = true
+        }
+    }
+}
+</script>
+
+<style scoped lang="scss">
+
+    @keyframes Glowing {
+        0% {
+            opacity: 0.3;
+        }
+        30% {
+            opacity: 0.8;
+        }
+        100% {
+            opacity: 1;
+        }
+    }
+
+    .callBtn {
+        z-index: 1000;
+        border-radius: 28px;
+        height: 56px !important;
+        width: 56px !important;
+        min-width: 56px !important;
+        overflow-x: hidden;
+        overflow-y: hidden;
+
+        transition: all 0.5s;
+
+        transform: translateX(25vw);
+
+        .btnHeader {
+            animation: 0.7s Glowing infinite alternate ease-out;
+        }
+
+        &.active {
+            transform: translateX(0);
+        }
+
+        .incomingCallMsg {
+            transition: all 0.3s;
+            display: none;
+            opacity: 0;
+        }
+
+        &:hover, &.showingAll {
+            width: 240px !important;
+
+            .incomingCallMsg {
+                display: block;
+                opacity: 1;
+            }
+        }
+
+        /* Full State */
+
+        .fullDetails {
+            .ctrlBtns span {
+                &:hover {
+                    text-decoration: underline;
+                }
+            }
+        }
+
+        &.showingAll {
+            height: 400px !important;
+        }
+
+        &:not(.showingAll) {
+            .fullDetails {
+                display: none !important;
+            }
+        }
+
+    }
+
+</style>

+ 32 - 0
resources/sass/app.scss

@@ -1,8 +1,40 @@
 //
 //
 //Vuetify
 //Vuetify
 
 
+$darker-green: #0DBDA7;
+$orange: #F08322;
+
 @import '~vuetify/dist/vuetify.min.css';
 @import '~vuetify/dist/vuetify.min.css';
 
 
 .col, .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col-auto, .col-lg, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-auto, .col-md, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md-auto, .col-sm, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-auto {
 .col, .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col-auto, .col-lg, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-auto, .col-md, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md-auto, .col-sm, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-auto {
     padding: 0 12px;
     padding: 0 12px;
+}
+
+.v-input__slot, .v-label {
+    margin-bottom: 0;
+}
+
+.no-v-padding {
+    .v-expansion-panel-content__wrap {
+        padding: 0 !important;
+    }
+}
+
+.status {
+    position: relative;
+	&:before {
+		position: absolute;
+		content: "";
+		width: 8px;
+		height: 8px;
+		border-radius: 8px;
+		left: -1rem;
+		bottom: 5px;
+		background-color: $orange;
+	}
+	&.is-active {
+        &:before {
+            background-color: $darker-green;
+        }
+	}
 }
 }

+ 1 - 1
resources/views/mc.blade.php

@@ -17,7 +17,7 @@
             <iframe src="{{ $page }}" frameborder="0" class="h-100 w-100"></iframe>
             <iframe src="{{ $page }}" frameborder="0" class="h-100 w-100"></iframe>
         </div>
         </div>
         <div id="meetingsApp" class="col-3 border-left app-right-panel">
         <div id="meetingsApp" class="col-3 border-left app-right-panel">
-            <meetings-app-root></meetings-app-root>
+            <meetings-app-root :user-prop="{!! str_replace('"','\'',str_replace('\'','\\\'',json_encode($user))) !!}"></meetings-app-root>
         </div>
         </div>
     </div>
     </div>
     <script src="/AdminLTE-3.0.5/plugins/jquery/jquery.min.js"></script>
     <script src="/AdminLTE-3.0.5/plugins/jquery/jquery.min.js"></script>