Skip to content

Commit b891e82

Browse files
committed
fixup! Init vue comments tab
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
1 parent 7144cdc commit b891e82

10 files changed

Lines changed: 592 additions & 43 deletions

File tree

apps/comments/src/components/Comment.vue

Lines changed: 244 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,49 +20,275 @@
2020
-
2121
-->
2222
<template>
23-
<div class="comment">
23+
<div v-show="!deleted"
24+
:class="{'comment--loading': loading}"
25+
class="comment">
26+
<!-- Comment header toolbar -->
2427
<div class="comment__header">
25-
<Avatar class="comment__avatar" :display-name="source.actorDisplayName" :user="source.actorId" />
26-
<span class="comment__author">{{ source.actorDisplayName }}</span>
27-
<Actions class="comment__actions">
28-
<ActionButton icon="icon-rename">
29-
{{ t('comments', 'Edit comment') }}
30-
</ActionButton>
31-
<ActionButton icon="icon-delete">
32-
{{ t('comments', 'Delete comment') }}
28+
<!-- Author -->
29+
<Avatar class="comment__avatar"
30+
:display-name="actorDisplayName"
31+
:user="actorId"
32+
:size="32" />
33+
<span class="comment__author">{{ actorDisplayName }}</span>
34+
35+
<!-- Comment actions,
36+
show if we have a message id and current user is author -->
37+
<Actions v-if="isOwnComment && id && !loading" class="comment__actions">
38+
<template v-if="!editing">
39+
<ActionButton
40+
:close-after-click="true"
41+
icon="icon-rename"
42+
@click="onEdit">
43+
{{ t('comments', 'Edit comment') }}
44+
</ActionButton>
45+
<ActionSeparator />
46+
<ActionButton
47+
:close-after-click="true"
48+
icon="icon-delete"
49+
@click="onDeleteWithUndo">
50+
{{ t('comments', 'Delete comment') }}
51+
</ActionButton>
52+
</template>
53+
54+
<ActionButton v-else
55+
icon="icon-close"
56+
@click="onEditCancel">
57+
{{ t('comments', 'Cancel edit') }}
3358
</ActionButton>
3459
</Actions>
35-
<span class="comment__timestamp"></span>
60+
61+
<!-- Show loading if we're editing or deleting, not on new ones -->
62+
<div v-if="id && loading" class="comment_loading icon-loading-small" />
63+
64+
<!-- Relative time to the comment creation -->
65+
<Moment v-else-if="creationDateTime" class="comment__timestamp" :timestamp="timestamp" />
3666
</div>
37-
<div class="comment__message">
38-
{{ source.message }}
67+
68+
<!-- Message editor -->
69+
<div class="comment__message" v-if="editor || editing">
70+
<RichContenteditable v-model="localMessage" :auto-complete="autoComplete" :contenteditable="!loading" />
71+
<input v-tooltip="t('comments', 'Post comment')"
72+
:class="loading ? 'icon-loading-small' :'icon-confirm'"
73+
class="comment__submit"
74+
type="submit"
75+
:disabled="isEmptyMessage"
76+
value=""
77+
@click="onSubmit">
3978
</div>
79+
80+
<!-- Message content -->
81+
<!-- The html is escaped and sanitized before rendering -->
82+
<!-- eslint-disable-next-line vue/no-v-html-->
83+
<div v-else class="comment__message" v-html="renderedContent" />
4084
</div>
4185
</template>
4286

4387
<script>
44-
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
45-
import Actions from '@nextcloud/vue/dist/Components/Actions'
88+
import { getCurrentUser } from '@nextcloud/auth'
89+
import moment from 'moment'
90+
4691
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
92+
import Actions from '@nextcloud/vue/dist/Components/Actions'
93+
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
94+
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
95+
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
96+
import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor'
97+
98+
import Moment from './Moment'
99+
import CommentMixin from '../mixins/CommentMixin'
47100
48101
export default {
49102
name: 'Comment',
103+
104+
components: {
105+
ActionButton,
106+
Actions,
107+
ActionSeparator,
108+
Avatar,
109+
Moment,
110+
RichContenteditable,
111+
},
112+
113+
mixins: [RichEditorMixin, CommentMixin],
114+
50115
props: {
51116
source: {
52117
type: Object,
53118
default: () => ({}),
54119
},
120+
actorDisplayName: {
121+
type: String,
122+
required: true,
123+
},
124+
actorId: {
125+
type: String,
126+
required: true,
127+
},
128+
creationDateTime: {
129+
type: String,
130+
default: null,
131+
},
132+
133+
/**
134+
* Force the editor display
135+
*/
136+
editor: {
137+
type: Boolean,
138+
default: false,
139+
},
140+
141+
/**
142+
* Provide the autocompletion data
143+
*/
144+
autoComplete: {
145+
type: Function,
146+
required: true,
147+
},
55148
},
56149
57-
components: {
58-
ActionButton,
59-
Actions,
60-
Avatar,
150+
data() {
151+
return {
152+
// Only change data locally and update the original
153+
// parent data when the request is sent and resolved
154+
localMessage: '',
155+
}
156+
},
157+
158+
computed: {
159+
160+
/**
161+
* Is the current user the author of this comment
162+
* @returns {boolean}
163+
*/
164+
isOwnComment() {
165+
return getCurrentUser().uid === this.actorId
166+
},
167+
168+
/**
169+
* Rendered content as html string
170+
* @returns {string}
171+
*/
172+
renderedContent() {
173+
if (this.isEmptyMessage) {
174+
return ''
175+
}
176+
return this.renderContent(this.localMessage)
177+
},
178+
179+
isEmptyMessage() {
180+
return !this.localMessage || this.localMessage.trim() === ''
181+
},
182+
183+
timestamp() {
184+
// seconds, not milliseconds
185+
return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000
186+
},
187+
},
188+
189+
watch: {
190+
// If the data change, update the local value
191+
message(message) {
192+
console.debug('Message changed', message)
193+
this.updateLocalMessage(message)
194+
},
195+
},
196+
197+
beforeMount() {
198+
// Init localMessage
199+
this.updateLocalMessage(this.message)
200+
},
201+
202+
methods: {
203+
/**
204+
* Update local Message on outer change
205+
* @param {string} message the message to set
206+
*/
207+
updateLocalMessage(message) {
208+
this.localMessage = message.toString()
209+
},
210+
211+
/**
212+
* Dispatch message between edit and create
213+
*/
214+
onSubmit() {
215+
if (this.editor) {
216+
this.onNewComment(this.localMessage)
217+
return
218+
}
219+
this.onEditComment(this.localMessage)
220+
},
61221
},
62222
63223
}
64224
</script>
65225

66226
<style lang="scss" scoped>
227+
$comment-padding: 10px;
228+
229+
.comment {
230+
position: relative;
231+
padding: $comment-padding 0 $comment-padding * 1.5;
232+
233+
&__header {
234+
display: flex;
235+
align-items: center;
236+
min-height: 44px;
237+
padding: $comment-padding / 2 0;
238+
}
239+
240+
&__author,
241+
&__actions {
242+
margin-left: $comment-padding !important;
243+
}
244+
245+
&__author {
246+
overflow: hidden;
247+
white-space: nowrap;
248+
text-overflow: ellipsis;
249+
color: var(--color-text-maxcontrast);
250+
}
251+
252+
&_loading,
253+
&__timestamp {
254+
margin-left: auto;
255+
color: var(--color-text-maxcontrast);
256+
}
257+
258+
&__message {
259+
position: relative;
260+
// Avatar size, align with author name
261+
padding-left: 32px + $comment-padding;
262+
}
263+
264+
&__submit {
265+
position: absolute;
266+
right: 0;
267+
bottom: 0;
268+
width: 44px;
269+
height: 44px;
270+
// Align with input border
271+
margin: 1px;
272+
cursor: pointer;
273+
opacity: .7;
274+
border: none;
275+
background-color: transparent !important;
276+
277+
&:disabled {
278+
cursor: not-allowed;
279+
opacity: .5;
280+
}
281+
282+
&:focus,
283+
&:hover {
284+
opacity: 1;
285+
}
286+
}
287+
}
288+
289+
.rich-contenteditable__input {
290+
margin: 0;
291+
padding: $comment-padding;
292+
}
67293
68294
</style>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!-- TODO: Move to vue components -->
2+
3+
<template>
4+
<span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span>
5+
</template>
6+
7+
<script>
8+
import moment from '@nextcloud/moment'
9+
10+
export default {
11+
name: 'Moment',
12+
props: {
13+
timestamp: {
14+
type: Number,
15+
required: true,
16+
},
17+
format: {
18+
type: String,
19+
default: 'LLL',
20+
},
21+
},
22+
computed: {
23+
title() {
24+
return moment.unix(this.timestamp).format(this.format)
25+
},
26+
formatted() {
27+
return moment.unix(this.timestamp).fromNow()
28+
},
29+
},
30+
}
31+
</script>

0 commit comments

Comments
 (0)