diff options
-rw-r--r-- | css/icons.scss | 1 | ||||
-rw-r--r-- | img/folder-multiple-image.svg | 3 | ||||
-rw-r--r-- | lib/Controller/ChatController.php | 2 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue | 2 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue | 66 | ||||
-rw-r--r-- | src/components/RightSidebar/RightSidebar.vue | 26 | ||||
-rw-r--r-- | src/components/RightSidebar/SharedItems/SharedItems.vue | 169 | ||||
-rw-r--r-- | src/components/RightSidebar/SharedItems/SharedItemsTab.vue | 103 | ||||
-rw-r--r-- | src/services/conversationSharedItemsService.js | 49 | ||||
-rw-r--r-- | src/store/conversationSharedItemsStore.js | 100 | ||||
-rw-r--r-- | src/store/storeConfig.js | 2 |
11 files changed, 497 insertions, 26 deletions
diff --git a/css/icons.scss b/css/icons.scss index 6c9780045..1b898f5f2 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -10,6 +10,7 @@ @include icon-black-white('text', 'filetypes', 1, true); @include icon-black-white('promoted-view', 'spreed', 1); @include icon-black-white('grid-view', 'spreed', 1); +@include icon-black-white('folder-multiple-image', 'spreed', 1); .app-talk, .talk-modal, diff --git a/img/folder-multiple-image.svg b/img/folder-multiple-image.svg new file mode 100644 index 000000000..25333527a --- /dev/null +++ b/img/folder-multiple-image.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path d="M7,15L11.5,9L15,13.5L17.5,10.5L21,15M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" /> +</svg>
\ No newline at end of file diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index c60885bc5..de0a0d7b7 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -750,7 +750,7 @@ class ChatController extends AEnvironmentAwareController { $attachments = $this->attachmentService->getAttachmentsByType($this->room, $objectType, 0, $limit); $messageIdsByType[$objectType] = array_map(static fn (Attachment $attachment): int => $attachment->getMessageId(), $attachments); } - $comments = $this->chatManager->getMessagesById($this->room, array_merge(...$messageIdsByType)); + $comments = $this->chatManager->getMessagesById($this->room, array_merge(...array_values($messageIdsByType))); foreach ($comments as $comment) { $message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l); diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue index 58dc5a4b2..fb0c58d48 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue @@ -115,7 +115,7 @@ <span>❤️</span> </template> </Button> - <EmojiPicker :container="`#message_${id}`" + <EmojiPicker :container="`#message_${id} .message-buttons-bar`" @select="handleReactionClick" @after-show="onEmojiPickerOpen" @after-hide="onEmojiPickerClose"> diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 0655686fe..09cda5d64 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -25,7 +25,10 @@ <file-preview v-bind="filePreview" :tabindex="wrapperTabIndex" class="file-preview" - :class="{ 'file-preview--viewer-available': isViewerAvailable, 'file-preview--upload-editor': isUploadEditor }" + :class="{ 'file-preview--viewer-available': isViewerAvailable, + 'file-preview--upload-editor': isUploadEditor, + 'file-preview--shared-items-grid': isSharedItemsTab && !rowLayout, + 'file-preview--row-layout': rowLayout }" @click.exact="handleClick" @keydown.enter="handleClick"> <div v-if="!isLoading" @@ -63,8 +66,8 @@ </template> </Button> <ProgressBar v-if="isTemporaryUpload && !isUploadEditor" :value="uploadProgress" /> - <div class="name-container"> - <strong v-if="shouldShowFileDetail">{{ fileDetail }}</strong> + <div v-if="shouldShowFileDetail" class="name-container"> + {{ fileDetail }} </div> </file-preview> </template> @@ -203,6 +206,16 @@ export default { type: Boolean, default: false, }, + + rowLayout: { + type: Boolean, + default: false, + }, + + isSharedItemsTab: { + type: Boolean, + default: false, + }, }, data() { return { @@ -212,6 +225,9 @@ export default { }, computed: { shouldShowFileDetail() { + if (this.isSharedItemsTab && !this.rowLayout) { + return false + } // display the file detail below the preview if the preview // is not easily recognizable, when: return ( @@ -473,6 +489,7 @@ export default { .file-preview { position: relative; + min-width: 0; width: 100%; /* The file preview can not be a block; otherwise it would fill the whole width of the container and the loading icon would not be centered on the @@ -522,8 +539,8 @@ export default { } .image-container { - display: inline-block; - position: relative; + display: flex; + height: 100%; &.playable { .preview { @@ -554,19 +571,11 @@ export default { } .name-container { - /* Ellipsis with 100% width */ - display: table; - table-layout: fixed; + font-weight: bold; width: 100%; - - strong { - /* As the file preview is an inline block the name is set as a block to - force it to be on its own line below the preview. */ - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } &:not(.file-preview--viewer-available) { @@ -589,6 +598,29 @@ export default { width: 100%; } } + + &--row-layout { + display: flex; + align-items: center; + height: 36px; + border-radius: var(--border-radius); + padding: 2px 4px; + + .image-container { + height: 100%; + } + + .name-container { + padding: 0 4px; + } + } + + &--shared-items-grid { + aspect-ratio: 1; + .preview { + width: 100%; + } + } } .remove-file { diff --git a/src/components/RightSidebar/RightSidebar.vue b/src/components/RightSidebar/RightSidebar.vue index f9e4766f3..9d218b271 100644 --- a/src/components/RightSidebar/RightSidebar.vue +++ b/src/components/RightSidebar/RightSidebar.vue @@ -26,6 +26,7 @@ :title="title" :title-tooltip="title" :starred="isFavorited" + :active="activeTab" :title-editable="canModerate && isRenamingConversation" :class="'active-tab-' + activeTab" @update:active="handleUpdateActive" @@ -55,7 +56,8 @@ :can-search="canSearchParticipants" :can-add="canAddParticipants" /> </AppSidebarTab> - <AppSidebarTab id="details-tab" + <AppSidebarTab v-if="!getUserId || showSIPSettings" + id="details-tab" :order="3" :name="t('spreed', 'Details')" icon="icon-details"> @@ -63,10 +65,6 @@ <SipSettings v-if="showSIPSettings" :meeting-id="conversation.token" :attendee-pin="conversation.attendeePin" /> - <CollectionList v-if="getUserId && conversation.token" - :id="conversation.token" - type="room" - :name="conversation.displayName" /> <div v-if="!getUserId" id="app-settings"> <div id="app-settings-header"> <Button type="tertiary" @click="showSettings"> @@ -80,6 +78,14 @@ </div> </div> </AppSidebarTab> + <AppSidebarTab v-if="getUserId" + id="shared-items" + ref="sharedItemsTab" + :order="4" + icon="icon-folder-multiple-image" + :name="t('spreed', 'Shared items')"> + <SharedItemsTab :active="activeTab === 'shared-items'" /> + </AppSidebarTab> </AppSidebar> </template> @@ -87,8 +93,8 @@ import { emit } from '@nextcloud/event-bus' import AppSidebar from '@nextcloud/vue/dist/Components/AppSidebar' import AppSidebarTab from '@nextcloud/vue/dist/Components/AppSidebarTab' +import SharedItemsTab from './SharedItems/SharedItemsTab' import ChatView from '../ChatView' -import { CollectionList } from 'nextcloud-vue-collections' import BrowserStorage from '../../services/BrowserStorage' import { CONVERSATION, WEBINAR, PARTICIPANT } from '../../constants' import ParticipantsTab from './Participants/ParticipantsTab' @@ -104,8 +110,8 @@ export default { components: { AppSidebar, AppSidebarTab, + SharedItemsTab, ChatView, - CollectionList, ParticipantsTab, SetGuestUsername, SipSettings, @@ -226,6 +232,12 @@ export default { if (!this.isRenamingConversation) { this.conversationName = this.conversation.displayName } + + if (this.isOneToOne) { + this.activeTab = 'shared-items' + } else { + this.activeTab = 'participants' + } }, token() { diff --git a/src/components/RightSidebar/SharedItems/SharedItems.vue b/src/components/RightSidebar/SharedItems/SharedItems.vue new file mode 100644 index 000000000..1c1b4c165 --- /dev/null +++ b/src/components/RightSidebar/SharedItems/SharedItems.vue @@ -0,0 +1,169 @@ +<!-- + - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me> + - + - @author Marco Ambrosini <marcoambrosini@pm.me> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. +--> + +<template> + <div class="shared-items"> + <AppNavigationCaption :title="title" /> + <div class="files" :class="{'files__list' : isList}"> + <template v-for="file in filesToDisplay"> + <FilePreview :key="file.id" + :small-preview="isList" + :row-layout="isList" + :is-shared-items-tab="true" + v-bind="file.messageParameters.file" /> + </template> + </div> + <Button v-if="hasMore" + type="tertiary" + class="shared-items__more" + :wide="true" + @click="handleCaptionClick"> + <template #icon> + <DotsHorizontal :size="20" + decorative + title="" /> + </template> + {{ buttonTitle }} + </Button> + </div> +</template> + +<script> +import Button from '@nextcloud/vue/dist/Components/Button' +import FilePreview from '../../MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue' +import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' +import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption' +import { showMessage } from '@nextcloud/dialogs' + +export default { + name: 'SharedItems', + + components: { + Button, + FilePreview, + DotsHorizontal, + AppNavigationCaption, + }, + + props: { + type: { + type: String, + required: true, + }, + + items: { + type: Object, + required: true, + }, + }, + + computed: { + filesToDisplay() { + return Object.values(this.items).reverse().slice(0, 6) + }, + + title() { + switch (this.type) { + case 'media': + return t('spreed', 'Media') + case 'file': + return t('spreed', 'Files') + case 'deck-card': + return t('spreed', 'Deck cards') + case 'voice': + return t('spreed', 'Voice messages') + case 'location': + return t('spreed', 'Locations') + case 'audio': + return t('spreed', 'Audio') + case 'other': + return t('spreed', 'Other') + default: + return '' + } + }, + + buttonTitle() { + switch (this.type) { + case 'media': + return t('spreed', 'Show all media') + case 'file': + return t('spreed', 'Show all files') + case 'deck-card': + return t('spreed', 'Show all deck cards') + case 'voice': + return t('spreed', 'Show all voice messages') + case 'location': + return t('spreed', 'Show all locations') + case 'audio': + return t('spreed', 'Show all audio') + case 'other': + return t('spreed', 'Show all other') + default: + return '' + } + }, + + isList() { + switch (this.type) { + case 'media': + return false + case 'locations': + return false + default: + return true + } + }, + + hasMore() { + return Object.values(this.items).length > 6 + }, + }, + + methods: { + handleCaptionClick() { + showMessage('Screenshot feature only. Implementation of the real feature will come soon! 😎') + console.debug('Show more') + }, + }, +} +</script> + +<style lang="scss" scoped> +.files { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + grid-gap: 4px; + &__list { + display: flex; + flex-direction: column; + } + +} + +.shared-items { + margin-bottom: 16px; + &__more { + margin-top: 8px; + } +} +</style> diff --git a/src/components/RightSidebar/SharedItems/SharedItemsTab.vue b/src/components/RightSidebar/SharedItems/SharedItemsTab.vue new file mode 100644 index 000000000..4dc990b63 --- /dev/null +++ b/src/components/RightSidebar/SharedItems/SharedItemsTab.vue @@ -0,0 +1,103 @@ +<!-- + - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me> + - + - @author Marco Ambrosini <marcoambrosini@pm.me> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. +--> + +<template> + <div v-if="!loading && active"> + <template v-for="type in sharedItemsOrder"> + <SharedItems v-if="sharedItems[type]" + :key="type" + :type="type" + :items="sharedItems[type]" /> + </template> + <AppNavigationCaption :title="t('spreed', 'Projects')" /> + <CollectionList v-if="getUserId && token" + :id="token" + type="room" + :name="conversation.displayName" /> + </div> +</template> + +<script> +import { CollectionList } from 'nextcloud-vue-collections' +import SharedItems from './SharedItems' +import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption' + +export default { + + name: 'SharedItemsTab', + + components: { + SharedItems, + CollectionList, + AppNavigationCaption, + }, + + props: { + + active: { + type: Boolean, + required: true, + }, + }, + + computed: { + getUserId() { + return this.$store.getters.getUserId() + }, + + token() { + return this.$store.getters.getToken() + }, + + conversation() { + return this.$store.getters.conversation(this.token) + }, + + loading() { + return !this.sharedItems + }, + + sharedItems() { + return this.$store.getters.sharedItems(this.token) + }, + + // Defines the order of the sections + sharedItemsOrder() { + // FIXME restore when non files work return ['media', 'file', 'voice', 'audio', 'location', 'deckcard', 'other'] + return ['media', 'file', 'voice', 'audio'] + }, + }, + + watch: { + active(newValue) { + if (newValue) { + this.getSharedItemsOverview() + } + }, + }, + + methods: { + getSharedItemsOverview() { + this.$store.dispatch('getSharedItemsOverview', { token: this.token }) + }, + }, +} +</script> diff --git a/src/services/conversationSharedItemsService.js b/src/services/conversationSharedItemsService.js new file mode 100644 index 000000000..12922175e --- /dev/null +++ b/src/services/conversationSharedItemsService.js @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me> + * + * @author Marco Ambrosini <marcoambrosini@pm.me> + * + * @license AGPL-3.0-or-later + * + * 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 axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +// Returns the last n shared items for each category and for a given conversation +// (n = limit) +const getSharedItemsOverview = async function(token, limit) { + return axios.get(generateOcsUrl('apps/spreed/api/v1/chat/{token}/share/overview', { + token, + limit, + })) +} + +// Returns the last 200 (or limit) shared items, given a conversation and the type +// of shared item +const getSharedItems = async function(token, objectType, lastKnownMessageId, limit,) { + return axios.get(generateOcsUrl('apps/spreed/api/v1/chat/{token}/share', { + token, + objectType, + lastKnownMessageId, + limit, + })) +} + +export { + getSharedItems, + getSharedItemsOverview, +} diff --git a/src/store/conversationSharedItemsStore.js b/src/store/conversationSharedItemsStore.js new file mode 100644 index 000000000..9ea68d9c9 --- /dev/null +++ b/src/store/conversationSharedItemsStore.js @@ -0,0 +1,100 @@ +/** + * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me> + * + * @author Marco Ambrosini <marcoambrosini@pm.me> + * + * @license AGPL-3.0-or-later + * + * 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 { getSharedItemsOverview, getSharedItems } from '../services/conversationSharedItemsService' + +// Store structure +// token: { +// media: {}, +// file: {}, +// voice: {}, +// audio: {}, +// location: {} +// deckcard: {}, +// other: {}, + +const state = () => ({ + state: {}, +}) + +const getters = { + sharedItems: state => token => { + const sharedItems = {} + if (!state[token]) { + return {} + } + for (const type of Object.keys(state[token])) { + if (Object.keys(state[token][type]).length !== 0) { + sharedItems[type] = state[token][type] + } + } + return sharedItems + }, +} + +export const mutations = { + addSharedItemsOverview: (state, { token, data }) => { + if (!state[token]) { + Vue.set(state, token, {}) + } + for (const type of Object.keys(data)) { + if (!state[token][type]) { + Vue.set(state[token], type, {}) + for (const message of data[type]) { + if (!state[token][type]?.[message.id]) { + Vue.set(state[token][type], message.id, message) + } + } + } + } + }, +} + +const actions = { + async getSharedItems({ commit }, { token, type, lastKnownMessageId, limit }) { + try { + const response = await getSharedItems(token, type, lastKnownMessageId, limit) + // loop over the response elements and add them to the store + for (const sharedItem in response) { + commit('addSharedItem', sharedItem) + } + + } catch (error) { + console.debug(error) + } + }, + + async getSharedItemsOverview({ commit }, { token }) { + try { + const response = await getSharedItemsOverview(token, 10) + commit('addSharedItemsOverview', { + token, + data: response.data.ocs.data, + }) + } catch (error) { + console.debug(error) + } + }, +} + +export default { state, mutations, getters, actions } diff --git a/src/store/storeConfig.js b/src/store/storeConfig.js index 8d772fa26..9f5fbe9c1 100644 --- a/src/store/storeConfig.js +++ b/src/store/storeConfig.js @@ -39,6 +39,7 @@ import uiModeStore from './uiModeStore' import windowVisibilityStore from './windowVisibilityStore' import messageActionsStore from './messageActionsStore' import reactionsStore from './reactionsStore' +import conversationSharedItemStore from './conversationSharedItemsStore' export default { modules: { @@ -61,6 +62,7 @@ export default { windowVisibilityStore, messageActionsStore, reactionsStore, + conversationSharedItemStore, }, mutations: {}, |