Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. <!--
  2. - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. - @author Richard Steinmetz <richard@steinmetz.cloud>
  6. -
  7. - @license GNU AGPL version 3 or any later version
  8. -
  9. - This program is free software: you can redistribute it and/or modify
  10. - it under the terms of the GNU Affero General Public License as
  11. - published by the Free Software Foundation, either version 3 of the
  12. - License, or (at your option) any later version.
  13. -
  14. - This program is distributed in the hope that it will be useful,
  15. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. - GNU Affero General Public License for more details.
  18. -
  19. - You should have received a copy of the GNU Affero General Public License
  20. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. -
  22. -->
  23. <template>
  24. <div v-element-visibility="onVisibilityChange"
  25. class="comments"
  26. :class="{ 'icon-loading': isFirstLoading }">
  27. <!-- Editor -->
  28. <Comment v-bind="editorData"
  29. :auto-complete="autoComplete"
  30. :resource-type="resourceType"
  31. :editor="true"
  32. :user-data="userData"
  33. :resource-id="currentResourceId"
  34. class="comments__writer"
  35. @new="onNewComment" />
  36. <template v-if="!isFirstLoading">
  37. <NcEmptyContent v-if="!hasComments && done"
  38. class="comments__empty"
  39. :name="t('comments', 'No comments yet, start the conversation!')">
  40. <template #icon>
  41. <MessageReplyTextIcon />
  42. </template>
  43. </NcEmptyContent>
  44. <ul v-else>
  45. <!-- Comments -->
  46. <Comment v-for="comment in comments"
  47. :key="comment.props.id"
  48. tag="li"
  49. v-bind="comment.props"
  50. :auto-complete="autoComplete"
  51. :resource-type="resourceType"
  52. :message.sync="comment.props.message"
  53. :resource-id="currentResourceId"
  54. :user-data="genMentionsData(comment.props.mentions)"
  55. class="comments__list"
  56. @delete="onDelete" />
  57. </ul>
  58. <!-- Loading more message -->
  59. <div v-if="loading && !isFirstLoading" class="comments__info icon-loading" />
  60. <div v-else-if="hasComments && done" class="comments__info">
  61. {{ t('comments', 'No more messages') }}
  62. </div>
  63. <!-- Error message -->
  64. <template v-else-if="error">
  65. <NcEmptyContent class="comments__error" :name="error">
  66. <template #icon>
  67. <AlertCircleOutlineIcon />
  68. </template>
  69. </NcEmptyContent>
  70. <NcButton class="comments__retry" @click="getComments">
  71. <template #icon>
  72. <RefreshIcon />
  73. </template>
  74. {{ t('comments', 'Retry') }}
  75. </NcButton>
  76. </template>
  77. </template>
  78. </div>
  79. </template>
  80. <script>
  81. import { showError } from '@nextcloud/dialogs'
  82. import { translate as t } from '@nextcloud/l10n'
  83. import { vElementVisibility as elementVisibility } from '@vueuse/components'
  84. import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
  85. import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
  86. import RefreshIcon from 'vue-material-design-icons/Refresh.vue'
  87. import MessageReplyTextIcon from 'vue-material-design-icons/MessageReplyText.vue'
  88. import AlertCircleOutlineIcon from 'vue-material-design-icons/AlertCircleOutline.vue'
  89. import Comment from '../components/Comment.vue'
  90. import CommentView from '../mixins/CommentView'
  91. import cancelableRequest from '../utils/cancelableRequest.js'
  92. import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts'
  93. import { markCommentsAsRead } from '../services/ReadComments.ts'
  94. export default {
  95. name: 'Comments',
  96. components: {
  97. Comment,
  98. NcEmptyContent,
  99. NcButton,
  100. RefreshIcon,
  101. MessageReplyTextIcon,
  102. AlertCircleOutlineIcon,
  103. },
  104. directives: {
  105. elementVisibility,
  106. },
  107. mixins: [CommentView],
  108. data() {
  109. return {
  110. error: '',
  111. loading: false,
  112. done: false,
  113. currentResourceId: this.resourceId,
  114. offset: 0,
  115. comments: [],
  116. cancelRequest: () => {},
  117. Comment,
  118. userData: {},
  119. }
  120. },
  121. computed: {
  122. hasComments() {
  123. return this.comments.length > 0
  124. },
  125. isFirstLoading() {
  126. return this.loading && this.offset === 0
  127. },
  128. },
  129. watch: {
  130. resourceId() {
  131. this.currentResourceId = this.resourceId
  132. },
  133. },
  134. methods: {
  135. t,
  136. async onVisibilityChange(isVisible) {
  137. if (isVisible) {
  138. try {
  139. await markCommentsAsRead(this.resourceType, this.currentResourceId, new Date())
  140. } catch (e) {
  141. showError(e.message || t('comments', 'Failed to mark comments as read'))
  142. }
  143. }
  144. },
  145. /**
  146. * Update current resourceId and fetch new data
  147. *
  148. * @param {number} resourceId the current resourceId (fileId...)
  149. */
  150. async update(resourceId) {
  151. this.currentResourceId = resourceId
  152. this.resetState()
  153. this.getComments()
  154. },
  155. /**
  156. * Ran when the bottom of the tab is reached
  157. */
  158. onScrollBottomReached() {
  159. /**
  160. * Do not fetch more if we:
  161. * - are showing an error
  162. * - already fetched everything
  163. * - are currently loading
  164. */
  165. if (this.error || this.done || this.loading) {
  166. return
  167. }
  168. this.getComments()
  169. },
  170. /**
  171. * Get the existing shares infos
  172. */
  173. async getComments() {
  174. // Cancel any ongoing request
  175. this.cancelRequest('cancel')
  176. try {
  177. this.loading = true
  178. this.error = ''
  179. // Init cancellable request
  180. const { request, abort } = cancelableRequest(getComments)
  181. this.cancelRequest = abort
  182. // Fetch comments
  183. const { data: comments } = await request({
  184. resourceType: this.resourceType,
  185. resourceId: this.currentResourceId,
  186. }, { offset: this.offset }) || { data: [] }
  187. this.logger.debug(`Processed ${comments.length} comments`, { comments })
  188. // We received less than the requested amount,
  189. // we're done fetching comments
  190. if (comments.length < DEFAULT_LIMIT) {
  191. this.done = true
  192. }
  193. // Insert results
  194. this.comments.push(...comments)
  195. // Increase offset for next fetch
  196. this.offset += DEFAULT_LIMIT
  197. } catch (error) {
  198. if (error.message === 'cancel') {
  199. return
  200. }
  201. this.error = t('comments', 'Unable to load the comments list')
  202. console.error('Error loading the comments list', error)
  203. } finally {
  204. this.loading = false
  205. }
  206. },
  207. /**
  208. * Add newly created comment to the list
  209. *
  210. * @param {object} comment the new comment
  211. */
  212. onNewComment(comment) {
  213. this.comments.unshift(comment)
  214. },
  215. /**
  216. * Remove deleted comment from the list
  217. *
  218. * @param {number} id the deleted comment
  219. */
  220. onDelete(id) {
  221. const index = this.comments.findIndex(comment => comment.props.id === id)
  222. if (index > -1) {
  223. this.comments.splice(index, 1)
  224. } else {
  225. console.error('Could not find the deleted comment in the list', id)
  226. }
  227. },
  228. /**
  229. * Reset the current view to its default state
  230. */
  231. resetState() {
  232. this.error = ''
  233. this.loading = false
  234. this.done = false
  235. this.offset = 0
  236. this.comments = []
  237. },
  238. },
  239. }
  240. </script>
  241. <style lang="scss" scoped>
  242. .comments {
  243. min-height: 100%;
  244. display: flex;
  245. flex-direction: column;
  246. &__empty,
  247. &__error {
  248. flex: 1 0;
  249. }
  250. &__retry {
  251. margin: 0 auto;
  252. }
  253. &__info {
  254. height: 60px;
  255. color: var(--color-text-maxcontrast);
  256. text-align: center;
  257. line-height: 60px;
  258. }
  259. }
  260. </style>