Skip to content

Commit 6866267

Browse files
committed
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>
1 parent d0c2d97 commit 6866267

7 files changed

Lines changed: 163 additions & 3 deletions

File tree

apps/comments/appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
return [
99
'routes' => [
1010
['name' => 'Notifications#view', 'url' => '/notifications/view/{id}', 'verb' => 'GET'],
11+
['name' => 'Notifications#dismiss', 'url' => '/notifications/{id}', 'verb' => 'DELETE'],
1112
]
1213
];

apps/comments/lib/Controller/NotificationsController.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
1212
use OCP\AppFramework\Http\Attribute\OpenAPI;
1313
use OCP\AppFramework\Http\Attribute\PublicPage;
14+
use OCP\AppFramework\Http\DataResponse;
1415
use OCP\AppFramework\Http\NotFoundResponse;
1516
use OCP\AppFramework\Http\RedirectResponse;
1617
use OCP\Comments\IComment;
@@ -92,6 +93,37 @@ public function view(string $id): RedirectResponse|NotFoundResponse {
9293
}
9394
}
9495

96+
/**
97+
* Dismiss the mention notification for a comment
98+
*
99+
* @NoAdminRequired
100+
*
101+
* @param string $id ID of the comment
102+
*
103+
* @return DataResponse<Http::STATUS_OK, array{}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{}, array{}>
104+
*
105+
* 200: Notification dismissed successfully
106+
* 403: Not logged in
107+
* 404: Comment not found
108+
*/
109+
public function dismiss(string $id): DataResponse {
110+
$currentUser = $this->userSession->getUser();
111+
if (!$currentUser instanceof IUser) {
112+
return new DataResponse([], Http::STATUS_FORBIDDEN);
113+
}
114+
115+
try {
116+
$comment = $this->commentsManager->get($id);
117+
if ($comment->getObjectType() !== 'files') {
118+
return new DataResponse([], Http::STATUS_NOT_FOUND);
119+
}
120+
$this->markProcessed($comment, $currentUser);
121+
return new DataResponse([]);
122+
} catch (\Exception $e) {
123+
return new DataResponse([], Http::STATUS_NOT_FOUND);
124+
}
125+
}
126+
95127
/**
96128
* Marks the notification about a comment as processed
97129
*/

apps/comments/src/comments-activity-tab.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5+
import { getCurrentUser } from '@nextcloud/auth'
6+
import axios from '@nextcloud/axios'
57
import moment from '@nextcloud/moment'
8+
import { generateUrl } from '@nextcloud/router'
69
import Vue, { type ComponentPublicInstance } from 'vue'
710
import logger from './logger.js'
811
import { getComments } from './services/GetComments.js'
12+
import { markCommentsAsRead } from './services/ReadComments.js'
913

1014
import { PiniaVuePlugin, createPinia } from 'pinia'
1115

@@ -48,6 +52,23 @@ export function registerCommentsPlugins() {
4852
window.OCA.Activity.registerSidebarEntries(async ({ fileInfo, limit, offset }) => {
4953
const { data: comments } = await getComments({ resourceType: 'files', resourceId: fileInfo.id }, { limit, offset })
5054
logger.debug('Loaded comments', { fileInfo, comments })
55+
56+
// Mark all comments as read to clear the unread badge in the files list
57+
markCommentsAsRead('files', fileInfo.id, new Date()).catch(() => {})
58+
59+
// Mark mention notifications as read for comments that mention the current user
60+
const currentUser = getCurrentUser()
61+
if (currentUser) {
62+
for (const comment of comments) {
63+
const mentions = Object.values(comment.props?.mentions ?? {}) as { mentionType: string, mentionId: string }[]
64+
const isMentioned = comment.props?.id && mentions.some((m) => m.mentionType === 'user' && m.mentionId === currentUser.uid)
65+
if (isMentioned) {
66+
axios.delete(generateUrl('/apps/comments/notifications/{id}', { id: comment.props.id }))
67+
.catch(() => {})
68+
}
69+
}
70+
}
71+
5172
const { default: CommentView } = await import('./views/ActivityCommentEntry.vue')
5273
// @ts-expect-error Types are broken for Vue2
5374
const CommentsViewObject = Vue.extend(CommentView)

apps/comments/tests/Unit/Controller/NotificationsTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OCA\Comments\Tests\Unit\Controller;
1111

1212
use OCA\Comments\Controller\NotificationsController;
13+
use OCP\AppFramework\Http\DataResponse;
1314
use OCP\AppFramework\Http\NotFoundResponse;
1415
use OCP\AppFramework\Http\RedirectResponse;
1516
use OCP\Comments\IComment;
@@ -211,4 +212,102 @@ public function testViewNoFile(): void {
211212
$response = $this->notificationsController->view('42');
212213
$this->assertInstanceOf(NotFoundResponse::class, $response);
213214
}
215+
216+
public function testDismissNotLoggedIn(): void {
217+
$this->session->expects($this->once())
218+
->method('getUser')
219+
->willReturn(null);
220+
221+
$this->commentsManager->expects($this->never())
222+
->method('get');
223+
$this->notificationManager->expects($this->never())
224+
->method('markProcessed');
225+
226+
$response = $this->notificationsController->dismiss('42');
227+
$this->assertInstanceOf(DataResponse::class, $response);
228+
$this->assertSame(403, $response->getStatus());
229+
}
230+
231+
public function testDismissSuccess(): void {
232+
$comment = $this->createMock(IComment::class);
233+
$comment->expects($this->any())
234+
->method('getObjectType')
235+
->willReturn('files');
236+
$comment->expects($this->any())
237+
->method('getId')
238+
->willReturn('1234');
239+
240+
$this->commentsManager->expects($this->once())
241+
->method('get')
242+
->with('42')
243+
->willReturn($comment);
244+
245+
$user = $this->createMock(IUser::class);
246+
$user->expects($this->any())
247+
->method('getUID')
248+
->willReturn('user');
249+
250+
$this->session->expects($this->once())
251+
->method('getUser')
252+
->willReturn($user);
253+
254+
$notification = $this->createMock(INotification::class);
255+
$notification->expects($this->any())
256+
->method($this->anything())
257+
->willReturn($notification);
258+
259+
$this->notificationManager->expects($this->once())
260+
->method('createNotification')
261+
->willReturn($notification);
262+
$this->notificationManager->expects($this->once())
263+
->method('markProcessed')
264+
->with($notification);
265+
266+
$response = $this->notificationsController->dismiss('42');
267+
$this->assertInstanceOf(DataResponse::class, $response);
268+
$this->assertSame(200, $response->getStatus());
269+
}
270+
271+
public function testDismissInvalidComment(): void {
272+
$this->commentsManager->expects($this->once())
273+
->method('get')
274+
->with('42')
275+
->willThrowException(new NotFoundException());
276+
277+
$user = $this->createMock(IUser::class);
278+
$this->session->expects($this->once())
279+
->method('getUser')
280+
->willReturn($user);
281+
282+
$this->notificationManager->expects($this->never())
283+
->method('markProcessed');
284+
285+
$response = $this->notificationsController->dismiss('42');
286+
$this->assertInstanceOf(DataResponse::class, $response);
287+
$this->assertSame(404, $response->getStatus());
288+
}
289+
290+
public function testDismissNonFileComment(): void {
291+
$comment = $this->createMock(IComment::class);
292+
$comment->expects($this->any())
293+
->method('getObjectType')
294+
->willReturn('calendar');
295+
296+
$this->commentsManager->expects($this->once())
297+
->method('get')
298+
->with('42')
299+
->willReturn($comment);
300+
301+
$user = $this->createMock(IUser::class);
302+
$this->session->expects($this->once())
303+
->method('getUser')
304+
->willReturn($user);
305+
306+
$this->notificationManager->expects($this->never())
307+
->method('markProcessed');
308+
309+
$response = $this->notificationsController->dismiss('42');
310+
$this->assertInstanceOf(DataResponse::class, $response);
311+
$this->assertSame(404, $response->getStatus());
312+
}
214313
}

0 commit comments

Comments
 (0)