Browse Source

Merge pull request #8654 from nextcloud/feature/8339/broadcast-messages-to-all-breakout-rooms

Broadcast message to all breakout rooms
pull/8669/head
Marco 3 years ago
committed by GitHub
parent
commit
c0ba03046b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 109
      src/components/BreakoutRoomsEditor/BreakoutRoomsList.vue
  2. 22
      src/components/BreakoutRoomsEditor/SendMessageDialog.vue
  3. 55
      src/components/NewMessageForm/NewMessageForm.vue
  4. 179
      src/components/RightSidebar/BreakoutRooms/BreakoutRoomsTab.vue
  5. 50
      src/services/breakoutRoomsService.js
  6. 10
      src/store/breakoutRoomsStore.js

109
src/components/BreakoutRoomsEditor/BreakoutRoomsList.vue

@ -0,0 +1,109 @@
<!--
- @copyright Copyright (c) 2023 Marco Ambrosini <marcoambrosini@icloud.com>
-
- @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @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>
<template v-for="breakoutRoom in breakoutRooms">
<NcAppNavigationItem :key="breakoutRoom.displayName"
class="breakout-rooms__room"
:title="breakoutRoom.displayName"
:allow-collapse="true"
:open="true">
<template #icon>
<!-- TODO: choose final icon -->
<GoogleCircles :size="20" />
</template>
<template #actions>
<NcActionButton @click="openSendMessageForm(breakoutRoom.token)">
<template #icon>
<Send :size="16" />
</template>
{{ t('spreed', 'Send message to room') }}
</NcActionButton>
</template>
<!-- Send message dialog -->
<SendMessageDialog v-if="openedDialog === breakoutRoom.token"
:display-name="breakoutRoom.displayName"
:token="breakoutRoom.token"
@close="closeSendMessageForm(breakoutRoom.token)" />
<template v-for="participant in $store.getters.participantsList(breakoutRoom.token)">
<Participant :key="participant.actorId" :participant="participant" />
</template>
</NcAppNavigationItem>
</template>
</div>
</template>
<script>
// Components
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import Participant from '../RightSidebar/Participants/ParticipantsList/Participant/Participant.vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import SendMessageDialog from './SendMessageDialog.vue'
// Icons
import GoogleCircles from 'vue-material-design-icons/GoogleCircles.vue'
import Send from 'vue-material-design-icons/Send.vue'
export default {
name: 'BreakoutRoomsList',
components: {
// Components
NcAppNavigationItem,
Participant,
NcActionButton,
SendMessageDialog,
// Icons
GoogleCircles,
Send,
},
props: {
breakoutRooms: {
type: Array,
required: true,
},
},
data() {
return {
openedDialog: undefined,
}
},
methods: {
openSendMessageForm(token) {
this.openedDialog = token
},
closeSendMessageForm() {
this.openedDialog = undefined
},
},
}
</script>
<style lang="scss" scoped>
</style>

22
src/components/BreakoutRoomsEditor/SendMessageDialog.vue

@ -29,6 +29,7 @@
:token="token"
:breakout-room="true"
:aria-label="t('spreed', 'Post message')"
:broadcast="broadcast"
@sent="handleMessageSent"
@failure="handleMessageFailure" />
</div>
@ -58,24 +59,37 @@ export default {
},
/**
* The breakout room display name
* The conversation display name
*/
displayName: {
type: String,
required: true,
default: '',
},
/**
* Broadcast messages to all breakout rooms of a given conversation. In
* case this is true, the token needs to be from a conversation that
* has breakout rooms configured.
*/
broadcast: {
type: Boolean,
default: false,
},
},
computed: {
dialogTitle() {
return t('spreed', 'Post a message to "{roomName}"', { roomName: this.displayName })
return this.broadcast
? t('spreed', 'Send message to all breakout rooms')
: t('spreed', 'Send a message to "{roomName}"', { roomName: this.displayName })
},
},
methods: {
handleMessageSent() {
showSuccess(t('spreed', 'The message was sent to "{roomName}"', { roomName: this.displayName }))
showSuccess(this.broadcast
? t('spreed', 'The message was sent to all breakout rooms')
: t('spreed', 'The message was sent to "{roomName}"', { roomName: this.displayName }))
this.$emit('close')
},

55
src/components/NewMessageForm/NewMessageForm.vue

@ -35,7 +35,7 @@
<form class="new-message-form"
@submit.prevent>
<!-- Attachments menu -->
<div v-if="canUploadFiles || canShareFiles"
<div v-if="showAttachmentsMenu"
class="new-message-form__upload-menu">
<NcActions ref="attachmentsMenu"
:container="container"
@ -130,7 +130,8 @@
@audio-file="handleAudioFile" />
<!-- Send buttons -->
<template v-else>
<NcActions :force-menu="true">
<NcActions v-if="!broadcast"
:force-menu="true">
<!-- Silent send -->
<NcActionButton :close-after-click="true"
icon="icon-upload"
@ -295,6 +296,14 @@ export default {
type: Boolean,
default: false,
},
/**
* Broadcast messages to all breakout rooms of a given conversation.
*/
broadcast: {
type: Boolean,
default: false,
},
},
data() {
@ -436,6 +445,10 @@ export default {
'--height': this.fileTemplate.ratio ? Math.round(width / this.fileTemplate.ratio) + 'px' : null,
}
},
showAttachmentsMenu() {
return (this.canUploadFiles || this.canShareFiles) && !this.broadcast
},
},
watch: {
@ -555,7 +568,9 @@ export default {
if (this.parsedText !== '') {
const temporaryMessage = await this.$store.dispatch('createTemporaryMessage', { text: this.parsedText, token: this.token })
// FIXME: move "addTemporaryMessage" into "postNewMessage" as it's a pre-requisite anyway ?
this.$store.dispatch('addTemporaryMessage', temporaryMessage)
if (!this.broadcast) {
await this.$store.dispatch('addTemporaryMessage', temporaryMessage)
}
this.text = ''
this.parsedText = ''
@ -565,15 +580,31 @@ export default {
}
// Also remove the message to be replied for this conversation
this.$store.dispatch('removeMessageToBeReplied', this.token)
await this.$store.dispatch('removeMessageToBeReplied', this.token)
try {
await this.$store.dispatch('postNewMessage', { temporaryMessage, options })
this.$emit('sent')
} catch {
this.$emit('failure')
}
this.breakoutRoom
? await this.broadcastMessage(temporaryMessage, options)
: await this.postMessage(temporaryMessage, options)
}
},
// Post message to conversation
async postMessage(temporaryMessage, options) {
try {
await this.$store.dispatch('postNewMessage', { temporaryMessage, options })
this.$emit('sent')
} catch {
this.$emit('failure')
}
},
// Broadcast message to all breakout rooms
async broadcastMessage(temporaryMessage, options) {
try {
await this.$store.dispatch('broadcastMessageToBreakoutRoomsAction', { temporaryMessage, options })
this.$emit('sent')
} catch {
this.$emit('failure')
}
},
@ -632,7 +663,7 @@ export default {
if (!path.startsWith('/')) {
throw new Error(t('files', 'Invalid path selected'))
}
shareFile(path, this.token)
await shareFile(path, this.token)
this.$refs.advancedInput.focusInput()
})
@ -674,7 +705,7 @@ export default {
* @param {File[] | FileList} files pasted files list
*/
async handlePastedFiles(files) {
this.handleFiles(files, true)
await this.handleFiles(files, true)
},
/**

179
src/components/RightSidebar/BreakoutRooms/BreakoutRoomsTab.vue

@ -21,108 +21,92 @@
<template>
<div class="breakout-rooms">
<!-- Series of buttons at the top of the tab, these affect all
breakout rooms -->
<div class="breakout-rooms__actions">
<template v-if="breakoutRoomsConfigured">
<NcButton v-if="breakoutRoomsStarted"
:title="t('spreed', 'Start breakout rooms')"
:aria-label="t('spreed', 'Start breakout rooms')"
type="secondary"
@click="startBreakoutRooms">
<div class="breakout-rooms__actions-group">
<template v-if="breakoutRoomsConfigured">
<NcButton v-if="breakoutRoomsStarted"
:title="t('spreed', 'Start breakout rooms')"
:aria-label="t('spreed', 'Start breakout rooms')"
type="tertiary"
@click="startBreakoutRooms">
<template #icon>
<Play :size="20" />
</template>
</NcButton>
<NcButton v-else
:title="t('spreed', 'Stop breakout rooms')"
:aria-label="t('spreed', 'Stop breakout rooms')"
type="tertiary"
@click="stopBreakoutRooms">
<template #icon>
<StopIcon :size="20" />
</template>
</NcButton>
<NcButton :title="t('spreed', 'Send message to breakout rooms')"
:aria-label="t('spreed', 'Send message to breakout rooms')"
type="tertiary"
@click="openSendMessageDialog">
<template #icon>
<Message :size="18" />
</template>
</NcButton>
</template>
</div>
<div class="breakout-rooms__actions-group">
<!-- Configuration button -->
<NcButton :type="breakoutRoomsConfigured ? 'tertiary' : 'secondary'"
:title="configurationButtonTitle"
:aria-label="configurationButtonTitle"
@click="openBreakoutRoomsEditor">
<template #icon>
<Play :size="20" />
<Reload :size="20" />
</template>
</NcButton>
<NcButton v-else
:title="t('spreed', 'Stop breakout rooms')"
:aria-label="t('spreed', 'Stop breakout rooms')"
type="secondary"
@click="stopBreakoutRooms">
<NcButton v-if="breakoutRoomsConfigured"
:title="t('spreed', 'Delete breakout rooms')"
:aria-label="t('spreed', 'Delete breakout rooms')"
type="tertiary"
@click="deleteBreakoutRooms">
<template #icon>
<StopIcon :size="20" />
<Delete :size="20" />
</template>
</NcButton>
</template>
<!-- Configuration button -->
<NcButton :wide="true"
:type="breakoutRoomsConfigured ? 'tertiary' : 'secondary'"
@click="openBreakoutRoomsEditor">
<template #icon>
<DotsCircle :size="20" />
</template>
<template v-if="!breakoutRoomsConfigured">
{{ t('spreed', 'Configure breakout rooms') }}
</template>
<template v-else>
{{ t('spreed', 'Re-configure breakout rooms') }}
</template>
</NcButton>
<NcButton v-if="breakoutRoomsConfigured"
:title="t('spreed', 'Delete breakout rooms')"
:aria-label="t('spreed', 'Delete breakout rooms')"
type="tertiary-no-background"
@click="deleteBreakoutRooms">
<template #icon>
<Delete :size="20" />
</template>
</NcButton>
</div>
</div>
<template v-if="breakoutRoomsConfigured">
<template v-if="breakoutRooms">
<template v-for="breakoutRoom in breakoutRooms">
<NcAppNavigationItem :key="breakoutRoom.displayName"
class="breakout-rooms__room"
:title="breakoutRoom.displayName"
:allow-collapse="true"
:open="true">
<template #icon>
<!-- TODO: choose final icon -->
<GoogleCircles :size="20" />
</template>
<template #actions>
<NcActionButton @click="openSendMessageForm(breakoutRoom.token)">
<template #icon>
<Send :size="16" />
</template>
{{ t('spreed', 'Send message to room') }}
</NcActionButton>
</template>
<!-- Send message form -->
<SendMessageDialog v-if="openedDialog === breakoutRoom.token"
:display-name="breakoutRoom.displayName"
:token="breakoutRoom.token"
@close="closeSendMessageForm(breakoutRoom.token)" />
<template v-for="participant in $store.getters.participantsList(breakoutRoom.token)">
<Participant :key="participant.actorId" :participant="participant" />
</template>
</NcAppNavigationItem>
</template>
</template>
</template>
<!-- Breakout rooms list -->
<BreakoutRoomsList v-if="breakoutRooms" :breakout-rooms="breakoutRooms" />
<!-- Breakout rooms editor -->
<BreakoutRoomsEditor v-if="showBreakoutRoomsEditor"
:token="token"
@close="showBreakoutRoomsEditor = false" />
<!-- Breakout rooms editor -->
<BreakoutRoomsEditor v-if="showBreakoutRoomsEditor"
:token="token"
@close="showBreakoutRoomsEditor = false" />
<!-- Send message dialog -->
<SendMessageDialog v-if="sendMessageDialogOpened"
:token="token"
:broadcast="true"
@close="closeSendMessageDialog" />
</template>
</div>
</template>
<script>
// Components
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import Participant from '../Participants/ParticipantsList/Participant/Participant.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import BreakoutRoomsEditor from '../../BreakoutRoomsEditor/BreakoutRoomsEditor.vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import SendMessageDialog from '../../BreakoutRoomsEditor/SendMessageDialog.vue'
import BreakoutRoomsList from '../../BreakoutRoomsEditor/BreakoutRoomsList.vue'
// Icons
import GoogleCircles from 'vue-material-design-icons/GoogleCircles.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import Play from 'vue-material-design-icons/Play.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue'
import DotsCircle from 'vue-material-design-icons/DotsCircle.vue'
import Send from 'vue-material-design-icons/Send.vue'
import Reload from 'vue-material-design-icons/Reload.vue'
import Message from 'vue-material-design-icons/Message.vue'
// Constants
import { CONVERSATION } from '../../../constants.js'
@ -132,20 +116,17 @@ export default {
components: {
// Components
NcAppNavigationItem,
Participant,
NcButton,
BreakoutRoomsEditor,
NcActionButton,
SendMessageDialog,
BreakoutRoomsList,
// Icons
GoogleCircles,
Delete,
Play,
DotsCircle,
Reload,
StopIcon,
Send,
Message,
},
props: {
@ -163,8 +144,7 @@ export default {
data() {
return {
showBreakoutRoomsEditor: false,
openedDialog: undefined,
referencesHaveChanged: false,
sendMessageDialogOpened: false,
}
},
@ -190,6 +170,10 @@ export default {
breakoutRoomsStarted() {
return this.conversation.breakoutRoomStatus !== CONVERSATION.BREAKOUT_ROOM_STATUS.STARTED
},
configurationButtonTitle() {
return this.breakoutRoomsConfigured ? t('spreed', 'Re-configure breakout rooms') : t('spreed', 'Configure breakout rooms')
},
},
mounted() {
@ -253,12 +237,12 @@ export default {
this.$store.dispatch('stopBreakoutRoomsAction', this.token)
},
openSendMessageForm(token) {
this.openedDialog = token
openSendMessageDialog() {
this.sendMessageDialogOpened = true
},
closeSendMessageForm() {
this.openedDialog = undefined
closeSendMessageDialog() {
this.sendMessageDialogOpened = false
},
},
}
@ -269,7 +253,14 @@ export default {
.breakout-rooms {
&__actions {
display: flex;
justify-content: flex-end;
justify-content: space-between;
margin-bottom: calc(var(--default-grid-baseline) * 3);
}
&__actions-group {
display: flex;
gap: var(--default-grid-baseline);
}
&__room {
@ -283,7 +274,7 @@ export default {
// TODO: upstream collapse icon position fix
::v-deep .icon-collapse {
position: absolute !important;
left: 0;
position: absolute !important;
left: 0;
}
</style>

50
src/services/breakoutRoomsService.js

@ -22,6 +22,16 @@
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
/**
* Create breakout rooms for a given conversation
*
* @param token The conversation token
* @param mode Either manual, auto, or free, see constants file
* @param amount The amount of breakout rooms to be created
* @param attendeeMap A json encoded Map of attendeeId => room number (0 based)
* (Only considered when the mode is "manual")
* @return {Promise<AxiosResponse<any>>}
*/
const configureBreakoutRooms = async function(token, mode, amount, attendeeMap) {
return await axios.post(generateOcsUrl('/apps/spreed/api/v1/breakout-rooms/{token}', { token }), {
mode,
@ -30,26 +40,66 @@ const configureBreakoutRooms = async function(token, mode, amount, attendeeMap)
})
}
/**
* Deletes all breakout rooms for a given conversation
*
* @param token
* @return {Promise<AxiosResponse<any>>}
*/
const deleteBreakoutRooms = async function(token) {
return await axios.delete(generateOcsUrl('/apps/spreed/api/v1/breakout-rooms/{token}', { token }))
}
/**
* Fetches the breakout rooms for given conversation
*
* @param token The conversation token
* @return {Promise<AxiosResponse<any>>} The array of conversations
*/
const getBreakoutRooms = async function(token) {
return await axios.get(generateOcsUrl('/apps/spreed/api/v4/room/{token}/breakout-rooms', { token }))
}
/**
*
* @param token The conversation token
* @return {Promise<AxiosResponse<any>>} The array of conversations
*/
const startBreakoutRooms = async function(token) {
return await axios.post(generateOcsUrl('/apps/spreed/api/v1/breakout-rooms/{token}/rooms', { token }))
}
/**
* Stops the breakout rooms
*
* @param token The conversation token
* @return {Promise<AxiosResponse<any>>} The array of conversations
*/
const stopBreakoutRooms = async function(token) {
return await axios.delete(generateOcsUrl('/apps/spreed/api/v1/breakout-rooms/{token}/rooms', { token }))
}
/**
*
* @param token the conversation token
* @param message The message to be posted
* @param token The conversation token
* @return {Promise<AxiosResponse<any>>} The array of conversations
*/
const broadcastMessageToBreakoutRooms = async function(message, token) {
return await axios.post(generateOcsUrl('/apps/spreed/api/v1/breakout-rooms/{token}/broadcast', {
token,
}), {
message,
token,
})
}
export {
configureBreakoutRooms,
deleteBreakoutRooms,
getBreakoutRooms,
startBreakoutRooms,
stopBreakoutRooms,
broadcastMessageToBreakoutRooms,
}

10
src/store/breakoutRoomsStore.js

@ -25,6 +25,7 @@ import {
getBreakoutRooms,
startBreakoutRooms,
stopBreakoutRooms,
broadcastMessageToBreakoutRooms,
} from '../services/breakoutRoomsService.js'
import { showError } from '@nextcloud/dialogs'
import { set } from 'vue'
@ -100,6 +101,15 @@ const actions = {
showError(t('spreed', 'An error occurred while stopping breakout rooms'))
}
},
async broadcastMessageToBreakoutRoomsAction(context, { temporaryMessage }) {
try {
await broadcastMessageToBreakoutRooms(temporaryMessage.message, temporaryMessage.token)
} catch (error) {
console.error(error)
showError(t('spreed', 'An error occurred while sending a message to the breakout rooms'))
}
},
}
export default { state, getters, mutations, actions }
Loading…
Cancel
Save