Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(comments): Plug in comments into activity sidebar tab if available #41491

Merged
merged 4 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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,
resourceId: 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({ resourceType: 'files', resourceId: 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,
resourceId: 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
21 changes: 13 additions & 8 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 @@ -35,10 +36,14 @@ export default {
type: String,
default: '',
},
ressourceId: {
resourceId: {
type: [String, Number],
required: true,
},
resourceType: {
type: String,
default: 'files',
},
},

data() {
Expand All @@ -62,8 +67,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) {
Expand All @@ -85,8 +90,8 @@ 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 })
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'))
Expand All @@ -99,8 +104,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
Expand Down
72 changes: 72 additions & 0 deletions apps/comments/src/mixins/CommentView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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',
artonge marked this conversation as resolved.
Show resolved Hide resolved
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
},
},
})
19 changes: 9 additions & 10 deletions apps/comments/src/services/CommentsInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,18 @@ 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 = {}) {
// Merge options and set `resourceType` property
options = {
...options,
artonge marked this conversation as resolved.
Show resolved Hide resolved
propsData: {
...(options.propsData ?? {}),
resourceType,
},
})

}
// Init Comments component
const View = Vue.extend(CommentsApp)
return new View(options)
Expand Down
Loading
Loading