diff --git a/lib/Controller/APIv2Controller.php b/lib/Controller/APIv2Controller.php index 8ac519c10..2270bef69 100644 --- a/lib/Controller/APIv2Controller.php +++ b/lib/Controller/APIv2Controller.php @@ -25,6 +25,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\Notification\IManager as INotificationManager; class APIv2Controller extends OCSController { /** @var string */ @@ -63,6 +64,7 @@ public function __construct( protected IPreview $preview, protected IMimeTypeDetector $mimeTypeDetector, protected ViewInfoCache $infoCache, + protected INotificationManager $notificationManager, ) { parent::__construct($appName, $request); $this->activityManager = $activityManager; @@ -264,6 +266,22 @@ protected function get($filter, $since, $limit, $previews, $filterObjectType, $f $preparedActivities[] = $activity; } + // When viewing activities for a specific file, mark corresponding activity + // notifications as processed so they clear from the notification bell + if ($this->objectType !== '' && $this->objectId !== 0 && !empty($this->user)) { + $deferred = $this->notificationManager->defer(); + foreach ($preparedActivities as $activity) { + $notification = $this->notificationManager->createNotification(); + $notification->setApp($activity['app'] ?? 'activity') + ->setUser($this->user) + ->setObject('activity_notification', (string)$activity['activity_id']); + $this->notificationManager->markProcessed($notification); + } + if ($deferred) { + $this->notificationManager->flush(); + } + } + return new DataResponse($preparedActivities, Http::STATUS_OK, $headers); } diff --git a/tests/Controller/APIv2ControllerTest.php b/tests/Controller/APIv2ControllerTest.php index 8b6fa0452..ebdb1a147 100644 --- a/tests/Controller/APIv2ControllerTest.php +++ b/tests/Controller/APIv2ControllerTest.php @@ -43,6 +43,8 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; @@ -63,6 +65,7 @@ class APIv2ControllerTest extends TestCase { protected IUserSession&MockObject $userSession; protected IMimeTypeDetector&MockObject $mimeTypeDetector; protected ViewInfoCache&MockObject $infoCache; + protected INotificationManager&MockObject $notificationManager; protected IL10N $l10n; protected APIv2Controller $controller; @@ -78,6 +81,10 @@ protected function setUp(): void { $this->userSession = $this->createMock(IUserSession::class); $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class); $this->infoCache = $this->createMock(ViewInfoCache::class); + $this->notificationManager = $this->createMock(INotificationManager::class); + $notification = $this->createMock(INotification::class); + $notification->method($this->anything())->willReturnSelf(); + $this->notificationManager->method('createNotification')->willReturn($notification); $this->request = $this->createMock(IRequest::class); $this->controller = $this->getController(); @@ -96,7 +103,8 @@ protected function getController(array $methods = []): APIv2Controller|MockObjec $this->userSession, $this->preview, $this->mimeTypeDetector, - $this->infoCache + $this->infoCache, + $this->notificationManager, ); } @@ -113,6 +121,7 @@ protected function getController(array $methods = []): APIv2Controller|MockObjec $this->preview, $this->mimeTypeDetector, $this->infoCache, + $this->notificationManager, ]) ->onlyMethods($methods) ->getMock(); @@ -494,6 +503,116 @@ public function testGet(int $time, string $objectType, int $objectId, array $add ], $result->getData()); } + public function testGetMarksActivityNotificationsProcessedForFile(): void { + $controller = $this->getController(['validateParameters', 'generateHeaders']); + $controller->method('generateHeaders')->willReturnArgument(0); + $controller->method('validateParameters'); + + self::invokePrivate($controller, 'objectType', ['files']); + self::invokePrivate($controller, 'objectId', [42]); + self::invokePrivate($controller, 'user', ['user1']); + + $this->data->expects($this->once()) + ->method('get') + ->willReturn([ + 'data' => [ + ['activity_id' => 11, 'app' => 'files', 'timestamp' => 1234567890, 'object_type' => 'files', 'object_id' => 42], + ['activity_id' => 22, 'app' => 'comments', 'timestamp' => 1234567891, 'object_type' => 'files', 'object_id' => 42], + ], + 'headers' => ['ETag' => 'abc'], + 'has_more' => false, + ]); + + $notification = $this->createMock(INotification::class); + $notification->method($this->anything())->willReturnSelf(); + + $this->notificationManager->expects($this->once()) + ->method('defer') + ->willReturn(true); + $this->notificationManager->expects($this->exactly(2)) + ->method('createNotification') + ->willReturn($notification); + $this->notificationManager->expects($this->exactly(2)) + ->method('markProcessed') + ->with($notification); + $this->notificationManager->expects($this->once()) + ->method('flush'); + + $result = self::invokePrivate($controller, 'get', ['all', 0, 50, false, 'files', 42, 'desc']); + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } + + public function testGetSkipsFlushWhenAlreadyDeferred(): void { + $controller = $this->getController(['validateParameters', 'generateHeaders']); + $controller->method('generateHeaders')->willReturnArgument(0); + $controller->method('validateParameters'); + + self::invokePrivate($controller, 'objectType', ['files']); + self::invokePrivate($controller, 'objectId', [42]); + self::invokePrivate($controller, 'user', ['user1']); + + $this->data->expects($this->once()) + ->method('get') + ->willReturn([ + 'data' => [ + ['activity_id' => 11, 'app' => 'files', 'timestamp' => 1234567890, 'object_type' => 'files', 'object_id' => 42], + ], + 'headers' => ['ETag' => 'abc'], + 'has_more' => false, + ]); + + $notification = $this->createMock(INotification::class); + $notification->method($this->anything())->willReturnSelf(); + + $this->notificationManager->expects($this->once()) + ->method('defer') + ->willReturn(false); + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + $this->notificationManager->expects($this->once()) + ->method('markProcessed') + ->with($notification); + $this->notificationManager->expects($this->never()) + ->method('flush'); + + $result = self::invokePrivate($controller, 'get', ['all', 0, 50, false, 'files', 42, 'desc']); + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } + + public function testGetDoesNotMarkNotificationsForGlobalStream(): void { + $controller = $this->getController(['validateParameters', 'generateHeaders']); + $controller->method('generateHeaders')->willReturnArgument(0); + $controller->method('validateParameters'); + + // No objectType/objectId set — global stream + self::invokePrivate($controller, 'objectType', ['']); + self::invokePrivate($controller, 'objectId', [0]); + self::invokePrivate($controller, 'user', ['user1']); + + $this->data->expects($this->once()) + ->method('get') + ->willReturn([ + 'data' => [ + ['activity_id' => 11, 'app' => 'files', 'timestamp' => 1234567890, 'object_type' => 'files', 'object_id' => 42], + ], + 'headers' => ['ETag' => 'abc'], + 'has_more' => false, + ]); + + $this->notificationManager->expects($this->never()) + ->method('defer'); + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + $this->notificationManager->expects($this->never()) + ->method('markProcessed'); + $this->notificationManager->expects($this->never()) + ->method('flush'); + + $result = self::invokePrivate($controller, 'get', ['all', 0, 50, false, '', 0, 'desc']); + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } + public static function dataGetNotModified(): array { return [ [[], null],