aboutsummaryrefslogtreecommitdiffstats
path: root/apps/comments/src/components/Comment.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/comments/src/components/Comment.vue')
-rw-r--r--apps/comments/src/components/Comment.vue295
1 files changed, 295 insertions, 0 deletions
diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue
new file mode 100644
index 00000000000..acacb156f75
--- /dev/null
+++ b/apps/comments/src/components/Comment.vue
@@ -0,0 +1,295 @@
+<!--
+ - @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 v-show="!deleted"
+ :class="{'comment--loading': loading}"
+ class="comment">
+ <!-- Comment header toolbar -->
+ <div class="comment__header">
+ <!-- Author -->
+ <Avatar 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 class="comment__message" v-if="editor || editing">
+ <RichContenteditable v-model="localMessage" :auto-complete="autoComplete" :contenteditable="!loading" />
+ <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>
+
+ <!-- Message content -->
+ <!-- The html is escaped and sanitized before rendering -->
+ <!-- eslint-disable-next-line vue/no-v-html-->
+ <div v-else class="comment__message" v-html="renderedContent" />
+ </div>
+</template>
+
+<script>
+import { getCurrentUser } from '@nextcloud/auth'
+import moment from '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'
+
+export default {
+ name: 'Comment',
+
+ components: {
+ ActionButton,
+ Actions,
+ ActionSeparator,
+ Avatar,
+ Moment,
+ RichContenteditable,
+ },
+ mixins: [RichEditorMixin, CommentMixin],
+
+ inheritAttrs: false,
+
+ props: {
+ source: {
+ type: Object,
+ default: () => ({}),
+ },
+ 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,
+ },
+ },
+
+ data() {
+ return {
+ // Only change data locally and update the original
+ // parent data when the request is sent and resolved
+ localMessage: '',
+ }
+ },
+
+ computed: {
+
+ /**
+ * Is the current user the author of this comment
+ * @returns {boolean}
+ */
+ isOwnComment() {
+ return getCurrentUser().uid === this.actorId
+ },
+
+ /**
+ * Rendered content as html string
+ * @returns {string}
+ */
+ renderedContent() {
+ if (this.isEmptyMessage) {
+ return ''
+ }
+ return this.renderContent(this.localMessage)
+ },
+
+ isEmptyMessage() {
+ return !this.localMessage || this.localMessage.trim() === ''
+ },
+
+ timestamp() {
+ // seconds, not milliseconds
+ return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000
+ },
+ },
+
+ watch: {
+ // If the data change, update the local value
+ message(message) {
+ this.updateLocalMessage(message)
+ },
+ },
+
+ beforeMount() {
+ // Init localMessage
+ this.updateLocalMessage(this.message)
+ },
+
+ methods: {
+ /**
+ * Update local Message on outer change
+ * @param {string} message the message to set
+ */
+ updateLocalMessage(message) {
+ this.localMessage = message.toString()
+ },
+
+ /**
+ * Dispatch message between edit and create
+ */
+ onSubmit() {
+ if (this.editor) {
+ this.onNewComment(this.localMessage)
+ return
+ }
+ this.onEditComment(this.localMessage)
+ },
+ },
+
+}
+</script>
+
+<style lang="scss" scoped>
+$comment-padding: 10px;
+
+.comment {
+ position: relative;
+ padding: $comment-padding 0 $comment-padding * 1.5;
+
+ &__header {
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ padding: $comment-padding / 2 0;
+ }
+
+ &__author,
+ &__actions {
+ margin-left: $comment-padding !important;
+ }
+
+ &__author {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ color: var(--color-text-maxcontrast);
+ }
+
+ &_loading,
+ &__timestamp {
+ margin-left: auto;
+ color: var(--color-text-maxcontrast);
+ }
+
+ &__message {
+ 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;
+ }
+
+ &:focus,
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
+
+.rich-contenteditable__input {
+ margin: 0;
+ padding: $comment-padding;
+ min-height: 44px;
+}
+
+</style>