You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
14 KiB
561 lines
14 KiB
<!--
|
|
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
-->
|
|
|
|
<template>
|
|
<NcAppSidebar v-if="isSidebarAvailable"
|
|
:open="opened"
|
|
:name="sidebarTitle"
|
|
:title="sidebarTitle"
|
|
:active.sync="activeTab"
|
|
:class="'active-tab-' + activeTab"
|
|
:toggle-classes="{ 'chat-button-sidebar-toggle': isInCall }"
|
|
:toggle-attrs="isInCall ? inCallToggleAttrs : undefined"
|
|
@update:open="handleUpdateOpen"
|
|
@update:active="handleUpdateActive"
|
|
@closed="handleClosed">
|
|
<!-- Use a custom icon when sidebar is used for chat messages during the call -->
|
|
<template v-if="isInCall" #toggle-icon>
|
|
<IconMessageText :size="20" />
|
|
<span v-if="unreadMessagesCounter > 0" class="chat-button-unread-marker" />
|
|
</template>
|
|
<!-- search in messages button-->
|
|
<template v-if="!showSearchMessagesTab && getUserId" #secondary-actions>
|
|
<NcActionButton type="tertiary"
|
|
:title="t('spreed', 'Search messages')"
|
|
@click="handleShowSearch(true)">
|
|
<template #icon>
|
|
<IconMagnify :size="20" />
|
|
</template>
|
|
</NcActionButton>
|
|
</template>
|
|
<template v-else-if="getUserId" #tertiary-actions>
|
|
<NcButton type="tertiary"
|
|
:title="t('spreed', 'Back')"
|
|
@click="handleShowSearch(false)">
|
|
<template #icon>
|
|
<IconArrowLeft class="bidirectional-icon" :size="20" />
|
|
</template>
|
|
</NcButton>
|
|
</template>
|
|
<template #description>
|
|
<InternalSignalingHint />
|
|
<LobbyStatus v-if="canFullModerate && hasLobbyEnabled" :token="token" />
|
|
</template>
|
|
<NcAppSidebarTab v-if="showSearchMessagesTab"
|
|
id="search-messages"
|
|
key="search-messages"
|
|
:order="0"
|
|
:name="t('spreed', 'Search messages')">
|
|
<SearchMessagesTab :is-active="activeTab === 'search-messages'"
|
|
@close="handleShowSearch(false)" />
|
|
</NcAppSidebarTab>
|
|
<template v-else>
|
|
<NcAppSidebarTab v-if="isInCall"
|
|
id="chat"
|
|
key="chat"
|
|
:order="1"
|
|
:name="t('spreed', 'Chat')">
|
|
<template #icon>
|
|
<IconMessage :size="20" />
|
|
</template>
|
|
<ChatView :is-visible="opened" is-sidebar />
|
|
</NcAppSidebarTab>
|
|
<NcAppSidebarTab v-if="showParticipantsTab"
|
|
id="participants"
|
|
key="participants"
|
|
ref="participantsTab"
|
|
:order="2"
|
|
:name="participantsText">
|
|
<template #icon>
|
|
<IconAccountMultiple :size="20" />
|
|
</template>
|
|
<ParticipantsTab :is-active="activeTab === 'participants'"
|
|
:can-search="canSearchParticipants"
|
|
:can-add="canAddParticipants" />
|
|
</NcAppSidebarTab>
|
|
<NcAppSidebarTab v-if="showBreakoutRoomsTab"
|
|
id="breakout-rooms"
|
|
key="breakout-rooms"
|
|
ref="breakout-rooms"
|
|
:order="3"
|
|
:name="breakoutRoomsText">
|
|
<template #icon>
|
|
<IconDotsCircle :size="20" />
|
|
</template>
|
|
<BreakoutRoomsTab :main-token="mainConversationToken"
|
|
:main-conversation="mainConversation"
|
|
:is-active="activeTab === 'breakout-rooms'" />
|
|
</NcAppSidebarTab>
|
|
<NcAppSidebarTab v-if="showDetailsTab"
|
|
id="details-tab"
|
|
key="details-tab"
|
|
:order="4"
|
|
:name="t('spreed', 'Details')">
|
|
<template #icon>
|
|
<IconInformationOutline :size="20" />
|
|
</template>
|
|
<SetGuestUsername v-if="!getUserId" />
|
|
<SipSettings v-if="showSIPSettings" :conversation="conversation" />
|
|
<div v-if="!getUserId" id="app-settings">
|
|
<div id="app-settings-header">
|
|
<NcButton type="tertiary" @click="showSettings">
|
|
<template #icon>
|
|
<IconCog :size="20" />
|
|
</template>
|
|
{{ t('spreed', 'Settings') }}
|
|
</NcButton>
|
|
</div>
|
|
</div>
|
|
</NcAppSidebarTab>
|
|
<NcAppSidebarTab v-if="showSharedItemsTab"
|
|
id="shared-items"
|
|
key="shared-items"
|
|
ref="sharedItemsTab"
|
|
:order="5"
|
|
:name="t('spreed', 'Shared items')">
|
|
<template #icon>
|
|
<IconFolderMultipleImage :size="20" />
|
|
</template>
|
|
<SharedItemsTab :active="activeTab === 'shared-items'" />
|
|
</NcAppSidebarTab>
|
|
</template>
|
|
</NcAppSidebar>
|
|
</template>
|
|
|
|
<script>
|
|
import IconAccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
|
|
import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
|
|
import IconCog from 'vue-material-design-icons/Cog.vue'
|
|
import IconDotsCircle from 'vue-material-design-icons/DotsCircle.vue'
|
|
import IconFolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage.vue'
|
|
import IconInformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
|
|
import IconMessage from 'vue-material-design-icons/Message.vue'
|
|
import IconMessageText from 'vue-material-design-icons/MessageText.vue'
|
|
|
|
import { showMessage } from '@nextcloud/dialogs'
|
|
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
import { t } from '@nextcloud/l10n'
|
|
|
|
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
|
import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
|
|
import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
|
|
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
|
|
|
import BreakoutRoomsTab from './BreakoutRooms/BreakoutRoomsTab.vue'
|
|
import InternalSignalingHint from './InternalSignalingHint.vue'
|
|
import LobbyStatus from './LobbyStatus.vue'
|
|
import ParticipantsTab from './Participants/ParticipantsTab.vue'
|
|
import SearchMessagesTab from './SearchMessages/SearchMessagesTab.vue'
|
|
import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
|
|
import SipSettings from './SipSettings.vue'
|
|
import ChatView from '../ChatView.vue'
|
|
import SetGuestUsername from '../SetGuestUsername.vue'
|
|
|
|
import { CONVERSATION, WEBINAR, PARTICIPANT } from '../../constants.js'
|
|
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
|
|
import { useSidebarStore } from '../../stores/sidebar.ts'
|
|
|
|
export default {
|
|
name: 'RightSidebar',
|
|
components: {
|
|
BreakoutRoomsTab,
|
|
ChatView,
|
|
InternalSignalingHint,
|
|
LobbyStatus,
|
|
NcActionButton,
|
|
NcAppSidebar,
|
|
NcAppSidebarTab,
|
|
NcButton,
|
|
ParticipantsTab,
|
|
SearchMessagesTab,
|
|
SetGuestUsername,
|
|
SharedItemsTab,
|
|
SipSettings,
|
|
// Icons
|
|
IconAccountMultiple,
|
|
IconArrowLeft,
|
|
IconCog,
|
|
IconDotsCircle,
|
|
IconFolderMultipleImage,
|
|
IconInformationOutline,
|
|
IconMagnify,
|
|
IconMessage,
|
|
IconMessageText,
|
|
},
|
|
|
|
props: {
|
|
isInCall: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
},
|
|
|
|
setup() {
|
|
return {
|
|
sidebarStore: useSidebarStore()
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
activeTab: 'participants',
|
|
contactsLoading: false,
|
|
unreadNotificationHandle: null,
|
|
showSearchMessagesTab: false,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
isSidebarAvailable() {
|
|
return this.token && !this.isInLobby
|
|
},
|
|
show() {
|
|
return this.sidebarStore.show
|
|
},
|
|
opened() {
|
|
return this.isSidebarAvailable && this.show
|
|
},
|
|
token() {
|
|
return this.$store.getters.getToken()
|
|
},
|
|
|
|
conversation() {
|
|
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
|
|
},
|
|
|
|
mainConversationToken() {
|
|
if (this.conversation.objectType === CONVERSATION.OBJECT_TYPE.BREAKOUT_ROOM) {
|
|
return this.conversation.objectId
|
|
}
|
|
return this.token
|
|
},
|
|
|
|
mainConversation() {
|
|
return this.$store.getters.conversation(this.mainConversationToken) || this.$store.getters.dummyConversation
|
|
},
|
|
|
|
getUserId() {
|
|
return this.$store.getters.getUserId()
|
|
},
|
|
|
|
canAddParticipants() {
|
|
return this.canFullModerate && this.canSearchParticipants
|
|
},
|
|
|
|
canSearchParticipants() {
|
|
return (this.conversation.type === CONVERSATION.TYPE.GROUP
|
|
|| (this.conversation.type === CONVERSATION.TYPE.PUBLIC && this.conversation.objectType !== CONVERSATION.OBJECT_TYPE.VIDEO_VERIFICATION))
|
|
},
|
|
|
|
participantType() {
|
|
return this.conversation.participantType
|
|
},
|
|
|
|
canFullModerate() {
|
|
return this.participantType === PARTICIPANT.TYPE.OWNER || this.participantType === PARTICIPANT.TYPE.MODERATOR
|
|
},
|
|
|
|
isModeratorOrUser() {
|
|
return this.$store.getters.isModeratorOrUser
|
|
},
|
|
|
|
isInLobby() {
|
|
return this.$store.getters.isInLobby
|
|
},
|
|
|
|
showSIPSettings() {
|
|
return this.conversation.sipEnabled !== WEBINAR.SIP.DISABLED
|
|
&& this.conversation.attendeePin
|
|
},
|
|
|
|
hasLobbyEnabled() {
|
|
return this.conversation.lobbyState === WEBINAR.LOBBY.NON_MODERATORS
|
|
},
|
|
|
|
isOneToOne() {
|
|
return this.conversation.type === CONVERSATION.TYPE.ONE_TO_ONE
|
|
|| this.conversation.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER
|
|
},
|
|
|
|
participantsText() {
|
|
const participants = this.$store.getters.participantsList(this.token)
|
|
return t('spreed', 'Participants ({count})', { count: participants.length })
|
|
},
|
|
|
|
breakoutRoomsConfigured() {
|
|
return this.conversation.breakoutRoomMode !== CONVERSATION.BREAKOUT_ROOM_MODE.NOT_CONFIGURED
|
|
},
|
|
|
|
supportFederationV1() {
|
|
return hasTalkFeature(this.token, 'federation-v1')
|
|
},
|
|
|
|
showBreakoutRoomsTab() {
|
|
return this.getUserId && !this.isOneToOne
|
|
&& (!this.supportFederationV1 || !this.conversation.remoteServer)
|
|
&& (this.breakoutRoomsConfigured || this.conversation.breakoutRoomMode === CONVERSATION.BREAKOUT_ROOM_MODE.FREE || this.conversation.objectType === CONVERSATION.OBJECT_TYPE.BREAKOUT_ROOM)
|
|
},
|
|
|
|
showParticipantsTab() {
|
|
return (this.getUserId || this.isModeratorOrUser) && (!this.isOneToOne || this.isInCall) && !this.isNoteToSelf
|
|
},
|
|
|
|
showSharedItemsTab() {
|
|
return this.getUserId && (!this.supportFederationV1 || !this.conversation.remoteServer)
|
|
},
|
|
|
|
showDetailsTab() {
|
|
return !this.getUserId || this.showSIPSettings
|
|
},
|
|
|
|
isNoteToSelf() {
|
|
return this.conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF
|
|
},
|
|
|
|
breakoutRoomsText() {
|
|
return t('spreed', 'Breakout rooms')
|
|
},
|
|
|
|
unreadMessagesCounter() {
|
|
return this.conversation.unreadMessages
|
|
},
|
|
hasUnreadMentions() {
|
|
return this.conversation.unreadMention
|
|
},
|
|
|
|
inCallToggleAttrs() {
|
|
return {
|
|
'data-theme-dark': true,
|
|
'aria-label': t('spreed', 'Open chat'),
|
|
title: t('spreed', 'Open chat')
|
|
}
|
|
},
|
|
|
|
sidebarTitle() {
|
|
return this.showSearchMessagesTab
|
|
? t('spreed', 'Search in {name}', { name: this.conversation.displayName }, undefined, {
|
|
escape: false,
|
|
sanitize: false,
|
|
})
|
|
: this.conversation.displayName
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
conversation(newConversation, oldConversation) {
|
|
if (newConversation.token === oldConversation.token || !this.showParticipantsTab) {
|
|
return
|
|
}
|
|
|
|
// Remain on "breakout-rooms" tab, when switching back to main room
|
|
if (this.breakoutRoomsConfigured && this.activeTab === 'breakout-rooms') {
|
|
return
|
|
}
|
|
|
|
// In other case switch to other tabs
|
|
if (this.isInCall) {
|
|
this.activeTab = 'chat'
|
|
} else {
|
|
this.activeTab = 'participants'
|
|
}
|
|
},
|
|
|
|
showParticipantsTab: {
|
|
immediate: true,
|
|
handler(value) {
|
|
if (!value) {
|
|
this.activeTab = 'shared-items'
|
|
}
|
|
},
|
|
},
|
|
|
|
unreadMessagesCounter(newValue, oldValue) {
|
|
if (!this.isInCall || this.opened) {
|
|
return
|
|
}
|
|
|
|
// new messages arrived
|
|
if (newValue > 0 && oldValue === 0 && !this.hasUnreadMentions) {
|
|
this.notifyUnreadMessages(t('spreed', 'You have new unread messages in the chat.'))
|
|
}
|
|
},
|
|
|
|
hasUnreadMentions(newValue) {
|
|
if (!this.isInCall || this.opened) {
|
|
return
|
|
}
|
|
|
|
if (newValue) {
|
|
this.notifyUnreadMessages(t('spreed', 'You have been mentioned in the chat.'))
|
|
}
|
|
},
|
|
|
|
isInCall(newValue) {
|
|
if (newValue) {
|
|
// Set 'chat' tab as active, and switch to it if sidebar is open
|
|
this.activeTab = 'chat'
|
|
return
|
|
}
|
|
|
|
// discard notification if the call ends
|
|
this.notifyUnreadMessages(null)
|
|
|
|
// If 'chat' tab wasn't active, leave it as is
|
|
if (this.activeTab !== 'chat') {
|
|
return
|
|
}
|
|
|
|
// In other case switch to other tabs
|
|
if (!this.isOneToOne) {
|
|
this.activeTab = 'participants'
|
|
}
|
|
},
|
|
|
|
token() {
|
|
if (this.$refs.participantsTab) {
|
|
this.$refs.participantsTab.$el.scrollTop = 0
|
|
}
|
|
|
|
// Discard notification if the conversation changes or closed
|
|
this.notifyUnreadMessages(null)
|
|
},
|
|
|
|
isModeratorOrUser(newValue) {
|
|
if (newValue) {
|
|
// Fetch participants list if guest was promoted to moderators
|
|
this.$nextTick(() => {
|
|
emit('guest-promoted', { token: this.token })
|
|
})
|
|
} else {
|
|
// Switch active tab to chat if guest was demoted from moderators
|
|
this.activeTab = 'chat'
|
|
}
|
|
},
|
|
|
|
},
|
|
|
|
mounted() {
|
|
subscribe('spreed:select-active-sidebar-tab', this.handleUpdateActive)
|
|
},
|
|
|
|
beforeDestroy() {
|
|
unsubscribe('spreed:select-active-sidebar-tab', this.handleUpdateActive)
|
|
},
|
|
|
|
methods: {
|
|
t,
|
|
|
|
handleUpdateOpen(open) {
|
|
if (open) {
|
|
// In call ('Open chat') by default
|
|
if (this.isInCall) {
|
|
this.activeTab = 'chat'
|
|
}
|
|
this.sidebarStore.showSidebar()
|
|
} else {
|
|
this.sidebarStore.hideSidebar()
|
|
}
|
|
},
|
|
|
|
handleUpdateActive(active) {
|
|
this.activeTab = active
|
|
},
|
|
|
|
handleShowSearch(value) {
|
|
this.showSearchMessagesTab = value
|
|
// FIXME upstream: NcAppSidebar should emit update:active
|
|
if (value) {
|
|
this.activeTab = 'search-messages'
|
|
} else {
|
|
this.activeTab = this.isInCall ? 'chat' : 'participants'
|
|
}
|
|
},
|
|
|
|
showSettings() {
|
|
emit('show-settings', {})
|
|
},
|
|
|
|
handleClosed() {
|
|
emit('files:sidebar:closed', {})
|
|
},
|
|
|
|
notifyUnreadMessages(message) {
|
|
if (this.unreadNotificationHandle) {
|
|
this.unreadNotificationHandle.hideToast()
|
|
this.unreadNotificationHandle = null
|
|
}
|
|
if (message) {
|
|
this.unreadNotificationHandle = showMessage(message, {
|
|
onClick: () => {
|
|
this.activeTab = 'chat'
|
|
this.sidebarStore.showSidebar()
|
|
},
|
|
})
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
/* Override style set in server for "#app-sidebar" to match the style set in
|
|
* nextcloud-vue for ".app-sidebar". */
|
|
#app-sidebar {
|
|
display: flex;
|
|
}
|
|
|
|
:deep(.app-sidebar-header__description) {
|
|
flex-direction: column;
|
|
}
|
|
|
|
// FIXME upstream: move styles to nextcloud-vue library
|
|
:deep(.app-sidebar-tabs__nav) {
|
|
padding: 0 10px;
|
|
|
|
.checkbox-radio-switch__label {
|
|
text-align: center;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.checkbox-radio-switch__icon {
|
|
flex-basis: auto;
|
|
|
|
span {
|
|
margin: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.app-sidebar-tabs__content #tab-chat {
|
|
/* Remove padding to maximize the space for the chat view. */
|
|
padding: 0;
|
|
height: 100%;
|
|
}
|
|
|
|
.chat-button-unread-marker {
|
|
position: absolute;
|
|
top: 4px;
|
|
right: 4px;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 8px;
|
|
background-color: var(--color-primary-element);
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
/*
|
|
* NcAppSidebar toggle it rendered on the page outside the sidebar element, so we need global styles here.
|
|
* It is _quite_ safe, as chat-button-sidebar-toggle class is defined here manually, not an internal class.
|
|
*/
|
|
.chat-button-sidebar-toggle {
|
|
position: relative;
|
|
// Allow unread counter to overflow rounded button
|
|
overflow: visible !important;
|
|
}
|
|
</style>
|