Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(comments): Use activity tab to mount comments sidebar section if…
… available

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Nov 16, 2023
commit db2fec1cec8ec490c71eb47180c4cae422bf4f75
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 <[email protected]>
*
* @author Ferdinand Thiessen <[email protected]>
*
* @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