diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2023-11-14 18:30:12 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2023-11-16 11:54:52 +0100 |
commit | db2fec1cec8ec490c71eb47180c4cae422bf4f75 (patch) | |
tree | 5f97c4bd16428dd40dceeb0e609f100c146cc16c /apps/comments | |
parent | 9c3350b3131312e60155c26a8a15b95981c6dd0a (diff) | |
download | nextcloud-server-db2fec1cec8ec490c71eb47180c4cae422bf4f75.tar.gz nextcloud-server-db2fec1cec8ec490c71eb47180c4cae422bf4f75.zip |
feat(comments): Use activity tab to mount comments sidebar section if available
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/comments')
-rw-r--r-- | apps/comments/lib/Listener/LoadSidebarScripts.php | 6 | ||||
-rw-r--r-- | apps/comments/src/comments-activity-tab.ts | 85 | ||||
-rw-r--r-- | apps/comments/src/comments-tab.js | 79 | ||||
-rw-r--r-- | apps/comments/src/components/Comment.vue | 5 | ||||
-rw-r--r-- | apps/comments/src/mixins/CommentMixin.js | 10 | ||||
-rw-r--r-- | apps/comments/src/mixins/CommentView.ts | 68 | ||||
-rw-r--r-- | apps/comments/src/services/GetComments.ts | 15 | ||||
-rw-r--r-- | apps/comments/src/views/ActivityCommentAction.vue | 70 | ||||
-rw-r--r-- | apps/comments/src/views/ActivityCommentEntry.vue | 86 | ||||
-rw-r--r-- | apps/comments/src/views/Comments.vue | 60 |
10 files changed, 386 insertions, 98 deletions
diff --git a/apps/comments/lib/Listener/LoadSidebarScripts.php b/apps/comments/lib/Listener/LoadSidebarScripts.php index a77cd4e0af3..39c81c03ad1 100644 --- a/apps/comments/lib/Listener/LoadSidebarScripts.php +++ b/apps/comments/lib/Listener/LoadSidebarScripts.php @@ -28,6 +28,8 @@ namespace OCA\Comments\Listener; use OCA\Comments\AppInfo\Application; use OCA\Files\Event\LoadSidebar; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IInitialState; use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -36,6 +38,8 @@ use OCP\Util; class LoadSidebarScripts implements IEventListener { public function __construct( private ICommentsManager $commentsManager, + private IInitialState $initialState, + private IAppManager $appManager, ) { } @@ -46,6 +50,8 @@ class LoadSidebarScripts implements IEventListener { $this->commentsManager->load(); + $this->initialState->provideInitialState('activityEnabled', $this->appManager->isEnabledForUser('activity')); + // TODO: make sure to only include the sidebar script when // we properly split it between files list and sidebar Util::addScript(Application::APP_ID, '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..30c1e38d8e7 --- /dev/null +++ b/apps/comments/src/comments-activity-tab.ts @@ -0,0 +1,85 @@ +/** + * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> + * + * @author Ferdinand Thiessen <opensource@fthiessen.de> + * + * @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 moment from '@nextcloud/moment' +import Vue from 'vue' +import logger from './logger.js' +import { getComments } from './services/GetComments.js' + +let ActivityTabPluginView +let ActivityTabPluginInstance + +/** + * Register the comments plugins for the Activity sidebar + */ +export function registerCommentsPlugins() { + window.OCA.Activity.registerSidebarAction({ + mount: async (el, { context, fileInfo, reload }) => { + if (!ActivityTabPluginView) { + const { default: ActivityCommmentAction } = await import('./views/ActivityCommentAction.vue') + ActivityTabPluginView = Vue.extend(ActivityCommmentAction) + } + ActivityTabPluginInstance = new ActivityTabPluginView({ + parent: context, + propsData: { + reloadCallback: reload, + ressourceId: fileInfo.id, + }, + }) + ActivityTabPluginInstance.$mount(el) + 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({ commentsType: 'files', ressourceId: fileInfo.id }, { limit, offset }) + logger.debug('Loaded comments', { fileInfo, comments }) + const { default: CommentView } = await import('./views/ActivityCommentEntry.vue') + const CommentsViewObject = Vue.extend(CommentView) + + return comments.map((comment) => ({ + timestamp: moment(comment.props.creationDateTime).toDate().getTime(), + mount(element, { context, reload }) { + this._CommentsViewInstance = new CommentsViewObject({ + parent: context, + propsData: { + comment, + ressourceId: fileInfo.id, + reloadCallback: reload, + }, + }) + this._CommentsViewInstance.$mount(element) + }, + 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-tab.js b/apps/comments/src/comments-tab.js index 121b8d686f4..1a367cc18ee 100644 --- a/apps/comments/src/comments-tab.js +++ b/apps/comments/src/comments-tab.js @@ -22,40 +22,53 @@ // eslint-disable-next-line n/no-missing-import, import/no-unresolved import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw' +import { getRequestToken } 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__ = btoa(getRequestToken()) - 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, + }) + // 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/components/Comment.vue b/apps/comments/src/components/Comment.vue index e8ae9a88e77..a5b72efb74b 100644 --- a/apps/comments/src/components/Comment.vue +++ b/apps/comments/src/components/Comment.vue @@ -111,6 +111,7 @@ <script> import { getCurrentUser } from '@nextcloud/auth' +import { translate as t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' @@ -235,6 +236,8 @@ export default { }, methods: { + t, + /** * Update local Message on outer change * @@ -279,7 +282,7 @@ $comment-padding: 10px; .comment { display: flex; - gap: 16px; + gap: 8px; padding: 5px $comment-padding; &__side { diff --git a/apps/comments/src/mixins/CommentMixin.js b/apps/comments/src/mixins/CommentMixin.js index 545625ab97e..bcb72af2315 100644 --- a/apps/comments/src/mixins/CommentMixin.js +++ b/apps/comments/src/mixins/CommentMixin.js @@ -20,10 +20,11 @@ * */ +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 { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs' +import logger from '../logger.js' export default { props: { @@ -46,6 +47,7 @@ export default { deleted: false, editing: false, loading: false, + commentsType: 'files', } }, @@ -63,7 +65,7 @@ export default { 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 }) + logger.debug('Comment edited', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id, message }) this.$emit('update:message', message) this.editing = false } catch (error) { @@ -86,7 +88,7 @@ export default { 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 }) + logger.debug('Comment deleted', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id }) this.$emit('delete', this.id) } catch (error) { showError(t('comments', 'An error occurred while trying to delete the comment')) @@ -100,7 +102,7 @@ export default { 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 }) + logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, 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..9cd14904875 --- /dev/null +++ b/apps/comments/src/mixins/CommentView.ts @@ -0,0 +1,68 @@ +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: { + ressourceId: { + type: Number, + required: true, + }, + }, + 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.ressourceId, + 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/GetComments.ts b/apps/comments/src/services/GetComments.ts index d74e92bce68..2ca725c2ae6 100644 --- a/apps/comments/src/services/GetComments.ts +++ b/apps/comments/src/services/GetComments.ts @@ -20,7 +20,7 @@ * */ -import { parseXML, type DAVResult, type FileStat } from 'webdav' +import { parseXML, type DAVResult, type FileStat, type ResponseDataDetailed } from 'webdav' // https://github.com/perry-mitchell/webdav-client/issues/339 import { processResponsePayload } from '../../../../node_modules/webdav/dist/node/response.js' @@ -37,11 +37,13 @@ export const DEFAULT_LIMIT = 20 * @param {number} data.ressourceId the ressource ID * @param {object} [options] optional options for axios * @param {number} [options.offset] the pagination offset - * @return {object[]} the comments list + * @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({ commentsType, ressourceId }, options: { offset: number }) { +export const getComments = async function({ commentsType, ressourceId }, options: { offset: number, limit?: number, datetime?: Date }) { const ressourcePath = ['', commentsType, ressourceId].join('/') - + const datetime = options.datetime ? `<oc:datetime>${options.datetime.toISOString()}</oc:datetime>` : '' const response = await client.customRequest(ressourcePath, Object.assign({ method: 'REPORT', data: `<?xml version="1.0"?> @@ -50,15 +52,16 @@ export const getComments = async function({ commentsType, ressourceId }, options 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: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) + return processResponsePayload(response, stat, true) as ResponseDataDetailed<FileStat[]> } // https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts diff --git a/apps/comments/src/views/ActivityCommentAction.vue b/apps/comments/src/views/ActivityCommentAction.vue new file mode 100644 index 00000000000..96edaf6d46f --- /dev/null +++ b/apps/comments/src/views/ActivityCommentAction.vue @@ -0,0 +1,70 @@ +<!-- + - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> + - + - @author Ferdinand Thiessen <opensource@fthiessen.de> + - + - @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/>. + - + --> + +<template> + <Comment v-bind="editorData" + :auto-complete="autoComplete" + :user-data="userData" + :editor="true" + :ressource-id="ressourceId" + 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..21c600dcddb --- /dev/null +++ b/apps/comments/src/views/ActivityCommentEntry.vue @@ -0,0 +1,86 @@ +<!-- + - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> + - + - @author Ferdinand Thiessen <opensource@fthiessen.de> + - + - @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/>. + - + --> + +<template> + <Comment ref="comment" + tag="li" + v-bind="comment.props" + :auto-complete="autoComplete" + :message="commentMessage" + :ressource-id="ressourceId" + :user-data="genMentionsData(comment.props.mentions)" + class="comments-activity" + @delete="reloadCallback()" /> +</template> + +<script lang="ts"> +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, + 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 93e9031df5a..7a36823299e 100644 --- a/apps/comments/src/views/Comments.vue +++ b/apps/comments/src/views/Comments.vue @@ -82,11 +82,8 @@ </template> <script> -import { generateOcsUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' -import { loadState } from '@nextcloud/initial-state' import { showError } from '@nextcloud/dialogs' -import axios from '@nextcloud/axios' +import { translate as t } from '@nextcloud/l10n' import VTooltip from 'v-tooltip' import Vue from 'vue' import VueObserveVisibility from 'vue-observe-visibility' @@ -101,6 +98,7 @@ import Comment from '../components/Comment.vue' import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts' import cancelableRequest from '../utils/cancelableRequest.js' import { markCommentsAsRead } from '../services/ReadComments.ts' +import CommentView from '../mixins/CommentView' Vue.use(VTooltip) Vue.use(VueObserveVisibility) @@ -109,7 +107,6 @@ export default { name: 'Comments', components: { - // Avatar, Comment, NcEmptyContent, NcButton, @@ -118,6 +115,8 @@ export default { AlertCircleOutlineIcon, }, + mixins: [CommentView], + data() { return { error: '', @@ -130,12 +129,6 @@ export default { cancelRequest: () => {}, - editorData: { - actorDisplayName: getCurrentUser().displayName, - actorId: getCurrentUser().uid, - key: 'editor', - }, - Comment, userData: {}, } @@ -151,6 +144,8 @@ export default { }, methods: { + t, + async onVisibilityChange(isVisible) { if (isVisible) { try { @@ -189,28 +184,6 @@ export default { }, /** - * Make sure we have all mentions as Array of objects - * - * @param {any[]} mentions the mentions list - * @return {Record<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() { @@ -256,27 +229,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 |