<!-- - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.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> <div class="comments" :class="{ 'icon-loading': isFirstLoading }"> <!-- Editor --> <Comment v-bind="editorData" :auto-complete="autoComplete" :editor="true" :ressource-id="ressourceId" class="comments__writer" @new="onNewComment" /> <template v-if="!isFirstLoading"> <EmptyContent v-if="!hasComments && done" icon="icon-comment"> {{ t('comments', 'No comments yet, start the conversation!') }} </EmptyContent> <!-- Comments --> <Comment v-for="comment in comments" v-else :key="comment.props.id" v-bind="comment.props" :auto-complete="autoComplete" :message.sync="comment.props.message" :ressource-id="ressourceId" :user-data="genMentionsData(comment.props.mentions)" class="comments__list" @delete="onDelete" /> <!-- Loading more message --> <div v-if="loading && !isFirstLoading" class="comments__info icon-loading" /> <div v-else-if="hasComments && done" class="comments__info"> {{ t('comments', 'No more messages') }} </div> <!-- Error message --> <EmptyContent v-else-if="error" class="comments__error" icon="icon-error"> {{ error }} <template #desc> <button icon="icon-history" @click="getComments"> {{ t('comments', 'Retry') }} </button> </template> </EmptyContent> </template> </div> </template> <script> import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' import axios from '@nextcloud/axios' import VTooltip from 'v-tooltip' import Vue from 'vue' import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import Comment from '../components/Comment' import getComments, { DEFAULT_LIMIT } from '../services/GetComments' import cancelableRequest from '../utils/cancelableRequest' Vue.use(VTooltip) export default { name: 'Comments', components: { // Avatar, Comment, EmptyContent, }, data() { return { error: '', loading: false, done: false, ressourceId: null, offset: 0, comments: [], cancelRequest: () => {}, editorData: { actorDisplayName: getCurrentUser().displayName, actorId: getCurrentUser().uid, key: 'editor', }, Comment, } }, computed: { hasComments() { return this.comments.length > 0 }, isFirstLoading() { return this.loading && this.offset === 0 }, }, methods: { /** * Update current ressourceId and fetch new data * @param {Number} ressourceId the current ressourceId (fileId...) */ async update(ressourceId) { this.ressourceId = ressourceId this.resetState() this.getComments() }, /** * Ran when the bottom of the tab is reached */ onScrollBottomReached() { /** * Do not fetch more if we: * - are showing an error * - already fetched everything * - are currently loading */ if (this.error || this.done || this.loading) { return } this.getComments() }, /** * Make sure we have all mentions as Array of objects * @param {Array} mentions the mentions list * @returns {Object[]} */ genMentionsData(mentions) { const list = Object.values(mentions).flat() return list.reduce((mentions, mention) => { mentions[mention.mentionId] = { // TODO: support groups icon: 'icon-user', id: mention.mentionId, label: mention.mentionDisplayName, source: 'users', primary: getCurrentUser().uid === mention.mentionId, } return mentions }, {}) }, /** * Get the existing shares infos */ async getComments() { // Cancel any ongoing request this.cancelRequest('cancel') try { this.loading = true this.error = '' // Init cancellable request const { request, cancel } = cancelableRequest(getComments) this.cancelRequest = cancel // Fetch comments const comments = await request({ commentsType: this.commentsType, ressourceId: this.ressourceId, }, { offset: this.offset }) this.logger.debug(`Processed ${comments.length} comments`, { comments }) // We received less than the requested amount, // we're done fetching comments if (comments.length < DEFAULT_LIMIT) { this.done = true } // Insert results this.comments.push(...comments) // Increase offset for next fetch this.offset += DEFAULT_LIMIT } catch (error) { if (error.message === 'cancel') { return } this.error = t('comments', 'Unable to load the comments list') console.error('Error loading the comments list', error) } finally { this.loading = false } }, /** * Autocomplete @mentions * @param {string} search the query * @param {Function} callback the callback to process the results with */ async autoComplete(search, callback) { const results = await axios.get(generateOcsUrl('core/autocomplete/get'), { params: { search, itemType: 'files', itemId: this.ressourceId, sorter: 'commenters|share-recipients', limit: loadState('comments', 'maxAutoCompleteResults'), }, }) return callback(results.data.ocs.data) }, /** * Add newly created comment to the list * @param {Object} comment the new comment */ onNewComment(comment) { this.comments.unshift(comment) }, /** * Remove deleted comment from the list * @param {number} id the deleted comment */ onDelete(id) { const index = this.comments.findIndex(comment => comment.props.id === id) if (index > -1) { this.comments.splice(index, 1) } else { console.error('Could not find the deleted comment in the list', id) } }, /** * Reset the current view to its default state */ resetState() { this.error = '' this.loading = false this.done = false this.offset = 0 this.comments = [] }, }, } </script> <style lang="scss" scoped> .comments { // Do not add emptycontent top margin &__error{ margin-top: 0; } &__info { height: 60px; color: var(--color-text-maxcontrast); text-align: center; line-height: 60px; } } </style>