From b685361a34da89de1e04c2ce332e1f587bb82cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 2 Jan 2020 19:33:10 +0100 Subject: Add action store to delete a conversation by token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/store/conversationsStore.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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; -- cgit v1.2.3 From ed8ef2dd760dc493e42c765606426ba097ddcc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 23 Dec 2019 14:15:50 +0100 Subject: Replace placeholder with actual call button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CallButton component uses the current conversation and participant from the Vuex store to set its state. When changing to a different conversation and joining a previous conversation again the CallButton may be shown before the conversation and participants have been fetched again, so they need to be removed when the conversation is left. Otherwise the CallButton may show a wrong state for a while (like being in a call if the user changed to a different conversation while being in a call) when joining the conversation again. Signed-off-by: Daniel Calviño Sánchez --- src/FilesSidebarTabApp.vue | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue index f0f8d843a..2293f22d6 100644 --- a/src/FilesSidebarTabApp.vue +++ b/src/FilesSidebarTabApp.vue @@ -39,9 +39,7 @@ @@ -55,6 +53,7 @@ import { joinConversation, leaveConversation } from './services/participantsServ import CancelableRequest from './utils/cancelableRequest' 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 +61,7 @@ export default { name: 'FilesSidebarTabApp', components: { + CallButton, ChatView, }, @@ -131,14 +131,23 @@ 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() }, leaveConversation() { + // 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', { -- cgit v1.2.3 From c3a7f1b8d7525348df1d0460dd5f6432e13c28d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 2 Jan 2020 20:21:56 +0100 Subject: Refresh current conversation periodically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/FilesSidebarTabApp.vue | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue index 2293f22d6..55b43a987 100644 --- a/src/FilesSidebarTabApp.vue +++ b/src/FilesSidebarTabApp.vue @@ -47,10 +47,12 @@ diff --git a/src/mainFilesSidebar.js b/src/mainFilesSidebar.js index 6ac645fd9..b2e94d141 100644 --- a/src/mainFilesSidebar.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/mainFilesSidebarLoader.js b/src/mainFilesSidebarLoader.js index d5500fa63..d2ab43e6c 100644 --- a/src/mainFilesSidebarLoader.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/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 + * + * @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 . + * + */ + +/** + * 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) + } + +} -- cgit v1.2.3 From c133d5ed50feb4700a308ab22f4cd9c3fe54ce2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 27 Dec 2019 20:34:22 +0100 Subject: Replace sidebar header contents with call view during calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During calls the call view is now moved from the header actions to the header itself, and all the other elements in the header (except the close button) are hidden. This is done by setting a special CSS class, "hidden-by-call", which is defined in a style sheet added dynamically. When the file is changed the sidebar is cleared until the new file is loaded. However, "setFileInfo" is called once the new file has loaded, so the call view can not be hidden based on when a new fileInfo is set, as that would keep the call view shown while the sidebar only shows the loading spinner. However, "OCA.Talk.fileInfo" is cleared by "FilesSidebarTab" when the tab is destroyed, which happens when the rest of the sidebar is cleared, so that fileInfo is the one used to show and hide the call view. Signed-off-by: Daniel Calviño Sánchez --- src/FilesSidebarCallViewApp.vue | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue index f172245b9..b663a7cd7 100644 --- a/src/FilesSidebarCallViewApp.vue +++ b/src/FilesSidebarCallViewApp.vue @@ -31,12 +31,41 @@ export default { name: 'FilesSidebarCallViewApp', + 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() + }, + isInCall() { + // FIXME Remove participants as soon as the file changes so this + // condition is not needed. + if (this.fileId !== this.fileIdForToken) { + return false + } + const participantIndex = this.$store.getters.getParticipantIndex(this.token, this.$store.getters.getParticipantIdentifier()) if (participantIndex === -1) { return false @@ -48,9 +77,104 @@ export default { }, }, + watch: { + isInCall: function(isInCall) { + if (isInCall) { + this.replaceSidebarHeaderContentsWithCallView() + } else { + 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 !== 1) { + 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. + + 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('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('hidden-by-call') + } + } + + const headerAction = document.querySelector('.app-sidebar-header__action') + if (headerAction) { + headerAction.append(this.$el) + } + }, }, } -- cgit v1.2.3 From 1459983a5febfa0992404edc4a4bd7e4e7e1f20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 29 Dec 2019 23:47:10 +0100 Subject: Replace placeholder with actual call view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the call view has a black background the close button of the sidebar is forced to white during calls to make it visible. Signed-off-by: Daniel Calviño Sánchez --- css/icons.scss | 10 +++++++++- src/FilesSidebarCallViewApp.vue | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/css/icons.scss b/css/icons.scss index 20d03346c..82a0ee6c9 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)); @@ -87,3 +88,10 @@ color: var(--color-primary-text); } } + +.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/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue index b663a7cd7..65802f3c6 100644 --- a/src/FilesSidebarCallViewApp.vue +++ b/src/FilesSidebarCallViewApp.vue @@ -19,18 +19,21 @@ --> + + -- cgit v1.2.3 From 678c1b0de037fa77035a8a60de17c723a0e306b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 31 Dec 2019 01:17:10 +0100 Subject: Add a "constrained layout" for call view to be used in the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently this mimics the layout used in Talk 7 when the call view was shown in the Files app sidebar (smaller avatars, videos and paddings, no screensharing button...), but even once the call view is moved to the new layout a special handling will be needed when the space is limited. Ideally the call view should automatically change between layouts based on its size (probably using something like ResizeObserver/MutationObserver), but for the time being it is explicitly enabled when the call view is shown in the Files app sidebar. Signed-off-by: Daniel Calviño Sánchez --- src/FilesSidebarCallViewApp.vue | 2 +- src/components/CallView/CallView.vue | 63 ++++++++++++++++++++------ src/components/CallView/LocalMediaControls.vue | 10 ++-- src/components/CallView/LocalVideo.vue | 15 +++--- src/components/CallView/Video.vue | 19 +++++--- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue index 65802f3c6..8fdac4ba4 100644 --- a/src/FilesSidebarCallViewApp.vue +++ b/src/FilesSidebarCallViewApp.vue @@ -19,7 +19,7 @@ --> 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 @@ @@ -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, -- cgit v1.2.3 From 0cb55195888c2a7f34870d2faa0141e3c8fb3763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 2 Jan 2020 14:14:01 +0100 Subject: Extract function to update data based on CallViewParticipantModels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/CallView.vue | 46 +++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index 411d8c538..cc3c0d05f 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -142,6 +142,36 @@ export default { }, callParticipantModels: function(models) { + this.updateDataFromCallParticipantModels(models) + }, + + 'speakers': function() { + this._setPromotedParticipant() + }, + + 'screenSharingActive': function() { + this._setPromotedParticipant() + }, + + 'screens': function() { + this._setScreenVisible() + }, + + }, + + 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) @@ -192,22 +222,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 -- cgit v1.2.3 From d204b09fe04c834fc4997dcf49c40c8b85d151ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 2 Jan 2020 15:24:33 +0100 Subject: Fix frozen video after closing and opening the sidebar during a call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a video element is removed from the DOM and then added again it will be frozen in its last frame until its "srcObject" is set again (even overwriting itself with "videoElement.srcObject = videoElement.srcObject" would be enough). Closing and opening the sidebar removes it from and adds it back to the DOM, but as "srcObject" is only set when the stream changes the video element was frozen after opening the sidebar again. Instead of notifying the child views that they were shown again (as it does not seem to be possible to detect it from the child views themselves) so they can refresh the "srcObjects", for simplicity now the CallView is fully rendered again when the sidebar is opened after being closed. Signed-off-by: Daniel Calviño Sánchez --- src/FilesSidebarCallViewApp.vue | 25 +++++++++++++++++++++++-- src/components/CallView/CallView.vue | 6 ++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue index 8fdac4ba4..99f188d47 100644 --- a/src/FilesSidebarCallViewApp.vue +++ b/src/FilesSidebarCallViewApp.vue @@ -19,7 +19,10 @@ -->