aboutsummaryrefslogtreecommitdiffstats
path: root/apps/comments/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/comments/src')
-rw-r--r--apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts179
-rw-r--r--apps/comments/src/actions/inlineUnreadCommentsAction.ts46
-rw-r--r--apps/comments/src/activitytabviewplugin.js73
-rw-r--r--apps/comments/src/app.js32
-rw-r--r--apps/comments/src/comments-activity-tab.ts78
-rw-r--r--apps/comments/src/comments-app.js23
-rw-r--r--apps/comments/src/comments-tab.js105
-rw-r--r--apps/comments/src/comments.js29
-rw-r--r--apps/comments/src/components/Comment.vue240
-rw-r--r--apps/comments/src/components/Moment.vue31
-rw-r--r--apps/comments/src/filesplugin.js141
-rw-r--r--apps/comments/src/init.ts8
-rw-r--r--apps/comments/src/logger.js11
-rw-r--r--apps/comments/src/mixins/CommentMixin.js56
-rw-r--r--apps/comments/src/mixins/CommentView.ts76
-rw-r--r--apps/comments/src/services/CommentsInstance.js58
-rw-r--r--apps/comments/src/services/DavClient.js49
-rw-r--r--apps/comments/src/services/DeleteComment.js31
-rw-r--r--apps/comments/src/services/EditComment.js31
-rw-r--r--apps/comments/src/services/GetComments.js92
-rw-r--r--apps/comments/src/services/GetComments.ts67
-rw-r--r--apps/comments/src/services/NewComment.js43
-rw-r--r--apps/comments/src/services/ReadComments.ts38
-rw-r--r--apps/comments/src/store/deletedCommentLimbo.js28
-rw-r--r--apps/comments/src/templates.js17
-rw-r--r--apps/comments/src/templates/filesplugin.handlebars3
-rw-r--r--apps/comments/src/utils/cancelableRequest.js43
-rw-r--r--apps/comments/src/utils/davUtils.js21
-rw-r--r--apps/comments/src/utils/decodeHtmlEntities.js21
-rw-r--r--apps/comments/src/utils/numberUtil.js30
-rw-r--r--apps/comments/src/views/ActivityCommentAction.vue54
-rw-r--r--apps/comments/src/views/ActivityCommentEntry.vue71
-rw-r--r--apps/comments/src/views/Comments.vue205
33 files changed, 1046 insertions, 984 deletions
diff --git a/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts b/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts
new file mode 100644
index 00000000000..e8020f1f029
--- /dev/null
+++ b/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts
@@ -0,0 +1,179 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { File, Permission, View, FileAction } from '@nextcloud/files'
+import { describe, expect, test, vi } from 'vitest'
+
+import { action } from './inlineUnreadCommentsAction'
+import logger from '../logger'
+
+const view = {
+ id: 'files',
+ name: 'Files',
+} as View
+
+describe('Inline unread comments action display name tests', () => {
+ test('Default values', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 1,
+ },
+ })
+
+ expect(action).toBeInstanceOf(FileAction)
+ expect(action.id).toBe('comments-unread')
+ expect(action.displayName([file], view)).toBe('')
+ expect(action.title!([file], view)).toBe('1 new comment')
+ expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
+ expect(action.enabled!([file], view)).toBe(true)
+ expect(action.inline!(file, view)).toBe(true)
+ expect(action.default).toBeUndefined()
+ expect(action.order).toBe(-140)
+ })
+
+ test('Display name when file has two new comments', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 2,
+ },
+ })
+
+ expect(action.displayName([file], view)).toBe('')
+ expect(action.title!([file], view)).toBe('2 new comments')
+ })
+})
+
+describe('Inline unread comments action enabled tests', () => {
+ test('Action is disabled when comments-unread attribute is missing', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: { },
+ })
+
+ expect(action.enabled!([file], view)).toBe(false)
+ })
+
+ test('Action is disabled when file does not have unread comments', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 0,
+ },
+ })
+
+ expect(action.enabled!([file], view)).toBe(false)
+ })
+
+ test('Action is enabled when file has a single unread comment', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 1,
+ },
+ })
+
+ expect(action.enabled!([file], view)).toBe(true)
+ })
+
+ test('Action is enabled when file has a two unread comments', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 2,
+ },
+ })
+
+ expect(action.enabled!([file], view)).toBe(true)
+ })
+})
+
+describe('Inline unread comments action execute tests', () => {
+ test('Action opens sidebar', async () => {
+ const openMock = vi.fn()
+ const setActiveTabMock = vi.fn()
+ window.OCA = {
+ Files: {
+ Sidebar: {
+ open: openMock,
+ setActiveTab: setActiveTabMock,
+ },
+ },
+ }
+
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 1,
+ },
+ })
+
+ const result = await action.exec!(file, view, '/')
+
+ expect(result).toBe(null)
+ expect(setActiveTabMock).toBeCalledWith('comments')
+ expect(openMock).toBeCalledWith('/foobar.txt')
+ })
+
+ test('Action handles sidebar open failure', async () => {
+ const openMock = vi.fn(() => { throw new Error('Mock error') })
+ const setActiveTabMock = vi.fn()
+ window.OCA = {
+ Files: {
+ Sidebar: {
+ open: openMock,
+ setActiveTab: setActiveTabMock,
+ },
+ },
+ }
+ vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
+
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ attributes: {
+ 'comments-unread': 1,
+ },
+ })
+
+ const result = await action.exec!(file, view, '/')
+
+ expect(result).toBe(false)
+ expect(setActiveTabMock).toBeCalledWith('comments')
+ expect(openMock).toBeCalledWith('/foobar.txt')
+ expect(logger.error).toBeCalledTimes(1)
+ })
+})
diff --git a/apps/comments/src/actions/inlineUnreadCommentsAction.ts b/apps/comments/src/actions/inlineUnreadCommentsAction.ts
new file mode 100644
index 00000000000..0afd93d7606
--- /dev/null
+++ b/apps/comments/src/actions/inlineUnreadCommentsAction.ts
@@ -0,0 +1,46 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { FileAction, Node } from '@nextcloud/files'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw'
+
+import logger from '../logger'
+
+export const action = new FileAction({
+ id: 'comments-unread',
+
+ title(nodes: Node[]) {
+ const unread = nodes[0].attributes['comments-unread'] as number
+ if (unread >= 0) {
+ return n('comments', '1 new comment', '{unread} new comments', unread, { unread })
+ }
+ return t('comments', 'Comment')
+ },
+
+ // Empty string when rendered inline
+ displayName: () => '',
+
+ iconSvgInline: () => CommentProcessingSvg,
+
+ enabled(nodes: Node[]) {
+ const unread = nodes[0].attributes['comments-unread'] as number|undefined
+ return typeof unread === 'number' && unread > 0
+ },
+
+ async exec(node: Node) {
+ try {
+ window.OCA.Files.Sidebar.setActiveTab('comments')
+ await window.OCA.Files.Sidebar.open(node.path)
+ return null
+ } catch (error) {
+ logger.error('Error while opening sidebar', { error })
+ return false
+ }
+ },
+
+ inline: () => true,
+
+ order: -140,
+})
diff --git a/apps/comments/src/activitytabviewplugin.js b/apps/comments/src/activitytabviewplugin.js
deleted file mode 100644
index 6b6e7ccda90..00000000000
--- a/apps/comments/src/activitytabviewplugin.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright (c) 2016
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-/** @typedef {import('jquery')} jQuery */
-
-(function() {
- OCA.Comments.ActivityTabViewPlugin = {
-
- /**
- * Prepare activity for display
- *
- * @param {OCA.Activity.ActivityModel} model for this activity
- * @param {jQuery} $el jQuery handle for this activity
- * @param {string} view The view that displays this activity
- */
- prepareModelForDisplay(model, $el, view) {
- if (model.get('app') !== 'comments' || model.get('type') !== 'comments') {
- return
- }
-
- if (view === 'ActivityTabView') {
- $el.addClass('comment')
- if (model.get('message') && this._isLong(model.get('message'))) {
- $el.addClass('collapsed')
- const $overlay = $('<div>').addClass('message-overlay')
- $el.find('.activitymessage').after($overlay)
- $el.on('click', this._onClickCollapsedComment)
- }
- }
- },
-
- /*
- * Copy of CommentsTabView._onClickComment()
- */
- _onClickCollapsedComment(ev) {
- let $row = $(ev.target)
- if (!$row.is('.comment')) {
- $row = $row.closest('.comment')
- }
- $row.removeClass('collapsed')
- },
-
- /*
- * Copy of CommentsTabView._isLong()
- */
- _isLong(message) {
- return message.length > 250 || (message.match(/\n/g) || []).length > 1
- },
- }
-
-})()
-
-OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin)
diff --git a/apps/comments/src/app.js b/apps/comments/src/app.js
deleted file mode 100644
index 7b5229cbd44..00000000000
--- a/apps/comments/src/app.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-(function() {
- if (!OCA.Comments) {
- /**
- * @namespace
- */
- OCA.Comments = {}
- }
-
-})()
diff --git a/apps/comments/src/comments-activity-tab.ts b/apps/comments/src/comments-activity-tab.ts
new file mode 100644
index 00000000000..77f6c9bca04
--- /dev/null
+++ b/apps/comments/src/comments-activity-tab.ts
@@ -0,0 +1,78 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import moment from '@nextcloud/moment'
+import Vue, { type ComponentPublicInstance } from 'vue'
+import logger from './logger.js'
+import { getComments } from './services/GetComments.js'
+
+import { PiniaVuePlugin, createPinia } from 'pinia'
+
+Vue.use(PiniaVuePlugin)
+
+let ActivityTabPluginView
+let ActivityTabPluginInstance
+
+/**
+ * Register the comments plugins for the Activity sidebar
+ */
+export function registerCommentsPlugins() {
+ window.OCA.Activity.registerSidebarAction({
+ mount: async (el, { fileInfo, reload }) => {
+ const pinia = createPinia()
+
+ if (!ActivityTabPluginView) {
+ const { default: ActivityCommentAction } = await import('./views/ActivityCommentAction.vue')
+ // @ts-expect-error Types are broken for Vue2
+ ActivityTabPluginView = Vue.extend(ActivityCommentAction)
+ }
+ ActivityTabPluginInstance = new ActivityTabPluginView({
+ el,
+ pinia,
+ propsData: {
+ reloadCallback: reload,
+ resourceId: fileInfo.id,
+ },
+ })
+ logger.info('Comments plugin mounted in Activity sidebar action', { fileInfo })
+ },
+ unmount: () => {
+ // destroy previous instance if available
+ if (ActivityTabPluginInstance) {
+ ActivityTabPluginInstance.$destroy()
+ }
+ },
+ })
+
+ window.OCA.Activity.registerSidebarEntries(async ({ fileInfo, limit, offset }) => {
+ const { data: comments } = await getComments({ resourceType: 'files', resourceId: fileInfo.id }, { limit, offset })
+ logger.debug('Loaded comments', { fileInfo, comments })
+ const { default: CommentView } = await import('./views/ActivityCommentEntry.vue')
+ // @ts-expect-error Types are broken for Vue2
+ const CommentsViewObject = Vue.extend(CommentView)
+
+ return comments.map((comment) => ({
+ _CommentsViewInstance: undefined as ComponentPublicInstance | undefined,
+
+ timestamp: moment(comment.props?.creationDateTime).toDate().getTime(),
+
+ mount(element: HTMLElement, { reload }) {
+ this._CommentsViewInstance = new CommentsViewObject({
+ el: element,
+ propsData: {
+ comment,
+ resourceId: fileInfo.id,
+ reloadCallback: reload,
+ },
+ })
+ },
+ unmount() {
+ this._CommentsViewInstance?.$destroy()
+ },
+ }))
+ })
+
+ window.OCA.Activity.registerSidebarFilter((activity) => activity.type !== 'comments')
+ logger.info('Comments plugin registered for Activity sidebar action')
+}
diff --git a/apps/comments/src/comments-app.js b/apps/comments/src/comments-app.js
index c42481029ce..a91a4bb37bb 100644
--- a/apps/comments/src/comments-app.js
+++ b/apps/comments/src/comments-app.js
@@ -1,26 +1,9 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import CommentsInstance from './services/CommentsInstance'
+import CommentsInstance from './services/CommentsInstance.js'
// Init Comments
if (window.OCA && !window.OCA.Comments) {
diff --git a/apps/comments/src/comments-tab.js b/apps/comments/src/comments-tab.js
index 78bfb610af7..d3ebe3e9596 100644
--- a/apps/comments/src/comments-tab.js
+++ b/apps/comments/src/comments-tab.js
@@ -1,61 +1,60 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-// eslint-disable-next-line node/no-missing-import, import/no-unresolved
+// eslint-disable-next-line n/no-missing-import, import/no-unresolved
import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw'
+import { getCSPNonce } from '@nextcloud/auth'
+import { loadState } from '@nextcloud/initial-state'
+import { registerCommentsPlugins } from './comments-activity-tab.ts'
-// Init Comments tab component
-let TabInstance = null
-const commentTab = new OCA.Files.Sidebar.Tab({
- id: 'comments',
- name: t('comments', 'Comments'),
- iconSvg: MessageReplyText,
+// @ts-expect-error __webpack_nonce__ is injected by webpack
+__webpack_nonce__ = getCSPNonce()
- async mount(el, fileInfo, context) {
- if (TabInstance) {
+if (loadState('comments', 'activityEnabled', false) && OCA?.Activity?.registerSidebarAction !== undefined) {
+ // Do not mount own tab but mount into activity
+ window.addEventListener('DOMContentLoaded', function() {
+ registerCommentsPlugins()
+ })
+} else {
+ // Init Comments tab component
+ let TabInstance = null
+ const commentTab = new OCA.Files.Sidebar.Tab({
+ id: 'comments',
+ name: t('comments', 'Comments'),
+ iconSvg: MessageReplyText,
+
+ async mount(el, fileInfo, context) {
+ if (TabInstance) {
+ TabInstance.$destroy()
+ }
+ TabInstance = new OCA.Comments.View('files', {
+ // Better integration with vue parent component
+ parent: context,
+ propsData: {
+ resourceId: fileInfo.id,
+ },
+ })
+ // Only mount after we have all the info we need
+ await TabInstance.update(fileInfo.id)
+ TabInstance.$mount(el)
+ },
+ update(fileInfo) {
+ TabInstance.update(fileInfo.id)
+ },
+ destroy() {
TabInstance.$destroy()
- }
- TabInstance = new OCA.Comments.View('files', {
- // Better integration with vue parent component
- parent: context,
- })
- // Only mount after we have all the info we need
- await TabInstance.update(fileInfo.id)
- TabInstance.$mount(el)
- },
- update(fileInfo) {
- TabInstance.update(fileInfo.id)
- },
- destroy() {
- TabInstance.$destroy()
- TabInstance = null
- },
- scrollBottomReached() {
- TabInstance.onScrollBottomReached()
- },
-})
+ TabInstance = null
+ },
+ scrollBottomReached() {
+ TabInstance.onScrollBottomReached()
+ },
+ })
-window.addEventListener('DOMContentLoaded', function() {
- if (OCA.Files && OCA.Files.Sidebar) {
- OCA.Files.Sidebar.registerTab(commentTab)
- }
-})
+ window.addEventListener('DOMContentLoaded', function() {
+ if (OCA.Files && OCA.Files.Sidebar) {
+ OCA.Files.Sidebar.registerTab(commentTab)
+ }
+ })
+}
diff --git a/apps/comments/src/comments.js b/apps/comments/src/comments.js
deleted file mode 100644
index 95e6d3d5dc7..00000000000
--- a/apps/comments/src/comments.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-import './app'
-import './templates'
-import './filesplugin'
-import './activitytabviewplugin'
-
-window.OCA.Comments = OCA.Comments
diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue
index df9a22a1709..80f035530fb 100644
--- a/apps/comments/src/components/Comment.vue
+++ b/apps/comments/src/components/Comment.vue
@@ -1,26 +1,10 @@
<!--
- - @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/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div v-show="!deleted"
+ <component :is="tag"
+ v-show="!deleted && !isLimbo"
:class="{'comment--loading': loading}"
class="comment">
<!-- Comment header toolbar -->
@@ -39,22 +23,27 @@
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="true"
- icon="icon-rename"
+ <NcActionButton close-after-click
@click="onEdit">
+ <template #icon>
+ <IconPencilOutline :size="20" />
+ </template>
{{ t('comments', 'Edit comment') }}
</NcActionButton>
<NcActionSeparator />
- <NcActionButton :close-after-click="true"
- icon="icon-delete"
+ <NcActionButton close-after-click
@click="onDeleteWithUndo">
+ <template #icon>
+ <IconTrashCanOutline :size="20" />
+ </template>
{{ t('comments', 'Delete comment') }}
</NcActionButton>
</template>
- <NcActionButton v-else
- icon="icon-close"
- @click="onEditCancel">
+ <NcActionButton v-else @click="onEditCancel">
+ <template #icon>
+ <IconClose :size="20" />
+ </template>
{{ t('comments', 'Cancel edit') }}
</NcActionButton>
</NcActions>
@@ -63,73 +52,99 @@
<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" />
+ <NcDateTime v-else-if="creationDateTime"
+ class="comment__timestamp"
+ :timestamp="timestamp"
+ :ignore-seconds="true" />
</div>
<!-- Message editor -->
- <div v-if="editor || editing" class="comment__editor ">
- <NcRichContenteditable ref="editor"
- :auto-complete="autoComplete"
- :contenteditable="!loading"
- :value="localMessage"
- :user-data="userData"
- @update:value="updateLocalMessage"
- @submit="onSubmit" />
- <NcButton class="comment__submit"
- type="tertiary-no-background"
- native-type="submit"
- :aria-label="t('comments', 'Post comment')"
- :disabled="isEmptyMessage"
- @click="onSubmit">
- <template #icon>
- <span v-if="loading" class="icon-loading-small" />
- <ArrowRight v-else :size="20" />
- </template>
- </NcButton>
- </div>
+ <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 …')"
+ :value="localMessage"
+ :user-data="userData"
+ aria-describedby="tab-comments__editor-description"
+ @update:value="updateLocalMessage"
+ @submit="onSubmit" />
+ <div class="comment__submit">
+ <NcButton type="tertiary-no-background"
+ native-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 -->
- <!-- The html is escaped and sanitized before rendering -->
- <!-- eslint-disable-next-line vue/no-v-html-->
- <div v-else
- :class="{'comment__message--expanded': expanded}"
+ <NcRichText v-else
class="comment__message"
- @click="onExpand"
- v-html="renderedContent" />
+ :class="{'comment__message--expanded': expanded}"
+ :text="richContent.message"
+ :arguments="richContent.mentions"
+ @click="onExpand" />
</div>
- </div>
+ </component>
</template>
<script>
import { getCurrentUser } from '@nextcloud/auth'
-import moment from '@nextcloud/moment'
-
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions'
-import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton'
-import NcRichContenteditable from '@nextcloud/vue/dist/Components/NcRichContenteditable'
-import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor'
-import ArrowRight from 'vue-material-design-icons/ArrowRight'
-
-import Moment from './Moment'
-import CommentMixin from '../mixins/CommentMixin'
+import { translate as t } from '@nextcloud/l10n'
+
+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 IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
+import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
+
+import CommentMixin from '../mixins/CommentMixin.js'
+import { mapStores } from 'pinia'
+import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js'
+
+// Dynamic loading
+const NcRichContenteditable = () => import('@nextcloud/vue/components/NcRichContenteditable')
+const NcRichText = () => import('@nextcloud/vue/components/NcRichText')
export default {
name: 'Comment',
components: {
+ IconArrowRight,
+ IconClose,
+ IconTrashCanOutline,
+ IconPencilOutline,
NcActionButton,
NcActions,
NcActionSeparator,
- ArrowRight,
NcAvatar,
NcButton,
- Moment,
+ NcDateTime,
+ NcLoadingIcon,
NcRichContenteditable,
+ NcRichText,
},
- mixins: [RichEditorMixin, CommentMixin],
+ mixins: [CommentMixin],
inheritAttrs: false,
@@ -162,6 +177,15 @@ export default {
type: Function,
required: true,
},
+ userData: {
+ type: Object,
+ default: () => ({}),
+ },
+
+ tag: {
+ type: String,
+ default: 'div',
+ },
},
data() {
@@ -170,10 +194,12 @@ export default {
// 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
@@ -184,25 +210,40 @@ export default {
return getCurrentUser().uid === this.actorId
},
- /**
- * Rendered content as html string
- *
- * @return {string}
- */
- renderedContent() {
- if (this.isEmptyMessage) {
- return ''
- }
- return this.renderContent(this.localMessage)
+ 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() {
- // seconds, not milliseconds
- return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000
+ return Date.parse(this.creationDateTime)
+ },
+
+ isLimbo() {
+ return this.deletedCommentLimboStore.checkForId(this.id)
},
},
@@ -219,6 +260,8 @@ export default {
},
methods: {
+ t,
+
/**
* Update local Message on outer change
*
@@ -226,6 +269,7 @@ export default {
*/
updateLocalMessage(message) {
this.localMessage = message.toString()
+ this.submitted = false
},
/**
@@ -263,14 +307,13 @@ $comment-padding: 10px;
.comment {
display: flex;
- gap: 16px;
- position: relative;
+ gap: 8px;
padding: 5px $comment-padding;
&__side {
display: flex;
align-items: flex-start;
- padding-top: 16px;
+ padding-top: 6px;
}
&__body {
@@ -286,7 +329,7 @@ $comment-padding: 10px;
}
&__actions {
- margin-left: $comment-padding !important;
+ margin-inline-start: $comment-padding !important;
}
&__author {
@@ -298,23 +341,30 @@ $comment-padding: 10px;
&_loading,
&__timestamp {
- margin-left: auto;
- text-align: right;
+ 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;
- right: 0;
- bottom: 0;
- // Align with input border
- margin: 1px;
+ bottom: 5px;
+ inset-inline-end: 0;
}
&__message {
white-space: pre-wrap;
- word-break: break-word;
+ word-break: normal;
max-height: 70px;
overflow: hidden;
margin-top: -6px;
diff --git a/apps/comments/src/components/Moment.vue b/apps/comments/src/components/Moment.vue
deleted file mode 100644
index a91ed8b9ce6..00000000000
--- a/apps/comments/src/components/Moment.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<!-- TODO: Move to vue components -->
-
-<template>
- <span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span>
-</template>
-
-<script>
-import moment from '@nextcloud/moment'
-
-export default {
- name: 'Moment',
- props: {
- timestamp: {
- type: Number,
- required: true,
- },
- format: {
- type: String,
- default: 'LLL',
- },
- },
- computed: {
- title() {
- return moment.unix(this.timestamp).format(this.format)
- },
- formatted() {
- return moment.unix(this.timestamp).fromNow()
- },
- },
-}
-</script>
diff --git a/apps/comments/src/filesplugin.js b/apps/comments/src/filesplugin.js
deleted file mode 100644
index 45d7372dfc7..00000000000
--- a/apps/comments/src/filesplugin.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/**
- * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-(function() {
-
- _.extend(OC.Files.Client, {
- PROPERTY_COMMENTS_UNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}comments-unread',
- })
-
- OCA.Comments = _.extend({}, OCA.Comments)
- if (!OCA.Comments) {
- /**
- * @namespace
- */
- OCA.Comments = {}
- }
-
- /**
- * @namespace
- */
- OCA.Comments.FilesPlugin = {
- ignoreLists: [
- 'trashbin',
- 'files.public',
- ],
-
- _formatCommentCount(count) {
- return OCA.Comments.Templates.filesplugin({
- count,
- countMessage: n('comments', '%n unread comment', '%n unread comments', count),
- iconUrl: OC.imagePath('core', 'actions/comment'),
- })
- },
-
- attach(fileList) {
- const self = this
- if (this.ignoreLists.indexOf(fileList.id) >= 0) {
- return
- }
-
- const oldGetWebdavProperties = fileList._getWebdavProperties
- fileList._getWebdavProperties = function() {
- const props = oldGetWebdavProperties.apply(this, arguments)
- props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD)
- return props
- }
-
- fileList.filesClient.addFileInfoParser(function(response) {
- const data = {}
- const props = response.propStat[0].properties
- const commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD]
- if (!_.isUndefined(commentsUnread) && commentsUnread !== '') {
- data.commentsUnread = parseInt(commentsUnread, 10)
- }
- return data
- })
-
- fileList.$el.addClass('has-comments')
- const oldCreateRow = fileList._createRow
- fileList._createRow = function(fileData) {
- const $tr = oldCreateRow.apply(this, arguments)
- if (fileData.commentsUnread) {
- $tr.attr('data-comments-unread', fileData.commentsUnread)
- }
- return $tr
- }
-
- // register "comment" action for reading comments
- fileList.fileActions.registerAction({
- name: 'Comment',
- displayName(context) {
- if (context && context.$file) {
- const unread = parseInt(context.$file.data('comments-unread'), 10)
- if (unread >= 0) {
- return n('comments', '1 new comment', '{unread} new comments', unread, { unread })
- }
- }
- return t('comments', 'Comment')
- },
- mime: 'all',
- order: -140,
- iconClass: 'icon-comment',
- permissions: OC.PERMISSION_READ,
- type: OCA.Files.FileActions.TYPE_INLINE,
- render(actionSpec, isDefault, context) {
- const $file = context.$file
- const unreadComments = $file.data('comments-unread')
- if (unreadComments) {
- const $actionLink = $(self._formatCommentCount(unreadComments))
- context.$file.find('a.name>span.fileactions').append($actionLink)
- return $actionLink
- }
- return ''
- },
- actionHandler(fileName, context) {
- context.$file.find('.action-comment').tooltip('hide')
- // open sidebar in comments section
- OCA.Files.Sidebar.setActiveTab('comments')
- OCA.Files.Sidebar.open(context.dir + '/' + fileName)
- },
- })
-
- // add attribute to "elementToFile"
- const oldElementToFile = fileList.elementToFile
- fileList.elementToFile = function($el) {
- const fileInfo = oldElementToFile.apply(this, arguments)
- const commentsUnread = $el.data('comments-unread')
- if (commentsUnread) {
- fileInfo.commentsUnread = commentsUnread
- }
- return fileInfo
- }
- },
- }
-
-})()
-
-OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin)
diff --git a/apps/comments/src/init.ts b/apps/comments/src/init.ts
new file mode 100644
index 00000000000..675274b1b40
--- /dev/null
+++ b/apps/comments/src/init.ts
@@ -0,0 +1,8 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { registerFileAction } from '@nextcloud/files'
+import { action } from './actions/inlineUnreadCommentsAction'
+
+registerFileAction(action)
diff --git a/apps/comments/src/logger.js b/apps/comments/src/logger.js
new file mode 100644
index 00000000000..a51bc6d750b
--- /dev/null
+++ b/apps/comments/src/logger.js
@@ -0,0 +1,11 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+
+export default getLoggerBuilder()
+ .setApp('comments')
+ .detectUser()
+ .build()
diff --git a/apps/comments/src/mixins/CommentMixin.js b/apps/comments/src/mixins/CommentMixin.js
index e029a4b589a..722ad3444ce 100644
--- a/apps/comments/src/mixins/CommentMixin.js
+++ b/apps/comments/src/mixins/CommentMixin.js
@@ -1,29 +1,15 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import NewComment from '../services/NewComment'
-import DeleteComment from '../services/DeleteComment'
-import EditComment from '../services/EditComment'
import { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs'
+import NewComment from '../services/NewComment.js'
+import DeleteComment from '../services/DeleteComment.js'
+import EditComment from '../services/EditComment.js'
+import { mapStores } from 'pinia'
+import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js'
+import logger from '../logger.js'
export default {
props: {
@@ -35,10 +21,14 @@ export default {
type: String,
default: '',
},
- ressourceId: {
+ resourceId: {
type: [String, Number],
required: true,
},
+ resourceType: {
+ type: String,
+ default: 'files',
+ },
},
data() {
@@ -49,6 +39,10 @@ export default {
}
},
+ computed: {
+ ...mapStores(useDeletedCommentLimbo),
+ },
+
methods: {
// EDITION
onEdit() {
@@ -62,8 +56,8 @@ export default {
async onEditComment(message) {
this.loading = true
try {
- await EditComment(this.commentsType, this.ressourceId, this.id, message)
- this.logger.debug('Comment edited', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id, message })
+ await EditComment(this.resourceType, this.resourceId, this.id, message)
+ logger.debug('Comment edited', { resourceType: this.resourceType, resourceId: this.resourceId, id: this.id, message })
this.$emit('update:message', message)
this.editing = false
} catch (error) {
@@ -76,22 +70,26 @@ export default {
// DELETION
onDeleteWithUndo() {
+ this.$emit('delete')
this.deleted = true
+ this.deletedCommentLimboStore.addId(this.id)
const timeOutDelete = setTimeout(this.onDelete, TOAST_UNDO_TIMEOUT)
showUndo(t('comments', 'Comment deleted'), () => {
clearTimeout(timeOutDelete)
this.deleted = false
+ this.deletedCommentLimboStore.removeId(this.id)
})
},
async onDelete() {
try {
- await DeleteComment(this.commentsType, this.ressourceId, this.id)
- this.logger.debug('Comment deleted', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id })
+ await DeleteComment(this.resourceType, this.resourceId, this.id)
+ logger.debug('Comment deleted', { resourceType: this.resourceType, resourceId: this.resourceId, id: this.id })
this.$emit('delete', this.id)
} catch (error) {
showError(t('comments', 'An error occurred while trying to delete the comment'))
console.error(error)
this.deleted = false
+ this.deletedCommentLimboStore.removeId(this.id)
}
},
@@ -99,8 +97,8 @@ export default {
async onNewComment(message) {
this.loading = true
try {
- const newComment = await NewComment(this.commentsType, this.ressourceId, message)
- this.logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, newComment })
+ const newComment = await NewComment(this.resourceType, this.resourceId, message)
+ logger.debug('New comment posted', { resourceType: this.resourceType, resourceId: this.resourceId, newComment })
this.$emit('new', newComment)
// Clear old content
diff --git a/apps/comments/src/mixins/CommentView.ts b/apps/comments/src/mixins/CommentView.ts
new file mode 100644
index 00000000000..c6cb3aa9ee0
--- /dev/null
+++ b/apps/comments/src/mixins/CommentView.ts
@@ -0,0 +1,76 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import axios from '@nextcloud/axios'
+import { getCurrentUser } from '@nextcloud/auth'
+import { loadState } from '@nextcloud/initial-state'
+import { generateOcsUrl } from '@nextcloud/router'
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ props: {
+ resourceId: {
+ type: Number,
+ required: true,
+ },
+ resourceType: {
+ type: String,
+ default: 'files',
+ },
+ },
+ data() {
+ return {
+ editorData: {
+ actorDisplayName: getCurrentUser()!.displayName as string,
+ actorId: getCurrentUser()!.uid as string,
+ key: 'editor',
+ },
+ userData: {},
+ }
+ },
+ methods: {
+ /**
+ * Autocomplete @mentions
+ *
+ * @param {string} search the query
+ * @param {Function} callback the callback to process the results with
+ */
+ async autoComplete(search, callback) {
+ const { data } = await axios.get(generateOcsUrl('core/autocomplete/get'), {
+ params: {
+ search,
+ itemType: 'files',
+ itemId: this.resourceId,
+ sorter: 'commenters|share-recipients',
+ limit: loadState('comments', 'maxAutoCompleteResults'),
+ },
+ })
+ // Save user data so it can be used by the editor to replace mentions
+ data.ocs.data.forEach(user => { this.userData[user.id] = user })
+ return callback(Object.values(this.userData))
+ },
+
+ /**
+ * Make sure we have all mentions as Array of objects
+ *
+ * @param mentions the mentions list
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ genMentionsData(mentions: any[]): Record<string, object> {
+ Object.values(mentions)
+ .flat()
+ .forEach(mention => {
+ this.userData[mention.mentionId] = {
+ // TODO: support groups
+ icon: 'icon-user',
+ id: mention.mentionId,
+ label: mention.mentionDisplayName,
+ source: 'users',
+ primary: getCurrentUser()?.uid === mention.mentionId,
+ }
+ })
+ return this.userData
+ },
+ },
+})
diff --git a/apps/comments/src/services/CommentsInstance.js b/apps/comments/src/services/CommentsInstance.js
index f5263f35c3d..cc45d0cbea7 100644
--- a/apps/comments/src/services/CommentsInstance.js
+++ b/apps/comments/src/services/CommentsInstance.js
@@ -1,34 +1,18 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getLoggerBuilder } from '@nextcloud/logger'
-import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import CommentsApp from '../views/Comments'
+import { getCSPNonce } from '@nextcloud/auth'
+import { t, n } from '@nextcloud/l10n'
+import { PiniaVuePlugin, createPinia } from 'pinia'
import Vue from 'vue'
+import CommentsApp from '../views/Comments.vue'
+import logger from '../logger.js'
-const logger = getLoggerBuilder()
- .setApp('comments')
- .detectUser()
- .build()
+Vue.use(PiniaVuePlugin)
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = getCSPNonce()
// Add translates functions
Vue.mixin({
@@ -48,19 +32,21 @@ export default class CommentInstance {
/**
* Initialize a new Comments instance for the desired type
*
- * @param {string} commentsType the comments endpoint type
+ * @param {string} resourceType the comments endpoint type
* @param {object} options the vue options (propsData, parent, el...)
*/
- constructor(commentsType = 'files', options) {
- // Add comments type as a global mixin
- Vue.mixin({
- data() {
- return {
- commentsType,
- }
- },
- })
+ constructor(resourceType = 'files', options = {}) {
+ const pinia = createPinia()
+ // Merge options and set `resourceType` property
+ options = {
+ ...options,
+ propsData: {
+ ...(options.propsData ?? {}),
+ resourceType,
+ },
+ pinia,
+ }
// Init Comments component
const View = Vue.extend(CommentsApp)
return new View(options)
diff --git a/apps/comments/src/services/DavClient.js b/apps/comments/src/services/DavClient.js
index ee5aec129c1..3e9a529283f 100644
--- a/apps/comments/src/services/DavClient.js
+++ b/apps/comments/src/services/DavClient.js
@@ -1,38 +1,27 @@
/**
- * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { createClient, getPatcher } from 'webdav'
-import axios from '@nextcloud/axios'
-
-import { getRootPath } from '../utils/davUtils'
-
-// Add this so the server knows it is an request from the browser
-axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
-
-// force our axios
-const patcher = getPatcher()
-patcher.patch('request', axios)
+import { createClient } from 'webdav'
+import { getRootPath } from '../utils/davUtils.js'
+import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
// init webdav client
const client = createClient(getRootPath())
+// set CSRF token header
+const setHeaders = (token) => {
+ client.setHeaders({
+ // Add this so the server knows it is an request from the browser
+ 'X-Requested-With': 'XMLHttpRequest',
+ // Inject user auth
+ requesttoken: token ?? '',
+ })
+}
+
+// refresh headers when request token changes
+onRequestTokenUpdate(setHeaders)
+setHeaders(getRequestToken())
+
export default client
diff --git a/apps/comments/src/services/DeleteComment.js b/apps/comments/src/services/DeleteComment.js
index 871f6b6c5b8..1ed63d7836a 100644
--- a/apps/comments/src/services/DeleteComment.js
+++ b/apps/comments/src/services/DeleteComment.js
@@ -1,36 +1,19 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import client from './DavClient'
+import client from './DavClient.js'
/**
* Delete a comment
*
- * @param {string} commentsType the ressource type
- * @param {number} ressourceId the ressource ID
+ * @param {string} resourceType the resource type
+ * @param {number} resourceId the resource ID
* @param {number} commentId the comment iD
*/
-export default async function(commentsType, ressourceId, commentId) {
- const commentPath = ['', commentsType, ressourceId, commentId].join('/')
+export default async function(resourceType, resourceId, commentId) {
+ const commentPath = ['', resourceType, resourceId, commentId].join('/')
// Fetch newly created comment data
await client.deleteFile(commentPath)
diff --git a/apps/comments/src/services/EditComment.js b/apps/comments/src/services/EditComment.js
index f3a2d857356..4ec33415a72 100644
--- a/apps/comments/src/services/EditComment.js
+++ b/apps/comments/src/services/EditComment.js
@@ -1,37 +1,20 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import client from './DavClient'
+import client from './DavClient.js'
/**
* Edit an existing comment
*
- * @param {string} commentsType the ressource type
- * @param {number} ressourceId the ressource ID
+ * @param {string} resourceType the resource type
+ * @param {number} resourceId the resource ID
* @param {number} commentId the comment iD
* @param {string} message the message content
*/
-export default async function(commentsType, ressourceId, commentId, message) {
- const commentPath = ['', commentsType, ressourceId, commentId].join('/')
+export default async function(resourceType, resourceId, commentId, message) {
+ const commentPath = ['', resourceType, resourceId, commentId].join('/')
return await client.customRequest(commentPath, Object.assign({
method: 'PROPPATCH',
diff --git a/apps/comments/src/services/GetComments.js b/apps/comments/src/services/GetComments.js
deleted file mode 100644
index bf3dd31b5f1..00000000000
--- a/apps/comments/src/services/GetComments.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-import { parseXML, prepareFileFromProps } from 'webdav/dist/node/tools/dav'
-import { processResponsePayload } from 'webdav/dist/node/response'
-import { decodeHtmlEntities } from '../utils/decodeHtmlEntities'
-import client from './DavClient'
-
-export const DEFAULT_LIMIT = 20
-/**
- * Retrieve the comments list
- *
- * @param {object} data destructuring object
- * @param {string} data.commentsType the ressource type
- * @param {number} data.ressourceId the ressource ID
- * @param {object} [options] optional options for axios
- * @return {object[]} the comments list
- */
-export default async function({ commentsType, ressourceId }, options = {}) {
- let response = null
- const ressourcePath = ['', commentsType, ressourceId].join('/')
-
- return await client.customRequest(ressourcePath, Object.assign({
- method: 'REPORT',
- data: `<?xml version="1.0"?>
- <oc:filter-comments
- xmlns:d="DAV:"
- xmlns:oc="http://owncloud.org/ns"
- xmlns:nc="http://nextcloud.org/ns"
- xmlns:ocs="http://open-collaboration-services.org/ns">
- <oc:limit>${DEFAULT_LIMIT}</oc:limit>
- <oc:offset>${options.offset || 0}</oc:offset>
- </oc:filter-comments>`,
- }, options))
- // See example on how it's done normally
- // https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/stat.js#L19
- // Waiting for proper REPORT integration https://github.com/perry-mitchell/webdav-client/issues/207
- .then(res => {
- response = res
- return res.data
- })
- .then(parseXML)
- .then(xml => processMultistatus(xml, true))
- .then(comments => processResponsePayload(response, comments, true))
- .then(response => response.data)
-}
-
-// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/directoryContents.js#L32
-/**
- * @param {any} result -
- * @param {any} isDetailed -
- */
-function processMultistatus(result, isDetailed = false) {
- // Extract the response items (directory contents)
- const {
- multistatus: { response: responseItems },
- } = result
- return responseItems.map(item => {
- // Each item should contain a stat object
- const {
- propstat: { prop: props },
- } = item
- // Decode HTML entities
- const decodedProps = {
- ...props,
- // Decode twice to handle potentially double-encoded entities
- // FIXME Remove this once https://github.com/nextcloud/server/issues/29306 is resolved
- actorDisplayName: decodeHtmlEntities(props.actorDisplayName, 2),
- message: decodeHtmlEntities(props.message, 2),
- }
- return prepareFileFromProps(decodedProps, decodedProps.id.toString(), isDetailed)
- })
-}
diff --git a/apps/comments/src/services/GetComments.ts b/apps/comments/src/services/GetComments.ts
new file mode 100644
index 00000000000..c42aa21d6cb
--- /dev/null
+++ b/apps/comments/src/services/GetComments.ts
@@ -0,0 +1,67 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { parseXML, type DAVResult, type FileStat, type ResponseDataDetailed } from 'webdav'
+
+// https://github.com/perry-mitchell/webdav-client/issues/339
+import { processResponsePayload } from 'webdav/dist/node/response.js'
+import { prepareFileFromProps } from 'webdav/dist/node/tools/dav.js'
+import client from './DavClient.js'
+
+export const DEFAULT_LIMIT = 20
+
+/**
+ * Retrieve the comments list
+ *
+ * @param {object} data destructuring object
+ * @param {string} data.resourceType the resource type
+ * @param {number} data.resourceId the resource ID
+ * @param {object} [options] optional options for axios
+ * @param {number} [options.offset] the pagination offset
+ * @param {number} [options.limit] the pagination limit, defaults to 20
+ * @param {Date} [options.datetime] optional date to query
+ * @return {{data: object[]}} the comments list
+ */
+export const getComments = async function({ resourceType, resourceId }, options: { offset: number, limit?: number, datetime?: Date }) {
+ const resourcePath = ['', resourceType, resourceId].join('/')
+ const datetime = options.datetime ? `<oc:datetime>${options.datetime.toISOString()}</oc:datetime>` : ''
+ const response = await client.customRequest(resourcePath, Object.assign({
+ method: 'REPORT',
+ data: `<?xml version="1.0"?>
+ <oc:filter-comments
+ xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <oc:limit>${options.limit ?? DEFAULT_LIMIT}</oc:limit>
+ <oc:offset>${options.offset || 0}</oc:offset>
+ ${datetime}
+ </oc:filter-comments>`,
+ }, options))
+
+ const responseData = await response.text()
+ const result = await parseXML(responseData)
+ const stat = getDirectoryFiles(result, true)
+ return processResponsePayload(response, stat, true) as ResponseDataDetailed<FileStat[]>
+}
+
+// https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts
+const getDirectoryFiles = function(
+ result: DAVResult,
+ isDetailed = false,
+): Array<FileStat> {
+ // Extract the response items (directory contents)
+ const {
+ multistatus: { response: responseItems },
+ } = result
+
+ // Map all items to a consistent output structure (results)
+ return responseItems.map(item => {
+ // Each item should contain a stat object
+ const props = item.propstat!.prop!
+
+ return prepareFileFromProps(props, props.id!.toString(), isDetailed)
+ })
+}
diff --git a/apps/comments/src/services/NewComment.js b/apps/comments/src/services/NewComment.js
index 27d227ed656..663b4d72e02 100644
--- a/apps/comments/src/services/NewComment.js
+++ b/apps/comments/src/services/NewComment.js
@@ -1,55 +1,38 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCurrentUser } from '@nextcloud/auth'
-import { getRootPath } from '../utils/davUtils'
-import { decodeHtmlEntities } from '../utils/decodeHtmlEntities'
+import { getRootPath } from '../utils/davUtils.js'
+import { decodeHtmlEntities } from '../utils/decodeHtmlEntities.js'
import axios from '@nextcloud/axios'
-import client from './DavClient'
+import client from './DavClient.js'
/**
* Retrieve the comments list
*
- * @param {string} commentsType the ressource type
- * @param {number} ressourceId the ressource ID
+ * @param {string} resourceType the resource type
+ * @param {number} resourceId the resource ID
* @param {string} message the message
* @return {object} the new comment
*/
-export default async function(commentsType, ressourceId, message) {
- const ressourcePath = ['', commentsType, ressourceId].join('/')
+export default async function(resourceType, resourceId, message) {
+ const resourcePath = ['', resourceType, resourceId].join('/')
- const response = await axios.post(getRootPath() + ressourcePath, {
+ const response = await axios.post(getRootPath() + resourcePath, {
actorDisplayName: getCurrentUser().displayName,
actorId: getCurrentUser().uid,
actorType: 'users',
creationDateTime: (new Date()).toUTCString(),
message,
- objectType: 'files',
+ objectType: resourceType,
verb: 'comment',
})
- // Retrieve comment id from ressource location
+ // Retrieve comment id from resource location
const commentId = parseInt(response.headers['content-location'].split('/').pop())
- const commentPath = ressourcePath + '/' + commentId
+ const commentPath = resourcePath + '/' + commentId
// Fetch newly created comment data
const comment = await client.stat(commentPath, {
diff --git a/apps/comments/src/services/ReadComments.ts b/apps/comments/src/services/ReadComments.ts
new file mode 100644
index 00000000000..73682e21d95
--- /dev/null
+++ b/apps/comments/src/services/ReadComments.ts
@@ -0,0 +1,38 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import client from './DavClient.js'
+
+import type { Response } from 'webdav'
+
+/**
+ * Mark comments older than the date timestamp as read
+ *
+ * @param resourceType the resource type
+ * @param resourceId the resource ID
+ * @param date the date object
+ */
+export const markCommentsAsRead = (
+ resourceType: string,
+ resourceId: number,
+ date: Date,
+): Promise<Response> => {
+ const resourcePath = ['', resourceType, resourceId].join('/')
+ const readMarker = date.toUTCString()
+
+ return client.customRequest(resourcePath, {
+ method: 'PROPPATCH',
+ data: `<?xml version="1.0"?>
+ <d:propertyupdate
+ xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns">
+ <d:set>
+ <d:prop>
+ <oc:readMarker>${readMarker}</oc:readMarker>
+ </d:prop>
+ </d:set>
+ </d:propertyupdate>`,
+ })
+}
diff --git a/apps/comments/src/store/deletedCommentLimbo.js b/apps/comments/src/store/deletedCommentLimbo.js
new file mode 100644
index 00000000000..3e511addebb
--- /dev/null
+++ b/apps/comments/src/store/deletedCommentLimbo.js
@@ -0,0 +1,28 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { defineStore } from 'pinia'
+
+export const useDeletedCommentLimbo = defineStore('deletedCommentLimbo', {
+ state: () => ({
+ idsInLimbo: [],
+ }),
+ actions: {
+ addId(id) {
+ this.idsInLimbo.push(id)
+ },
+
+ removeId(id) {
+ const index = this.idsInLimbo.indexOf(id)
+ if (index > -1) {
+ this.idsInLimbo.splice(index, 1)
+ }
+ },
+
+ checkForId(id) {
+ this.idsInLimbo.includes(id)
+ },
+ },
+})
diff --git a/apps/comments/src/templates.js b/apps/comments/src/templates.js
deleted file mode 100644
index 6064854f084..00000000000
--- a/apps/comments/src/templates.js
+++ /dev/null
@@ -1,17 +0,0 @@
-(function() {
- var template = Handlebars.template, templates = OCA.Comments.Templates = OCA.Comments.Templates || {};
-templates['filesplugin'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
- var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
- if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
- return parent[propertyName];
- }
- return undefined
- };
-
- return "<a class=\"action action-comment permanent\" title=\""
- + alias4(((helper = (helper = lookupProperty(helpers,"countMessage") || (depth0 != null ? lookupProperty(depth0,"countMessage") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"countMessage","hash":{},"data":data,"loc":{"start":{"line":1,"column":50},"end":{"line":1,"column":66}}}) : helper)))
- + "\" href=\"#\">\n <img class=\"svg\" src=\""
- + alias4(((helper = (helper = lookupProperty(helpers,"iconUrl") || (depth0 != null ? lookupProperty(depth0,"iconUrl") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconUrl","hash":{},"data":data,"loc":{"start":{"line":2,"column":23},"end":{"line":2,"column":34}}}) : helper)))
- + "\"/>\n</a>\n";
-},"useData":true});
-})(); \ No newline at end of file
diff --git a/apps/comments/src/templates/filesplugin.handlebars b/apps/comments/src/templates/filesplugin.handlebars
deleted file mode 100644
index 8f3699380be..00000000000
--- a/apps/comments/src/templates/filesplugin.handlebars
+++ /dev/null
@@ -1,3 +0,0 @@
-<a class="action action-comment permanent" title="{{countMessage}}" href="#">
- <img class="svg" src="{{iconUrl}}"/>
-</a>
diff --git a/apps/comments/src/utils/cancelableRequest.js b/apps/comments/src/utils/cancelableRequest.js
index cdb31441926..c2d380c80f9 100644
--- a/apps/comments/src/utils/cancelableRequest.js
+++ b/apps/comments/src/utils/cancelableRequest.js
@@ -1,34 +1,8 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import axios from '@nextcloud/axios'
-
-/**
- * Create a cancel token
- *
- * @return {import('axios').CancelTokenSource}
- */
-const createCancelToken = () => axios.CancelToken.source()
-
/**
* Creates a cancelable axios 'request object'.
*
@@ -36,10 +10,8 @@ const createCancelToken = () => axios.CancelToken.source()
* @return {object}
*/
const cancelableRequest = function(request) {
- /**
- * Generate an axios cancel token
- */
- const cancelToken = createCancelToken()
+ const controller = new AbortController()
+ const signal = controller.signal
/**
* Execute the request
@@ -48,15 +20,16 @@ const cancelableRequest = function(request) {
* @param {object} [options] optional config for the request
*/
const fetch = async function(url, options) {
- return request(
+ const response = await request(
url,
- Object.assign({ cancelToken: cancelToken.token }, options)
+ Object.assign({ signal }, options),
)
+ return response
}
return {
request: fetch,
- cancel: cancelToken.cancel,
+ abort: () => controller.abort(),
}
}
diff --git a/apps/comments/src/utils/davUtils.js b/apps/comments/src/utils/davUtils.js
index 1a2686bbdab..33efc8e7d10 100644
--- a/apps/comments/src/utils/davUtils.js
+++ b/apps/comments/src/utils/davUtils.js
@@ -1,23 +1,6 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateRemoteUrl } from '@nextcloud/router'
diff --git a/apps/comments/src/utils/decodeHtmlEntities.js b/apps/comments/src/utils/decodeHtmlEntities.js
index 60c08163faa..4c492954256 100644
--- a/apps/comments/src/utils/decodeHtmlEntities.js
+++ b/apps/comments/src/utils/decodeHtmlEntities.js
@@ -1,23 +1,6 @@
/**
- * @copyright Copyright (c) 2021 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
diff --git a/apps/comments/src/utils/numberUtil.js b/apps/comments/src/utils/numberUtil.js
deleted file mode 100644
index cbbd6964019..00000000000
--- a/apps/comments/src/utils/numberUtil.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-const isNumber = function(num) {
- if (!num) {
- return false
- }
- return Number(num).toString() === num.toString()
-}
-
-export { isNumber }
diff --git a/apps/comments/src/views/ActivityCommentAction.vue b/apps/comments/src/views/ActivityCommentAction.vue
new file mode 100644
index 00000000000..f9a9a97796f
--- /dev/null
+++ b/apps/comments/src/views/ActivityCommentAction.vue
@@ -0,0 +1,54 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <Comment v-bind="editorData"
+ :auto-complete="autoComplete"
+ :resource-type="resourceType"
+ :editor="true"
+ :user-data="userData"
+ :resource-id="resourceId"
+ class="comments-action"
+ @new="onNewComment" />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Comment from '../components/Comment.vue'
+import CommentView from '../mixins/CommentView.js'
+import logger from '../logger'
+import { showError } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+
+export default defineComponent({
+ components: {
+ Comment,
+ },
+ mixins: [CommentView],
+ props: {
+ reloadCallback: {
+ type: Function,
+ required: true,
+ },
+ },
+ methods: {
+ onNewComment() {
+ try {
+ // just force reload
+ this.reloadCallback()
+ } catch (e) {
+ showError(t('comments', 'Could not reload comments'))
+ logger.debug(e)
+ }
+ },
+ },
+})
+</script>
+
+<style scoped>
+.comments-action {
+ padding: 0;
+}
+</style>
diff --git a/apps/comments/src/views/ActivityCommentEntry.vue b/apps/comments/src/views/ActivityCommentEntry.vue
new file mode 100644
index 00000000000..bbfe530b2e3
--- /dev/null
+++ b/apps/comments/src/views/ActivityCommentEntry.vue
@@ -0,0 +1,71 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <Comment ref="comment"
+ tag="li"
+ v-bind="comment.props"
+ :auto-complete="autoComplete"
+ :resource-type="resourceType"
+ :message="commentMessage"
+ :resource-id="resourceId"
+ :user-data="genMentionsData(comment.props.mentions)"
+ class="comments-activity"
+ @delete="reloadCallback()" />
+</template>
+
+<script lang="ts">
+import type { PropType } from 'vue'
+import { translate as t } from '@nextcloud/l10n'
+
+import Comment from '../components/Comment.vue'
+import CommentView from '../mixins/CommentView'
+
+export default {
+ name: 'ActivityCommentEntry',
+
+ components: {
+ Comment,
+ },
+
+ mixins: [CommentView],
+ props: {
+ comment: {
+ type: Object,
+ required: true,
+ },
+ reloadCallback: {
+ type: Function as PropType<() => void>,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ commentMessage: '',
+ }
+ },
+
+ watch: {
+ comment() {
+ this.commentMessage = this.comment.props.message
+ },
+ },
+
+ mounted() {
+ this.commentMessage = this.comment.props.message
+ },
+
+ methods: {
+ t,
+ },
+}
+</script>
+
+<style scoped>
+.comments-activity {
+ padding: 0;
+}
+</style>
diff --git a/apps/comments/src/views/Comments.vue b/apps/comments/src/views/Comments.vue
index 288fe44e96b..657af888a12 100644
--- a/apps/comments/src/views/Comments.vue
+++ b/apps/comments/src/views/Comments.vue
@@ -1,57 +1,44 @@
<!--
- - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- - @author Richard Steinmetz <richard@steinmetz.cloud>
- -
- - @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/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div class="comments" :class="{ 'icon-loading': isFirstLoading }">
+ <div v-element-visibility="onVisibilityChange"
+ class="comments"
+ :class="{ 'icon-loading': isFirstLoading }">
<!-- Editor -->
<Comment v-bind="editorData"
:auto-complete="autoComplete"
- :user-data="userData"
+ :resource-type="resourceType"
:editor="true"
- :ressource-id="ressourceId"
+ :user-data="userData"
+ :resource-id="currentResourceId"
class="comments__writer"
@new="onNewComment" />
<template v-if="!isFirstLoading">
<NcEmptyContent v-if="!hasComments && done"
class="comments__empty"
- :title="t('comments', 'No comments yet, start the conversation!')">
+ :name="t('comments', 'No comments yet, start the conversation!')">
<template #icon>
- <MessageReplyTextIcon />
+ <IconMessageReplyTextOutline />
</template>
</NcEmptyContent>
-
- <!-- Comments -->
- <Comment v-for="comment in comments"
- v-else
- :key="comment.props.id"
- v-bind="comment.props"
- :auto-complete="autoComplete"
- :message.sync="comment.props.message"
- :ressource-id="ressourceId"
- :user-data="genMentionsData(comment.props.mentions)"
- class="comments__list"
- @delete="onDelete" />
+ <ul v-else>
+ <!-- Comments -->
+ <Comment v-for="comment in comments"
+ :key="comment.props.id"
+ tag="li"
+ v-bind="comment.props"
+ :auto-complete="autoComplete"
+ :resource-type="resourceType"
+ :message.sync="comment.props.message"
+ :resource-id="currentResourceId"
+ :user-data="genMentionsData(comment.props.mentions)"
+ class="comments__list"
+ @delete="onDelete" />
+ </ul>
<!-- Loading more message -->
<div v-if="loading && !isFirstLoading" class="comments__info icon-loading" />
@@ -62,14 +49,14 @@
<!-- Error message -->
<template v-else-if="error">
- <NcEmptyContent class="comments__error" :title="error">
+ <NcEmptyContent class="comments__error" :name="error">
<template #icon>
- <AlertCircleOutlineIcon />
+ <IconAlertCircleOutline />
</template>
</NcEmptyContent>
<NcButton class="comments__retry" @click="getComments">
<template #icon>
- <RefreshIcon />
+ <IconRefresh />
</template>
{{ t('comments', 'Retry') }}
</NcButton>
@@ -79,56 +66,52 @@
</template>
<script>
-import { generateOcsUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
-import { loadState } from '@nextcloud/initial-state'
-import axios from '@nextcloud/axios'
-import VTooltip from 'v-tooltip'
-import Vue from 'vue'
-
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton'
-import RefreshIcon from 'vue-material-design-icons/Refresh'
-import MessageReplyTextIcon from 'vue-material-design-icons/MessageReplyText'
-import AlertCircleOutlineIcon from 'vue-material-design-icons/AlertCircleOutline'
+import { showError } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+import { vElementVisibility as elementVisibility } from '@vueuse/components'
+
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import IconRefresh from 'vue-material-design-icons/Refresh.vue'
+import IconMessageReplyTextOutline from 'vue-material-design-icons/MessageReplyTextOutline.vue'
+import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
import Comment from '../components/Comment.vue'
-import getComments, { DEFAULT_LIMIT } from '../services/GetComments.js'
+import CommentView from '../mixins/CommentView'
import cancelableRequest from '../utils/cancelableRequest.js'
-
-Vue.use(VTooltip)
+import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts'
+import { markCommentsAsRead } from '../services/ReadComments.ts'
export default {
name: 'Comments',
components: {
- // Avatar,
Comment,
NcEmptyContent,
NcButton,
- RefreshIcon,
- MessageReplyTextIcon,
- AlertCircleOutlineIcon,
+ IconRefresh,
+ IconMessageReplyTextOutline,
+ IconAlertCircleOutline,
+ },
+
+ directives: {
+ elementVisibility,
},
+ mixins: [CommentView],
+
data() {
return {
error: '',
loading: false,
done: false,
- ressourceId: null,
+ currentResourceId: this.resourceId,
offset: 0,
comments: [],
cancelRequest: () => {},
- editorData: {
- actorDisplayName: getCurrentUser().displayName,
- actorId: getCurrentUser().uid,
- key: 'editor',
- },
-
Comment,
userData: {},
}
@@ -143,14 +126,32 @@ export default {
},
},
+ watch: {
+ resourceId() {
+ this.currentResourceId = this.resourceId
+ },
+ },
+
methods: {
+ t,
+
+ async onVisibilityChange(isVisible) {
+ if (isVisible) {
+ try {
+ await markCommentsAsRead(this.resourceType, this.currentResourceId, new Date())
+ } catch (e) {
+ showError(e.message || t('comments', 'Failed to mark comments as read'))
+ }
+ }
+ },
+
/**
- * Update current ressourceId and fetch new data
+ * Update current resourceId and fetch new data
*
- * @param {number} ressourceId the current ressourceId (fileId...)
+ * @param {number} resourceId the current resourceId (fileId...)
*/
- async update(ressourceId) {
- this.ressourceId = ressourceId
+ async update(resourceId) {
+ this.currentResourceId = resourceId
this.resetState()
this.getComments()
},
@@ -172,28 +173,6 @@ export default {
},
/**
- * Make sure we have all mentions as Array of objects
- *
- * @param {Array} mentions the mentions list
- * @return {Object<string, object>}
- */
- genMentionsData(mentions) {
- Object.values(mentions)
- .flat()
- .forEach(mention => {
- this.userData[mention.mentionId] = {
- // TODO: support groups
- icon: 'icon-user',
- id: mention.mentionId,
- label: mention.mentionDisplayName,
- source: 'users',
- primary: getCurrentUser().uid === mention.mentionId,
- }
- })
- return this.userData
- },
-
- /**
* Get the existing shares infos
*/
async getComments() {
@@ -205,14 +184,14 @@ export default {
this.error = ''
// Init cancellable request
- const { request, cancel } = cancelableRequest(getComments)
- this.cancelRequest = cancel
+ const { request, abort } = cancelableRequest(getComments)
+ this.cancelRequest = abort
// Fetch comments
- const comments = await request({
- commentsType: this.commentsType,
- ressourceId: this.ressourceId,
- }, { offset: this.offset })
+ const { data: comments } = await request({
+ resourceType: this.resourceType,
+ resourceId: this.currentResourceId,
+ }, { offset: this.offset }) || { data: [] }
this.logger.debug(`Processed ${comments.length} comments`, { comments })
@@ -239,27 +218,6 @@ export default {
},
/**
- * Autocomplete @mentions
- *
- * @param {string} search the query
- * @param {Function} callback the callback to process the results with
- */
- async autoComplete(search, callback) {
- const results = await axios.get(generateOcsUrl('core/autocomplete/get'), {
- params: {
- search,
- itemType: 'files',
- itemId: this.ressourceId,
- sorter: 'commenters|share-recipients',
- limit: loadState('comments', 'maxAutoCompleteResults'),
- },
- })
- // Save user data so it can be used by the editor to replace mentions
- results.data.ocs.data.forEach(user => { this.userData[user.id] = user })
- return callback(Object.values(this.userData))
- },
-
- /**
* Add newly created comment to the list
*
* @param {object} comment the new comment
@@ -298,10 +256,13 @@ export default {
<style lang="scss" scoped>
.comments {
- // Do not add emptycontent top margin
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
+
&__empty,
&__error {
- margin-top: 0 !important;
+ flex: 1 0;
}
&__retry {