Skip to main content

Mark Notification Tray Seen

Global Tray Tracking

Update the tray’s seen timestamp on server when user visits the Notification Tray screen

Badge Management

Control UI badge indicators based on global seen status

Overview

Use markNotificationTraySeen() to update the tray’s seen timestamp on the server when the user visits the Notification Tray screen. This method supports global-level read tracking, separate from per-item seen state. Once invoked, future calls to getNotificationTraySeen() will return the new isSeen value. It is recommended to call this method as soon as the tray UI is opened.
This method updates the global tray seen state, not individual notification items. Use markSeen() on individual items for fine-grained tracking.

Implementation Guide

import AmitySDK

class NotificationTrayViewController: UIViewController {
    private let repository = AmityNotificationTrayRepository()
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // Mark tray as seen when user opens it
        markTrayAsSeen()
    }
    
    private func markTrayAsSeen() {
        repository.markNotificationTraySeen { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success:
                    self?.updateGlobalBadgeState()
                case .failure(let error):
                    print("Failed to mark tray as seen: \(error)")
                }
            }
        }
    }
    
    private func updateGlobalBadgeState() {
        // Update global app badge
        NotificationCenter.default.post(
            name: .notificationTrayMarkedAsSeen,
            object: nil
        )
    }
}

Usage Patterns

Screen Visibility

Primary Triggers
  • User opens/navigates to notification tray screen
  • Tray becomes visible in UI (viewDidAppear, onResume)
  • User focuses on notification tray tab/section

User Intent

Secondary Triggers
  • User pulls-to-refresh in tray
  • User explicitly marks β€œall as read”
  • User interacts with tray controls

Implementation Timing Examples

// Option 1: Mark when view appears (recommended)
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    markTrayAsSeen()
}

// Option 2: Mark with slight delay for better UX
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.markTrayAsSeen()
    }
}

// Option 3: Mark when user scrolls (indicating engagement)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if !hasMarkedAsSeen && scrollView.contentOffset.y > 50 {
        hasMarkedAsSeen = true
        markTrayAsSeen()
    }
}

Global Badge Management Flow

Cross-screen Badge Updates

Tab Bar Badges

Update navigation tab indicators when tray is marked as seen

App Icon Badge

Clear app icon badge count when global tray is seen

Header Indicators

Update header notification bells and counters

Menu Badges

Clear sidebar or menu notification indicators

Before and After Visual States

/* Notification indicators showing unseen state */
.notification-badge {
    display: inline-block;
    background-color: #ef4444;
    color: white;
    border-radius: 50%;
    width: 8px;
    height: 8px;
}

.tab-item.has-notifications::after {
    content: '';
    position: absolute;
    top: 4px;
    right: 4px;
    width: 6px;
    height: 6px;
    background-color: #ef4444;
    border-radius: 50%;
}

Error Handling

Network Connectivity

Handle offline scenarios gracefully without blocking UI

Concurrent Operations

Prevent multiple simultaneous mark-seen operations

Authentication Issues

Handle token expiration during badge updates

State Inconsistency

Manage UI state when server operations fail
class NotificationTrayErrorHandler {
    private let repository = AmityNotificationTrayRepository()
    private var retryAttempts = 0
    private let maxRetryAttempts = 3
    
    func markTraySeenWithErrorHandling(completion: @escaping (Bool) -> Void) {
        repository.markNotificationTraySeen { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success:
                    self?.retryAttempts = 0
                    completion(true)
                    
                case .failure(let error):
                    self?.handleMarkSeenError(error, completion: completion)
                }
            }
        }
    }
    
    private func handleMarkSeenError(_ error: Error, completion: @escaping (Bool) -> Void) {
        retryAttempts += 1
        
        if let amityError = error as? AmityError {
            switch amityError.code {
            case .networkError:
                if retryAttempts < maxRetryAttempts {
                    // Exponential backoff retry
                    let delay = pow(2.0, Double(retryAttempts))
                    DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
                        self?.markTraySeenWithErrorHandling(completion: completion)
                    }
                } else {
                    // Fail silently but log error - don't block UI
                    print("Failed to mark tray as seen after \(maxRetryAttempts) attempts")
                    completion(false)
                }
                
            case .permissionDenied:
                // Handle authentication issues
                handleAuthenticationError()
                completion(false)
                
            default:
                // For other errors, fail silently
                print("Error marking tray as seen: \(error)")
                completion(false)
            }
        }
    }
    
    private func handleAuthenticationError() {
        // Trigger re-authentication flow
        NotificationCenter.default.post(name: .authenticationRequired, object: nil)
    }
}

Best Practices

Timing

Call Timing
  • As soon as tray UI becomes visible
  • On screen/tab focus, not just app launch
  • Consider slight delay for better UX
  • Avoid calling multiple times for same session

Error Handling

Failure Management
  • Fail silently to avoid blocking UI
  • Implement exponential backoff for retries
  • Log errors for debugging purposes
  • Don’t show error dialogs for this operation
  • Debounce Calls: Prevent multiple rapid calls during screen transitions
  • Cache State: Store last known seen state locally for immediate UI updates
  • Background Processing: Perform operation off main thread where possible
  • Batch Operations: Combine with other notification operations when feasible
  • Immediate Feedback: Update UI badges immediately, sync with server async
  • Smooth Animations: Use transitions when hiding badge indicators
  • Progressive Enhancement: UI should work even if operation fails
  • Accessibility: Announce seen state changes to screen readers
  • Authentication: Verify user is authenticated before calling
  • Rate Limiting: Implement client-side rate limiting to prevent abuse
  • Privacy: Handle seen states according to privacy policies
  • Logging: Avoid logging sensitive notification data