Browse Source

Merge pull request #6382 from nextcloud/feature/6362/add-ability-to-set-conversation-permissions

Add ability to set default permissions for a conversation
pull/6450/head
Marco 4 years ago
committed by GitHub
parent
commit
b431ed2917
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 266
      src/components/ConversationSettings/ConversationPermissionsSettings.vue
  2. 9
      src/components/ConversationSettings/ConversationSettingsDialog.vue
  3. 236
      src/components/PermissionsEditor/PermissionsEditor.vue
  4. 16
      src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
  5. 48
      src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.spec.js
  6. 163
      src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue
  7. 33
      src/services/conversationsService.js
  8. 20
      src/store/conversationsStore.js
  9. 27
      src/store/conversationsStore.spec.js

266
src/components/ConversationSettings/ConversationPermissionsSettings.vue

@ -0,0 +1,266 @@
<!--
- @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
-
- @author Marco Ambrosini <marcoambrosini@pm.me>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="conversation-permissions-editor">
<h4 class="conversation-permissions-editor__title">
{{ t('spreed', 'Edit the default permissions for participants in this conversation. These settings do not affect moderators.') }}
</h4>
<p>{{ t('spreed', 'Warning: every time permissions are modified in this section, custom permissions previously assigned to individual participants will be lost.') }}</p>
<!-- All permissions -->
<div class="conversation-permissions-editor__setting">
<CheckboxRadioSwitch :checked.sync="radioValue"
:disabled="loading"
value="all"
name="permission_radio"
type="radio"
@update:checked="handleSubmitPermissions">
{{ t('spreed', 'All permissions') }}
</CheckboxRadioSwitch>
<span v-show="loading && radioValue === 'all'" class="icon-loading-small" />
</div>
<p>{{ t('spreed', 'Participants have permissions to start a call, join a call, enable audio, video and screenshare.') }}</p>
<!-- No permissions -->
<div class="conversation-permissions-editor__setting">
<CheckboxRadioSwitch :checked.sync="radioValue"
value="restricted"
:disabled="loading"
name="permission_radio"
type="radio"
@update:checked="handleSubmitPermissions">
{{ t('spreed', 'Restricted') }}
</CheckboxRadioSwitch>
<span v-show="loading && radioValue === 'restricted'" class="icon-loading-small" />
</div>
<p>{{ t('spreed', 'Participants can only join calls, but not enable audio, video or screenshare until a moderator manually grants their permissions manually.') }}</p>
<!-- Advanced permissions -->
<div class="conversation-permissions-editor__setting--advanced">
<CheckboxRadioSwitch :checked.sync="radioValue"
value="advanced"
:disabled="loading"
name="permission_radio"
type="radio"
@update:checked="showPermissionsEditor = true">
{{ t('spreed', 'Advanced permissions') }}
</CheckboxRadioSwitch>
<!-- Edit advanced permissions -->
<button
v-show="showEditButton"
:aria-label="t('spreed', 'Edit permissions')"
class="nc-button nc-button__main"
@click="showPermissionsEditor = true">
<Pencil
:size="20"
decorative
title="" />
</button>
</div>
<PermissionEditor
v-if="showPermissionsEditor"
:conversation-name="conversationName"
:permissions="conversationPermissions"
:loading="loading"
@close="handleClosePermissionsEditor"
@submit="handleSubmitPermissions" />
</div>
</template>
<script>
import PermissionEditor from '../PermissionsEditor/PermissionsEditor.vue'
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import Pencil from 'vue-material-design-icons/Pencil.vue'
import { PARTICIPANT } from '../../constants'
import { showError, showSuccess } from '@nextcloud/dialogs'
const PERMISSIONS = PARTICIPANT.PERMISSIONS
export default {
name: 'ConversationPermissionsSettings',
components: {
PermissionEditor,
CheckboxRadioSwitch,
Pencil,
},
props: {
token: {
type: String,
default: null,
},
},
data() {
return {
showPermissionsEditor: false,
isEditingPermissions: false,
loading: false,
radioValue: '',
}
},
computed: {
/**
* The participant's name.
*/
conversationName() {
return this.$store.getters.conversation(this.token).name
},
/**
* The current conversation permissions.
*/
conversationPermissions() {
return this.$store.getters.conversation(this.token).defaultPermissions
},
/**
* Hides and shows the edit button for advanced permissions.
*/
showEditButton() {
return this.radioValue === 'advanced' && !this.showPermissionsEditor
},
},
mounted() {
/**
* Set the initial radio value.
*/
this.setCurrentRadioValue()
},
methods: {
/**
* Binary sum all the permissions and make the request to change them.
*
* @param {string | number} value - The permissions value, which is a
* string (e.g. 'restricted' or 'all') unless this method is called by
* the click event emitted by the `permissionsEditor` component, in
* which case it's a number indicating the permissions value.
*/
async handleSubmitPermissions(value) {
let permissions
// Compute the permissions value
switch (value) {
case 'all':
permissions = PERMISSIONS.MAX_DEFAULT
break
case 'restricted':
permissions = PERMISSIONS.CALL_JOIN
break
default:
permissions = value
}
this.loading = true
// Make the store call
try {
await this.$store.dispatch('setConversationPermissions', {
token: this.token,
permissions,
})
showSuccess(t('spreed', 'Default permissions modified for {conversationName}', { conversationName: this.conversationName }))
// Modify the radio buttons value
this.radioValue = this.getPermissionRadioValue(permissions)
this.showPermissionsEditor = false
} catch (error) {
console.debug(error)
showError(t('spreed', 'Could not modify default permissions for {conversationName}', { conversationName: this.conversationName }))
// Go back to the previous radio value
this.radioValue = this.getPermissionRadioValue(this.conversationPermissions)
} finally {
this.loading = false
}
},
/**
* Get the radio button string value given a permission number.
*
* @param {number} value - The permissions value.
*/
getPermissionRadioValue(value) {
switch (value) {
case PERMISSIONS.MAX_DEFAULT:
return 'all'
case PERMISSIONS.CALL_JOIN:
return 'restricted'
default:
return 'advanced'
}
},
/**
* Set the radio value that corresponds to the current default
* permissions in the store.
*/
setCurrentRadioValue() {
this.radioValue = this.getPermissionRadioValue(this.conversationPermissions)
},
/**
* Hides the modal and resets conversation permissions to the previous
* value.
*/
handleClosePermissionsEditor() {
this.showPermissionsEditor = false
this.setCurrentRadioValue()
},
},
}
</script>
<style lang="scss" scoped>
@import '../../assets/buttons.scss';
::v-deep .mx-input {
margin: 0;
}
.conversation-permissions-editor {
&__setting {
display: flex;
justify-content: space-between;
&--advanced {
display: flex;
justify-content: flex-start;
// Edit button
button {
margin-left: 16px;
}
}
}
}
p {
color: var(--color-text-lighter);
margin-bottom: 16px;
}
</style>

9
src/components/ConversationSettings/ConversationSettingsDialog.vue

@ -72,6 +72,13 @@
<LockingSettings :token="token" />
</AppSettingsSection>
<!-- Conversation permissions -->
<AppSettingsSection
v-if="canFullModerate"
:title="t('spreed', 'Participants permissions')">
<ConversationPermissionsSettings :token="token" />
</AppSettingsSection>
<!-- Meeting settings -->
<AppSettingsSection
v-if="canFullModerate"
@ -115,6 +122,7 @@ import { showError } from '@nextcloud/dialogs'
import Description from '../Description/Description'
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import BrowserStorage from '../../services/BrowserStorage'
import ConversationPermissionsSettings from './ConversationPermissionsSettings.vue'
export default {
name: 'ConversationSettingsDialog',
@ -132,6 +140,7 @@ export default {
NotificationsSettings,
Description,
CheckboxRadioSwitch,
ConversationPermissionsSettings,
},
data() {

236
src/components/PermissionsEditor/PermissionsEditor.vue

@ -0,0 +1,236 @@
<!--
- @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
-
- @author Marco Ambrosini <marcoambrosini@pm.me>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<Modal v-on="$listeners">
<div class="wrapper">
<template v-if="!loading">
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="title" v-html="modalTitle" />
<form @submit.prevent="handleSubmitPermissions">
<CheckboxRadioSwitch
ref="callStart"
:checked.sync="callStart"
class="checkbox">
{{ t('spreed', 'Start a call') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="lobbyIgnore"
:checked.sync="lobbyIgnore"
class="checkbox">
{{ t('spreed', 'Skip the lobby') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishAudio"
:checked.sync="publishAudio"
class="checkbox">
{{ t('spreed', 'Enable the microphone') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishVideo"
:checked.sync="publishVideo"
class="checkbox">
{{ t('spreed', 'Enable the camera') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishScreen"
:checked.sync="publishScreen"
class="checkbox">
{{ t('spreed', 'Share the screen') }}
</CheckboxRadioSwitch>
<button ref="submit"
type="submit"
:disabled="submitButtonDisabled"
class="nc-button primary">
{{ t('spreed', 'Update permissions') }}
</button>
</form>
</template>
<div v-if="loading" class="loading-screen">
<span class="icon-loading" />
<p>{{ t('spreed', 'Updating permissions') }}</p>
</div>
</div>
</Modal>
</template>
<script>
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import { PARTICIPANT } from '../../constants'
import Modal from '@nextcloud/vue/dist/Components/Modal'
const PERMISSIONS = PARTICIPANT.PERMISSIONS
export default {
name: 'PermissionsEditor',
components: {
CheckboxRadioSwitch,
Modal,
},
props: {
permissions: {
type: Number,
default: null,
},
/**
* The user's displayname. Don't provide this only when modifying the
* default conversation's permissions.
*/
displayName: {
type: String,
default: '',
},
/**
* The conversation's name. Don't provide this when modifying
* participants' permissions.
*/
conversationName: {
type: String,
default: '',
},
/**
* Displays the loading state of this component
*/
loading: {
type: Boolean,
default: false,
},
},
data() {
return {
// Permission to start a call
callStart: false,
// Permission to bypass the lobby
lobbyIgnore: false,
// Permission to enable the microphone
publishAudio: false,
// Permission to enable the camera
publishVideo: false,
// Permission to start a screenshare
publishScreen: false,
}
},
computed: {
modalTitle() {
if (this.displayName) {
return t('spreed', 'In this conversation <strong>{user}</strong> can:', {
user: this.displayName,
})
} else if (this.conversationName) {
return t('spreed', 'Edit default permissions for participants in <strong>{conversationName}</strong>', {
conversationName: this.conversationName,
})
} else throw Error('you need to fill either the conversationName or the displayName props')
},
/**
* The number of the edited permissions during the editing of the form.
* We use this to compare it with the actual permissions of the
* participant in the store and enable or disable the submit button
* accordingly.
*/
formPermissions() {
return (this.callStart ? PERMISSIONS.CALL_START : 0)
| (this.lobbyIgnore ? PERMISSIONS.LOBBY_IGNORE : 0)
| (this.publishAudio ? PERMISSIONS.PUBLISH_AUDIO : 0)
| (this.publishVideo ? PERMISSIONS.PUBLISH_VIDEO : 0)
| (this.publishScreen ? PERMISSIONS.PUBLISH_SCREEN : 0)
| PERMISSIONS.CUSTOM
},
/**
* If the permissions are not changed, the submit button will stay
* disabled.
*/
submitButtonDisabled() {
return this.permissions === this.formPermissions
},
},
mounted() {
this.writePermissionsToComponent(this.permissions)
},
methods: {
/**
* Takes the permissions from the store and writes them in the data of
* this component.
*
* @param {number} permissions - the permissions number.
*/
writePermissionsToComponent(permissions) {
permissions & PERMISSIONS.CALL_START ? this.callStart = true : this.callStart = false
permissions & PERMISSIONS.LOBBY_IGNORE ? this.lobbyIgnore = true : this.lobbyIgnore = false
permissions & PERMISSIONS.PUBLISH_AUDIO ? this.publishAudio = true : this.publishAudio = false
permissions & PERMISSIONS.PUBLISH_VIDEO ? this.publishVideo = true : this.publishVideo = false
permissions & PERMISSIONS.PUBLISH_SCREEN ? this.publishScreen = true : this.publishScreen = false
},
handleSubmitPermissions() {
this.$emit('submit', this.formPermissions)
},
},
}
</script>
<style lang="scss" scoped>
@import '../../assets/buttons.scss';
$editor-width: 350px;
.nc-button {
width: 100%;
margin-top: 12px;
}
.wrapper {
width: $editor-width;
padding: 0 24px 24px 24px;
}
.title {
font-size: 16px;
margin-bottom: 12px;
padding-top: 24px;
}
.loading-screen {
width: $editor-width;
height: 200px;
text-align: center;
font-weight: bold;
display: flex;
flex-direction: column;
justify-content: center;
// Loader
span {
margin-bottom: 16px;
}
}
</style>

16
src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue

@ -182,7 +182,7 @@
</ActionButton>
<ActionButton
:close-after-click="true"
@click="showParticipantPermissionsEditor">
@click="showPermissionsEditor">
<template #icon>
<Pencil
:size="20"
@ -213,12 +213,12 @@
</ActionButton>
</Actions>
<ParticipantPermissionsEditor
v-if="participantPermissionsEditor"
v-if="permissionsEditor"
:actor-id="participant.actorId"
:close-after-click="true"
:participant="participant"
:token="token"
@close="hideParticipantPermissionsEditor" />
@close="hidePermissionsEditor" />
<!-- Checkmark in case the current participant is selected -->
<div v-if="isSelected" class="icon-checkmark participant-row__utils utils__checkmark" />
</li>
@ -312,7 +312,7 @@ export default {
return {
isUserNameTooltipVisible: false,
isStatusTooltipVisible: false,
participantPermissionsEditor: false,
permissionsEditor: false,
}
},
@ -670,12 +670,12 @@ export default {
}
},
showParticipantPermissionsEditor() {
this.participantPermissionsEditor = true
showPermissionsEditor() {
this.permissionsEditor = true
},
hideParticipantPermissionsEditor() {
this.participantPermissionsEditor = false
hidePermissionsEditor() {
this.permissionsEditor = false
},
},
}

48
src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.spec.js

@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash'
import storeConfig from '../../../../../../store/storeConfig'
import { PARTICIPANT, ATTENDEE } from '../../../../../../constants'
import PermissionsEditor from '../../../../../PermissionsEditor/PermissionsEditor.vue'
import ParticipantPermissionsEditor from './ParticipantPermissionsEditor'
describe('ParticipantPermissionsEditor.vue', () => {
@ -23,7 +24,7 @@ describe('ParticipantPermissionsEditor.vue', () => {
actorId: 'alice-actor-id',
actorType: ATTENDEE.ACTOR_TYPE.USERS,
participantType: PARTICIPANT.TYPE.USER,
permissions: PARTICIPANT.PERMISSIONS.CALL_START
attendeePermissions: PARTICIPANT.PERMISSIONS.CALL_START
| PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO
| PARTICIPANT.PERMISSIONS.PUBLISH_VIDEO,
attendeeId: 'alice-attendee-id',
@ -53,8 +54,7 @@ describe('ParticipantPermissionsEditor.vue', () => {
/**
* @param {object} participant Participant with optional user status data
*/
function mountParticipantPermissionsEditor(participant) {
const mountParticipantPermissionsEditor = (participant) => {
return mount(ParticipantPermissionsEditor, {
localVue,
store,
@ -66,47 +66,47 @@ describe('ParticipantPermissionsEditor.vue', () => {
}
describe('Properly renders the checkboxes when mounted', () => {
test('Properly renders the call start checkbox', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const callStartCheckbox = wrapper.findComponent({ ref: 'callStart' })
test('Properly renders the call start checkbox', async () => {
const wrapper = await mountParticipantPermissionsEditor(participant)
const callStartCheckbox = wrapper.findComponent(PermissionsEditor).findComponent({ ref: 'callStart' })
expect(callStartCheckbox.vm.$options.propsData.checked).toBe(true)
})
test('Properly renders the lobby Ignore checkbox', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const lobbyIgnoreCheckbox = wrapper.findComponent({ ref: 'lobbyIgnore' })
test('Properly renders the lobby Ignore checkbox', async () => {
const wrapper = await mountParticipantPermissionsEditor(participant)
const lobbyIgnoreCheckbox = wrapper.findComponent(PermissionsEditor).findComponent({ ref: 'lobbyIgnore' })
expect(lobbyIgnoreCheckbox.vm.$options.propsData.checked).toBe(false)
})
test('Properly renders the publish audio checkbox', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const publishAudioCheckbox = wrapper.findComponent({ ref: 'publishAudio' })
test('Properly renders the publish audio checkbox', async () => {
const wrapper = await mountParticipantPermissionsEditor(participant)
const publishAudioCheckbox = wrapper.findComponent(PermissionsEditor).findComponent({ ref: 'publishAudio' })
expect(publishAudioCheckbox.vm.$options.propsData.checked).toBe(true)
})
test('Properly renders the publish video checkbox', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const publishVideoCheckbox = wrapper.findComponent({ ref: 'publishVideo' })
test('Properly renders the publish video checkbox', async () => {
const wrapper = await mountParticipantPermissionsEditor(participant)
const publishVideoCheckbox = wrapper.findComponent(PermissionsEditor).findComponent({ ref: 'publishVideo' })
expect(publishVideoCheckbox.vm.$options.propsData.checked).toBe(true)
})
test('Properly renders the publish screen checkbox', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const publishScreenCheckbox = wrapper.findComponent({ ref: 'publishScreen' })
test('Properly renders the publish screen checkbox', async () => {
const wrapper = await mountParticipantPermissionsEditor(participant)
const publishScreenCheckbox = wrapper.findComponent(PermissionsEditor).findComponent({ ref: 'publishScreen' })
expect(publishScreenCheckbox.vm.$options.propsData.checked).toBe(false)
})
})
describe('Dispatches the aciton to set the right permissions', () => {
describe('Dispatches the aciton to set the right permissions', async () => {
test('Dispatches setPermissions with the correct permissions value when a permission is subtracted', async () => {
const wrapper = mountParticipantPermissionsEditor(participant)
const wrapper = await mountParticipantPermissionsEditor(participant)
// Add a permission
wrapper.setData({ lobbyIgnore: true })
wrapper.findComponent(PermissionsEditor).setData({ lobbyIgnore: true })
// Click the submit button
await wrapper.find({ ref: 'submit' }).trigger('click')
await wrapper.findComponent(PermissionsEditor).find({ ref: 'submit' }).trigger('click')
expect(testStoreConfig.modules.participantsStore.actions.setPermissions).toHaveBeenCalledWith(
// The first argument is the context object
@ -125,10 +125,10 @@ describe('ParticipantPermissionsEditor.vue', () => {
const wrapper = mountParticipantPermissionsEditor(participant)
// Remove a permission
wrapper.setData({ publishAudio: false })
wrapper.findComponent(PermissionsEditor).setData({ publishAudio: false })
// Click the submit button
await wrapper.find({ ref: 'submit' }).trigger('click')
await wrapper.findComponent(PermissionsEditor).find({ ref: 'submit' }).trigger('click')
expect(testStoreConfig.modules.participantsStore.actions.setPermissions).toHaveBeenCalledWith(
// The first argument is the context object

163
src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue

@ -20,66 +20,24 @@
-->
<template>
<Modal v-on="$listeners">
<div class="wrapper">
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="title" v-html="modalTitle" />
<form @submit.prevent="handleSubmitPermissions">
<CheckboxRadioSwitch
ref="callStart"
:checked.sync="callStart"
class="checkbox">
{{ t('spreed', 'Start a call') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="lobbyIgnore"
:checked.sync="lobbyIgnore"
class="checkbox">
{{ t('spreed', 'Skip the lobby') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishAudio"
:checked.sync="publishAudio"
class="checkbox">
{{ t('spreed', 'Enable the microphone') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishVideo"
:checked.sync="publishVideo"
class="checkbox">
{{ t('spreed', 'Enable the camera') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch
ref="publishScreen"
:checked.sync="publishScreen"
class="checkbox">
{{ t('spreed', 'Share the screen') }}
</CheckboxRadioSwitch>
<button ref="submit"
type="submit"
:disabled="submitButtonDisabled"
class="nc-button primary">
{{ t('spreed', 'Update permissions') }}
</button>
</form>
</div>
</Modal>
<div class="wrapper">
<PermissionEditor
:display-name="displayName"
:permissions="attendeePermissions"
v-on="$listeners"
@submit="handleSubmitPermissions" />
</div>
</template>
<script>
import Modal from '@nextcloud/vue/dist/Components/Modal'
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import { PARTICIPANT } from '../../../../../../constants'
import { showError, showSuccess } from '@nextcloud/dialogs'
const PERMISSIONS = PARTICIPANT.PERMISSIONS
import PermissionEditor from '../../../../../PermissionsEditor/PermissionsEditor.vue'
export default {
name: 'ParticipantPermissionsEditor',
components: {
Modal,
CheckboxRadioSwitch,
PermissionEditor,
},
props: {
@ -100,21 +58,6 @@ export default {
},
},
data() {
return {
// Permission to start a call
callStart: false,
// Permission to bypass the lobby
lobbyIgnore: false,
// Permission to enable the microphone
publishAudio: false,
// Permission to enable the camera
publishVideo: false,
// Permission to start a screenshare
publishScreen: false,
}
},
computed: {
/**
* The participant's name.
@ -123,12 +66,6 @@ export default {
return this.participant.displayName
},
modalTitle() {
return t('spreed', 'In this conversation <strong>{user}</strong> can:', {
user: this.displayName,
})
},
/**
* The participant's attendeeId: a unique number that identifies the
* participant.
@ -141,74 +78,23 @@ export default {
* Permisssions of the current participant, from the participants
* store.
*/
currentPermissions() {
return this.participant.permissions
},
/**
* The number of the edited permissions during the editing of the form.
* We use this to compare it with the actual permissions of the
* participant in the store and enable or disable the submit button
* accordingly.
*/
formPermissions() {
return (this.callStart ? PERMISSIONS.CALL_START : 0)
| (this.lobbyIgnore ? PERMISSIONS.LOBBY_IGNORE : 0)
| (this.publishAudio ? PERMISSIONS.PUBLISH_AUDIO : 0)
| (this.publishVideo ? PERMISSIONS.PUBLISH_VIDEO : 0)
| (this.publishScreen ? PERMISSIONS.PUBLISH_SCREEN : 0)
| PERMISSIONS.CUSTOM
},
/**
* If the permissions are not changed, the submit button will stay
* disabled.
*/
submitButtonDisabled() {
return this.currentPermissions === this.formPermissions
},
},
watch: {
/**
* Every time that the permissions change in the store, we write them
* to this component's data.
*
* @param {number} newValue - the updated permissions.
*/
currentPermissions(newValue) {
this.writePermissionsToComponent(newValue)
attendeePermissions() {
return this.participant.attendeePermissions
},
},
beforeMount() {
this.writePermissionsToComponent(this.currentPermissions)
},
methods: {
/**
* Takes the permissions from the store and writes them in the data of
* this component.
*
* @param {number} permissions - the permissions number.
*/
writePermissionsToComponent(permissions) {
permissions & PERMISSIONS.CALL_START ? this.callStart = true : this.callStart = false
permissions & PERMISSIONS.LOBBY_IGNORE ? this.lobbyIgnore = true : this.lobbyIgnore = false
permissions & PERMISSIONS.PUBLISH_AUDIO ? this.publishAudio = true : this.publishAudio = false
permissions & PERMISSIONS.PUBLISH_VIDEO ? this.publishVideo = true : this.publishVideo = false
permissions & PERMISSIONS.PUBLISH_SCREEN ? this.publishScreen = true : this.publishScreen = false
},
/**
* Binary sum all the permissions and make the request to change them.
*
* @param {number} permissions - the permission number.
*/
handleSubmitPermissions() {
handleSubmitPermissions(permissions) {
try {
this.$store.dispatch('setPermissions', {
token: this.token,
attendeeId: this.attendeeId,
permissions: this.formPermissions,
permissions,
})
showSuccess(t('spreed', 'Permissions modified for {displayName}', { displayName: this.displayName }))
} catch (error) {
@ -222,22 +108,3 @@ export default {
},
}
</script>
<style lang="scss" scoped>
@import '../../../../../../assets/buttons.scss';
.wrapper {
width: 350px;
padding: 24px;
}
.nc-button {
width: 100%;
margin-top: 12px;
}
.title {
font-size: 16px;
margin-bottom: 12px;
}
</style>

33
src/services/conversationsService.js

@ -354,6 +354,37 @@ const setConversationDescription = async function(token, description) {
return response
}
/**
* Set the default permissions for participants in a conversation.
*
* @param {string} token conversation token
* @param {number} permissions the type of permission to be granted. Valid values are
* any sums of 'DEFAULT', 'CUSTOM', 'CALL_START', 'CALL_JOIN', 'LOBBY_IGNORE',
* 'PUBLISH_AUDIO', 'PUBLISH_VIDEO', 'PUBLISH_SCREEN'.
*/
const setConversationPermissions = async (token, permissions) => {
await axios.put(generateOcsUrl('apps/spreed/api/v4/room/{token}/permissions/default', { token }),
{
permissions,
})
}
/**
* Set the default permissions for participants in a call. These will be reset
* to default once the call has ended.
*
* @param {string} token conversation token
* @param {number} permissions the type of permission to be granted. Valid values are
* any sums of 'DEFAULT', 'CUSTOM', 'CALL_START', 'CALL_JOIN', 'LOBBY_IGNORE',
* 'PUBLISH_AUDIO', 'PUBLISH_VIDEO', 'PUBLISH_SCREEN'.
*/
const setCallPermissions = async (token, permissions) => {
await axios.put(generateOcsUrl('apps/spreed/api/v4/room/{token}/permissions/call', { token }),
{
permissions,
})
}
export {
fetchConversations,
fetchConversation,
@ -378,4 +409,6 @@ export {
setConversationName,
setConversationDescription,
clearConversationHistory,
setConversationPermissions,
setCallPermissions,
}

20
src/store/conversationsStore.js

@ -38,6 +38,8 @@ import {
clearConversationHistory,
setNotificationLevel,
setNotificationCalls,
setConversationPermissions,
setCallPermissions,
} from '../services/conversationsService'
import { getCurrentUser } from '@nextcloud/auth'
import { CONVERSATION, WEBINAR, PARTICIPANT } from '../constants'
@ -141,6 +143,14 @@ const mutations = {
setNotificationCalls(state, { token, notificationCalls }) {
Vue.set(state.conversations[token], 'notificationCalls', notificationCalls)
},
setConversationPermissions(state, { token, permissions }) {
Vue.set(state.conversations[token], 'defaultPermissions', permissions)
},
setCallPermissions(state, { token, permissions }) {
Vue.set(state.conversations[token], 'callPermissions', permissions)
},
}
const actions = {
@ -465,6 +475,16 @@ const actions = {
return conversation
},
async setConversationPermissions(context, { token, permissions }) {
await setConversationPermissions(token, permissions)
context.commit('setConversationPermissions', { token, permissions })
},
async setCallPermissions(context, { token, permissions }) {
await setCallPermissions(token, permissions)
context.commit('setCallPermissions', { token, permissions })
},
}
export default { state, mutations, getters, actions }

27
src/store/conversationsStore.spec.js

@ -24,6 +24,8 @@ import {
fetchConversation,
fetchConversations,
deleteConversation,
setConversationPermissions,
setCallPermissions,
} from '../services/conversationsService'
jest.mock('../services/conversationsService', () => ({
@ -42,6 +44,8 @@ jest.mock('../services/conversationsService', () => ({
fetchConversation: jest.fn(),
fetchConversations: jest.fn(),
deleteConversation: jest.fn(),
setConversationPermissions: jest.fn(),
setCallPermissions: jest.fn(),
}))
describe('conversationsStore', () => {
@ -51,6 +55,7 @@ describe('conversationsStore', () => {
let localVue = null
let store = null
let addParticipantOnceAction = null
const permissions = PARTICIPANT.PERMISSIONS.MAX_CUSTOM
beforeEach(() => {
localVue = createLocalVue()
@ -65,6 +70,8 @@ describe('conversationsStore', () => {
attendeeId: 'attendee-id-1',
actorType: ATTENDEE.ACTOR_TYPE.USERS,
actorId: 'actor-id',
defaultPermissions: PARTICIPANT.PERMISSIONS.CUSTOM,
callPermissions: PARTICIPANT.PERMISSIONS.CUSTOM,
}
testStoreConfig = cloneDeep(storeConfig)
@ -611,4 +618,24 @@ describe('conversationsStore', () => {
expect(addedConversation).toBe(newConversation)
})
})
test('sets default permissions for a conversation', async () => {
expect(store.getters.selectedParticipants).toStrictEqual([])
await store.dispatch('setConversationPermissions', { token: testToken, permissions })
expect(setConversationPermissions).toHaveBeenCalledWith(testToken, permissions)
expect(store.getters.conversation(testToken).defaultPermissions).toBe(permissions)
})
test('sets default permissions for a call', async () => {
expect(store.getters.selectedParticipants).toStrictEqual([])
await store.dispatch('setCallPermissions', { token: testToken, permissions })
expect(setCallPermissions).toHaveBeenCalledWith(testToken, permissions)
expect(store.getters.conversation(testToken).callPermissions).toBe(permissions)
})
})
Loading…
Cancel
Save