diff options
Diffstat (limited to 'apps/comments/src/components/Comment.vue')
-rw-r--r-- | apps/comments/src/components/Comment.vue | 240 |
1 files changed, 145 insertions, 95 deletions
diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue index df9a22a1709..80f035530fb 100644 --- a/apps/comments/src/components/Comment.vue +++ b/apps/comments/src/components/Comment.vue @@ -1,26 +1,10 @@ <!-- - - @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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div v-show="!deleted" + <component :is="tag" + v-show="!deleted && !isLimbo" :class="{'comment--loading': loading}" class="comment"> <!-- Comment header toolbar --> @@ -39,22 +23,27 @@ show if we have a message id and current user is author --> <NcActions v-if="isOwnComment && id && !loading" class="comment__actions"> <template v-if="!editing"> - <NcActionButton :close-after-click="true" - icon="icon-rename" + <NcActionButton close-after-click @click="onEdit"> + <template #icon> + <IconPencilOutline :size="20" /> + </template> {{ t('comments', 'Edit comment') }} </NcActionButton> <NcActionSeparator /> - <NcActionButton :close-after-click="true" - icon="icon-delete" + <NcActionButton close-after-click @click="onDeleteWithUndo"> + <template #icon> + <IconTrashCanOutline :size="20" /> + </template> {{ t('comments', 'Delete comment') }} </NcActionButton> </template> - <NcActionButton v-else - icon="icon-close" - @click="onEditCancel"> + <NcActionButton v-else @click="onEditCancel"> + <template #icon> + <IconClose :size="20" /> + </template> {{ t('comments', 'Cancel edit') }} </NcActionButton> </NcActions> @@ -63,73 +52,99 @@ <div v-if="id && loading" class="comment_loading icon-loading-small" /> <!-- Relative time to the comment creation --> - <Moment v-else-if="creationDateTime" class="comment__timestamp" :timestamp="timestamp" /> + <NcDateTime v-else-if="creationDateTime" + class="comment__timestamp" + :timestamp="timestamp" + :ignore-seconds="true" /> </div> <!-- Message editor --> - <div v-if="editor || editing" class="comment__editor "> - <NcRichContenteditable ref="editor" - :auto-complete="autoComplete" - :contenteditable="!loading" - :value="localMessage" - :user-data="userData" - @update:value="updateLocalMessage" - @submit="onSubmit" /> - <NcButton class="comment__submit" - type="tertiary-no-background" - native-type="submit" - :aria-label="t('comments', 'Post comment')" - :disabled="isEmptyMessage" - @click="onSubmit"> - <template #icon> - <span v-if="loading" class="icon-loading-small" /> - <ArrowRight v-else :size="20" /> - </template> - </NcButton> - </div> + <form v-if="editor || editing" class="comment__editor" @submit.prevent> + <div class="comment__editor-group"> + <NcRichContenteditable ref="editor" + :auto-complete="autoComplete" + :contenteditable="!loading" + :label="editor ? t('comments', 'New comment') : t('comments', 'Edit comment')" + :placeholder="t('comments', 'Write a comment …')" + :value="localMessage" + :user-data="userData" + aria-describedby="tab-comments__editor-description" + @update:value="updateLocalMessage" + @submit="onSubmit" /> + <div class="comment__submit"> + <NcButton type="tertiary-no-background" + native-type="submit" + :aria-label="t('comments', 'Post comment')" + :disabled="isEmptyMessage" + @click="onSubmit"> + <template #icon> + <NcLoadingIcon v-if="loading" /> + <IconArrowRight v-else :size="20" /> + </template> + </NcButton> + </div> + </div> + <div id="tab-comments__editor-description" class="comment__editor-description"> + {{ t('comments', '@ for mentions, : for emoji, / for smart picker') }} + </div> + </form> <!-- Message content --> - <!-- The html is escaped and sanitized before rendering --> - <!-- eslint-disable-next-line vue/no-v-html--> - <div v-else - :class="{'comment__message--expanded': expanded}" + <NcRichText v-else class="comment__message" - @click="onExpand" - v-html="renderedContent" /> + :class="{'comment__message--expanded': expanded}" + :text="richContent.message" + :arguments="richContent.mentions" + @click="onExpand" /> </div> - </div> + </component> </template> <script> import { getCurrentUser } from '@nextcloud/auth' -import moment from '@nextcloud/moment' - -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton' -import NcActions from '@nextcloud/vue/dist/Components/NcActions' -import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar' -import NcButton from '@nextcloud/vue/dist/Components/NcButton' -import NcRichContenteditable from '@nextcloud/vue/dist/Components/NcRichContenteditable' -import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor' -import ArrowRight from 'vue-material-design-icons/ArrowRight' - -import Moment from './Moment' -import CommentMixin from '../mixins/CommentMixin' +import { translate as t } from '@nextcloud/l10n' + +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcUserBubble from '@nextcloud/vue/components/NcUserBubble' + +import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue' +import IconClose from 'vue-material-design-icons/Close.vue' +import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' +import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue' + +import CommentMixin from '../mixins/CommentMixin.js' +import { mapStores } from 'pinia' +import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js' + +// Dynamic loading +const NcRichContenteditable = () => import('@nextcloud/vue/components/NcRichContenteditable') +const NcRichText = () => import('@nextcloud/vue/components/NcRichText') export default { name: 'Comment', components: { + IconArrowRight, + IconClose, + IconTrashCanOutline, + IconPencilOutline, NcActionButton, NcActions, NcActionSeparator, - ArrowRight, NcAvatar, NcButton, - Moment, + NcDateTime, + NcLoadingIcon, NcRichContenteditable, + NcRichText, }, - mixins: [RichEditorMixin, CommentMixin], + mixins: [CommentMixin], inheritAttrs: false, @@ -162,6 +177,15 @@ export default { type: Function, required: true, }, + userData: { + type: Object, + default: () => ({}), + }, + + tag: { + type: String, + default: 'div', + }, }, data() { @@ -170,10 +194,12 @@ export default { // Only change data locally and update the original // parent data when the request is sent and resolved localMessage: '', + submitted: false, } }, computed: { + ...mapStores(useDeletedCommentLimbo), /** * Is the current user the author of this comment @@ -184,25 +210,40 @@ export default { return getCurrentUser().uid === this.actorId }, - /** - * Rendered content as html string - * - * @return {string} - */ - renderedContent() { - if (this.isEmptyMessage) { - return '' - } - return this.renderContent(this.localMessage) + richContent() { + const mentions = {} + let message = this.localMessage + + Object.keys(this.userData).forEach((user, index) => { + const key = `mention-${index}` + const regex = new RegExp(`@${user}|@"${user}"`, 'g') + message = message.replace(regex, `{${key}}`) + mentions[key] = { + component: NcUserBubble, + props: { + user, + displayName: this.userData[user].label, + primary: this.userData[user].primary, + }, + } + }) + + return { mentions, message } }, isEmptyMessage() { return !this.localMessage || this.localMessage.trim() === '' }, + /** + * Timestamp of the creation time (in ms UNIX time) + */ timestamp() { - // seconds, not milliseconds - return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000 + return Date.parse(this.creationDateTime) + }, + + isLimbo() { + return this.deletedCommentLimboStore.checkForId(this.id) }, }, @@ -219,6 +260,8 @@ export default { }, methods: { + t, + /** * Update local Message on outer change * @@ -226,6 +269,7 @@ export default { */ updateLocalMessage(message) { this.localMessage = message.toString() + this.submitted = false }, /** @@ -263,14 +307,13 @@ $comment-padding: 10px; .comment { display: flex; - gap: 16px; - position: relative; + gap: 8px; padding: 5px $comment-padding; &__side { display: flex; align-items: flex-start; - padding-top: 16px; + padding-top: 6px; } &__body { @@ -286,7 +329,7 @@ $comment-padding: 10px; } &__actions { - margin-left: $comment-padding !important; + margin-inline-start: $comment-padding !important; } &__author { @@ -298,23 +341,30 @@ $comment-padding: 10px; &_loading, &__timestamp { - margin-left: auto; - text-align: right; + margin-inline-start: auto; + text-align: end; white-space: nowrap; color: var(--color-text-maxcontrast); } + &__editor-group { + position: relative; + } + + &__editor-description { + color: var(--color-text-maxcontrast); + padding-block: var(--default-grid-baseline); + } + &__submit { position: absolute !important; - right: 0; - bottom: 0; - // Align with input border - margin: 1px; + bottom: 5px; + inset-inline-end: 0; } &__message { white-space: pre-wrap; - word-break: break-word; + word-break: normal; max-height: 70px; overflow: hidden; margin-top: -6px; |