Skip to content

Commit

Permalink
Merge pull request #6595 from nextcloud/feat/add-talk-calendar
Browse files Browse the repository at this point in the history
feat:select existing talk room for conversations on calendar events
  • Loading branch information
GretaD authored Jan 9, 2025
2 parents 6fcbef2 + 1d63a89 commit 091fba8
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 5 deletions.
257 changes: 257 additions & 0 deletions src/components/Editor/AddTalkModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<NcModal size="normal"
class="modal"
:name="t('calendar', 'Select a Talk Room')"
@close="$emit('close', $event)">
<div class="modal-content">
<h2>{{ t('calendar', 'Add Talk conversation') }}</h2>
<div class="talk-room-list">
<NcEmptyContent v-if="loading"
icon="icon-loading"
class="modal__content__loading"
:description="t('calendar','Fetching Talk rooms...')" />
<NcEmptyContent v-else-if="talkConversations.length === 0"
:description="t('calendar','No Talk room available')" />
<ul v-else>
<li v-for="conversation in talkConversations"
:key="conversation.id"
:class="{ selected: selectedRoom && selectedRoom.id === conversation.id }"
class="talk-room-list__item"
@click="selectRoom(conversation)">
<NcAvatar :url="avatarUrl(conversation)"
:size="28"
:disable-tooltip="true" />
<span>{{ conversation.displayName }}</span>
</li>
</ul>
</div>
<div class="sticky-footer">
<NcButton class="talk_new-room" :disabled="creatingTalkRoom" @click="createTalkRoom">
<template #icon>
<IconAdd :size="20" />
</template>
{{ t('calendar', 'Create a new conversation') }}
</NcButton>
<NcButton type="primary"
class="talk_select-room"
:disabled="!selectedRoom"
@click="selectConversation(selectedRoom)">
{{ t('calendar', 'Select conversation') }}
</NcButton>
</div>
</div>
</NcModal>
</template>

<script>
import {
NcButton,
NcModal,
NcAvatar,
NcEmptyContent,
} from '@nextcloud/vue'
import axios from '@nextcloud/axios'
import { createTalkRoom, generateURLForToken } from '../../services/talkService.js'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { generateOcsUrl } from '@nextcloud/router'
import IconAdd from 'vue-material-design-icons/Plus.vue'
import useCalendarObjectInstanceStore from '../../store/calendarObjectInstance.js'
import { mapStores } from 'pinia'

// Ref https://github.com/nextcloud/spreed/blob/main/docs/constants.md
const CONVERSATION_TYPE_GROUP = 2
const CONVERSATION_TYPE_PUBLIC = 3
const CONVERSATION_OBJECT_TYPE_VIDEO_VERIFICATION = 'share:password'
const PARTICIPANT_TYPE_OWNER = 1
const PARTICIPANT_TYPE_MODERATOR = 2

export default {
name: 'AddTalkModal',
components: {
NcButton,
NcModal,
IconAdd,
NcAvatar,
NcEmptyContent,
},
props: {
calendarObjectInstance: {
type: Object,
required: true,
},
conversations: {
type: Array,
required: true,
},
},
data() {
return {
talkConversations: [],
selectedConversation: null,
creatingTalkRoom: false,
selectedRoom: false,
loading: true,
}
},
computed: {
...mapStores(useCalendarObjectInstanceStore, ['calendarObjectInstance']),
},
async mounted() {
await this.fetchTalkConversations()
},
methods: {
avatarUrl(conversation) {
return generateOcsUrl('apps/spreed/api/v1/room/{token}/avatar', {
token: conversation.token,
})
},
async fetchTalkConversations() {
try {
const response = await axios.get(generateOcsUrl('apps/spreed/api/v4/room'))
this.talkConversations = response.data.ocs.data.filter(conversation =>
(conversation.participantType === PARTICIPANT_TYPE_OWNER
|| conversation.participantType === PARTICIPANT_TYPE_MODERATOR)
&& (conversation.type === CONVERSATION_TYPE_GROUP
|| (conversation.type === CONVERSATION_TYPE_PUBLIC
&& conversation.objectType !== CONVERSATION_OBJECT_TYPE_VIDEO_VERIFICATION)),
)
} catch (error) {
console.error('Error fetching Talk conversations:', error)
showError(this.$t('calendar', 'Error fetching Talk conversations.'))
} finally {
this.loading = false
}
},
selectRoom(conversation) {
this.selectedRoom = conversation
},

async selectConversation(conversation) {
try {
const url = generateURLForToken(conversation.token)

if (!url) {
showError(this.$t('calendar', 'Conversation does not have a valid URL.'))
return
}

if ((this.calendarObjectInstance.location ?? '').trim() === '') {
this.calendarObjectInstanceStore.changeLocation({
calendarObjectInstance: this.calendarObjectInstance,
location: url,
})
showSuccess(this.$t('calendar', 'Successfully added Talk room link to location.'))
} else {
const NEW_LINE = '\r\n'
const updatedDescription = this.calendarObjectInstance.description
? this.calendarObjectInstance.description + NEW_LINE + NEW_LINE + url
: url

this.calendarObjectInstanceStore.changeDescription({
calendarObjectInstance: this.calendarObjectInstance,
description: updatedDescription,
})
showSuccess(this.$t('calendar', 'Successfully added Talk room link to description.'))
}

this.selectedConversation = conversation
} catch (error) {
console.error('Error applying conversation to event:', error)
showError(this.$t('calendar', 'Failed to apply Talk room.'))
} finally {
this.closeModal()
}
},

async createTalkRoom() {
const NEW_LINE = '\r\n'
try {
this.creatingTalkRoom = true
const url = await createTalkRoom(
this.calendarObjectInstance.title,
this.calendarObjectInstance.description,
)

if (!url) {
throw new Error('No URL returned from createTalkRoom')
}

if ((this.calendarObjectInstance.location ?? '').trim() === '') {
this.$emit('update-location', url)
showSuccess(this.$t('calendar', 'Successfully added Talk room link to location.'))
} else {
const newDescription = this.calendarObjectInstance.description
? this.calendarObjectInstance.description + NEW_LINE + NEW_LINE + url + NEW_LINE
: url

this.$emit('update-description', newDescription)
showSuccess(this.$t('calendar', 'Successfully added Talk room link to description.'))
}
} catch (error) {
console.error('Error creating Talk room:', error)
showError(this.$t('calendar', 'Error creating Talk room.'))
} finally {
this.creatingTalkRoom = false
}
},
closeModal() {
this.$emit('close')
},
},
}
</script>

<style lang="scss" scoped>
.talk-room-list {
flex: 1;
overflow-y: auto;
padding: 10px;
font-weight: 600;

&__item {
display: flex;
gap: calc(var(--default-grid-baseline) * 2);
align-items: center;
padding: 6px 6px 6px 9px;
height: 34px;
&:hover {
background-color: var(--color-background-hover);
border-radius: var(--border-radius-large);

}
&.selected {
background-color: var(--color-primary-element);
border-radius: var(--border-radius-large);
color: white;
}
}
}

.sticky-footer {
position: sticky;
bottom: 0;
padding: 16px;
text-align: right;
display: flex;
}
.talk_new-room {
margin-right: auto;
}

.talk_select-room {
margin-left: auto;
}

h2 {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
height: 30px;
}
</style>
2 changes: 1 addition & 1 deletion src/services/talkService.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function doesContainTalkLink(text) {
* @param {string} token The token to the call room
* @return {string}
*/
function generateURLForToken(token = '') {
export function generateURLForToken(token = '') {
return window.location.protocol + '//' + window.location.host + generateUrl('/call/' + token)
}

Expand Down
2 changes: 1 addition & 1 deletion src/store/calendarObjectInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ export default defineStore('calendarObjectInstance', {
* @param {string=} data.language Preferred language of the attendee
* @param {string=} data.timezoneId Preferred timezone of the attendee
* @param {object=} data.organizer Principal of the organizer to be set if not present
* @param {string|array} data.member Group membership(s)
* @param {string | Array} data.member Group membership(s)
*/
addAttendee({
calendarObjectInstance,
Expand Down
Loading

0 comments on commit 091fba8

Please sign in to comment.