mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 16:26:59 -04:00
fix(comments): mark comments as read and dismiss notifications in Activity sidebar
Backport of #60617 to stable32. When the Activity app integration is active, comments are loaded in the Activity sidebar instead of the Comments tab. The read marker and mention notification dismissal were never triggered in this path, leaving the unread bubble in the file list and keeping mention notifications active. Also adds a new DELETE /notifications/{id} endpoint to dismiss individual mention notifications by comment ID. AI-Assisted-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
parent
d0c2d97202
commit
4aac879e21
7 changed files with 171 additions and 3 deletions
|
|
@ -8,5 +8,6 @@
|
|||
return [
|
||||
'routes' => [
|
||||
['name' => 'Notifications#view', 'url' => '/notifications/view/{id}', 'verb' => 'GET'],
|
||||
['name' => 'Notifications#dismiss', 'url' => '/notifications/{id}', 'verb' => 'DELETE'],
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use OCP\AppFramework\Http;
|
|||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\Comments\IComment;
|
||||
|
|
@ -92,6 +93,37 @@ class NotificationsController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the mention notification for a comment
|
||||
*
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $id ID of the comment
|
||||
*
|
||||
* @return DataResponse<Http::STATUS_OK, array{}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{}, array{}>
|
||||
*
|
||||
* 200: Notification dismissed successfully
|
||||
* 403: Not logged in
|
||||
* 404: Comment not found
|
||||
*/
|
||||
public function dismiss(string $id): DataResponse {
|
||||
$currentUser = $this->userSession->getUser();
|
||||
if (!$currentUser instanceof IUser) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
try {
|
||||
$comment = $this->commentsManager->get($id);
|
||||
if ($comment->getObjectType() !== 'files') {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
$this->markProcessed($comment, $currentUser);
|
||||
return new DataResponse([]);
|
||||
} catch (\Exception $e) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the notification about a comment as processed
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import Vue, { type ComponentPublicInstance } from 'vue'
|
||||
import logger from './logger.js'
|
||||
import { getComments } from './services/GetComments.js'
|
||||
import { markCommentsAsRead } from './services/ReadComments.js'
|
||||
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia'
|
||||
|
||||
|
|
@ -48,6 +52,31 @@ export function registerCommentsPlugins() {
|
|||
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 })
|
||||
|
||||
// Optimistically clear the unread bubble immediately via the global event bus
|
||||
// (window._nc_event_bus) so the UI updates without a page refresh.
|
||||
// fileInfo.node is the underlying @nextcloud/files Node set by the Files sidebar.
|
||||
const node = fileInfo.node
|
||||
if (node) {
|
||||
node.attributes['comments-unread'] = 0
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(window as any)._nc_event_bus?.emit('files:node:updated', node)
|
||||
}
|
||||
markCommentsAsRead('files', fileInfo.id, new Date()).catch(() => {})
|
||||
|
||||
// Mark mention notifications as read for comments that mention the current user
|
||||
const currentUser = getCurrentUser()
|
||||
if (currentUser) {
|
||||
for (const comment of comments) {
|
||||
const mentions = Object.values(comment.props?.mentions ?? {}) as { mentionType: string, mentionId: string }[]
|
||||
const isMentioned = comment.props?.id && mentions.some((m) => m.mentionType === 'user' && m.mentionId === currentUser.uid)
|
||||
if (isMentioned) {
|
||||
axios.delete(generateUrl('/apps/comments/notifications/{id}', { id: comment.props.id }))
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { default: CommentView } = await import('./views/ActivityCommentEntry.vue')
|
||||
// @ts-expect-error Types are broken for Vue2
|
||||
const CommentsViewObject = Vue.extend(CommentView)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
namespace OCA\Comments\Tests\Unit\Controller;
|
||||
|
||||
use OCA\Comments\Controller\NotificationsController;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\Comments\IComment;
|
||||
|
|
@ -211,4 +212,102 @@ class NotificationsTest extends TestCase {
|
|||
$response = $this->notificationsController->view('42');
|
||||
$this->assertInstanceOf(NotFoundResponse::class, $response);
|
||||
}
|
||||
|
||||
public function testDismissNotLoggedIn(): void {
|
||||
$this->session->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn(null);
|
||||
|
||||
$this->commentsManager->expects($this->never())
|
||||
->method('get');
|
||||
$this->notificationManager->expects($this->never())
|
||||
->method('markProcessed');
|
||||
|
||||
$response = $this->notificationsController->dismiss('42');
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame(403, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testDismissSuccess(): void {
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->any())
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
$comment->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn('1234');
|
||||
|
||||
$this->commentsManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('42')
|
||||
->willReturn($comment);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->session->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->any())
|
||||
->method($this->anything())
|
||||
->willReturn($notification);
|
||||
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('createNotification')
|
||||
->willReturn($notification);
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('markProcessed')
|
||||
->with($notification);
|
||||
|
||||
$response = $this->notificationsController->dismiss('42');
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame(200, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testDismissInvalidComment(): void {
|
||||
$this->commentsManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('42')
|
||||
->willThrowException(new NotFoundException());
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->session->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$this->notificationManager->expects($this->never())
|
||||
->method('markProcessed');
|
||||
|
||||
$response = $this->notificationsController->dismiss('42');
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame(404, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testDismissNonFileComment(): void {
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->any())
|
||||
->method('getObjectType')
|
||||
->willReturn('calendar');
|
||||
|
||||
$this->commentsManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('42')
|
||||
->willReturn($comment);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->session->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$this->notificationManager->expects($this->never())
|
||||
->method('markProcessed');
|
||||
|
||||
$response = $this->notificationsController->dismiss('42');
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame(404, $response->getStatus());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
dist/comments-comments-tab.js
vendored
4
dist/comments-comments-tab.js
vendored
File diff suppressed because one or more lines are too long
7
dist/comments-comments-tab.js.license
vendored
7
dist/comments-comments-tab.js.license
vendored
|
|
@ -21,6 +21,7 @@ SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
|
|||
SPDX-FileCopyrightText: Olivier Scherrer <pode.fr@gmail.com>
|
||||
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-FileCopyrightText: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)
|
||||
SPDX-FileCopyrightText: Matt Zabriskie
|
||||
SPDX-FileCopyrightText: Mathias Bynens
|
||||
SPDX-FileCopyrightText: Julian Gruber
|
||||
SPDX-FileCopyrightText: Joyent
|
||||
|
|
@ -48,6 +49,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- @nextcloud/auth
|
||||
- version: 2.6.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/axios
|
||||
- version: 2.5.2
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/browser-storage
|
||||
- version: 0.5.0
|
||||
- license: GPL-3.0-or-later
|
||||
|
|
@ -81,6 +85,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- available-typed-arrays
|
||||
- version: 1.0.7
|
||||
- license: MIT
|
||||
- axios
|
||||
- version: 1.13.4
|
||||
- license: MIT
|
||||
- balanced-match
|
||||
- version: 1.0.2
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/comments-comments-tab.js.map
vendored
2
dist/comments-comments-tab.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue