You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Comment.vue 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <!--
  2. - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <div v-show="!deleted"
  24. :class="{'comment--loading': loading}"
  25. class="comment">
  26. <!-- Comment header toolbar -->
  27. <div class="comment__header">
  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. <!-- Comment actions,
  35. show if we have a message id and current user is author -->
  36. <Actions v-if="isOwnComment && id && !loading" class="comment__actions">
  37. <template v-if="!editing">
  38. <ActionButton
  39. :close-after-click="true"
  40. icon="icon-rename"
  41. @click="onEdit">
  42. {{ t('comments', 'Edit comment') }}
  43. </ActionButton>
  44. <ActionSeparator />
  45. <ActionButton
  46. :close-after-click="true"
  47. icon="icon-delete"
  48. @click="onDeleteWithUndo">
  49. {{ t('comments', 'Delete comment') }}
  50. </ActionButton>
  51. </template>
  52. <ActionButton v-else
  53. icon="icon-close"
  54. @click="onEditCancel">
  55. {{ t('comments', 'Cancel edit') }}
  56. </ActionButton>
  57. </Actions>
  58. <!-- Show loading if we're editing or deleting, not on new ones -->
  59. <div v-if="id && loading" class="comment_loading icon-loading-small" />
  60. <!-- Relative time to the comment creation -->
  61. <Moment v-else-if="creationDateTime" class="comment__timestamp" :timestamp="timestamp" />
  62. </div>
  63. <!-- Message editor -->
  64. <div class="comment__message" v-if="editor || editing">
  65. <RichContenteditable v-model="localMessage"
  66. :auto-complete="autoComplete"
  67. :contenteditable="!loading"
  68. @submit="onSubmit" />
  69. <input v-tooltip="t('comments', 'Post comment')"
  70. :class="loading ? 'icon-loading-small' :'icon-confirm'"
  71. class="comment__submit"
  72. type="submit"
  73. :disabled="isEmptyMessage"
  74. value=""
  75. @click="onSubmit">
  76. </div>
  77. <!-- Message content -->
  78. <!-- The html is escaped and sanitized before rendering -->
  79. <!-- eslint-disable-next-line vue/no-v-html-->
  80. <div v-else class="comment__message" v-html="renderedContent" />
  81. </div>
  82. </template>
  83. <script>
  84. import { getCurrentUser } from '@nextcloud/auth'
  85. import moment from 'moment'
  86. import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
  87. import Actions from '@nextcloud/vue/dist/Components/Actions'
  88. import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
  89. import Avatar from '@nextcloud/vue/dist/Components/Avatar'
  90. import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
  91. import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor'
  92. import Moment from './Moment'
  93. import CommentMixin from '../mixins/CommentMixin'
  94. export default {
  95. name: 'Comment',
  96. components: {
  97. ActionButton,
  98. Actions,
  99. ActionSeparator,
  100. Avatar,
  101. Moment,
  102. RichContenteditable,
  103. },
  104. mixins: [RichEditorMixin, CommentMixin],
  105. inheritAttrs: false,
  106. props: {
  107. source: {
  108. type: Object,
  109. default: () => ({}),
  110. },
  111. actorDisplayName: {
  112. type: String,
  113. required: true,
  114. },
  115. actorId: {
  116. type: String,
  117. required: true,
  118. },
  119. creationDateTime: {
  120. type: String,
  121. default: null,
  122. },
  123. /**
  124. * Force the editor display
  125. */
  126. editor: {
  127. type: Boolean,
  128. default: false,
  129. },
  130. /**
  131. * Provide the autocompletion data
  132. */
  133. autoComplete: {
  134. type: Function,
  135. required: true,
  136. },
  137. },
  138. data() {
  139. return {
  140. // Only change data locally and update the original
  141. // parent data when the request is sent and resolved
  142. localMessage: '',
  143. }
  144. },
  145. computed: {
  146. /**
  147. * Is the current user the author of this comment
  148. * @returns {boolean}
  149. */
  150. isOwnComment() {
  151. return getCurrentUser().uid === this.actorId
  152. },
  153. /**
  154. * Rendered content as html string
  155. * @returns {string}
  156. */
  157. renderedContent() {
  158. if (this.isEmptyMessage) {
  159. return ''
  160. }
  161. return this.renderContent(this.localMessage)
  162. },
  163. isEmptyMessage() {
  164. return !this.localMessage || this.localMessage.trim() === ''
  165. },
  166. timestamp() {
  167. // seconds, not milliseconds
  168. return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000
  169. },
  170. },
  171. watch: {
  172. // If the data change, update the local value
  173. message(message) {
  174. this.updateLocalMessage(message)
  175. },
  176. },
  177. beforeMount() {
  178. // Init localMessage
  179. this.updateLocalMessage(this.message)
  180. },
  181. methods: {
  182. /**
  183. * Update local Message on outer change
  184. * @param {string} message the message to set
  185. */
  186. updateLocalMessage(message) {
  187. this.localMessage = message.toString()
  188. },
  189. /**
  190. * Dispatch message between edit and create
  191. */
  192. onSubmit() {
  193. if (this.editor) {
  194. this.onNewComment(this.localMessage)
  195. return
  196. }
  197. this.onEditComment(this.localMessage)
  198. },
  199. },
  200. }
  201. </script>
  202. <style lang="scss" scoped>
  203. $comment-padding: 10px;
  204. .comment {
  205. position: relative;
  206. padding: $comment-padding 0 $comment-padding * 1.5;
  207. &__header {
  208. display: flex;
  209. align-items: center;
  210. min-height: 44px;
  211. padding: $comment-padding / 2 0;
  212. }
  213. &__author,
  214. &__actions {
  215. margin-left: $comment-padding !important;
  216. }
  217. &__author {
  218. overflow: hidden;
  219. white-space: nowrap;
  220. text-overflow: ellipsis;
  221. color: var(--color-text-maxcontrast);
  222. }
  223. &_loading,
  224. &__timestamp {
  225. margin-left: auto;
  226. color: var(--color-text-maxcontrast);
  227. }
  228. &__message {
  229. position: relative;
  230. // Avatar size, align with author name
  231. padding-left: 32px + $comment-padding;
  232. }
  233. &__submit {
  234. position: absolute;
  235. right: 0;
  236. bottom: 0;
  237. width: 44px;
  238. height: 44px;
  239. // Align with input border
  240. margin: 1px;
  241. cursor: pointer;
  242. opacity: .7;
  243. border: none;
  244. background-color: transparent !important;
  245. &:disabled {
  246. cursor: not-allowed;
  247. opacity: .5;
  248. }
  249. &:focus,
  250. &:hover {
  251. opacity: 1;
  252. }
  253. }
  254. }
  255. .rich-contenteditable__input {
  256. margin: 0;
  257. padding: $comment-padding;
  258. min-height: 44px;
  259. }
  260. </style>