nextcloud/apps/comments/src/components/Comment.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

403 lines
8.8 KiB
Vue
Raw Normal View History

<!--
- SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<component
:is="tag"
v-show="!deleted && !isLimbo"
:class="{ 'comment--loading': loading }"
class="comment">
<!-- Comment header toolbar -->
<div class="comment__side">
<!-- Author -->
<NcAvatar
class="comment__avatar"
:display-name="actorDisplayName"
:user="actorId"
:size="32" />
</div>
<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 …')"
:model-value="localMessage"
:user-data="userData"
aria-describedby="tab-comments__editor-description"
@update:value="updateLocalMessage"
@submit="onSubmit" />
<div class="comment__submit">
<NcButton
variant="tertiary-no-background"
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"
use-markdown
@click.native="onExpand" />
</div>
</component>
</template>
<script>
import { getCurrentUser } from '@nextcloud/auth'
import { translate as t } from '@nextcloud/l10n'
import { mapStores } from 'pinia'
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 IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
import CommentMixin from '../mixins/CommentMixin.js'
import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js'
// Dynamic loading
const NcRichContenteditable = () => import('@nextcloud/vue/components/NcRichContenteditable')
const NcRichText = () => import('@nextcloud/vue/components/NcRichText')
export default {
/* eslint vue/multi-word-component-names: "warn" */
name: 'Comment',
components: {
IconArrowRight,
IconClose,
IconTrashCanOutline,
IconPencilOutline,
NcActionButton,
NcActions,
NcActionSeparator,
NcAvatar,
NcButton,
NcDateTime,
NcLoadingIcon,
NcRichContenteditable,
NcRichText,
},
mixins: [CommentMixin],
inheritAttrs: false,
props: {
actorDisplayName: {
type: String,
required: true,
},
actorId: {
type: String,
required: true,
},
creationDateTime: {
type: String,
default: null,
},
/**
* Force the editor display
*/
editor: {
type: Boolean,
default: false,
},
/**
* Provide the autocompletion data
*/
autoComplete: {
type: Function,
required: true,
},
userData: {
type: Object,
default: () => ({}),
},
tag: {
type: String,
default: 'div',
},
},
data() {
return {
expanded: false,
// 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
*
* @return {boolean}
*/
isOwnComment() {
return getCurrentUser().uid === this.actorId
},
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() {
return Date.parse(this.creationDateTime)
},
isLimbo() {
return this.deletedCommentLimboStore.checkForId(this.id)
},
},
watch: {
// If the data change, update the local value
message(message) {
this.updateLocalMessage(message)
},
},
beforeMount() {
// Init localMessage
this.updateLocalMessage(this.message)
},
methods: {
t,
/**
* Update local Message on outer change
*
* @param {string} message the message to set
*/
updateLocalMessage(message) {
this.localMessage = message.toString()
this.submitted = false
},
/**
* Dispatch message between edit and create
*/
onSubmit() {
// Do not submit if message is empty
if (this.localMessage.trim() === '') {
return
}
if (this.editor) {
this.onNewComment(this.localMessage.trim())
this.$nextTick(() => {
// Focus the editor again
this.$refs.editor.$el.focus()
})
return
}
this.onEditComment(this.localMessage.trim())
},
onExpand() {
this.expanded = true
},
},
}
</script>
<style lang="scss" scoped>
@use "sass:math";
$comment-padding: 10px;
.comment {
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;
container-type: inline-size;
}
&__header {
display: flex;
align-items: center;
min-height: 44px;
}
&__actions {
margin-inline-start: $comment-padding !important;
}
&__author {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-text-maxcontrast);
}
&_loading,
&__timestamp {
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;
bottom: 5px;
inset-inline-end: 0;
}
&__message {
white-space: pre-wrap;
word-break: normal;
max-height: 200px;
overflow: auto;
scrollbar-gutter: stable;
scrollbar-width: thin;
margin-top: -6px;
&--expanded {
max-height: none;
overflow: visible;
}
:deep(img) {
max-width: 100%;
height: auto;
}
}
}
.rich-contenteditable__input {
min-height: 44px;
margin: 0;
padding: $comment-padding;
}
</style>