Get Notification Tray Seen
Unseen Status Detection Determine whether there are unseen notifications in the tray
Cross-device Sync Manage seen status across multiple client devices
Overview
The getNotificationTraySeen() method allows your app to determine whether there are unseen notifications by retrieving the current isSeen status of the notification tray.
This is particularly useful for reflecting read/unread indicators in the UI—such as toggling a bell icon badge—based on whether new notifications have arrived since the tray was last viewed.
The seen status is managed on the server and may be affected by actions from other devices. However, the state is not updated via real-time events , and thus requires manual refresh to stay current.
Refresh Strategies
🔁 On-demand Refresh (Recommended)
The preferred approach for maintaining accurate seen status across clients:
UI-Triggered Updates Invoke getNotificationTraySeen() whenever the Notification Tray UI is refreshed
Pull-to-Refresh Implement pull-to-refresh gestures or dedicated refresh buttons for user-initiated updates
Implementation Pattern Basic Implementation
Advanced with Pull-to-Refresh
class NotificationTrayViewController : UIViewController {
private let repository = AmityNotificationTrayRepository ()
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear (animated)
refreshTraySeenStatus ()
}
private func refreshTraySeenStatus () {
repository. getNotificationTraySeen { [ weak self ] result in
DispatchQueue. main . async {
switch result {
case . success ( let trayData) :
self ? . updateBadgeIndicator ( hasUnseen : ! trayData. isSeen )
case . failure ( let error) :
self ? . handleError (error)
}
}
}
}
}
Basic Implementation
Advanced with SwipeRefreshLayout
class NotificationTrayActivity : AppCompatActivity () {
private lateinit var repository: AmityNotificationTrayRepository
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
repository = AmityCoreClient. newNotificationRepository ()
}
override fun onResume () {
super . onResume ()
refreshTraySeenStatus ()
}
private fun refreshTraySeenStatus () {
repository. getNotificationTraySeen ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe ({ trayData ->
updateBadgeIndicator ( ! trayData.isSeen)
}, { error ->
handleError (error)
})
}
}
Basic Implementation
React Component with Refresh
class NotificationTrayManager {
private repository : AmityNotificationTrayRepository ;
constructor () {
this . repository = new AmityNotificationTrayRepository ();
}
async refreshTraySeenStatus () : Promise < void > {
try {
const trayData = await this . repository . getNotificationTraySeen ();
this . updateBadgeIndicator ( ! trayData . isSeen );
} catch ( error ) {
this . handleError ( error );
}
}
// Call this when tray component mounts or becomes visible
async onTrayVisible () : Promise < void > {
await this . refreshTraySeenStatus ();
}
}
⏱️ Polling Strategy (Use Sparingly)
Polling should be avoided unless absolutely necessary due to server rate limiting risks and battery drain concerns.
If polling is required:
Minimum interval : 120 seconds or more
Use case : Critical real-time applications only
Implementation : Include proper error handling and exponential backoff
class NotificationPollingManager {
private let repository = AmityNotificationTrayRepository ()
private var timer: Timer ?
private let pollingInterval: TimeInterval = 120.0 // 2 minutes minimum
func startPolling () {
stopPolling () // Ensure no duplicate timers
timer = Timer. scheduledTimer ( withTimeInterval : pollingInterval, repeats : true ) { [ weak self ] _ in
self ? . pollTrayStatus ()
}
}
func stopPolling () {
timer ? . invalidate ()
timer = nil
}
private func pollTrayStatus () {
repository. getNotificationTraySeen { [ weak self ] result in
switch result {
case . success ( let trayData) :
DispatchQueue. main . async {
self ? . updateBadgeIndicator ( ! trayData. isSeen )
}
case . failure ( let error) :
// Consider exponential backoff on errors
print ( "Polling error: \( error ) " )
}
}
}
deinit {
stopPolling ()
}
}
class NotificationPollingManager {
private val repository = AmityCoreClient. newNotificationRepository ()
private val handler = Handler (Looper. getMainLooper ())
private val pollingInterval = 120_000L // 2 minutes minimum
private var pollingRunnable: Runnable ? = null
fun startPolling () {
stopPolling ()
pollingRunnable = object : Runnable {
override fun run () {
pollTrayStatus ()
handler. postDelayed ( this , pollingInterval)
}
}
handler. post (pollingRunnable !! )
}
fun stopPolling () {
pollingRunnable?. let { handler. removeCallbacks (it) }
pollingRunnable = null
}
private fun pollTrayStatus () {
repository. getNotificationTraySeen ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe ({ trayData ->
updateBadgeIndicator ( ! trayData.isSeen)
}, { error ->
// Consider exponential backoff on errors
Log. e ( "NotificationPolling" , "Polling error" , error)
})
}
}
class NotificationPollingManager {
private repository : AmityNotificationTrayRepository ;
private pollingInterval : number = 120000 ; // 2 minutes minimum
private intervalId : NodeJS . Timeout | null = null ;
constructor () {
this . repository = new AmityNotificationTrayRepository ();
}
startPolling () : void {
this . stopPolling ();
this . intervalId = setInterval ( async () => {
await this . pollTrayStatus ();
}, this . pollingInterval );
}
stopPolling () : void {
if ( this . intervalId ) {
clearInterval ( this . intervalId );
this . intervalId = null ;
}
}
private async pollTrayStatus () : Promise < void > {
try {
const trayData = await this . repository . getNotificationTraySeen ();
this . updateBadgeIndicator ( ! trayData . isSeen );
} catch ( error ) {
// Consider exponential backoff on errors
console . error ( 'Polling error:' , error );
}
}
}
Cross-device Update Behavior
🔄 Local vs Cross-device Updates
Understanding how seen status synchronizes across different scenarios:
Same Client Updates When markNotificationTraySeen() is called on the same client , the isSeen value updates immediately if LiveObject is being observed
Cross-device Updates No real-time sync between devices. Manual getNotificationTraySeen() call required to retrieve updated state
🔗 Synchronization Patterns
Scenario 1: Same Device Update Scenario 2: Cross-device Update
📱 Multi-device Implementation
Cross-device Sync Handler
class CrossDeviceSyncManager {
private let repository = AmityNotificationTrayRepository ()
// Call this when app becomes active
func handleAppDidBecomeActive () {
refreshFromServer ()
}
// Call this when returning from background
func handleAppWillEnterForeground () {
refreshFromServer ()
}
private func refreshFromServer () {
repository. getNotificationTraySeen { [ weak self ] result in
DispatchQueue. main . async {
switch result {
case . success ( let trayData) :
self ? . syncLocalState ( with : trayData)
case . failure ( let error) :
self ? . handleSyncError (error)
}
}
}
}
private func syncLocalState ( with trayData : AmityNotificationTraySeen) {
// Update UI badges, counters, etc.
NotificationCenter. default . post (
name : . notificationTrayStateChanged ,
object : nil ,
userInfo : [ "hasUnseen" : ! trayData. isSeen ]
)
}
}
Cross-device Sync Handler
class CrossDeviceSyncManager ( private val context: Context ) {
private val repository = AmityCoreClient. newNotificationRepository ()
// Call this in onResume() of main activities
fun handleAppResume () {
refreshFromServer ()
}
// Call this when app returns from background
fun handleAppForeground () {
refreshFromServer ()
}
private fun refreshFromServer () {
repository. getNotificationTraySeen ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe ({ trayData ->
syncLocalState (trayData)
}, { error ->
handleSyncError (error)
})
}
private fun syncLocalState (trayData: AmityNotificationTraySeen ) {
// Update UI badges, counters, etc.
val intent = Intent (ACTION_NOTIFICATION_TRAY_CHANGED)
intent. putExtra ( "hasUnseen" , ! trayData.isSeen)
LocalBroadcastManager. getInstance (context). sendBroadcast (intent)
}
companion object {
const val ACTION_NOTIFICATION_TRAY_CHANGED = "com.app.notification_tray_changed"
}
}
Cross-device Sync Handler
class CrossDeviceSyncManager {
private repository : AmityNotificationTrayRepository ;
private eventEmitter : EventTarget ;
constructor () {
this . repository = new AmityNotificationTrayRepository ();
this . eventEmitter = new EventTarget ();
this . setupVisibilityHandlers ();
}
private setupVisibilityHandlers () : void {
// Refresh when tab becomes visible
document . addEventListener ( 'visibilitychange' , () => {
if ( ! document . hidden ) {
this . refreshFromServer ();
}
});
// Refresh when window gains focus
window . addEventListener ( 'focus' , () => {
this . refreshFromServer ();
});
}
private async refreshFromServer () : Promise < void > {
try {
const trayData = await this . repository . getNotificationTraySeen ();
this . syncLocalState ( trayData );
} catch ( error ) {
this . handleSyncError ( error );
}
}
private syncLocalState ( trayData : AmityNotificationTraySeen ) : void {
// Emit custom event for components to listen
const event = new CustomEvent ( 'notificationTrayChanged' , {
detail: { hasUnseen: ! trayData . isSeen }
});
this . eventEmitter . dispatchEvent ( event );
}
// Allow components to subscribe to state changes
onStateChange ( callback : ( hasUnseen : boolean ) => void ) : void {
this . eventEmitter . addEventListener ( 'notificationTrayChanged' , ( event : any ) => {
callback ( event . detail . hasUnseen );
});
}
}
Error Handling
Network Connectivity Handle offline scenarios gracefully with cached states
Rate Limiting Implement exponential backoff for frequent API calls
Authentication Handle token expiration and re-authentication flows
Server Errors Provide fallback UI states for server-side issues
Error Handling Implementation
Comprehensive Error Handling
class NotificationTrayErrorHandler {
private let repository = AmityNotificationTrayRepository ()
private var retryCount = 0
private let maxRetries = 3
func getNotificationTraySeenWithRetry ( completion : @escaping (Result<AmityNotificationTraySeen, Error >) -> Void ) {
repository. getNotificationTraySeen { [ weak self ] result in
switch result {
case . success ( let trayData) :
self ? . retryCount = 0
completion (. success (trayData))
case . failure ( let error) :
self ? . handleError (error, completion : completion)
}
}
}
private func handleError ( _ error : Error , completion : @escaping (Result<AmityNotificationTraySeen, Error >) -> Void ) {
guard retryCount < maxRetries else {
completion (. failure (error))
return
}
let delay = pow ( 2.0 , Double (retryCount)) // Exponential backoff
retryCount += 1
DispatchQueue. main . asyncAfter ( deadline : . now () + delay) { [ weak self ] in
self ? . getNotificationTraySeenWithRetry ( completion : completion)
}
}
}
Comprehensive Error Handling
class NotificationTrayErrorHandler {
private val repository = AmityCoreClient. newNotificationRepository ()
private var retryCount = 0
private val maxRetries = 3
fun getNotificationTraySeenWithRetry (): Single < AmityNotificationTraySeen > {
return repository. getNotificationTraySeen ()
. retryWhen { errors ->
errors. zipWith (Observable. range ( 1 , maxRetries)) { error, attempt ->
val delay = ( 2.0 . pow (attempt. toDouble ())). toLong ()
Observable. timer (delay, TimeUnit.SECONDS)
}. flatMap { it }
}
. doOnError { error ->
handleError (error)
}
}
private fun handleError (error: Throwable ) {
when {
error is NetworkException -> {
// Handle network connectivity issues
showOfflineMessage ()
}
error.message?. contains ( "rate limit" ) == true -> {
// Handle rate limiting
showRateLimitMessage ()
}
else -> {
// Handle generic errors
showGenericErrorMessage ()
}
}
}
}
Comprehensive Error Handling
class NotificationTrayErrorHandler {
private repository : AmityNotificationTrayRepository ;
private retryCount : number = 0 ;
private maxRetries : number = 3 ;
constructor () {
this . repository = new AmityNotificationTrayRepository ();
}
async getNotificationTraySeenWithRetry () : Promise < AmityNotificationTraySeen > {
try {
const result = await this . repository . getNotificationTraySeen ();
this . retryCount = 0 ; // Reset on success
return result ;
} catch ( error ) {
return await this . handleError ( error );
}
}
private async handleError ( error : any ) : Promise < AmityNotificationTraySeen > {
if ( this . retryCount >= this . maxRetries ) {
throw error ;
}
// Exponential backoff
const delay = Math . pow ( 2 , this . retryCount ) * 1000 ;
this . retryCount ++ ;
await new Promise ( resolve => setTimeout ( resolve , delay ));
return await this . getNotificationTraySeenWithRetry ();
}
private handleSpecificErrors ( error : any ) : void {
if ( error . code === 'NETWORK_ERROR' ) {
this . showOfflineMessage ();
} else if ( error . code === 'RATE_LIMIT_EXCEEDED' ) {
this . showRateLimitMessage ();
} else if ( error . code === 'UNAUTHORIZED' ) {
this . handleAuthenticationError ();
} else {
this . showGenericErrorMessage ();
}
}
}
Best Practices
🎯 Implementation Guidelines
Timing When to Call
App launch or resume
Tray UI becomes visible
User pull-to-refresh action
After significant user inactivity
Frequency Call Frequency
On-demand only (recommended)
Minimum 120s intervals if polling
Avoid excessive API calls
Implement rate limiting client-side
🔧 Performance Optimization
Visual Indicators : Use clear badges, dots, or colors for unseen states
Loading States : Show appropriate loading indicators during refresh
Error States : Provide meaningful error messages and retry options
Accessibility : Ensure notification states are accessible to screen readers
Authentication : Verify user authentication before making calls
Permissions : Request appropriate notification permissions
Data Privacy : Handle notification data according to privacy policies
Logging : Avoid logging sensitive notification content