spreed/src/FilesSidebarTabApp.vue

440 строки
14 KiB
Vue

<!--
- @copyright Copyright (c) 2019 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 class="talkChatTab">
<div v-if="isTalkSidebarSupportedForFile === undefined" class="emptycontent ui-not-ready-placeholder">
<div class="icon icon-loading" />
</div>
<div v-else-if="!isTalkSidebarSupportedForFile" class="emptycontent file-not-shared">
<div class="icon icon-talk" />
<h2>{{ t('spreed', 'Discuss this file') }}</h2>
<p>{{ t('spreed', 'Share this file with others to discuss it') }}</p>
<NcButton type="primary" @click="openSharingTab">
{{ t('spreed', 'Share this file') }}
</NcButton>
</div>
<div v-else-if="isTalkSidebarSupportedForFile && !token" class="emptycontent room-not-joined">
<div class="icon icon-talk" />
<h2>{{ t('spreed', 'Discuss this file') }}</h2>
<NcButton type="primary" @click="joinConversation">
{{ t('spreed', 'Join conversation') }}
</NcButton>
</div>
<template v-else>
<CallButton class="call-button" />
<ChatView />
<UploadEditor />
<DeviceChecker :initialize-on-mounted="false" />
</template>
</div>
</template>
<script>
import { EventBus } from './services/EventBus.js'
import { getFileConversation } from './services/filesIntegrationServices.js'
import {
leaveConversationSync,
} from './services/participantsService.js'
import CancelableRequest from './utils/cancelableRequest.js'
import { signalingKill } from './utils/webrtc/index.js'
import { getCurrentUser } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import Axios from '@nextcloud/axios'
import UploadEditor from './components/UploadEditor.vue'
import CallButton from './components/TopBar/CallButton.vue'
import ChatView from './components/ChatView.vue'
import sessionIssueHandler from './mixins/sessionIssueHandler.js'
import browserCheck from './mixins/browserCheck.js'
import '@nextcloud/dialogs/styles/toast.scss'
import DeviceChecker from './components/DeviceChecker/DeviceChecker.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
export default {
name: 'FilesSidebarTabApp',
components: {
CallButton,
ChatView,
UploadEditor,
DeviceChecker,
NcButton,
},
mixins: [
browserCheck,
sessionIssueHandler,
],
data() {
return {
// needed for reactivity
Talk: OCA.Talk,
sidebarState: OCA.Files.Sidebar.state,
/**
* Stores the cancel function returned by `cancelableLookForNewMessages`,
*/
cancelGetFileConversation: () => {},
isTalkSidebarSupportedForFile: undefined,
}
},
computed: {
fileInfo() {
return this.Talk.fileInfo || {}
},
fileId() {
return this.fileInfo.id
},
token() {
return this.$store.getters.getToken()
},
fileIdForToken() {
return this.$store.getters.getFileIdForToken()
},
isChatTheActiveTab() {
// FIXME check for empty active tab is currently needed because the
// activeTab is not set when opening the sidebar from the "Details"
// action (which opens the first tab, which is the Chat tab).
return !this.sidebarState.activeTab || this.sidebarState.activeTab === 'chat'
},
},
watch: {
fileInfo: {
immediate: true,
handler(fileInfo) {
if (this.token && (!fileInfo || fileInfo.id !== this.fileIdForToken)) {
this.leaveConversation()
}
this.setTalkSidebarSupportedForFile(fileInfo)
},
},
isChatTheActiveTab: {
immediate: true,
handler(isChatTheActiveTab) {
this.forceTabsContentStyleWhenChatTabIsActive(isChatTheActiveTab)
// recheck the file info in case the sharing info was changed
this.setTalkSidebarSupportedForFile(this.fileInfo)
},
},
},
created() {
// The fetchCurrentConversation event handler/callback is started and
// stopped from different FilesSidebarTabApp instances, so it needs to
// be stored in a common place. Moreover, as the bound method would be
// overriden when a new instance is created the one used as handler is
// a wrapper that calls the latest bound method. This makes possible to
// register and unregister it from different instances.
if (!OCA.Talk.fetchCurrentConversationWrapper) {
OCA.Talk.fetchCurrentConversationWrapper = function() {
OCA.Talk.fetchCurrentConversationBound()
}
}
OCA.Talk.fetchCurrentConversationBound = this.fetchCurrentConversation.bind(this)
},
beforeMount() {
this.$store.dispatch('setCurrentUser', getCurrentUser())
window.addEventListener('unload', () => {
console.info('Navigating away, leaving conversation')
if (this.token) {
// We have to do this synchronously, because in unload and beforeunload
// Promises, async and await are prohibited.
signalingKill()
if (!this.isLeavingAfterSessionIssue) {
leaveConversationSync(this.token)
}
}
})
},
methods: {
async joinConversation() {
// see browserCheck mixin
this.checkBrowser()
try {
await this.getFileConversation()
} catch (error) {
console.debug('Could not get file conversation. Is it a file and shared?')
return
}
await this.$store.dispatch('joinConversation', { token: this.token })
// The current participant (which is automatically set when fetching
// the current conversation) is needed for the MessagesList to start
// getting the messages, and both the current conversation and the
// current participant are needed for CallButton. No need to wait
// for it, but fetching the conversation needs to be done once the
// user has joined the conversation (otherwise only limited data
// would be received if the user was not a participant of the
// conversation yet).
this.fetchCurrentConversation()
// FIXME The participant will not be updated with the server data
// when the conversation is got again (as "addParticipantOnce" is
// used), although that should not be a problem given that only the
// "inCall" flag (which is locally updated when joining and leaving
// a call) is currently used.
if (loadState('spreed', 'signaling_mode') !== 'internal') {
EventBus.$on('should-refresh-conversations', OCA.Talk.fetchCurrentConversationWrapper)
EventBus.$on('signaling-participant-list-changed', OCA.Talk.fetchCurrentConversationWrapper)
} else {
// The "should-refresh-conversations" event is triggered only when
// the external signaling server is used; when the internal
// signaling server is used periodic polling has to be used
// instead.
OCA.Talk.fetchCurrentConversationIntervalId = window.setInterval(OCA.Talk.fetchCurrentConversationWrapper, 30000)
}
},
leaveConversation() {
EventBus.$off('should-refresh-conversations', OCA.Talk.fetchCurrentConversationWrapper)
EventBus.$off('signaling-participant-list-changed', OCA.Talk.fetchCurrentConversationWrapper)
window.clearInterval(OCA.Talk.fetchCurrentConversationIntervalId)
// TODO: move to store under a special action ?
// Remove the conversation to ensure that the old data is not used
// before fetching it again if this conversation is joined again.
this.$store.dispatch('deleteConversation', this.token)
// Remove the participant to ensure that it will be set again fresh
// if this conversation is joined again.
this.$store.dispatch('purgeParticipantsStore', this.token)
this.$store.dispatch('leaveConversation', { token: this.token })
this.$store.dispatch('updateTokenAndFileIdForToken', {
newToken: null,
newFileId: null,
})
},
async getFileConversation() {
// Clear previous requests if there's one pending
this.cancelGetFileConversation('canceled')
// Get a new cancelable request function and cancel function pair
const { request, cancel } = CancelableRequest(getFileConversation)
// Assign the new cancel function to our data value
this.cancelGetFileConversation = cancel
// Make the request
try {
const response = await request({ fileId: this.fileId })
this.$store.dispatch('updateTokenAndFileIdForToken', {
newToken: response.data.ocs.data.token,
newFileId: this.fileId,
})
} catch (exception) {
if (Axios.isCancel(exception)) {
console.debug('The request has been canceled', exception)
} else {
throw exception
}
}
},
async fetchCurrentConversation() {
if (!this.token) {
return
}
await this.$store.dispatch('fetchConversation', { token: this.token })
},
/**
* Sets whether the Talk sidebar is supported for the file or not.
*
* In some cases it is not possible to know if the Talk sidebar is
* supported for the file or not just from the data in the FileInfo (for
* example, for files in a folder shared by the current user). Due to
* that this function is asynchronous; isTalkSidebarSupportedForFile
* will be set as soon as possible (in some cases, immediately) with
* either true or false, depending on whether the Talk sidebar is
* supported for the file or not.
*
* The Talk sidebar is supported for a file if the file is shared with
* the current user or by the current user to another user (as a user,
* group...), or if the file is a descendant of a folder that meets
* those conditions.
*
* @param {OCA.Files.FileInfo} fileInfo the FileInfo to check
*/
async setTalkSidebarSupportedForFile(fileInfo) {
this.isTalkSidebarSupportedForFile = undefined
if (!fileInfo) {
this.isTalkSidebarSupportedForFile = false
return
}
if (fileInfo.get('type') === 'dir') {
this.isTalkSidebarSupportedForFile = false
return
}
if (fileInfo.get('shareOwnerId')) {
// Shared with me
// TODO How to check that it is not a remote share? At least for
// local shares "shareTypes" is not defined when shared with me.
this.isTalkSidebarSupportedForFile = true
return
}
if (!fileInfo.get('shareTypes')) {
// When it is not possible to know whether the Talk sidebar is
// supported for a file or not only from the data in the
// FileInfo it is necessary to query the server.
// FIXME If the file is shared this will create the conversation
// if it does not exist yet.
try {
this.isTalkSidebarSupportedForFile = (await getFileConversation({ fileId: fileInfo.id })) || false
} catch (error) {
this.isTalkSidebarSupportedForFile = false
}
return
}
const shareTypes = fileInfo.get('shareTypes').filter(function(shareType) {
// Ensure that shareType is an integer (as in the past shareType
// could be an integer or a string depending on whether the
// Sharing tab was opened or not).
shareType = parseInt(shareType)
return shareType === OC.Share.SHARE_TYPE_USER
|| shareType === OC.Share.SHARE_TYPE_GROUP
|| shareType === OC.Share.SHARE_TYPE_CIRCLE
|| shareType === OC.Share.SHARE_TYPE_ROOM
|| shareType === OC.Share.SHARE_TYPE_LINK
|| shareType === OC.Share.SHARE_TYPE_EMAIL
})
if (shareTypes.length === 0) {
// When it is not possible to know whether the Talk sidebar is
// supported for a file or not only from the data in the
// FileInfo it is necessary to query the server.
// FIXME If the file is shared this will create the conversation
// if it does not exist yet.
try {
this.isTalkSidebarSupportedForFile = (await getFileConversation({ fileId: fileInfo.id })) || false
} catch (error) {
this.isTalkSidebarSupportedForFile = false
}
return
}
this.isTalkSidebarSupportedForFile = true
},
openSharingTab() {
OCA.Files.Sidebar.setActiveTab('sharing')
},
/**
* Dirty hack to set the style in the tabs container.
*
* This is needed to force the scroll bars on the tabs container instead
* of on the whole sidebar.
*
* Additionally a minimum height is forced to ensure that the height of
* the chat view will be at least 300px, even if the info view is large
* and the screen short; in that case a scroll bar will be shown for the
* sidebar, but even if that looks really bad it is better than an
* unusable chat view.
*
* @param {boolean} isChatTheActiveTab whether the active tab is the
* chat tab or not.
*/
forceTabsContentStyleWhenChatTabIsActive(isChatTheActiveTab) {
const tabs = document.querySelector('.app-sidebar-tabs')
const tabsContent = document.querySelector('.app-sidebar-tabs__content')
if (isChatTheActiveTab) {
this.savedTabsMinHeight = tabs.style.minHeight
this.savedTabsOverflow = tabs.style.overflow
this.savedTabsContentOverflow = tabsContent.style.overflow
this.savedTabsContentStyle = true
tabs.style.minHeight = '300px'
tabs.style.overflow = 'hidden'
tabsContent.style.overflow = 'hidden'
} else if (this.savedTabsContentStyle) {
tabs.style.minHeight = this.savedTabsMinHeight
tabs.style.overflow = this.savedTabsOverflow
tabsContent.style.overflow = this.savedTabsContentOverflow
delete this.savedTabsMinHeight
delete this.savedTabsOverflow
delete this.savedTabsContentOverflow
this.savedTabsContentStyle = false
}
},
},
}
</script>
<style scoped>
.talkChatTab {
height: 100%;
display: flex;
flex-grow: 1;
flex-direction: column;
}
.emptycontent {
/* Override default top margin set in server and center vertically
* instead. */
margin-top: unset;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.call-button {
/* Center button horizontally. */
margin-left: auto;
margin-right: auto;
margin-top: 10px;
margin-bottom: 10px;
}
.chatView {
overflow: hidden;
}
</style>