diff options
Diffstat (limited to 'apps/comments/src/components/Comment.vue')
-rw-r--r-- | apps/comments/src/components/Comment.vue | 339 |
1 files changed, 197 insertions, 142 deletions
diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue index f22754ea201..80f035530fb 100644 --- a/apps/comments/src/components/Comment.vue +++ b/apps/comments/src/components/Comment.vue @@ -1,122 +1,150 @@ <!-- - - @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 --> - <div class="comment__header"> + <div class="comment__side"> <!-- Author --> - <Avatar class="comment__avatar" + <NcAvatar class="comment__avatar" :display-name="actorDisplayName" :user="actorId" :size="32" /> - <span class="comment__author">{{ actorDisplayName }}</span> - - <!-- Comment actions, - show if we have a message id and current user is author --> - <Actions v-if="isOwnComment && id && !loading" class="comment__actions"> - <template v-if="!editing"> - <ActionButton :close-after-click="true" - icon="icon-rename" - @click="onEdit"> - {{ t('comments', 'Edit comment') }} - </ActionButton> - <ActionSeparator /> - <ActionButton :close-after-click="true" - icon="icon-delete" - @click="onDeleteWithUndo"> - {{ t('comments', 'Delete comment') }} - </ActionButton> - </template> - - <ActionButton v-else - icon="icon-close" - @click="onEditCancel"> - {{ t('comments', 'Cancel edit') }} - </ActionButton> - </Actions> - - <!-- Show loading if we're editing or deleting, not on new ones --> - <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" /> </div> - - <!-- Message editor --> - <div v-if="editor || editing" class="comment__editor "> - <RichContenteditable ref="editor" - :auto-complete="autoComplete" - :contenteditable="!loading" - :value="localMessage" - @update:value="updateLocalMessage" - @submit="onSubmit" /> - <input v-tooltip="t('comments', 'Post comment')" - :class="loading ? 'icon-loading-small' :'icon-confirm'" - class="comment__submit" - type="submit" - :disabled="isEmptyMessage" - value="" - @click="onSubmit"> + <div class="comment__body"> + <div class="comment__header"> + <span class="comment__author">{{ actorDisplayName }}</span> + + <!-- Comment actions, + 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 + @click="onEdit"> + <template #icon> + <IconPencilOutline :size="20" /> + </template> + {{ t('comments', 'Edit comment') }} + </NcActionButton> + <NcActionSeparator /> + <NcActionButton close-after-click + @click="onDeleteWithUndo"> + <template #icon> + <IconTrashCanOutline :size="20" /> + </template> + {{ t('comments', 'Delete comment') }} + </NcActionButton> + </template> + + <NcActionButton v-else @click="onEditCancel"> + <template #icon> + <IconClose :size="20" /> + </template> + {{ t('comments', 'Cancel edit') }} + </NcActionButton> + </NcActions> + + <!-- Show loading if we're editing or deleting, not on new ones --> + <div v-if="id && loading" class="comment_loading icon-loading-small" /> + + <!-- Relative time to the comment creation --> + <NcDateTime v-else-if="creationDateTime" + class="comment__timestamp" + :timestamp="timestamp" + :ignore-seconds="true" /> + </div> + + <!-- Message editor --> + <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 --> + <NcRichText v-else + class="comment__message" + :class="{'comment__message--expanded': expanded}" + :text="richContent.message" + :arguments="richContent.mentions" + @click="onExpand" /> </div> - - <!-- 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}" - class="comment__message" - @click="onExpand" - v-html="renderedContent" /> - </div> + </component> </template> <script> import { getCurrentUser } from '@nextcloud/auth' -import moment from '@nextcloud/moment' - -import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' -import Actions from '@nextcloud/vue/dist/Components/Actions' -import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator' -import Avatar from '@nextcloud/vue/dist/Components/Avatar' -import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable' -import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor' - -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: { - ActionButton, - Actions, - ActionSeparator, - Avatar, - Moment, - RichContenteditable, + IconArrowRight, + IconClose, + IconTrashCanOutline, + IconPencilOutline, + NcActionButton, + NcActions, + NcActionSeparator, + NcAvatar, + NcButton, + NcDateTime, + NcLoadingIcon, + NcRichContenteditable, + NcRichText, }, - mixins: [RichEditorMixin, CommentMixin], + mixins: [CommentMixin], inheritAttrs: false, @@ -149,6 +177,15 @@ export default { type: Function, required: true, }, + userData: { + type: Object, + default: () => ({}), + }, + + tag: { + type: String, + default: 'div', + }, }, data() { @@ -157,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 @@ -171,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) }, }, @@ -206,6 +260,8 @@ export default { }, methods: { + t, + /** * Update local Message on outer change * @@ -213,6 +269,7 @@ export default { */ updateLocalMessage(message) { this.localMessage = message.toString() + this.submitted = false }, /** @@ -249,19 +306,30 @@ export default { $comment-padding: 10px; .comment { - position: relative; - padding: $comment-padding 0 $comment-padding * 1.5; + display: flex; + gap: 8px; + padding: 5px $comment-padding; + + &__side { + display: flex; + align-items: flex-start; + padding-top: 6px; + } + + &__body { + display: flex; + flex-grow: 1; + flex-direction: column; + } &__header { display: flex; align-items: center; min-height: 44px; - padding: math.div($comment-padding, 2) 0; } - &__author, &__actions { - margin-left: $comment-padding !important; + margin-inline-start: $comment-padding !important; } &__author { @@ -273,46 +341,33 @@ $comment-padding: 10px; &_loading, &__timestamp { - margin-left: auto; + margin-inline-start: auto; + text-align: end; + white-space: nowrap; color: var(--color-text-maxcontrast); } - &__editor, - &__message { + &__editor-group { position: relative; - // Avatar size, align with author name - padding-left: 32px + $comment-padding; } - &__submit { - position: absolute; - right: 0; - bottom: 0; - width: 44px; - height: 44px; - // Align with input border - margin: 1px; - cursor: pointer; - opacity: .7; - border: none; - background-color: transparent !important; - - &:disabled { - cursor: not-allowed; - opacity: .5; - } + &__editor-description { + color: var(--color-text-maxcontrast); + padding-block: var(--default-grid-baseline); + } - &:focus, - &:hover { - opacity: 1; - } + &__submit { + position: absolute !important; + 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; &--expanded { max-height: none; overflow: visible; |