Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/comments/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
return [
'routes' => [
['name' => 'Notifications#view', 'url' => '/notifications/view/{id}', 'verb' => 'GET'],
['name' => 'Notifications#dismiss', 'url' => '/notifications/dismiss/{id}', 'verb' => 'DELETE'],
]
];
32 changes: 32 additions & 0 deletions apps/comments/lib/Controller/NotificationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
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;
Expand Down Expand Up @@ -89,6 +91,36 @@ public function view(string $id): RedirectResponse|NotFoundResponse {
}
}

/**
* Dismiss the mention notification for a comment
*
* @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
*/
#[NoAdminRequired]
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
*/
Expand Down
16 changes: 16 additions & 0 deletions apps/comments/src/comments-activity-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import type { INode } from '@nextcloud/files'
import type { App } from 'vue'

import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import logger from './logger.ts'
Expand Down Expand Up @@ -50,6 +53,19 @@ export function registerCommentsPlugins() {
},
)
logger.debug('Loaded comments', { node, comments })

// 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/dismiss/{id}', { id: comment.props.id }))
.catch(() => {})
}
}
}
const { default: CommentView } = await import('./views/ActivityCommentEntry.vue')

return comments.map((comment) => ({
Expand Down
99 changes: 99 additions & 0 deletions apps/comments/tests/Unit/Controller/NotificationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand Down Expand Up @@ -162,6 +163,104 @@ public function testViewInvalidComment(): void {
$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());
}

public function testViewNoFile(): void {
$comment = $this->createMock(IComment::class);
$comment->expects($this->any())
Expand Down
2 changes: 1 addition & 1 deletion dist/comments-comments-tab.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[window.OC.filePath('', '', 'dist/ActivityCommentAction-P9Iql4ja.chunk.mjs'),window.OC.filePath('', '', 'dist/index-C1xmmKTZ-r7P3d7Rj.chunk.mjs'),window.OC.filePath('', '', 'dist/preload-helper-HEeJwHUd.chunk.mjs'),window.OC.filePath('', '', 'dist/NcDialog-nDc1gW50-C6kKhPt3.chunk.mjs'),window.OC.filePath('', '', 'dist/NcModal-kyWZ3UFC-Bhycn9Yn.chunk.mjs'),window.OC.filePath('', '', 'dist/ArrowRight-b3XAkWmg.chunk.mjs'),window.OC.filePath('', '', 'dist/Web-D3MaWivE.chunk.mjs'),window.OC.filePath('', '', 'dist/translation-DoG5ZELJ-qG-IUmJ9.chunk.mjs'),window.OC.filePath('', '', 'dist/index-Qk7XyFpu.chunk.mjs'),window.OC.filePath('', '', 'dist/public-CvthP4YJ.chunk.mjs'),window.OC.filePath('', '', 'dist/common-Web-C_oBIsvc.chunk.css'),window.OC.filePath('', '', 'dist/common-ArrowRight-D7L4ZBkR.chunk.css'),window.OC.filePath('', '', 'dist/common-NcModal-kyWZ3UFC-DgqchLjq.chunk.css'),window.OC.filePath('', '', 'dist/TrashCanOutline-_KWmPu03.chunk.mjs'),window.OC.filePath('', '', 'dist/common-TrashCanOutline-BYHcrfvW.chunk.css'),window.OC.filePath('', '', 'dist/common-NcDialog-nDc1gW50-DYA_tnKg.chunk.css'),window.OC.filePath('', '', 'dist/mdi-io605rGS.chunk.mjs'),window.OC.filePath('', '', 'dist/common-mdi-Jq77EThs.chunk.css'),window.OC.filePath('', '', 'dist/CommentView-CS_3FLiZ.chunk.mjs'),window.OC.filePath('', '', 'dist/pinia-wb4HskLa.chunk.mjs'),window.OC.filePath('', '', 'dist/PencilOutline-Bf19fWD8.chunk.mjs'),window.OC.filePath('', '', 'dist/common-PencilOutline-DdQinVMt.chunk.css'),window.OC.filePath('', '', 'dist/NcAvatar-ruClKRzS-PI-nYw3k.chunk.mjs'),window.OC.filePath('', '', 'dist/index-hnFmNpnF.chunk.mjs'),window.OC.filePath('', '', 'dist/util-BAw8pNOw.chunk.mjs'),window.OC.filePath('', '', 'dist/colors-BfjxNgsx-DR9TkNfP.chunk.mjs'),window.OC.filePath('', '', 'dist/NcUserStatusIcon-JWiuiAXe-DCJTKIB_.chunk.mjs'),window.OC.filePath('', '', 'dist/common-NcUserStatusIcon-JWiuiAXe-Bq_6hmXG.chunk.css'),window.OC.filePath('', '', 'dist/NcDateTime.vue_vue_type_script_setup_true_lang-B4upiZjL-CEsVOC1q.chunk.mjs'),window.OC.filePath('', '', 'dist/common-NcDateTime-DS-ziNw6.chunk.css'),window.OC.filePath('', '', 'dist/common-NcAvatar-ruClKRzS-CVm1ngoc.chunk.css'),window.OC.filePath('', '', 'dist/NcUserBubble-BE6yD-R0-kBqLvCrF.chunk.mjs'),window.OC.filePath('', '', 'dist/common-NcUserBubble-BE6yD-R0-f2DD9EAL.chunk.css'),window.OC.filePath('', '', 'dist/activity-DTtkYccO.chunk.mjs'),window.OC.filePath('', '', 'dist/GetComments-Dxgh1PGl.chunk.mjs'),window.OC.filePath('', '', 'dist/index-3-MV6QIx.chunk.mjs'),window.OC.filePath('', '', 'dist/common-CommentView-D9eoYnuL.chunk.css'),window.OC.filePath('', '', 'dist/common-NcActionSeparator-Ct2RnclR-pXJ_-D_I.chunk.css'),window.OC.filePath('', '', 'dist/comments-ActivityCommentAction-D5Q48MrJ.chunk.css'),window.OC.filePath('', '', 'dist/ActivityCommentEntry-DrDiWRTT.chunk.mjs'),window.OC.filePath('', '', 'dist/comments-ActivityCommentEntry-CVfnMLz5.chunk.css'),window.OC.filePath('', '', 'dist/FilesSidebarTab-BYnZjnN1.chunk.mjs'),window.OC.filePath('', '', 'dist/NcEmptyContent-CDgWCt_m-CHPr_hW3.chunk.mjs'),window.OC.filePath('', '', 'dist/common-NcEmptyContent-CDgWCt_m-DoZPzs7J.chunk.css'),window.OC.filePath('', '', 'dist/common-FilesSidebarTab-D1FhhmK6.chunk.css')])))=>i.map(i=>d[i]);
import{_ as m,e as c,d as w}from"./preload-helper-HEeJwHUd.chunk.mjs";import{r as _}from"./index-BQA9mlvT.chunk.mjs";import{t as g}from"./translation-DoG5ZELJ-qG-IUmJ9.chunk.mjs";import{c as l}from"./pinia-wb4HskLa.chunk.mjs";import{l as r,i as A}from"./activity-DTtkYccO.chunk.mjs";import{g as v}from"./GetComments-Dxgh1PGl.chunk.mjs";import"./folder-29HuacU_-DFFcqWtL.chunk.mjs";import"./index-Qk7XyFpu.chunk.mjs";import"./util-BAw8pNOw.chunk.mjs";import"./public-CvthP4YJ.chunk.mjs";import"./index-3-MV6QIx.chunk.mjs";const y='<svg xmlns="http://www.w3.org/2000/svg" id="mdi-message-reply-text" viewBox="0 0 24 24"><path d="M18,8H6V6H18V8M18,11H6V9H18V11M18,14H6V12H18V14M22,4A2,2 0 0,0 20,2H4A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H18L22,22V4Z" /></svg>';function V(){let o;window.OCA.Activity.registerSidebarAction({mount:async(t,{node:e,reload:i})=>{const a=l();if(!o){const{default:s}=await m(async()=>{const{default:n}=await import("./ActivityCommentAction-P9Iql4ja.chunk.mjs");return{default:n}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38]),import.meta.url);o=c(s,{reloadCallback:i,resourceId:e.fileid})}o.use(a),o.mount(t),r.info("Comments plugin mounted in Activity sidebar action",{node:e})},unmount:()=>{o?.unmount()}}),window.OCA.Activity.registerSidebarEntries(async({node:t,limit:e,offset:i})=>{const{data:a}=await v({resourceType:"files",resourceId:t.fileid},{limit:e,offset:i??0});r.debug("Loaded comments",{node:t,comments:a});const{default:s}=await m(async()=>{const{default:n}=await import("./ActivityCommentEntry-DrDiWRTT.chunk.mjs");return{default:n}},__vite__mapDeps([39,7,8,18,2,19,20,5,6,9,10,11,4,12,21,22,23,24,25,26,27,28,29,13,14,30,31,32,1,3,15,16,17,33,34,35,36,37,40]),import.meta.url);return a.map(n=>({_CommentsViewInstance:void 0,timestamp:Date.parse(n.props?.creationDateTime??""),mount(f,{reload:p}){const d=c(s,{comment:n,resourceId:t.fileid,reloadCallback:p});d.mount(f),this._CommentsViewInstance=d},unmount(){this._CommentsViewInstance?.unmount()}}))}),window.OCA.Activity.registerSidebarFilter(t=>t.type!=="comments"),r.info("Comments plugin registered for Activity sidebar action")}const u="comments_files-sidebar-tab";A()?window.addEventListener("DOMContentLoaded",function(){V()}):_({id:"comments",displayName:g("comments","Comments"),iconSvgInline:y,order:50,tagName:u,async onInit(){const{default:o}=await m(async()=>{const{default:e}=await import("./FilesSidebarTab-BYnZjnN1.chunk.mjs").then(i=>i.F);return{default:e}},__vite__mapDeps([41,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,22,23,24,25,26,27,20,21,28,29,30,42,43,18,19,31,32,33,34,35,36,37,44]),import.meta.url),t=w(o,{configureApp(e){const i=l();e.use(i)},shadowRoot:!1});window.customElements.define(u,t)}});
import{_ as u,e as l,d as _}from"./preload-helper-HEeJwHUd.chunk.mjs";import{r as g}from"./index-BQA9mlvT.chunk.mjs";import{t as v}from"./translation-DoG5ZELJ-qG-IUmJ9.chunk.mjs";import{c as w}from"./pinia-wb4HskLa.chunk.mjs";import{a as y,b as A}from"./index-Qk7XyFpu.chunk.mjs";import{c as V}from"./index-hnFmNpnF.chunk.mjs";import{l as c,i as b}from"./activity-DTtkYccO.chunk.mjs";import{g as C}from"./GetComments-Dxgh1PGl.chunk.mjs";import"./folder-29HuacU_-DFFcqWtL.chunk.mjs";import"./util-BAw8pNOw.chunk.mjs";import"./public-CvthP4YJ.chunk.mjs";import"./index-3-MV6QIx.chunk.mjs";const I='<svg xmlns="http://www.w3.org/2000/svg" id="mdi-message-reply-text" viewBox="0 0 24 24"><path d="M18,8H6V6H18V8M18,11H6V9H18V11M18,14H6V12H18V14M22,4A2,2 0 0,0 20,2H4A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H18L22,22V4Z" /></svg>';function E(){let i;window.OCA.Activity.registerSidebarAction({mount:async(t,{node:e,reload:n})=>{const s=w();if(!i){const{default:a}=await u(async()=>{const{default:r}=await import("./ActivityCommentAction-P9Iql4ja.chunk.mjs");return{default:r}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38]),import.meta.url);i=l(a,{reloadCallback:n,resourceId:e.fileid})}i.use(s),i.mount(t),c.info("Comments plugin mounted in Activity sidebar action",{node:e})},unmount:()=>{i?.unmount()}}),window.OCA.Activity.registerSidebarEntries(async({node:t,limit:e,offset:n})=>{const{data:s}=await C({resourceType:"files",resourceId:t.fileid},{limit:e,offset:n??0});c.debug("Loaded comments",{node:t,comments:s});const a=y();if(a)for(const o of s){const d=Object.values(o.props?.mentions??{});o.props?.id&&d.some(m=>m.mentionType==="user"&&m.mentionId===a.uid)&&V.delete(A("/apps/comments/notifications/dismiss/{id}",{id:o.props.id})).catch(()=>{})}const{default:r}=await u(async()=>{const{default:o}=await import("./ActivityCommentEntry-DrDiWRTT.chunk.mjs");return{default:o}},__vite__mapDeps([39,7,8,18,2,19,20,5,6,9,10,11,4,12,21,22,23,24,25,26,27,28,29,13,14,30,31,32,1,3,15,16,17,33,34,35,36,37,40]),import.meta.url);return s.map(o=>({_CommentsViewInstance:void 0,timestamp:Date.parse(o.props?.creationDateTime??""),mount(d,{reload:m}){const p=l(r,{comment:o,resourceId:t.fileid,reloadCallback:m});p.mount(d),this._CommentsViewInstance=p},unmount(){this._CommentsViewInstance?.unmount()}}))}),window.OCA.Activity.registerSidebarFilter(t=>t.type!=="comments"),c.info("Comments plugin registered for Activity sidebar action")}const f="comments_files-sidebar-tab";b()?window.addEventListener("DOMContentLoaded",function(){E()}):g({id:"comments",displayName:v("comments","Comments"),iconSvgInline:I,order:50,tagName:f,async onInit(){const{default:i}=await u(async()=>{const{default:e}=await import("./FilesSidebarTab-BYnZjnN1.chunk.mjs").then(n=>n.F);return{default:e}},__vite__mapDeps([41,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,22,23,24,25,26,27,20,21,28,29,30,42,43,18,19,31,32,33,34,35,36,37,44]),import.meta.url),t=_(i,{configureApp(e){const n=w();e.use(n)},shadowRoot:!1});window.customElements.define(f,t)}});
//# sourceMappingURL=comments-comments-tab.mjs.map
Loading
Loading