diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2020-01-10 12:31:06 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-10 12:31:06 +0300 |
commit | 0d006641c250865aa39bd23ea56bda678b627add (patch) | |
tree | 09b11168659b7fab29eb0eaac23e56d3b43e4301 | |
parent | 2adb03af9951564070aae94d8b60666f24effb1c (diff) | |
parent | b9e8a9a3da6ae4a5c76de872045ee501de16c62f (diff) |
Merge pull request #2669 from nextcloud/add-call-view-to-sidebar-in-files-app
Add call view to sidebar in Files app
-rw-r--r-- | css/icons.scss | 10 | ||||
-rw-r--r-- | lib/Files/TemplateLoader.php | 4 | ||||
-rw-r--r-- | src/FilesSidebarCallViewApp.vue | 260 | ||||
-rw-r--r-- | src/FilesSidebarTabApp.vue | 60 | ||||
-rw-r--r-- | src/components/CallView/CallView.vue | 115 | ||||
-rw-r--r-- | src/components/CallView/LocalMediaControls.vue | 10 | ||||
-rw-r--r-- | src/components/CallView/LocalVideo.vue | 15 | ||||
-rw-r--r-- | src/components/CallView/Video.vue | 19 | ||||
-rw-r--r-- | src/mainFilesSidebar.js (renamed from src/mainChatTab.js) | 11 | ||||
-rw-r--r-- | src/mainFilesSidebarLoader.js (renamed from src/mainSidebarTab.js) | 2 | ||||
-rw-r--r-- | src/store/conversationsStore.js | 19 | ||||
-rw-r--r-- | src/views/FilesSidebarCallView.js | 54 | ||||
-rw-r--r-- | webpack.common.js | 4 |
13 files changed, 518 insertions, 65 deletions
diff --git a/css/icons.scss b/css/icons.scss index f048bed32..493815bbb 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -8,7 +8,8 @@ @include icon-black-white('emoji-smile', 'spreed', 1); @include icon-black-white('lobby', 'spreed', 1); -.app-Talk { +.app-Talk, +#call-container { // We always want to use the white icons, this is why we don't use var(--color-white) here. .icon-public { background-image: url(icon-color-path('public', 'actions', 'fff', 1, true)); @@ -77,3 +78,10 @@ background-image: url(icon-color-path('group', 'actions', 'fff', 1, true)); } } + +.app-files { + // Needed to use white color also in dark mode. + .app-sidebar__close.forced-white { + background-image: url(icon-color-path('close', 'actions', 'fff', 1, true)); + } +} diff --git a/lib/Files/TemplateLoader.php b/lib/Files/TemplateLoader.php index 18592a77b..dd330b565 100644 --- a/lib/Files/TemplateLoader.php +++ b/lib/Files/TemplateLoader.php @@ -56,8 +56,8 @@ class TemplateLoader implements IEventListener { } Util::addStyle(Application::APP_ID, 'merged-files'); - Util::addScript(Application::APP_ID, 'files-sidebar-tab'); - Util::addScript(Application::APP_ID, 'talk-chat-tab'); + Util::addScript(Application::APP_ID, 'talk-files-sidebar'); + Util::addScript(Application::APP_ID, 'talk-files-sidebar-loader'); } } diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue new file mode 100644 index 000000000..ec43d3df1 --- /dev/null +++ b/src/FilesSidebarCallViewApp.vue @@ -0,0 +1,260 @@ +<!-- + - @copyright Copyright (c) 2019, Daniel Calviño Sánchez <danxuliu@gmail.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> + <CallView v-if="isInFile" + v-show="isInCall" + :token="token" + :use-constrained-layout="true" /> +</template> + +<script> +import { PARTICIPANT } from './constants' +import CallView from './components/CallView/CallView' + +export default { + + name: 'FilesSidebarCallViewApp', + + components: { + CallView, + }, + + data() { + return { + // Needed for reactivity. + Talk: OCA.Talk, + } + }, + + computed: { + fileInfo() { + // When changing files OCA.Talk.fileInfo is cleared as soon as the + // new file starts to be loaded; "setFileInfo()" is called once the + // new file has loaded, so fileInfo is got from OCA.Talk to hide the + // call view at the same time as the rest of the sidebar UI. + return this.Talk.fileInfo || {} + }, + + fileId() { + return this.fileInfo.id + }, + + token() { + return this.$store.getters.getToken() + }, + + fileIdForToken() { + return this.$store.getters.getFileIdForToken() + }, + + /** + * Returns whether the sidebar is opened in the file of the current + * conversation or not. + * + * Note that false is returned too when the sidebar is closed, even if + * the conversation is active in the current file. + * + * @returns {Boolean} true if the sidebar is opened in the file, false + * otherwise. + */ + isInFile() { + if (this.fileId !== this.fileIdForToken) { + return false + } + + return true + }, + + isInCall() { + // FIXME Remove participants as soon as the file changes so this + // condition is not needed. + if (!this.isInFile) { + return false + } + + const participantIndex = this.$store.getters.getParticipantIndex(this.token, this.$store.getters.getParticipantIdentifier()) + if (participantIndex === -1) { + return false + } + + const participant = this.$store.getters.getParticipant(this.token, participantIndex) + + return participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED + }, + }, + + watch: { + isInCall: function(isInCall) { + if (isInCall) { + this.replaceSidebarHeaderContentsWithCallView() + } else { + this.restoreSidebarHeaderContents() + } + }, + + /** + * Force restoring the sidebar header contents on file changes. + * + * If the sidebar is opened in a different file during a call the + * sidebar header contents may not be properly restored due to the order + * in which the updates are handled, so it needs to be executed again + * when the FileInfo has been set and it does not match the current + * conversation. + * + * @param {Object} fileInfo the watched FileInfo + */ + fileInfo: function(fileInfo) { + if (!fileInfo) { + return + } + + if (this.isInFile) { + return + } + + const headerAction = document.querySelector('.app-sidebar-header__action') + if (!headerAction) { + return + } + + if (this.$el.parentElement === headerAction) { + return + } + + this.restoreSidebarHeaderContents() + }, + }, + + methods: { + setFileInfo(fileInfo) { + }, + + /** + * Adds a special style sheet to hide the sidebar header contents during + * a call. + * + * The style sheet contains a rule to hide ".hidden-by-call" elements, + * which is the CSS class set in the sidebar header contents during a + * call. + */ + addCallInFilesSidebarStyleSheet() { + for (let i = 0; i < document.styleSheets.length; i++) { + const sheet = document.styleSheets[i] + // None of the default properties of a style sheet can be used + // as an ID. Adding a "data-id" attribute would work in Firefox, + // but not in Chromium, as it does not provide a "dataset" + // property in styleSheet objects. Therefore it is necessary to + // check the rules themselves, but as the order is undefined a + // matching rule needs to be looked for in all of them. + if (sheet.cssRules.length !== 2) { + continue + } + + for (const cssRule of sheet.cssRules) { + if (cssRule.cssText === '.app-sidebar-header .hidden-by-call { display: none !important; }') { + return + } + } + } + + const style = document.createElement('style') + + document.head.appendChild(style) + + // "insertRule" calls below need to be kept in sync with the + // condition above. + + // Shadow is added to forced white icons to ensure that they are + // visible even against a bright video background. + // White color of forced white icons needs to be set in "icons.scss" + // file to be able to use the SCSS functions. + style.sheet.insertRule('.app-sidebar-header .forced-white { filter: drop-shadow(1px 1px 4px var(--color-box-shadow)); }', 0) + + style.sheet.insertRule('.app-sidebar-header .hidden-by-call { display: none !important; }', 0) + }, + + /** + * Hides the sidebar header contents (except the close button) and shows + * the call view instead. + */ + replaceSidebarHeaderContentsWithCallView() { + this.addCallInFilesSidebarStyleSheet() + + const header = document.querySelector('.app-sidebar-header') + if (!header) { + return + } + + for (let i = 0; i < header.children.length; i++) { + const headerChild = header.children[i] + + if (headerChild.classList.contains('app-sidebar__close')) { + headerChild.classList.add('forced-white') + } else { + headerChild.classList.add('hidden-by-call') + } + } + + header.append(this.$el) + }, + + /** + * Shows the sidebar header contents and moves the call view back to the + * actions. + */ + restoreSidebarHeaderContents() { + const header = document.querySelector('.app-sidebar-header') + if (!header) { + return + } + + for (let i = 0; i < header.children.length; i++) { + const headerChild = header.children[i] + + if (headerChild.classList.contains('app-sidebar__close')) { + headerChild.classList.remove('forced-white') + } else { + headerChild.classList.remove('hidden-by-call') + } + } + + const headerAction = document.querySelector('.app-sidebar-header__action') + if (headerAction) { + headerAction.append(this.$el) + } + }, + }, +} +</script> + +<style lang="scss" scoped> +#call-container { + position: relative; + + /* Prevent shadows of videos from leaking on other elements. */ + overflow: hidden; + + /* Show the call container in a 16/9 proportion based on the sidebar + * width. */ + padding-bottom: 56.25%; + max-height: 56.25%; +} +</style> diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue index f0f8d843a..55b43a987 100644 --- a/src/FilesSidebarTabApp.vue +++ b/src/FilesSidebarTabApp.vue @@ -39,9 +39,7 @@ </button> </div> <template v-else> - <button class="call-button primary" :disabled="true"> - Calls will return soon - </button> + <CallButton class="call-button" /> <ChatView :token="token" /> </template> </div> @@ -49,12 +47,15 @@ <script> +import { EventBus } from './services/EventBus' import { getFileConversation } from './services/filesIntegrationServices' import { fetchConversation } from './services/conversationsService' import { joinConversation, leaveConversation } from './services/participantsService' import CancelableRequest from './utils/cancelableRequest' +import { getSignaling } from './utils/webrtc/index' import { getCurrentUser } from '@nextcloud/auth' import Axios from '@nextcloud/axios' +import CallButton from './components/TopBar/CallButton' import ChatView from './components/ChatView' export default { @@ -62,6 +63,7 @@ export default { name: 'FilesSidebarTabApp', components: { + CallButton, ChatView, }, @@ -119,6 +121,22 @@ export default { }, }, + 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()) }, @@ -131,14 +149,42 @@ export default { // The current participant (which is automatically set when fetching // the current conversation) is needed for the MessagesList to start - // getting the messages. 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). + // 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. + const signaling = await getSignaling() + if (signaling.url) { + EventBus.$on('shouldRefreshConversations', OCA.Talk.fetchCurrentConversationWrapper) + } else { + // The "shouldRefreshConversations" 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('shouldRefreshConversations', OCA.Talk.fetchCurrentConversationWrapper) + window.clearInterval(OCA.Talk.fetchCurrentConversationIntervalId) + + // 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('deleteConversationByToken', 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) + leaveConversation(this.token) this.$store.dispatch('updateTokenAndFileIdForToken', { diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index 867b2b537..e89ad8d2a 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -26,17 +26,20 @@ :key="callParticipantModel.attributes.peerId" :model="callParticipantModel" :shared-data="sharedDatas[callParticipantModel.attributes.peerId]" + :use-constrained-layout="useConstrainedLayout" @switchScreenToId="_switchScreenToId" /> <Video :key="'placeholder' + callParticipantModel.attributes.peerId" :placeholder-for-promoted="true" :model="callParticipantModel" :shared-data="sharedDatas[callParticipantModel.attributes.peerId]" + :use-constrained-layout="useConstrainedLayout" @switchScreenToId="_switchScreenToId" /> </template> <LocalVideo ref="localVideo" :local-media-model="localMediaModel" :local-call-participant-model="localCallParticipantModel" + :use-constrained-layout="useConstrainedLayout" @switchScreenToId="_switchScreenToId" /> </div> <div id="screens"> @@ -68,6 +71,13 @@ export default { Video, }, + props: { + useConstrainedLayout: { + type: Boolean, + default: false, + }, + }, + data() { return { speakers: [], @@ -92,6 +102,7 @@ export default { const callViewClass = { 'incall': this.remoteParticipantsCount > 0, 'screensharing': this.screenSharingActive, + 'constrained-layout': this.useConstrainedLayout, } callViewClass['participants-' + (this.remoteParticipantsCount + 1)] = true @@ -131,6 +142,42 @@ export default { }, callParticipantModels: function(models) { + this.updateDataFromCallParticipantModels(models) + }, + + 'speakers': function() { + this._setPromotedParticipant() + }, + + 'screenSharingActive': function() { + this._setPromotedParticipant() + }, + + 'screens': function() { + this._setScreenVisible() + }, + + }, + + created() { + // Ensure that data is properly initialized before mounting the + // subviews. + this.updateDataFromCallParticipantModels(this.callParticipantModels) + }, + + methods: { + + /** + * Updates data properties that depend on the CallParticipantModels. + * + * The data contains some properties that can not be dynamically + * computed but that depend on the current CallParticipantModels, so + * this function adds and removes elements and watchers as needed based + * on the given CallParticipantModels. + * + * @param {Array} models the array of CallParticipantModels + */ + updateDataFromCallParticipantModels(models) { const addedModels = models.filter(model => !this.sharedDatas[model.attributes.peerId]) const removedModelIds = Object.keys(this.sharedDatas).filter(sharedDataId => models.find(model => model.attributes.peerId === sharedDataId) === undefined) @@ -181,22 +228,6 @@ export default { }) }, - 'speakers': function() { - this._setPromotedParticipant() - }, - - 'screenSharingActive': function() { - this._setPromotedParticipant() - }, - - 'screens': function() { - this._setScreenVisible() - }, - - }, - - methods: { - _setSpeaking(peerId, speaking) { if (speaking) { // Move the speaker to the first element of the list @@ -340,6 +371,15 @@ export default { max-height: 200px; } +.constrained-layout.screensharing .videoContainer { + max-height: 100px; + + /* Avatars slightly overflow the container; although they overlap the shared + * screen it is not too bad and it is better than compressing even further + * the shared screen. */ + overflow: visible; +} + ::v-deep video { z-index: 0; max-height: 100%; @@ -376,6 +416,12 @@ export default { box-shadow: 0 0 15px var(--color-box-shadow); } +.constrained-layout #videos .videoContainer:not(.promoted) ::v-deep video { + /* Make the unpromoted videos smaller to not overlap too much the promoted + * video */ + max-height: 100px; +} + #videos .videoContainer ::v-deep .avatardiv { box-shadow: 0 0 15px var(--color-box-shadow); } @@ -397,19 +443,6 @@ export default { background-color: #b9b9b9 !important; } -/* Text avatars need to be forced to 128px, as imageplaceholder() overrides - * the given size with the actual height of the element it was called on, so - * the text avatar may have any hardcoded height. Note that this does not - * apply to regular image avatars, as in that case they are always requested - * with a size of 128px. */ -.videoContainer ::v-deep .avatar-container .avatardiv { - width: 128px !important; - height: 128px !important; - line-height: 128px !important; - /* imageplaceholder() sets font-size to "height * 0.55" */ - font-size: 70.4px !important; -} - .videoContainer ::v-deep .avatar-container .avatardiv { display: block; margin-left: auto; @@ -470,6 +503,12 @@ export default { max-height: 35%; } } +.constrained-layout.participants-1 .videoView, +.constrained-layout.participants-2 .videoView { + /* Do not force the width to 200px, as otherwise the video is too tall and + * overlaps too much with the promoted video. */ + min-width: initial; +} .participants-1 .videoView ::v-deep video, .participants-2 .videoView ::v-deep video { position: absolute; @@ -487,6 +526,12 @@ export default { background-color: transparent; } +.constrained-layout.screensharing #screens { + /* The row with the participants is shorter in the constrained layout to + * make room for the promoted video and the shared screens. */ + height: calc(100% - 100px); +} + .screensharing .screenContainer { position: relative; width: 100%; @@ -509,6 +554,13 @@ export default { text-overflow: ellipsis; } +.constrained-layout ::v-deep .nameIndicator { + /* Reduce padding to bring the name closer to the bottom */ + padding: 3px; + /* Use default font size, as it takes too much space otherwise */ + font-size: initial; +} + ::v-deep .videoView .nameIndicator { padding: 0; overflow: visible; @@ -531,6 +583,11 @@ export default { padding: 12px 35%; } +.constrained-layout.participants-2 ::v-deep .videoContainer.promoted + .videoContainer-dummy .nameIndicator { + /* Reduce padding to bring the name closer to the bottom */ + padding: 3px 35%; +} + #videos .videoContainer.speaking:not(.videoView) ::v-deep .nameIndicator, #videos .videoContainer.videoView.speaking ::v-deep .nameIndicator .icon-audio { animation: pulse 1s; diff --git a/src/components/CallView/LocalMediaControls.vue b/src/components/CallView/LocalMediaControls.vue index 580873067..3876507fe 100644 --- a/src/components/CallView/LocalMediaControls.vue +++ b/src/components/CallView/LocalMediaControls.vue @@ -98,13 +98,16 @@ export default { type: Object, required: true, }, + screenSharingButtonHidden: { + type: Boolean, + default: false, + }, }, data() { return { mounted: false, speakingWhileMutedNotification: null, - screenSharingButtonHidden: false, screenSharingMenuOpen: false, splitScreenSharingMenu: false, } @@ -341,11 +344,6 @@ export default { } }) }, - - hideScreenSharingButton() { - this.screenSharingButtonHidden = true - }, - }, } </script> diff --git a/src/components/CallView/LocalVideo.vue b/src/components/CallView/LocalVideo.vue index 24751df81..26d9e5b64 100644 --- a/src/components/CallView/LocalVideo.vue +++ b/src/components/CallView/LocalVideo.vue @@ -37,6 +37,7 @@ <LocalMediaControls ref="localMediaControls" :model="localMediaModel" :local-call-participant-model="localCallParticipantModel" + :screen-sharing-button-hidden="useConstrainedLayout" @switchScreenToId="$emit('switchScreenToId', $event)" /> </div> </template> @@ -64,12 +65,10 @@ export default { type: Object, required: true, }, - }, - - data() { - return { - avatarSize: 128, - } + useConstrainedLayout: { + type: Boolean, + default: false, + }, }, computed: { @@ -86,6 +85,10 @@ export default { return this.localCallParticipantModel.attributes.guestName || localStorage.getItem('nick') || '?' }, + avatarSize() { + return this.useConstrainedLayout ? 64 : 128 + }, + }, watch: { diff --git a/src/components/CallView/Video.vue b/src/components/CallView/Video.vue index 81719bc1e..b6caefdab 100644 --- a/src/components/CallView/Video.vue +++ b/src/components/CallView/Video.vue @@ -97,12 +97,10 @@ export default { type: Object, required: true, }, - }, - - data() { - return { - avatarSize: 128, - } + useConstrainedLayout: { + type: Boolean, + default: false, + }, }, computed: { @@ -116,6 +114,10 @@ export default { } }, + avatarSize() { + return (this.useConstrainedLayout && !this.sharedData.promoted) ? 64 : 128 + }, + avatarClass() { return { 'icon-loading': this.model.attributes.connectionState !== ConnectionState.CONNECTED && this.model.attributes.connectionState !== ConnectionState.COMPLETED && this.model.attributes.connectionState !== ConnectionState.FAILED_NO_RESTART, @@ -248,6 +250,11 @@ export default { text-align: center; } +.constrained-layout .mediaIndicator { + /* Move the media indicator closer to the bottom */ + bottom: 16px; +} + .muteIndicator, .hideRemoteVideo, .screensharingIndicator, diff --git a/src/mainChatTab.js b/src/mainFilesSidebar.js index 6ac645fd9..b2e94d141 100644 --- a/src/mainChatTab.js +++ b/src/mainFilesSidebar.js @@ -23,7 +23,8 @@ */ import Vue from 'vue' -import App from './FilesSidebarTabApp' +import FilesSidebarCallViewApp from './FilesSidebarCallViewApp' +import FilesSidebarTabApp from './FilesSidebarTabApp' // Store import Vuex from 'vuex' @@ -56,9 +57,14 @@ Vue.prototype.OCA = OCA Vue.use(Vuex) Vue.use(vuescroll, { debounce: 600 }) +const newCallView = () => new Vue({ + store, + render: h => h(FilesSidebarCallViewApp), +}) + const newTab = () => new Vue({ store, - render: h => h(App), + render: h => h(FilesSidebarTabApp), }) if (!window.OCA.Talk) { @@ -66,6 +72,7 @@ if (!window.OCA.Talk) { } Object.assign(window.OCA.Talk, { fileInfo: null, + newCallView, newTab, store: store, }) diff --git a/src/mainSidebarTab.js b/src/mainFilesSidebarLoader.js index d5500fa63..d2ab43e6c 100644 --- a/src/mainSidebarTab.js +++ b/src/mainFilesSidebarLoader.js @@ -20,6 +20,7 @@ * */ +import FilesSidebarCallView from './views/FilesSidebarCallView' import FilesSidebarTab from './views/FilesSidebarTab' import { leaveConversation } from './services/participantsService' @@ -43,6 +44,7 @@ const isEnabled = function(fileInfo) { window.addEventListener('DOMContentLoaded', () => { if (OCA.Files && OCA.Files.Sidebar) { + OCA.Files.Sidebar.registerSecondaryView(new FilesSidebarCallView()) OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('talk-chat', FilesSidebarTab, isEnabled)) } }) diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index f758da753..271ea4be5 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -54,10 +54,10 @@ const mutations = { /** * Deletes a conversation from the store. * @param {object} state current store state; - * @param {object} conversation the message; + * @param {object} token the token of the conversation to delete; */ - deleteConversation(state, conversation) { - Vue.delete(state.conversations, conversation.token) + deleteConversation(state, token) { + Vue.delete(state.conversations, token) }, /** * Resets the store to it's original state @@ -99,8 +99,19 @@ const actions = { * @param {object} conversation the conversation to be deleted; */ deleteConversation(context, conversation) { - context.commit('deleteConversation', conversation) + context.commit('deleteConversation', conversation.token) }, + + /** + * Delete a object + * + * @param {object} context default store context; + * @param {object} token the token of the conversation to be deleted; + */ + deleteConversationByToken(context, token) { + context.commit('deleteConversation', token) + }, + /** * Resets the store to it's original state. * @param {object} context default store context; diff --git a/src/views/FilesSidebarCallView.js b/src/views/FilesSidebarCallView.js new file mode 100644 index 000000000..31c0eb08a --- /dev/null +++ b/src/views/FilesSidebarCallView.js @@ -0,0 +1,54 @@ +/** + * + * @copyright Copyright (c) 2019, Daniel Calviño Sánchez <danxuliu@gmail.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/>. + * + */ + +/** + * Helper class to wrap a Vue instance with a FilesSidebarCallViewApp component + * to be used as a secondary view in the Files sidebar. + * + * Although Vue instances/components can be added as tabs to the Files sidebar + * currently only legacy views can be added as secondary views to the Files + * sidebar. Those legacy views are expected to provide a root element, $el, with + * a "replaceAll" method that replaces the given element with the $el element, + * and a "setFileInfo" method that is called when the sidebar is opened or the + * current file changes. + */ +export default class FilesSidebarCallView { + + constructor() { + this.callViewInstance = OCA.Talk.newCallView() + + this.$el = document.createElement('div') + + this.callViewInstance.$mount(this.$el) + this.$el = this.callViewInstance.$el + + this.$el.replaceAll = function(target) { + target.replaceWith(this.$el) + }.bind(this) + } + + setFileInfo(fileInfo) { + // The FilesSidebarCallViewApp is the first (and only) child of the Vue + // instance. + this.callViewInstance.$children[0].setFileInfo(fileInfo) + } + +} diff --git a/webpack.common.js b/webpack.common.js index 958f533c0..fa0af8937 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -12,8 +12,8 @@ module.exports = { 'admin/turn-server': path.join(__dirname, 'src', 'TurnServerSettings.js'), 'collections': path.join(__dirname, 'src', 'collections.js'), 'talk': path.join(__dirname, 'src', 'main.js'), - 'talk-chat-tab': path.join(__dirname, 'src', 'mainChatTab.js'), - 'files-sidebar-tab': path.join(__dirname, 'src', 'mainSidebarTab.js'), + 'talk-files-sidebar': path.join(__dirname, 'src', 'mainFilesSidebar.js'), + 'talk-files-sidebar-loader': path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'), 'flow': path.join(__dirname, 'src', 'flow.js') }, output: { |