diff --git a/Forji/Forji/Views/MergedNotificationsOverviewView.swift b/Forji/Forji/Views/MergedNotificationsOverviewView.swift index f1caa9c..6ae7a68 100644 --- a/Forji/Forji/Views/MergedNotificationsOverviewView.swift +++ b/Forji/Forji/Views/MergedNotificationsOverviewView.swift @@ -56,6 +56,24 @@ struct MergedNotificationsOverviewView: View { Section { ForEach(pagination.items) { tagged in notificationRow(tagged) + .swipeActions(edge: .leading) { + if tagged.item.unread { + Button { + Task { await markAsRead(tagged) } + } label: { + Label("Mark Read", systemImage: "envelope.open") + } + .tint(.blue) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + Task { await dismissNotification(tagged) } + } label: { + Label("Dismiss", systemImage: "xmark") + } + .tint(.gray) + } } if pagination.hasMore { @@ -166,6 +184,48 @@ struct MergedNotificationsOverviewView: View { return TaggedItem.mergeAndDeduplicate(batches: batches, sources: sources) .sorted { $0.item.updatedAt > $1.item.updatedAt } } + + private func markAsRead(_ tagged: TaggedItem) async { + await setNotificationRead(tagged) + } + + private func dismissNotification(_ tagged: TaggedItem) async { + await setNotificationRead(tagged) + } + + private func setNotificationRead(_ tagged: TaggedItem) async { + guard let client = tagged.authService.client else { return } + let service = NotificationService(client: client) + let notification = tagged.item + do { + try await service.markAsRead(id: notification.id) + withAnimation { + if statusFilter == "unread" { + pagination.items.removeAll { $0.id == tagged.id } + } else if let index = pagination.items.firstIndex(where: { $0.id == tagged.id }) { + let updated = NotificationThread( + id: notification.id, + unread: false, + pinned: notification.pinned, + updatedAt: notification.updatedAt, + url: notification.url, + subject: notification.subject, + repository: notification.repository, + ) + pagination.items[index] = TaggedItem( + item: updated, + sourceKey: tagged.sourceKey, + instanceName: tagged.instanceName, + authService: tagged.authService, + ) + } + } + NotificationCenter.default.post(name: .notificationsDidChange, object: nil) + } catch { + pagination.errorMessage = error.localizedDescription + pagination.showError = true + } + } } private struct MergedNotificationRow: View { diff --git a/Forji/ForjiUITests/MergedInstanceUITests.swift b/Forji/ForjiUITests/MergedInstanceUITests.swift index ee4bd75..e255883 100644 --- a/Forji/ForjiUITests/MergedInstanceUITests.swift +++ b/Forji/ForjiUITests/MergedInstanceUITests.swift @@ -124,6 +124,34 @@ final class MergedInstanceUITests: ForgejoUITestBase { XCTAssertTrue(instanceList.waitForExistence(timeout: 10), "Should return to instance list after disconnect") } + @MainActor + func testMergedNotificationSwipeDismiss() throws { + try setupTwoInstances() + enterMergedMode() + + app.tabBars.buttons["Notifications"].tap() + + let notificationsList = app.collectionViews.firstMatch + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + let firstCell = notificationsList.cells.firstMatch + XCTAssertTrue(firstCell.waitForExistence(timeout: 10), "Should have at least one notification") + + // Swipe right to mark as read + firstCell.swipeRight() + let markReadButton = app.buttons["Mark Read"] + XCTAssertTrue(markReadButton.waitForExistence(timeout: 3), "Mark Read swipe action should appear") + markReadButton.tap() + + // Swipe left to dismiss + let nextCell = notificationsList.cells.firstMatch + XCTAssertTrue(nextCell.waitForExistence(timeout: 5), "Should have another notification to dismiss") + nextCell.swipeLeft() + let dismissButton = app.buttons["Dismiss"] + XCTAssertTrue(dismissButton.waitForExistence(timeout: 3), "Dismiss swipe action should appear") + dismissButton.tap() + } + // MARK: - Server URL Helpers private func resolveSecondServerURL() -> String? {