diff --git a/lib/Listener/LoadSidebarScripts.php b/lib/Listener/LoadSidebarScripts.php index 96194bbc3..e3c8b5a52 100644 --- a/lib/Listener/LoadSidebarScripts.php +++ b/lib/Listener/LoadSidebarScripts.php @@ -44,6 +44,6 @@ public function handle(Event $event): void { // TODO: make sure to only include the sidebar script when // we properly split it between files list and sidebar Util::addStyle(Application::APP_ID, 'style'); - Util::addScript(Application::APP_ID, 'activity-sidebar'); + Util::addScript(Application::APP_ID, 'activity-sidebar', 'files'); } } diff --git a/src/components/Comment.vue b/src/components/Comment.vue new file mode 100644 index 000000000..e8ae9a88e --- /dev/null +++ b/src/components/Comment.vue @@ -0,0 +1,356 @@ + + + + + + diff --git a/src/components/Moment.vue b/src/components/Moment.vue new file mode 100644 index 000000000..a91ed8b9c --- /dev/null +++ b/src/components/Moment.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/mixins/CommentMixin.js b/src/mixins/CommentMixin.js new file mode 100644 index 000000000..012d0d18d --- /dev/null +++ b/src/mixins/CommentMixin.js @@ -0,0 +1,117 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +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' + +export default { + props: { + id: { + type: Number, + default: null, + }, + message: { + type: String, + default: '', + }, + ressourceId: { + type: [String, Number], + required: true, + }, + }, + + data() { + return { + deleted: false, + editing: false, + loading: false, + } + }, + + methods: { + // EDITION + onEdit() { + this.editing = true + }, + onEditCancel() { + this.editing = false + // Restore original value + this.updateLocalMessage(this.message) + }, + 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 }) + this.$emit('update:message', message) + this.editing = false + } catch (error) { + showError(t('comments', 'An error occurred while trying to edit the comment')) + console.error(error) + } finally { + this.loading = false + } + }, + + // DELETION + onDeleteWithUndo() { + this.deleted = true + const timeOutDelete = setTimeout(this.onDelete, TOAST_UNDO_TIMEOUT) + showUndo(t('comments', 'Comment deleted'), () => { + clearTimeout(timeOutDelete) + this.deleted = false + }) + }, + 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 }) + 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 + } + }, + + // CREATION + async onNewComment(message) { + this.loading = true + try { + //const newComment = await NewComment(this.commentsType, this.ressourceId, message) + const newComment = await NewComment('files', 3, message) + // this.logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, newComment }) + this.$emit('new', newComment) + + // Clear old content + this.$emit('update:message', '') + this.localMessage = '' + } catch (error) { + showError(t('comments', 'An error occurred while trying to create the comment')) + } finally { + this.loading = false + } + }, + }, +} diff --git a/src/services/CommentsInstance.js b/src/services/CommentsInstance.js new file mode 100644 index 000000000..e283e833c --- /dev/null +++ b/src/services/CommentsInstance.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import { getRequestToken } from '@nextcloud/auth' +import Vue from 'vue' +import CommentsApp from '../views/Comments.vue' +import logger from '../logger.js' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(getRequestToken()) + +// Add translates functions +Vue.mixin({ + data() { + return { + logger, + } + }, + methods: { + t, + n, + }, +}) + +export default class CommentInstance { + + /** + * Initialize a new Comments instance for the desired type + * + * @param {string} commentsType 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, + } + }, + }) + + // Init Comments component + const View = Vue.extend(CommentsApp) + return new View(options) + } + +} diff --git a/src/services/DavClient.js b/src/services/DavClient.js new file mode 100644 index 000000000..5c2fc96e4 --- /dev/null +++ b/src/services/DavClient.js @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2021 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import { createClient } from 'webdav' +import { getRootPath } from '../utils/davUtils.js' +import { getRequestToken } from '@nextcloud/auth' + +// init webdav client +const client = createClient(getRootPath(), { + headers: { + // Add this so the server knows it is an request from the browser + 'X-Requested-With': 'XMLHttpRequest', + // Inject user auth + requesttoken: getRequestToken() ?? '', + }, +}) + +export default client diff --git a/src/services/DeleteComment.js b/src/services/DeleteComment.js new file mode 100644 index 000000000..43d53129f --- /dev/null +++ b/src/services/DeleteComment.js @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import client from './DavClient.js' + +/** + * Delete a comment + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource ID + * @param {number} commentId the comment iD + */ +export default async function(commentsType, ressourceId, commentId) { + const commentPath = ['', commentsType, ressourceId, commentId].join('/') + + // Fetch newly created comment data + await client.deleteFile(commentPath) +} diff --git a/src/services/EditComment.js b/src/services/EditComment.js new file mode 100644 index 000000000..51d0d4cca --- /dev/null +++ b/src/services/EditComment.js @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import client from './DavClient.js' + +/** + * Edit an existing comment + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource 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('/') + + return await client.customRequest(commentPath, Object.assign({ + method: 'PROPPATCH', + data: ` + + + + ${message} + + + `, + })) +} diff --git a/src/services/GetComments.ts b/src/services/GetComments.ts new file mode 100644 index 000000000..d74e92bce --- /dev/null +++ b/src/services/GetComments.ts @@ -0,0 +1,83 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import { parseXML, type DAVResult, type FileStat } from 'webdav' + +// https://github.com/perry-mitchell/webdav-client/issues/339 +import { processResponsePayload } from '../../../../node_modules/webdav/dist/node/response.js' +import { prepareFileFromProps } from '../../../../node_modules/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.commentsType the ressource type + * @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 + */ +export const getComments = async function({ commentsType, ressourceId }, options: { offset: number }) { + const ressourcePath = ['', commentsType, ressourceId].join('/') + + const response = await client.customRequest(ressourcePath, Object.assign({ + method: 'REPORT', + data: ` + + ${DEFAULT_LIMIT} + ${options.offset || 0} + `, + }, options)) + + const responseData = await response.text() + const result = await parseXML(responseData) + const stat = getDirectoryFiles(result, true) + return processResponsePayload(response, stat, true) +} + +// https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts +const getDirectoryFiles = function( + result: DAVResult, + isDetailed = false, +): Array { + // 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 { + propstat: { prop: props }, + } = item + + return prepareFileFromProps(props, props.id.toString(), isDetailed) + }) +} diff --git a/src/services/NewComment.js b/src/services/NewComment.js new file mode 100644 index 000000000..a7fb58e32 --- /dev/null +++ b/src/services/NewComment.js @@ -0,0 +1,67 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import { getCurrentUser } from '@nextcloud/auth' +import { getRootPath } from '../utils/davUtils.js' +import { decodeHtmlEntities } from '../utils/decodeHtmlEntities.js' +import axios from '@nextcloud/axios' +import client from './DavClient.js' + +/** + * Retrieve the comments list + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource ID + * @param {string} message the message + * @return {object} the new comment + */ +export default async function(commentsType, ressourceId, message) { + const ressourcePath = ['', commentsType, ressourceId].join('/') + + const response = await axios.post(getRootPath() + ressourcePath, { + actorDisplayName: getCurrentUser().displayName, + actorId: getCurrentUser().uid, + actorType: 'users', + creationDateTime: (new Date()).toUTCString(), + message, + objectType: 'files', + verb: 'comment', + }) + + // Retrieve comment id from ressource location + const commentId = parseInt(response.headers['content-location'].split('/').pop()) + const commentPath = ressourcePath + '/' + commentId + + // Fetch newly created comment data + const comment = await client.stat(commentPath, { + details: true, + }) + + const props = comment.data.props + // Decode twice to handle potentially double-encoded entities + // FIXME Remove this once https://github.com/nextcloud/server/issues/29306 + // is resolved + props.actorDisplayName = decodeHtmlEntities(props.actorDisplayName, 2) + props.message = decodeHtmlEntities(props.message, 2) + + return comment.data +} diff --git a/src/services/ReadComments.ts b/src/services/ReadComments.ts new file mode 100644 index 000000000..4c7e44fe2 --- /dev/null +++ b/src/services/ReadComments.ts @@ -0,0 +1,55 @@ +/** + * @copyright 2023 Christopher Ng + * + * @author Christopher Ng + * + * @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 . + * + */ + +import client from './DavClient.js' + +import type { Response } from 'webdav' + +/** + * Mark comments older than the date timestamp as read + * + * @param commentsType the ressource type + * @param ressourceId the ressource ID + * @param date the date object + */ +export const markCommentsAsRead = ( + commentsType: string, + ressourceId: number, + date: Date, +): Promise => { + const ressourcePath = ['', commentsType, ressourceId].join('/') + const readMarker = date.toUTCString() + + return client.customRequest(ressourcePath, { + method: 'PROPPATCH', + data: ` + + + + ${readMarker} + + + `, + }) +} diff --git a/src/sidebar.js b/src/sidebar.js index b56852890..8415afe57 100644 --- a/src/sidebar.js +++ b/src/sidebar.js @@ -68,9 +68,11 @@ const activityTab = new OCA.Files.Sidebar.Tab({ }) window.addEventListener('DOMContentLoaded', async function() { + console.debug('OCA.Activity initializing') if (OCA.Files && OCA.Files.Sidebar) { OCA.Files.Sidebar.registerTab(activityTab) const { default: ActivityTab } = await import(/* webpackPreload: true */ './views/ActivityTab.vue') ActivityTabView = ActivityTabView ?? Vue.extend(ActivityTab) + console.debug('OCA.Activity initialized') } }) diff --git a/src/utils/cancelableRequest.js b/src/utils/cancelableRequest.js new file mode 100644 index 000000000..1973de389 --- /dev/null +++ b/src/utils/cancelableRequest.js @@ -0,0 +1,53 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +/** + * Creates a cancelable axios 'request object'. + * + * @param {Function} request the axios promise request + * @return {object} + */ +const cancelableRequest = function(request) { + const controller = new AbortController() + const signal = controller.signal + + /** + * Execute the request + * + * @param {string} url the url to send the request to + * @param {object} [options] optional config for the request + */ + const fetch = async function(url, options) { + const response = await request( + url, + Object.assign({ signal }, options) + ) + return response + } + + return { + request: fetch, + abort: () => controller.abort(), + } +} + +export default cancelableRequest diff --git a/src/utils/davUtils.js b/src/utils/davUtils.js new file mode 100644 index 000000000..1a2686bbd --- /dev/null +++ b/src/utils/davUtils.js @@ -0,0 +1,29 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import { generateRemoteUrl } from '@nextcloud/router' + +const getRootPath = function() { + return generateRemoteUrl('dav/comments') +} + +export { getRootPath } diff --git a/js/activity-389.js.LICENSE.txt b/src/utils/decodeHtmlEntities.js similarity index 59% rename from js/activity-389.js.LICENSE.txt rename to src/utils/decodeHtmlEntities.js index 0d1e018ae..60c08163f 100644 --- a/js/activity-389.js.LICENSE.txt +++ b/src/utils/decodeHtmlEntities.js @@ -1,23 +1,7 @@ -/*! - * Determine if an object is a Buffer - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -/*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ - /** - * @copyright Copyright (c) 2021 Louis Chemineau + * @copyright Copyright (c) 2021 Christopher Ng * - * @author Louis Chemineau + * @author Christopher Ng * * @license AGPL-3.0-or-later * @@ -36,6 +20,15 @@ * */ -//! moment.js - -//! moment.js locale configuration +/** + * @param {any} value - + * @param {any} passes - + */ +export function decodeHtmlEntities(value, passes = 1) { + const parser = new DOMParser() + let decoded = value + for (let i = 0; i < passes; i++) { + decoded = parser.parseFromString(decoded, 'text/html').documentElement.textContent + } + return decoded +} diff --git a/src/utils/numberUtil.js b/src/utils/numberUtil.js new file mode 100644 index 000000000..cbbd69640 --- /dev/null +++ b/src/utils/numberUtil.js @@ -0,0 +1,30 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +const isNumber = function(num) { + if (!num) { + return false + } + return Number(num).toString() === num.toString() +} + +export { isNumber } diff --git a/src/views/ActivityTab.vue b/src/views/ActivityTab.vue index 1969e4512..d8142d109 100644 --- a/src/views/ActivityTab.vue +++ b/src/views/ActivityTab.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2021 Louis Chemineau - - @author Louis Chemineau + - @author Stephan Orbaugh - - @license AGPL-3.0-or-later - @@ -22,6 +23,15 @@