Page MenuHomePhorge

No OneTemporary

Size
56 KB
Referenced Files
None
Subscribers
None
diff --git a/src/.eslintrc.js b/src/.eslintrc.js
index fd082b1c..195d8ce8 100644
--- a/src/.eslintrc.js
+++ b/src/.eslintrc.js
@@ -1,15 +1,16 @@
module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
'plugin:vue/recommended'
],
rules: {
"vue/attributes-order": "off",
"vue/html-indent": ["error", 4],
"vue/html-self-closing": "off",
"vue/max-attributes-per-line": "off",
+ "vue/no-v-html": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off"
}
}
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
index e02cf809..b30fd825 100644
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -1,91 +1,91 @@
<template>
<router-view v-if="!isLoading && !routerReloading" :key="key" @hook:mounted="childMounted"></router-view>
</template>
<script>
export default {
+ data() {
+ return {
+ isLoading: true,
+ routerReloading: false
+ }
+ },
computed: {
key() {
// The 'key' property is used to reload the Page component
// whenever a route changes. Normally vue does not do that.
return this.$route.name == '404' ? this.$route.path : 'static'
}
},
- data() {
- return {
- isLoading: true,
- routerReloading: false
- }
- },
mounted() {
const token = localStorage.getItem('token')
if (token) {
this.$root.startLoading()
axios.defaults.headers.common.Authorization = 'Bearer ' + token
axios.get('/api/auth/info?refresh_token=1')
.then(response => {
this.$root.loginUser(response.data, false)
this.$root.stopLoading()
this.isLoading = false
})
.catch(error => {
// Release lock on the router-view, otherwise links (e.g. Logout) will not work
this.isLoading = false
this.$root.logoutUser(false)
this.$root.errorHandler(error)
})
} else {
this.isLoading = false
}
},
methods: {
childMounted() {
this.$root.updateBodyClass()
this.getFAQ()
},
getFAQ() {
let page = this.$route.path
if (page == '/' || page == '/login') {
return
}
axios.get('/content/faq' + page, { ignoreErrors: true })
.then(response => {
const result = response.data.faq
$('#faq').remove()
if (result && result.length) {
let faq = $('<div id="faq" class="faq mt-3"><h5>FAQ</h5><ul class="pl-4"></ul></div>')
let list = []
result.forEach(item => {
list.push($('<li>').append($('<a>').attr('href', item.href).text(item.title)))
// Handle internal links with the vue-router
if (item.href.charAt(0) == '/') {
list[list.length-1].find('a').on('click', event => {
event.preventDefault()
this.$router.push(item.href)
})
}
})
faq.find('ul').append(list)
$(this.$el).append(faq)
}
})
},
routerReload() {
// Together with beforeRouteUpdate even on a route component
// allows us to force reload the component. So it is possible
// to jump from/to page that uses currently loaded component.
this.routerReloading = true
this.$nextTick().then(() => {
this.routerReloading = false
})
}
}
}
</script>
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
index 67748bc8..1cdcb099 100644
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -1,762 +1,762 @@
<template>
<div id="meet-component">
<div id="meet-session-toolbar" class="hidden">
<span id="meet-session-logo" v-html="$root.logo()"></span>
<div id="meet-session-menu">
<button class="btn btn-link link-audio" @click="switchSound" :disabled="!isPublisher()" title="Mute audio">
<svg-icon icon="microphone-slash"></svg-icon>
</button>
<button class="btn btn-link link-video" @click="switchVideo" :disabled="!isPublisher()" title="Mute video">
<svg-icon icon="video-slash"></svg-icon>
</button>
<button class="btn btn-link link-screen text-danger" @click="switchScreen" :disabled="!canShareScreen || !isPublisher()" title="Share screen">
<svg-icon icon="desktop"></svg-icon>
</button>
<button class="btn btn-link link-hand text-danger" v-if="!isPublisher()" @click="switchHand" title="Raise hand">
<svg-icon icon="hand-paper"></svg-icon>
</button>
<span id="channel-select" :style="'display:' + (channels.length ? '' : 'none')" class="dropdown">
<button class="btn btn-link link-channel" title="Interpreted language channel" aria-haspopup="true" aria-expanded="false" data-toggle="dropdown">
<svg-icon icon="headphones"></svg-icon>
<span class="badge badge-danger" v-if="session.channel">{{ session.channel.toUpperCase() }}</span>
</button>
<div class="dropdown-menu">
<a :class="'dropdown-item' + (!session.channel ? ' active' : '')" href="#" data-code="" @click="switchChannel">- none -</a>
<a v-for="code in channels" :key="code" href="#" @click="switchChannel" :data-code="code"
- :class="'dropdown-item' + (session.channel == code ? ' active' : '')"
+ :class="'dropdown-item' + (session.channel == code ? ' active' : '')"
>{{ languages[code] }}</a>
</div>
</span>
<button class="btn btn-link link-chat text-danger" @click="switchChat" title="Chat">
<svg-icon icon="comment"></svg-icon>
</button>
<button class="btn btn-link link-fullscreen closed hidden" @click="switchFullscreen" title="Full screen">
<svg-icon icon="expand"></svg-icon>
</button>
<button class="btn btn-link link-fullscreen open hidden" @click="switchFullscreen" title="Full screen">
<svg-icon icon="compress"></svg-icon>
</button>
<button class="btn btn-link link-options" v-if="isRoomOwner()" @click="roomOptions" title="Room options">
<svg-icon icon="cog"></svg-icon>
</button>
<button class="btn btn-link link-logout" @click="logout" title="Leave session">
<svg-icon icon="power-off"></svg-icon>
</button>
</div>
</div>
<div id="meet-setup" class="card container mt-2 mt-md-5 mb-5">
<div class="card-body">
<div class="card-title">Set up your session</div>
<div class="card-text">
<form class="media-setup-form row" @submit.prevent="joinSession">
<div class="media-setup-preview col-sm-6 mb-3 mb-sm-0">
<video class="rounded"></video>
<div class="volume"><div class="bar"></div></div>
</div>
<div class="col-sm-6 align-self-center">
<div class="input-group">
<label for="setup-microphone" class="input-group-prepend mb-0">
<span class="input-group-text" title="Microphone"><svg-icon icon="microphone"></svg-icon></span>
</label>
<select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
<option value="">None</option>
<option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
</select>
</div>
<div class="input-group mt-2">
<label for="setup-camera" class="input-group-prepend mb-0">
<span class="input-group-text" title="Camera"><svg-icon icon="video"></svg-icon></span>
</label>
<select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
<option value="">None</option>
<option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
</select>
</div>
<div class="input-group mt-2">
<label for="setup-nickname" class="input-group-prepend mb-0">
<span class="input-group-text" title="Nickname"><svg-icon icon="user"></svg-icon></span>
</label>
<input class="form-control" type="text" id="setup-nickname" v-model="nickname" placeholder="Your name">
</div>
<div class="input-group mt-2" v-if="session.config && session.config.requires_password">
<label for="setup-password" class="input-group-prepend mb-0">
<span class="input-group-text" title="Password"><svg-icon icon="key"></svg-icon></span>
</label>
<input type="password" class="form-control" id="setup-password" v-model="password" placeholder="Password">
</div>
<div class="mt-3">
<button type="submit" id="join-button"
:class="'btn w-100 btn-' + (isRoomReady() ? 'success' : 'primary')"
>
<span v-if="isRoomReady()">JOIN NOW</span>
<span v-else-if="roomState == 323">I'm the owner</span>
<span v-else>JOIN</span>
</button>
</div>
</div>
<div class="mt-4 col-sm-12">
<status-message :status="roomState" :status-labels="roomStateLabels"></status-message>
</div>
</form>
</div>
</div>
</div>
<div id="meet-session-layout" class="d-flex hidden">
<div id="meet-queue">
<div class="head" title="Q &amp; A"><svg-icon icon="microphone-alt"></svg-icon></div>
</div>
<div id="meet-session"></div>
<div id="meet-chat">
<div class="chat"></div>
<div class="chat-input m-2">
<textarea class="form-control" rows="1"></textarea>
</div>
</div>
</div>
<logon-form id="meet-auth" class="hidden" :dashboard="false" @success="authSuccess"></logon-form>
<div id="leave-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Room closed</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>The session has been closed by the room owner.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger modal-action" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="media-setup-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Media setup</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form class="media-setup-form">
<div class="media-setup-preview"></div>
<div class="input-group mt-2">
<label for="setup-microphone" class="input-group-prepend mb-0">
<span class="input-group-text" title="Microphone"><svg-icon icon="microphone"></svg-icon></span>
</label>
<select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
<option value="">None</option>
<option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
</select>
</div>
<div class="input-group mt-2">
<label for="setup-camera" class="input-group-prepend mb-0">
<span class="input-group-text" title="Camera"><svg-icon icon="video"></svg-icon></span>
</label>
<select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
<option value="">None</option>
<option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-action" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<room-options v-if="session.config" :config="session.config" :room="room" @config-update="configUpdate"></room-options>
</div>
</template>
<script>
import { Meet, Roles } from '../../js/meet/app.js'
import StatusMessage from '../Widgets/StatusMessage'
import LogonForm from '../Login'
import RoomOptions from './RoomOptions'
// Register additional icons
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faComment,
faCog,
faCompress,
faCrown,
faDesktop,
faExpand,
faHandPaper,
faHeadphones,
faMicrophone,
faMicrophoneSlash,
faMicrophoneAlt,
faPowerOff,
faUser,
faVideo,
faVideoSlash,
faVolumeMute
} from '@fortawesome/free-solid-svg-icons'
// Register only these icons we need
library.add(
faComment,
faCog,
faCompress,
faCrown,
faDesktop,
faExpand,
faHandPaper,
faHeadphones,
faMicrophone,
faMicrophoneSlash,
faMicrophoneAlt,
faPowerOff,
faUser,
faVideo,
faVideoSlash,
faVolumeMute
)
let roomRequest
const authHeader = 'X-Meet-Auth-Token'
export default {
components: {
LogonForm,
RoomOptions,
StatusMessage
},
data() {
return {
setup: {
cameras: [],
microphones: [],
},
canShareScreen: false,
camera: '',
channels: [],
languages: {
en: 'English',
de: 'German',
fr: 'French',
it: 'Italian'
},
meet: null,
microphone: '',
nickname: '',
password: '',
room: null,
roomState: 'init',
roomStateLabels: {
init: 'Checking the room...',
323: 'The room is closed. Please, wait for the owner to start the session.',
324: 'The room is closed. It will be open for others after you join.',
325: 'The room is ready. Please, provide a valid password.',
326: 'The room is locked. Please, enter your name and try again.',
327: 'Waiting for permission to join the room.',
404: 'The room does not exist.',
429: 'Too many requests. Please, wait.',
500: 'Failed to connect to the room. Server error.'
},
session: {}
}
},
mounted() {
this.room = this.$route.params.room
// Initialize OpenVidu and do some basic checks
this.meet = new Meet($('#meet-session')[0]);
this.canShareScreen = this.meet.isScreenSharingSupported()
// Check the room and init the session
this.initSession()
// Setup the room UI
this.setupSession()
},
beforeDestroy() {
clearTimeout(roomRequest)
$('#app').removeClass('meet')
if (this.meet) {
this.meet.leaveRoom()
}
delete axios.defaults.headers.common[authHeader]
$(document.body).off('keydown.meet')
},
methods: {
authSuccess() {
// The user authentication succeeded, we still don't know it's really the room owner
this.initSession()
$('#meet-setup').removeClass('hidden')
$('#meet-auth').addClass('hidden')
},
configUpdate(config) {
this.session.config = Object.assign({}, this.session.config, config)
},
dismissParticipant(id) {
axios.post('/api/v4/openvidu/rooms/' + this.room + '/connections/' + id + '/dismiss')
},
initSession(init) {
const button = $('#join-button').prop('disabled', true)
this.post = {
password: this.password,
nickname: this.nickname,
screenShare: this.canShareScreen ? 1 : 0,
init: init ? 1 : 0,
picture: init ? this.makePicture() : '',
requestId: this.requestId(),
canPublish: !!this.camera || !!this.microphone
}
$('#setup-password,#setup-nickname').removeClass('is-invalid')
axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
.then(response => {
button.prop('disabled', false)
// We already have token, the response is redundant
if (this.roomState == 'ready' && this.session.token) {
return
}
this.roomState = 'ready'
this.session = response.data
if (init) {
this.joinSession()
}
if (this.session.authToken) {
axios.defaults.headers.common[authHeader] = this.session.authToken
}
})
.catch(error => {
if (!error.response) {
console.error(error)
return
}
const data = error.response.data || {}
if (data.code) {
this.roomState = data.code
} else {
this.roomState = error.response.status
}
button.prop('disabled', this.roomState == 'init' || this.roomState == 327 || this.roomState >= 400)
if (data.config) {
this.session.config = data.config
}
switch (this.roomState) {
case 323:
// Waiting for the owner to open the room...
// Update room state every 10 seconds
roomRequest = setTimeout(() => { this.initSession() }, 10000)
break;
case 324:
// Room is ready for the owner, but the 'init' was not requested yet
clearTimeout(roomRequest)
break;
case 325:
// Missing/invalid password
if (init) {
$('#setup-password').addClass('is-invalid').focus()
}
break;
case 326:
// Locked room prerequisites error
if (init && !$('#setup-nickname').val()) {
$('#setup-nickname').addClass('is-invalid').focus()
}
break;
case 327:
// Waiting for the owner's approval to join
// Update room state every 10 seconds
roomRequest = setTimeout(() => { this.initSession(true) }, 10000)
break;
case 429:
// Rate limited, wait and try again
const waitTime = error.response.headers['retry-after'] || 10
roomRequest = setTimeout(() => { this.initSession(init) }, waitTime * 1000)
break;
default:
if (this.roomState >= 400 && this.roomState != 404) {
this.roomState = 500
}
}
})
if (document.fullscreenEnabled) {
$('#meet-session-menu').find('.link-fullscreen.closed').removeClass('hidden')
}
},
isModerator() {
return this.isRoomOwner() || (!!this.session.role && (this.session.role & Roles.MODERATOR) > 0)
},
isPublisher() {
return !!this.session.role && (this.session.role & Roles.PUBLISHER) > 0
},
isRoomOwner() {
return !!this.session.role && (this.session.role & Roles.OWNER) > 0
},
isRoomReady() {
return ['ready', 322, 324, 325, 326, 327].includes(this.roomState)
},
// An event received by the room owner when a participant is asking for a permission to join the room
joinRequest(data) {
// The toast for this user request already exists, ignore
// It's not really needed as we do this on server-side already
if ($('#i' + data.requestId).length) {
return
}
// FIXME: Should the message close button act as the Deny button? Do we need the Deny button?
let body = $(
`<div>`
+ `<div class="picture"><img src="${data.picture}"></div>`
+ `<div class="content">`
+ `<p class="mb-2"></p>`
+ `<div class="text-right">`
+ `<button type="button" class="btn btn-sm btn-success accept">Accept</button>`
+ `<button type="button" class="btn btn-sm btn-danger deny ml-2">Deny</button>`
)
this.$toast.message({
className: 'join-request',
icon: 'user',
timeout: 0,
title: 'Join request',
// titleClassName: '',
body: body.html(),
onShow: element => {
const id = data.requestId
$(element).find('p').text((data.nickname || '') + ' requested to join.')
// add id attribute, so we can identify it
$(element).attr('id', 'i' + id)
// add action to the buttons
.find('button.accept,button.deny').on('click', e => {
const action = $(e.target).is('.accept') ? 'accept' : 'deny'
axios.post('/api/v4/openvidu/rooms/' + this.room + '/request/' + id + '/' + action)
.then(response => {
$('#i' + id).remove()
})
})
}
})
},
// Entering the room
joinSession() {
// The form can be submitted not only via the submit button,
// make sure the submit is allowed
if ($('#meet-setup [type=submit]').prop('disabled')) {
return;
}
if (this.roomState == 323) {
$('#meet-setup').addClass('hidden')
$('#meet-auth').removeClass('hidden')
return
}
if (this.roomState != 'ready' && !this.session.token) {
this.initSession(true)
return
}
clearTimeout(roomRequest)
this.session.nickname = this.nickname
this.session.languages = this.languages
this.session.menuElement = $('#meet-session-menu')[0]
this.session.chatElement = $('#meet-chat')[0]
this.session.queueElement = $('#meet-queue')[0]
this.session.onSuccess = () => {
$('#app').addClass('meet')
$('#meet-setup').addClass('hidden')
$('#meet-session-toolbar,#meet-session-layout').removeClass('hidden')
if (!this.canShareScreen) {
this.setMenuItem('screen', false, true)
}
}
this.session.onError = () => {
this.roomState = 500
}
this.session.onDestroy = event => {
// TODO: Display different message for each reason: forceDisconnectByUser,
// forceDisconnectByServer, sessionClosedByServer?
if (event.reason != 'disconnect' && event.reason != 'networkDisconnect' && !this.isRoomOwner()) {
$('#leave-dialog').on('hide.bs.modal', () => {
// FIXME: Where exactly the user should land? Currently he'll land
// on dashboard (if he's logged in) or login form (if he's not).
this.$router.push({ name: 'dashboard' })
}).modal()
}
}
this.session.onDismiss = connId => { this.dismissParticipant(connId) }
this.session.onSessionDataUpdate = data => { this.updateSession(data) }
this.session.onConnectionChange = (connId, data) => { this.updateParticipant(connId, data) }
this.session.onJoinRequest = data => { this.joinRequest(data) }
this.session.onMediaSetup = () => { this.setupMedia() }
this.meet.joinRoom(this.session)
this.keyboardShortcuts()
},
keyboardShortcuts() {
$(document.body).on('keydown.meet', e => {
if ($(e.target).is('select,input,textarea')) {
return
}
// Self-Mute with 'm' key
if (e.key == 'm' || e.key == 'M') {
if ($('#meet-session-menu').find('.link-audio:not(:disabled)').length) {
this.switchSound()
}
}
})
},
logout() {
const logout = () => {
this.meet.leaveRoom()
this.meet = null
this.$router.push({ name: 'dashboard' })
}
if (this.isRoomOwner()) {
axios.post('/api/v4/openvidu/rooms/' + this.room + '/close').then(logout)
} else {
logout()
}
},
makePicture() {
const video = $("#meet-setup video")[0];
// Skip if video is not "playing"
if (!video.videoWidth || !this.camera) {
return ''
}
// we're going to crop a square from the video and resize it
const maxSize = 64
// Calculate sizing
let sh = Math.floor(video.videoHeight / 1.5)
let sw = sh
let sx = (video.videoWidth - sw) / 2
let sy = (video.videoHeight - sh) / 2
let dh = Math.min(sh, maxSize)
let dw = sh < maxSize ? sw : Math.floor(sw * dh/sh)
const canvas = $("<canvas>")[0];
canvas.width = dw;
canvas.height = dh;
// draw the image on the canvas (square cropped and resized)
canvas.getContext('2d').drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
// convert it to a usable data URL (png format)
return canvas.toDataURL();
},
requestId() {
const key = 'kolab-meet-uid'
if (!this.reqId) {
this.reqId = localStorage.getItem(key)
}
if (!this.reqId) {
// We store the identifier in the browser to make sure that it is the same after
// page refresh for the avg user. This will not prevent hackers from sending
// the new identifier on every request.
// If we're afraid of a room owner being spammed with join requests we might invent
// a way to silently ignore all join requests after the owner pressed some button
// stating "all attendees already joined, lock the room for good!".
// This will create max. 24-char numeric string
this.reqId = (String(Date.now()) + String(Math.random()).substring(2)).substring(0, 24)
localStorage.setItem(key, this.reqId)
}
return this.reqId
},
roomOptions() {
$('#room-options-dialog').modal()
},
setMenuItem(type, state, disabled) {
let button = $('#meet-session-menu').find('.link-' + type)
button[state ? 'removeClass' : 'addClass']('text-danger')
if (disabled !== undefined) {
button.prop('disabled', disabled)
}
},
setupMedia() {
let dialog = $('#media-setup-dialog')
if (!dialog.find('video').length) {
$('#meet-setup').find('video,div.volume').appendTo(dialog.find('.media-setup-preview'))
}
dialog.on('show.bs.modal', () => { this.meet.setupStart() })
.on('hide.bs.modal', () => { this.meet.setupStop() })
.modal()
},
setupSession() {
this.meet.setupStart({
videoElement: $('#meet-setup video')[0],
volumeElement: $('#meet-setup .volume')[0],
onSuccess: setup => {
this.setup = setup
this.microphone = setup.audioSource
this.camera = setup.videoSource
this.setMenuItem('audio', setup.audioActive)
this.setMenuItem('video', setup.videoActive)
},
onError: error => {
this.setMenuItem('audio', false, true)
this.setMenuItem('video', false, true)
}
})
},
setupCameraChange() {
this.meet.setupSetVideoDevice(this.camera).then(enabled => {
this.setMenuItem('video', enabled)
})
},
setupMicrophoneChange() {
this.meet.setupSetAudioDevice(this.microphone).then(enabled => {
this.setMenuItem('audio', enabled)
})
},
switchChannel(e) {
let channel = $(e.target).data('code')
this.$set(this.session, 'channel', channel)
this.meet.switchChannel(channel)
},
switchChat() {
let chat = $('#meet-chat')
let enabled = chat.is('.open')
this.setMenuItem('chat', !enabled)
chat.toggleClass('open')
if (!enabled) {
chat.find('textarea').focus()
}
// Trigger resize, so participant matrix can update its layout
window.dispatchEvent(new Event('resize'));
},
switchFullscreen() {
const element = this.$el
$(element).off('fullscreenchange').on('fullscreenchange', (e) => {
let enabled = document.fullscreenElement == element
let buttons = $('#meet-session-menu').find('.link-fullscreen')
buttons.first()[enabled ? 'addClass' : 'removeClass']('hidden')
buttons.last()[!enabled ? 'addClass' : 'removeClass']('hidden')
})
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
element.requestFullscreen()
}
},
switchHand() {
let enabled = $('#meet-session-menu').find('.link-hand').is('.text-danger')
this.updateSelf({ hand: enabled }, () => { this.setMenuItem('hand', enabled) })
},
switchSound() {
const enabled = this.meet.switchAudio()
this.setMenuItem('audio', enabled)
},
switchVideo() {
const enabled = this.meet.switchVideo()
this.setMenuItem('video', enabled)
},
switchScreen() {
const switchScreenAction = () => {
this.meet.switchScreen((enabled, error) => {
this.setMenuItem('screen', enabled)
if (!enabled && !error) {
// Closing a screen sharing connection invalidates the token
delete this.session.shareToken
}
})
}
if (this.session.shareToken || !$('#meet-session-menu').find('.link-screen').is('.text-danger')) {
switchScreenAction()
} else {
axios.post('/api/v4/openvidu/rooms/' + this.room + '/connections')
.then(response => {
this.session.shareToken = response.data.token
this.meet.updateSession(this.session)
switchScreenAction()
})
}
},
updateParticipant(connId, params) {
if (this.isModerator()) {
axios.put('/api/v4/openvidu/rooms/' + this.room + '/connections/' + connId, params)
}
},
updateSelf(params, onSuccess) {
axios.put('/api/v4/openvidu/rooms/' + this.room + '/connections/' + this.session.connectionId, params)
.then(response => {
if (onSuccess) {
onSuccess(response)
}
})
},
updateSession(data) {
this.session = data
this.channels = data.channels || []
const isPublisher = this.isPublisher()
this.setMenuItem('video', isPublisher ? data.videoActive : false)
this.setMenuItem('audio', isPublisher ? data.audioActive : false)
this.setMenuItem('hand', data.hand)
}
}
}
</script>
diff --git a/src/resources/vue/Rooms.vue b/src/resources/vue/Rooms.vue
index bb3f017e..e20637e4 100644
--- a/src/resources/vue/Rooms.vue
+++ b/src/resources/vue/Rooms.vue
@@ -1,88 +1,86 @@
<template>
<div class="container" dusk="rooms-component">
<div id="meet-rooms" class="card">
<div class="card-body">
<div class="card-title">Voice &amp; Video Conferencing <small><sup class="badge badge-primary">beta</sup></small></div>
<div class="card-text">
<p>
Welcome to our beta program for Voice &amp; Video Conferencing.
</p>
<p>
You have a room of your own at the URL below. This room is only open when you yourself are in
attendance. Use this URL to invite people to join you.
</p>
<p>
<router-link v-if="href" :to="roomRoute">{{ href }}</router-link>
</p>
<p>
This is a work in progress and more features will be added over time. Current features include:
</p>
- <p>
- <dl>
- <dt>Screen Sharing</dt>
- <dd>
- Share your screen for presentations or show-and-tell.
- </dd>
+ <dl>
+ <dt>Screen Sharing</dt>
+ <dd>
+ Share your screen for presentations or show-and-tell.
+ </dd>
- <dt>Room Security</dt>
- <dd>
- Increase the room security by setting a password that attendees will need to know
- before they can enter, or lock the door so attendees will have to knock, and you (the
- moderator) can accept or deny those requests.
- </dd>
+ <dt>Room Security</dt>
+ <dd>
+ Increase the room security by setting a password that attendees will need to know
+ before they can enter, or lock the door so attendees will have to knock, and you (the
+ moderator) can accept or deny those requests.
+ </dd>
- <dt>Eject Attendees</dt>
- <dd>
- Eject attendees from the session in order to force them to reconnect, or address policy
- violations. Click the user icon for effective dismissal.
- </dd>
+ <dt>Eject Attendees</dt>
+ <dd>
+ Eject attendees from the session in order to force them to reconnect, or address policy
+ violations. Click the user icon for effective dismissal.
+ </dd>
- <dt>Silent Audience Members</dt>
- <dd>
- For a webinar-style session, have people that join choose 'None' for both the
- microphone and the camera so as to render them silent audience members, to allow more
- people in to the room.
- </dd>
- </dl>
- </p>
+ <dt>Silent Audience Members</dt>
+ <dd>
+ For a webinar-style session, have people that join choose 'None' for both the
+ microphone and the camera so as to render them silent audience members, to allow more
+ people in to the room.
+ </dd>
+ </dl>
<p>
Keep in mind that this is still in beta and might come with some issues.
Should you encounter any on your way, let us know by contacting support.
</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
rooms: [],
href: '',
roomRoute: ''
}
},
mounted() {
if (!this.$root.hasSKU('meet')) {
this.$root.errorPage(403)
return
}
this.$root.startLoading()
axios.get('/api/v4/openvidu/rooms')
.then(response => {
this.$root.stopLoading()
this.rooms = response.data.list
if (response.data.count) {
this.roomRoute = '/meet/' + encodeURI(this.rooms[0].name)
this.href = window.config['app.url'] + this.roomRoute
}
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
index 644db3ea..fa02dc76 100644
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,265 +1,264 @@
<template>
<div class="container">
<div id="step0">
<div class="plan-selector card-deck">
<div v-for="item in plans" :key="item.id" :class="'card bg-light plan-' + item.title">
<div class="card-header plan-header">
- <div class="plan-ico text-center">
- <svg-icon :icon="plan_icons[item.title]"></svg-icon>
+ <div class="plan-ico text-center">
+ <svg-icon :icon="plan_icons[item.title]"></svg-icon>
</div>
</div>
<div class="card-body text-center ">
<button class="btn btn-primary" :data-title="item.title" @click="selectPlan(item.title)" v-html="item.button"></button>
-
- <div class="plan-description text-left mt-3" v-html="item.description"></div>
- </div>
+ <div class="plan-description text-left mt-3" v-html="item.description"></div>
+ </div>
</div>
</div>
</div>
<div class="card d-none" id="step1">
<div class="card-body">
<h4 class="card-title">Sign Up - Step 1/3</h4>
<p class="card-text">
Sign up to start your free month.
</p>
<form @submit.prevent="submitStep1" data-validation-prefix="signup_">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" id="signup_first_name" placeholder="First Name" autofocus v-model="first_name">
<input type="text" class="form-control rounded-right" id="signup_last_name" placeholder="Surname" v-model="last_name">
</div>
</div>
<div class="form-group">
<label for="signup_email" class="sr-only">Existing Email Address</label>
<input type="text" class="form-control" id="signup_email" placeholder="Existing Email Address" required v-model="email">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">Back</button>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Continue</button>
</form>
</div>
</div>
<div class="card d-none" id="step2">
<div class="card-body">
<h4 class="card-title">Sign Up - Step 2/3</h4>
<p class="card-text">
We sent out a confirmation code to your email address.
Enter the code we sent you, or click the link in the message.
</p>
<form @submit.prevent="submitStep2" data-validation-prefix="signup_">
<div class="form-group">
<label for="signup_short_code" class="sr-only">Confirmation Code</label>
<input type="text" class="form-control" id="signup_short_code" placeholder="Confirmation Code" required v-model="short_code">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">Back</button>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Continue</button>
<input type="hidden" id="signup_code" v-model="code" />
</form>
</div>
</div>
<div class="card d-none" id="step3">
<div class="card-body">
<h4 class="card-title">Sign Up - Step 3/3</h4>
<p class="card-text">
Create your Kolab identity (you can choose additional addresses later).
</p>
<form @submit.prevent="submitStep3" data-validation-prefix="signup_">
<div class="form-group">
<label for="signup_login" class="sr-only"></label>
<div class="input-group">
<input type="text" class="form-control" id="signup_login" required v-model="login" placeholder="Login">
<span class="input-group-append">
<span class="input-group-text">@</span>
</span>
<input v-if="is_domain" type="text" class="form-control rounded-right" id="signup_domain" required v-model="domain" placeholder="Domain">
<select v-else class="custom-select rounded-right" id="signup_domain" required v-model="domain"></select>
</div>
</div>
<div class="form-group">
<label for="signup_password" class="sr-only">Password</label>
<input type="password" class="form-control" id="signup_password" placeholder="Password" required v-model="password">
</div>
<div class="form-group">
<label for="signup_confirm" class="sr-only">Confirm Password</label>
<input type="password" class="form-control" id="signup_confirm" placeholder="Confirm Password" required v-model="password_confirmation">
</div>
<div class="form-group pt-2 pb-2">
<label for="signup_voucher" class="sr-only">Voucher code</label>
<input type="text" class="form-control" id="signup_voucher" placeholder="Voucher code" v-model="voucher">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">Back</button>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
email: '',
first_name: '',
last_name: '',
code: '',
short_code: '',
login: '',
password: '',
password_confirmation: '',
domain: '',
plan: null,
is_domain: false,
plan_icons: {
individual: 'user',
group: 'users'
},
plans: [],
voucher: ''
}
},
mounted() {
let param = this.$route.params.param;
if (param) {
if (this.$route.path.indexOf('/signup/voucher/') === 0) {
// Voucher (discount) code
this.voucher = param
this.displayForm(0)
} else if (/^([A-Z0-9]+)-([a-zA-Z0-9]+)$/.test(param)) {
// Verification code provided, auto-submit Step 2
this.short_code = RegExp.$1
this.code = RegExp.$2
this.submitStep2(true)
} else if (/^([a-zA-Z_]+)$/.test(param)) {
// Plan title provided, save it and display Step 1
this.plan = param
this.displayForm(1, true)
} else {
this.$root.errorPage(404)
}
} else {
this.displayForm(0)
}
},
methods: {
selectPlan(plan) {
this.$router.push({path: '/signup/' + plan})
this.plan = plan
this.displayForm(1, true)
},
// Composes plan selection page
step0() {
if (!this.plans.length) {
this.$root.startLoading()
axios.get('/api/auth/signup/plans').then(response => {
this.$root.stopLoading()
this.plans = response.data.plans
})
.catch(error => {
this.$root.errorHandler(error)
})
}
},
// Submits data to the API, validates and gets verification code
submitStep1() {
this.$root.clearFormValidation($('#step1 form'))
axios.post('/api/auth/signup/init', {
email: this.email,
last_name: this.last_name,
first_name: this.first_name,
plan: this.plan,
voucher: this.voucher
}).then(response => {
this.displayForm(2, true)
this.code = response.data.code
})
},
// Submits the code to the API for verification
submitStep2(bylink) {
if (bylink === true) {
this.displayForm(2, false)
}
this.$root.clearFormValidation($('#step2 form'))
axios.post('/api/auth/signup/verify', {
code: this.code,
short_code: this.short_code
}).then(response => {
this.displayForm(3, true)
// Reset user name/email/plan, we don't have them if user used a verification link
this.first_name = response.data.first_name
this.last_name = response.data.last_name
this.email = response.data.email
this.is_domain = response.data.is_domain
this.voucher = response.data.voucher
// Fill the domain selector with available domains
if (!this.is_domain) {
let options = []
$('select#signup_domain').html('')
$.each(response.data.domains, (i, v) => {
options.push($('<option>').text(v).attr('value', v))
})
$('select#signup_domain').append(options)
this.domain = window.config['app.domain']
}
}).catch(error => {
if (bylink === true) {
// FIXME: display step 1, user can do nothing about it anyway
// Maybe we should display 404 error page?
this.displayForm(1, true)
}
})
},
// Submits the data to the API to create the user account
submitStep3() {
this.$root.clearFormValidation($('#step3 form'))
axios.post('/api/auth/signup', {
code: this.code,
short_code: this.short_code,
login: this.login,
domain: this.domain,
password: this.password,
password_confirmation: this.password_confirmation,
voucher: this.voucher
}).then(response => {
// auto-login and goto dashboard
this.$root.loginUser(response.data)
})
},
// Moves the user a step back in registration form
stepBack(e) {
var card = $(e.target).closest('.card')
card.prev().removeClass('d-none').find('input').first().focus()
card.addClass('d-none').find('form')[0].reset()
if (card.attr('id') == 'step1') {
this.step0()
this.$router.replace({path: '/signup'})
}
},
displayForm(step, focus) {
[0, 1, 2, 3].filter(value => value != step).forEach(value => {
$('#step' + value).addClass('d-none')
})
if (!step) {
return this.step0()
}
$('#step' + step).removeClass('d-none')
if (focus) {
$('#step' + step).find('input').first().focus()
}
}
}
}
</script>

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 5, 11:17 AM (8 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427910
Default Alt Text
(56 KB)

Event Timeline