diff options
Diffstat (limited to 'apps/comments/src/views')
-rw-r--r-- | apps/comments/src/views/ActivityCommentAction.vue | 54 | ||||
-rw-r--r-- | apps/comments/src/views/ActivityCommentEntry.vue | 71 | ||||
-rw-r--r-- | apps/comments/src/views/Comments.vue | 279 |
3 files changed, 404 insertions, 0 deletions
diff --git a/apps/comments/src/views/ActivityCommentAction.vue b/apps/comments/src/views/ActivityCommentAction.vue new file mode 100644 index 00000000000..f9a9a97796f --- /dev/null +++ b/apps/comments/src/views/ActivityCommentAction.vue @@ -0,0 +1,54 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <Comment v-bind="editorData" + :auto-complete="autoComplete" + :resource-type="resourceType" + :editor="true" + :user-data="userData" + :resource-id="resourceId" + class="comments-action" + @new="onNewComment" /> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue' +import Comment from '../components/Comment.vue' +import CommentView from '../mixins/CommentView.js' +import logger from '../logger' +import { showError } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' + +export default defineComponent({ + components: { + Comment, + }, + mixins: [CommentView], + props: { + reloadCallback: { + type: Function, + required: true, + }, + }, + methods: { + onNewComment() { + try { + // just force reload + this.reloadCallback() + } catch (e) { + showError(t('comments', 'Could not reload comments')) + logger.debug(e) + } + }, + }, +}) +</script> + +<style scoped> +.comments-action { + padding: 0; +} +</style> diff --git a/apps/comments/src/views/ActivityCommentEntry.vue b/apps/comments/src/views/ActivityCommentEntry.vue new file mode 100644 index 00000000000..bbfe530b2e3 --- /dev/null +++ b/apps/comments/src/views/ActivityCommentEntry.vue @@ -0,0 +1,71 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <Comment ref="comment" + tag="li" + v-bind="comment.props" + :auto-complete="autoComplete" + :resource-type="resourceType" + :message="commentMessage" + :resource-id="resourceId" + :user-data="genMentionsData(comment.props.mentions)" + class="comments-activity" + @delete="reloadCallback()" /> +</template> + +<script lang="ts"> +import type { PropType } from 'vue' +import { translate as t } from '@nextcloud/l10n' + +import Comment from '../components/Comment.vue' +import CommentView from '../mixins/CommentView' + +export default { + name: 'ActivityCommentEntry', + + components: { + Comment, + }, + + mixins: [CommentView], + props: { + comment: { + type: Object, + required: true, + }, + reloadCallback: { + type: Function as PropType<() => void>, + required: true, + }, + }, + + data() { + return { + commentMessage: '', + } + }, + + watch: { + comment() { + this.commentMessage = this.comment.props.message + }, + }, + + mounted() { + this.commentMessage = this.comment.props.message + }, + + methods: { + t, + }, +} +</script> + +<style scoped> +.comments-activity { + padding: 0; +} +</style> diff --git a/apps/comments/src/views/Comments.vue b/apps/comments/src/views/Comments.vue new file mode 100644 index 00000000000..657af888a12 --- /dev/null +++ b/apps/comments/src/views/Comments.vue @@ -0,0 +1,279 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div v-element-visibility="onVisibilityChange" + class="comments" + :class="{ 'icon-loading': isFirstLoading }"> + <!-- Editor --> + <Comment v-bind="editorData" + :auto-complete="autoComplete" + :resource-type="resourceType" + :editor="true" + :user-data="userData" + :resource-id="currentResourceId" + class="comments__writer" + @new="onNewComment" /> + + <template v-if="!isFirstLoading"> + <NcEmptyContent v-if="!hasComments && done" + class="comments__empty" + :name="t('comments', 'No comments yet, start the conversation!')"> + <template #icon> + <IconMessageReplyTextOutline /> + </template> + </NcEmptyContent> + <ul v-else> + <!-- Comments --> + <Comment v-for="comment in comments" + :key="comment.props.id" + tag="li" + v-bind="comment.props" + :auto-complete="autoComplete" + :resource-type="resourceType" + :message.sync="comment.props.message" + :resource-id="currentResourceId" + :user-data="genMentionsData(comment.props.mentions)" + class="comments__list" + @delete="onDelete" /> + </ul> + + <!-- 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 --> + <template v-else-if="error"> + <NcEmptyContent class="comments__error" :name="error"> + <template #icon> + <IconAlertCircleOutline /> + </template> + </NcEmptyContent> + <NcButton class="comments__retry" @click="getComments"> + <template #icon> + <IconRefresh /> + </template> + {{ t('comments', 'Retry') }} + </NcButton> + </template> + </template> + </div> +</template> + +<script> +import { showError } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import { vElementVisibility as elementVisibility } from '@vueuse/components' + +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcButton from '@nextcloud/vue/components/NcButton' +import IconRefresh from 'vue-material-design-icons/Refresh.vue' +import IconMessageReplyTextOutline from 'vue-material-design-icons/MessageReplyTextOutline.vue' +import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' + +import Comment from '../components/Comment.vue' +import CommentView from '../mixins/CommentView' +import cancelableRequest from '../utils/cancelableRequest.js' +import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts' +import { markCommentsAsRead } from '../services/ReadComments.ts' + +export default { + name: 'Comments', + + components: { + Comment, + NcEmptyContent, + NcButton, + IconRefresh, + IconMessageReplyTextOutline, + IconAlertCircleOutline, + }, + + directives: { + elementVisibility, + }, + + mixins: [CommentView], + + data() { + return { + error: '', + loading: false, + done: false, + + currentResourceId: this.resourceId, + offset: 0, + comments: [], + + cancelRequest: () => {}, + + Comment, + userData: {}, + } + }, + + computed: { + hasComments() { + return this.comments.length > 0 + }, + isFirstLoading() { + return this.loading && this.offset === 0 + }, + }, + + watch: { + resourceId() { + this.currentResourceId = this.resourceId + }, + }, + + methods: { + t, + + async onVisibilityChange(isVisible) { + if (isVisible) { + try { + await markCommentsAsRead(this.resourceType, this.currentResourceId, new Date()) + } catch (e) { + showError(e.message || t('comments', 'Failed to mark comments as read')) + } + } + }, + + /** + * Update current resourceId and fetch new data + * + * @param {number} resourceId the current resourceId (fileId...) + */ + async update(resourceId) { + this.currentResourceId = resourceId + 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() + }, + + /** + * 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, abort } = cancelableRequest(getComments) + this.cancelRequest = abort + + // Fetch comments + const { data: comments } = await request({ + resourceType: this.resourceType, + resourceId: this.currentResourceId, + }, { offset: this.offset }) || { data: [] } + + 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 + } + }, + + /** + * 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 { + min-height: 100%; + display: flex; + flex-direction: column; + + &__empty, + &__error { + flex: 1 0; + } + + &__retry { + margin: 0 auto; + } + + &__info { + height: 60px; + color: var(--color-text-maxcontrast); + text-align: center; + line-height: 60px; + } +} +</style> |