Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2020-01-10 12:31:06 +0300
committerGitHub <noreply@github.com>2020-01-10 12:31:06 +0300
commit0d006641c250865aa39bd23ea56bda678b627add (patch)
tree09b11168659b7fab29eb0eaac23e56d3b43e4301
parent2adb03af9951564070aae94d8b60666f24effb1c (diff)
parentb9e8a9a3da6ae4a5c76de872045ee501de16c62f (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.scss10
-rw-r--r--lib/Files/TemplateLoader.php4
-rw-r--r--src/FilesSidebarCallViewApp.vue260
-rw-r--r--src/FilesSidebarTabApp.vue60
-rw-r--r--src/components/CallView/CallView.vue115
-rw-r--r--src/components/CallView/LocalMediaControls.vue10
-rw-r--r--src/components/CallView/LocalVideo.vue15
-rw-r--r--src/components/CallView/Video.vue19
-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.js19
-rw-r--r--src/views/FilesSidebarCallView.js54
-rw-r--r--webpack.common.js4
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: {