diff options
-rw-r--r-- | css/merged-public-share.scss | 1 | ||||
-rw-r--r-- | css/publicshare.scss | 87 | ||||
-rw-r--r-- | lib/PublicShare/TemplateLoader.php | 2 | ||||
-rw-r--r-- | src/PublicShareSidebar.vue | 151 | ||||
-rw-r--r-- | src/mainPublicShareSidebar.js | 101 | ||||
-rw-r--r-- | src/services/filesIntegrationServices.js | 13 | ||||
-rw-r--r-- | webpack.common.js | 1 |
7 files changed, 355 insertions, 1 deletions
diff --git a/css/merged-public-share.scss b/css/merged-public-share.scss index 04a24cced..c38c70b10 100644 --- a/css/merged-public-share.scss +++ b/css/merged-public-share.scss @@ -1 +1,2 @@ @import 'icons.scss'; +@import 'publicshare.scss'; diff --git a/css/publicshare.scss b/css/publicshare.scss new file mode 100644 index 000000000..20ac3bd93 --- /dev/null +++ b/css/publicshare.scss @@ -0,0 +1,87 @@ +/* Special layout to include the Talk sidebar */ + +/* The standard layout defined in the server includes a fixed header with a + * sticky sidebar. This causes the scroll bar for the main area to appear to the + * right of the sidebar, which looks confusing for the chat. Thus that layout is + * overridden with a static header and a content with full height without header + * to limit the vertical scroll bar only to it. + * Note that the flex layout can not be cascaded from the body element, as a + * flex display is not compatible with the absolute position set for the + * autocompletion panel, which is reparented to the body when shown. */ +#body-user #header, +#body-public #header { + /* Override fixed position from server to include it in the body layout */ + position: static; +} + +#content { + &, + &.full-height { + /* Always full height without header. */ + height: calc(100% - 50px); + } + + display: flex; + flex-direction: row; + overflow: hidden; + + flex-grow: 1; + + /* Override "min-height: 100%" and "padding-top: 50px" set in server, as the + * header is part of the flex layout and thus the whole body is not + * available for the content. */ + min-height: 0; + padding-top: 0; + + /* Does not change anything in normal mode, but ensures that the element + * will stretch to the full width in full screen mode. */ + width: 100%; + + /* Override margin used in server, as the header is part of the flex layout + * and thus the content does not need to be pushed down. */ + margin-top: 0; +} + +#app-content { + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + + flex-grow: 1; + + margin-right: 0; +} + +#files-public-content { + flex-grow: 1; +} + +#content footer p a { + /* The server sets an height to the footer of 65px, but its contents are + * slightly larger, which causes a scroll bar to be added to the content + * even if there is enough space for the app content and the footer. + * The padding of links is 10px, so in practice reducing the bottom padding + * only affects the bottom padding of the last element (as in adjacent + * paragraphs the paddings would get merged and there will still be 10px + * from the top padding of the second element). */ + padding-bottom: 8px; +} + + + +#talk-sidebar-trigger { + width: 44px; + height: 44px; + + background-color: transparent; + border-color: transparent; + + opacity: 0.6; + + &:hover, + &:focus, + &:active { + opacity: 1; + } +} diff --git a/lib/PublicShare/TemplateLoader.php b/lib/PublicShare/TemplateLoader.php index a388e0690..21694218f 100644 --- a/lib/PublicShare/TemplateLoader.php +++ b/lib/PublicShare/TemplateLoader.php @@ -67,7 +67,7 @@ class TemplateLoader { } Util::addStyle('spreed', 'merged-public-share'); - Util::addScript('spreed', 'merged-public-share'); + Util::addScript('spreed', 'talk-public-share-sidebar'); } } diff --git a/src/PublicShareSidebar.vue b/src/PublicShareSidebar.vue new file mode 100644 index 000000000..50ed67d4b --- /dev/null +++ b/src/PublicShareSidebar.vue @@ -0,0 +1,151 @@ +<!-- + - @copyright Copyright (c) 2020, 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> + <aside v-if="isOpen" id="talk-sidebar"> + <div v-if="!conversation" class="emptycontent room-not-joined"> + <div class="icon icon-talk" /> + <h2>{{ t('spreed', 'Discuss this file') }}</h2> + <button class="primary" @click="joinConversation"> + {{ t('spreed', 'Join conversation') }} + </button> + </div> + <div v-else class="emptycontent"> + <div class="icon icon-talk" /> + <h2>Conversation joined</h2> + </div> + </aside> +</template> + +<script> +import { EventBus } from './services/EventBus' +import { fetchConversation } from './services/conversationsService' +import { getPublicShareConversationToken } from './services/filesIntegrationServices' +import { joinConversation } from './services/participantsService' +import { getSignaling } from './utils/webrtc/index' + +export default { + + name: 'PublicShareSidebar', + + props: { + shareToken: { + type: String, + required: true, + }, + + state: { + type: Object, + required: true, + }, + }, + + data() { + return { + fetchCurrentConversationIntervalId: null, + } + }, + + computed: { + token() { + return this.$store.getters.getToken() + }, + + conversation() { + return this.$store.getters.conversations[this.token] + }, + + isOpen() { + return this.state.isOpen + }, + }, + + methods: { + + async joinConversation() { + await this.getPublicShareConversationToken() + + await joinConversation(this.token) + + // 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', this.fetchCurrentConversation) + } 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. + this.fetchCurrentConversationIntervalId = window.setInterval(this.fetchCurrentConversation, 30000) + } + }, + + async getPublicShareConversationToken() { + const token = await getPublicShareConversationToken(this.shareToken) + + this.$store.dispatch('updateToken', token) + }, + + async fetchCurrentConversation() { + if (!this.token) { + return + } + + try { + const response = await fetchConversation(this.token) + this.$store.dispatch('addConversation', response.data.ocs.data) + } catch (exception) { + window.clearInterval(this.fetchCurrentConversationIntervalId) + + this.$store.dispatch('deleteConversationByToken', this.token) + this.$store.dispatch('updateToken', '') + } + }, + }, +} +</script> + +<style lang="scss" scoped> +/* Properties based on the app-sidebar */ +#talk-sidebar { + position: relative; + flex-shrink: 0; + width: 27vw; + min-width: 300px; + max-width: 500px; + + background: var(--color-main-background); + border-left: 1px solid var(--color-border); + + overflow-x: hidden; + overflow-y: auto; + z-index: 1500; +} +</style> diff --git a/src/mainPublicShareSidebar.js b/src/mainPublicShareSidebar.js new file mode 100644 index 000000000..53de6e139 --- /dev/null +++ b/src/mainPublicShareSidebar.js @@ -0,0 +1,101 @@ +/** + * @copyright Copyright (c) 2020 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/>. + * + */ + +import Vue from 'vue' +import PublicShareSidebar from './PublicShareSidebar' + +// Store +import Vuex from 'vuex' +import store from './store' + +// Utils +import { generateFilePath } from '@nextcloud/router' +import { getRequestToken } from '@nextcloud/auth' + +// Directives +import { translate, translatePlural } from '@nextcloud/l10n' + +// CSP config for webpack dynamic chunk loading +// eslint-disable-next-line +__webpack_nonce__ = btoa(getRequestToken()) + +// Correct the root of the app for chunk loading +// OC.linkTo matches the apps folders +// OC.generateUrl ensure the index.php (or not) +// We do not want the index.php since we're loading files +// eslint-disable-next-line +__webpack_public_path__ = generateFilePath('spreed', '', 'js/') + +Vue.prototype.t = translate +Vue.prototype.n = translatePlural +Vue.prototype.OC = OC +Vue.prototype.OCA = OCA + +Vue.use(Vuex) + +function adjustLayout() { + document.querySelector('#app-content').append(document.querySelector('footer')) + + const talkSidebarElement = document.createElement('div') + talkSidebarElement.setAttribute('id', 'talk-sidebar') + document.querySelector('#content').append(talkSidebarElement) +} + +adjustLayout() + +// An "isOpen" boolean should be passed to the component, but as it is a +// primitive it would not be reactive; it needs to be wrapped in an object and +// that object passed to the component to get reactivity. +const sidebarState = { + isOpen: false, +} + +// Open the sidebar by default based on the window width using the same +// threshold as in the main Talk UI (in Talk 7). +if (window.innerWidth > 1111) { + sidebarState.isOpen = true +} + +function addTalkSidebarTrigger() { + const talkSidebarTriggerElement = document.createElement('button') + talkSidebarTriggerElement.setAttribute('id', 'talk-sidebar-trigger') + talkSidebarTriggerElement.setAttribute('class', 'icon-menu-people-white') + talkSidebarTriggerElement.addEventListener('click', () => { + sidebarState.isOpen = !sidebarState.isOpen + }) + document.querySelector('.header-right').append(talkSidebarTriggerElement) +} + +addTalkSidebarTrigger() + +function getShareToken() { + const shareTokenElement = document.getElementById('sharingToken') + return shareTokenElement.value +} + +const talkSidebarVm = new Vue({ + store, + propsData: { + shareToken: getShareToken(), + state: sidebarState, + }, + ...PublicShareSidebar, +}) +talkSidebarVm.$mount(document.querySelector('#talk-sidebar')) diff --git a/src/services/filesIntegrationServices.js b/src/services/filesIntegrationServices.js index 70a69d79d..3782b7f71 100644 --- a/src/services/filesIntegrationServices.js +++ b/src/services/filesIntegrationServices.js @@ -39,6 +39,19 @@ const getFileConversation = async function({ fileId }, options) { } } +/** + * Gets the public share conversation token for a given share token. + * + * @param {String} shareToken the token of the share + * @returns {String} the conversation token + * @throws {Exception} if the conversation token could not be got + */ +const getPublicShareConversationToken = async function(shareToken) { + const response = await axios.get(generateOcsUrl('apps/spreed/api/v1', 2) + `publicshare/${shareToken}`) + return response.data.ocs.data.token +} + export { getFileConversation, + getPublicShareConversationToken, } diff --git a/webpack.common.js b/webpack.common.js index 62ce7c36a..0881a2a85 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -15,6 +15,7 @@ module.exports = { 'talk-files-sidebar': path.join(__dirname, 'src', 'mainFilesSidebar.js'), 'talk-files-sidebar-loader': path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'), 'talk-public-share-auth-sidebar': path.join(__dirname, 'src', 'mainPublicShareAuthSidebar.js'), + 'talk-public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'), 'flow': path.join(__dirname, 'src', 'flow.js') }, output: { |