Skip to content

Commit

Permalink
feat(comments): Use activity tab to mount comments sidebar section if…
Browse files Browse the repository at this point in the history
… available

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Nov 16, 2023
1 parent 9c3350b commit db2fec1
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 98 deletions.
6 changes: 6 additions & 0 deletions apps/comments/lib/Listener/LoadSidebarScripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

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;
Expand All @@ -36,6 +38,8 @@
class LoadSidebarScripts implements IEventListener {
public function __construct(
private ICommentsManager $commentsManager,
private IInitialState $initialState,
private IAppManager $appManager,
) {
}

Expand All @@ -46,6 +50,8 @@ public function handle(Event $event): void {

$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');
Expand Down
85 changes: 85 additions & 0 deletions apps/comments/src/comments-activity-tab.ts
Original file line number Diff line number Diff line change
@@ -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')
}
79 changes: 46 additions & 33 deletions apps/comments/src/comments-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
5 changes: 4 additions & 1 deletion apps/comments/src/components/Comment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -235,6 +236,8 @@ export default {
},

methods: {
t,

/**
* Update local Message on outer change
*
Expand Down Expand Up @@ -279,7 +282,7 @@ $comment-padding: 10px;

.comment {
display: flex;
gap: 16px;
gap: 8px;
padding: 5px $comment-padding;

&__side {
Expand Down
10 changes: 6 additions & 4 deletions apps/comments/src/mixins/CommentMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -46,6 +47,7 @@ export default {
deleted: false,
editing: false,
loading: false,
commentsType: 'files',
}
},

Expand All @@ -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) {
Expand All @@ -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'))
Expand All @@ -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
Expand Down
68 changes: 68 additions & 0 deletions apps/comments/src/mixins/CommentView.ts
Original file line number Diff line number Diff line change
@@ -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
},
},
})
15 changes: 9 additions & 6 deletions apps/comments/src/services/GetComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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"?>
Expand All @@ -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
Expand Down
Loading

0 comments on commit db2fec1

Please sign in to comment.