User Presence Tracking
Monitor individual user online status and activity patterns to create engaging, responsive user experiences. The social.plus SDK provides comprehensive user presence management with real-time updates, efficient synchronization, and smart offline detection.Overview
Build dynamic interfaces that show who’s online, track user activity, and provide real-time availability information. Perfect for chat applications, team collaboration tools, and social platforms where user engagement visibility drives interaction.Quick Start
Start tracking user presence in minutes:import { UserPresenceRepository, UserPresence } from '@social-plus/sdk';
// Initialize presence repository
const userPresence = new UserPresenceRepository();
// Query specific users' presence
const presences = await userPresence.getUserPresence(['user1', 'user2', 'user3']);
// Start real-time synchronization
await userPresence.syncUserPresence('user1');
await userPresence.syncUserPresence('user2');
// Listen for presence updates
userPresence.getSyncingUserPresence().subscribe(updates => {
updates.forEach(presence => {
console.log(`${presence.userId}: ${presence.isOnline ? 'Online' : 'Offline'}`);
console.log(`Last seen: ${new Date(presence.lastHeartbeat)}`);
});
});
User Presence Object
TheUserPresence object contains comprehensive presence information:
| Property | Type | Description |
|---|---|---|
userId | string | Unique identifier of the user |
lastHeartbeat | number | Unix timestamp of the user’s last heartbeat sync |
isOnline | boolean | Computed property indicating if user is online (heartbeat within 60 seconds) |
status | 'online' | 'away' | 'offline' | Detailed presence status based on activity patterns |
lastActivity | number | Timestamp of the user’s last activity |
Online Detection: A user is considered online if their last heartbeat was within the past 60 seconds. This threshold ensures real-time accuracy while accounting for network delays.
Query User Presence
Retrieve presence information for specific users with flexible querying options:- TypeScript
- iOS
- Android
- React Native
import { UserPresenceRepository, UserPresence } from '@social-plus/sdk';
const presenceRepo = new UserPresenceRepository();
// Query single user
const getSingleUserPresence = async (userId: string): Promise<UserPresence | null> => {
try {
const presences = await presenceRepo.getUserPresence([userId]);
return presences.length > 0 ? presences[0] : null;
} catch (error) {
console.error('Failed to get user presence:', error);
return null;
}
};
// Query multiple users (max 220)
const getMultipleUserPresence = async (userIds: string[]): Promise<UserPresence[]> => {
try {
const presences = await presenceRepo.getUserPresence(userIds);
return presences;
} catch (error) {
console.error('Failed to get user presences:', error);
return [];
}
};
// Query with error handling and retry logic
const getPresenceWithRetry = async (
userIds: string[],
maxRetries: number = 3
): Promise<UserPresence[]> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await presenceRepo.getUserPresence(userIds);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return [];
};
import SocialPlusSDK
class UserPresenceManager {
private let repository: AmityUserPresenceRepository
init(client: AmityClient) {
self.repository = AmityUserPresenceRepository(client: client)
}
func getUserPresence(userIds: [String], completion: @escaping (Result<[AmityUserPresence], Error>) -> Void) {
repository.getUserPresence(userIds: userIds) { result in
DispatchQueue.main.async {
completion(result)
}
}
}
func getSingleUserPresence(userId: String, completion: @escaping (AmityUserPresence?) -> Void) {
getUserPresence(userIds: [userId]) { result in
switch result {
case .success(let presences):
completion(presences.first)
case .failure(let error):
print("Error getting user presence: \(error)")
completion(nil)
}
}
}
func getPresenceWithRetry(userIds: [String], maxRetries: Int = 3, completion: @escaping (Result<[AmityUserPresence], Error>) -> Void) {
attemptGetPresence(userIds: userIds, attempt: 1, maxRetries: maxRetries, completion: completion)
}
private func attemptGetPresence(userIds: [String], attempt: Int, maxRetries: Int, completion: @escaping (Result<[AmityUserPresence], Error>) -> Void) {
repository.getUserPresence(userIds: userIds) { result in
switch result {
case .success(let presences):
completion(.success(presences))
case .failure(let error):
if attempt < maxRetries {
let delay = pow(2.0, Double(attempt)) * 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.attemptGetPresence(userIds: userIds, attempt: attempt + 1, maxRetries: maxRetries, completion: completion)
}
} else {
completion(.failure(error))
}
}
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityUserPresenceRepository
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
class UserPresenceManager(private val client: AmityClient) {
private val repository = AmityUserPresenceRepository(client)
fun getUserPresence(userIds: List<String>): Single<List<AmityUserPresence>> {
return repository.getUserPresence(userIds)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun getSingleUserPresence(userId: String): Single<AmityUserPresence?> {
return getUserPresence(listOf(userId))
.map { presences -> presences.firstOrNull() }
}
fun getPresenceWithRetry(userIds: List<String>, maxRetries: Int = 3): Single<List<AmityUserPresence>> {
return repository.getUserPresence(userIds)
.retryWhen { errors ->
errors.zipWith(Single.range(1, maxRetries)) { error, attempt ->
if (attempt >= maxRetries) {
throw error
}
attempt
}.flatMap { attempt ->
Single.timer(Math.pow(2.0, attempt.toDouble()).toLong(), TimeUnit.SECONDS)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun getPresenceWithCallback(userIds: List<String>, callback: (List<AmityUserPresence>) -> Unit, errorCallback: (Throwable) -> Unit) {
getUserPresence(userIds)
.subscribe(callback, errorCallback)
}
}
import { AmityUserPresenceRepository, AmityUserPresence } from '@amityco/react-native-sdk';
import { useEffect, useState, useCallback } from 'react';
export const useUserPresence = (userIds: string[]) => {
const [presences, setPresences] = useState<AmityUserPresence[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const repository = new AmityUserPresenceRepository();
const fetchPresence = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await repository.getUserPresence(userIds);
setPresences(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch presence');
} finally {
setLoading(false);
}
}, [userIds]);
useEffect(() => {
if (userIds.length > 0) {
fetchPresence();
}
}, [fetchPresence]);
const refetch = useCallback(() => {
fetchPresence();
}, [fetchPresence]);
return { presences, loading, error, refetch };
};
// Hook for single user presence
export const useSingleUserPresence = (userId: string) => {
const { presences, loading, error, refetch } = useUserPresence([userId]);
return {
presence: presences[0] || null,
loading,
error,
refetch
};
};
Real-time Presence Synchronization
Enable continuous presence updates for users currently visible in your application. This creates a live, responsive experience where presence changes are reflected immediately.Sync User Presence
Start real-time synchronization for specific users:Sync Limits: Maximum of 20 users can be synced simultaneously. Use
unsyncUserPresence() to remove users no longer needed to stay within limits.- TypeScript
- iOS
- Android
- Flutter
import { UserPresenceRepository } from '@social-plus/sdk';
class PresenceSyncManager {
private repository: UserPresenceRepository;
private syncedUsers = new Set<string>();
private readonly MAX_SYNC_USERS = 20;
constructor() {
this.repository = new UserPresenceRepository();
}
async syncUserPresence(userId: string, viewId?: string): Promise<void> {
if (this.syncedUsers.size >= this.MAX_SYNC_USERS) {
console.warn(`Maximum sync limit (${this.MAX_SYNC_USERS}) reached. Unsync some users first.`);
return;
}
try {
await this.repository.syncUserPresence(userId, viewId);
this.syncedUsers.add(userId);
console.log(`Started syncing presence for user: ${userId}`);
} catch (error) {
console.error(`Failed to sync user presence for ${userId}:`, error);
}
}
async unsyncUserPresence(userId: string, viewId?: string): Promise<void> {
try {
await this.repository.unsyncUserPresence(userId, viewId);
this.syncedUsers.delete(userId);
console.log(`Stopped syncing presence for user: ${userId}`);
} catch (error) {
console.error(`Failed to unsync user presence for ${userId}:`, error);
}
}
async unsyncAllUserPresence(): Promise<void> {
try {
await this.repository.unsyncAllUserPresence();
this.syncedUsers.clear();
console.log('Stopped syncing all user presences');
} catch (error) {
console.error('Failed to unsync all user presences:', error);
}
}
getSyncedUsers(): string[] {
return Array.from(this.syncedUsers);
}
canSyncMore(): boolean {
return this.syncedUsers.size < this.MAX_SYNC_USERS;
}
}
import SocialPlusSDK
class PresenceSyncManager {
private let repository: AmityUserPresenceRepository
private var syncedUsers: Set<String> = []
private let maxSyncUsers = 20
init(client: AmityClient) {
self.repository = AmityUserPresenceRepository(client: client)
}
func syncUserPresence(userId: String, viewId: String? = nil) {
guard syncedUsers.count < maxSyncUsers else {
print("Maximum sync limit (\(maxSyncUsers)) reached. Unsync some users first.")
return
}
repository.syncUserPresence(userId: userId, viewId: viewId)
syncedUsers.insert(userId)
print("Started syncing presence for user: \(userId)")
}
func unsyncUserPresence(userId: String, viewId: String? = nil) {
repository.unsyncUserPresence(userId: userId, viewId: viewId)
syncedUsers.remove(userId)
print("Stopped syncing presence for user: \(userId)")
}
func unsyncAllUserPresence() {
repository.unsyncAllUserPresence()
syncedUsers.removeAll()
print("Stopped syncing all user presences")
}
func getSyncedUsers() -> [String] {
return Array(syncedUsers)
}
func canSyncMore() -> Bool {
return syncedUsers.count < maxSyncUsers
}
// Smart sync management for UI lists
func updateVisibleUsers(_ userIds: [String]) {
let newUsers = Set(userIds)
// Unsync users no longer visible
for userId in syncedUsers {
if !newUsers.contains(userId) {
unsyncUserPresence(userId: userId)
}
}
// Sync newly visible users
for userId in newUsers {
if !syncedUsers.contains(userId) && canSyncMore() {
syncUserPresence(userId: userId)
}
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityUserPresenceRepository
class PresenceSyncManager(private val client: AmityClient) {
private val repository = AmityUserPresenceRepository(client)
private val syncedUsers = mutableSetOf<String>()
private val maxSyncUsers = 20
fun syncUserPresence(userId: String, viewId: String? = null) {
if (syncedUsers.size >= maxSyncUsers) {
println("Maximum sync limit ($maxSyncUsers) reached. Unsync some users first.")
return
}
try {
repository.syncUserPresence(userId, viewId)
syncedUsers.add(userId)
println("Started syncing presence for user: $userId")
} catch (error: Exception) {
println("Failed to sync user presence for $userId: $error")
}
}
fun unsyncUserPresence(userId: String, viewId: String? = null) {
try {
repository.unsyncUserPresence(userId, viewId)
syncedUsers.remove(userId)
println("Stopped syncing presence for user: $userId")
} catch (error: Exception) {
println("Failed to unsync user presence for $userId: $error")
}
}
fun unsyncAllUserPresence() {
try {
repository.unsyncAllUserPresence()
syncedUsers.clear()
println("Stopped syncing all user presences")
} catch (error: Exception) {
println("Failed to unsync all user presences: $error")
}
}
fun getSyncedUsers(): List<String> = syncedUsers.toList()
fun canSyncMore(): Boolean = syncedUsers.size < maxSyncUsers
// Efficient batch management
fun updateVisibleUsers(userIds: List<String>) {
val newUsers = userIds.toSet()
// Remove users no longer visible
val usersToUnsync = syncedUsers - newUsers
usersToUnsync.forEach { userId ->
unsyncUserPresence(userId)
}
// Add newly visible users
val usersToSync = (newUsers - syncedUsers).take(maxSyncUsers - syncedUsers.size)
usersToSync.forEach { userId ->
syncUserPresence(userId)
}
}
}
import 'package:amity_sdk/amity_sdk.dart';
class PresenceSyncManager {
late AmityUserPresenceRepository _repository;
final Set<String> _syncedUsers = <String>{};
static const int maxSyncUsers = 20;
PresenceSyncManager(AmityClient client) {
_repository = AmityUserPresenceRepository(client);
}
Future<void> syncUserPresence(String userId, {String? viewId}) async {
if (_syncedUsers.length >= maxSyncUsers) {
print('Maximum sync limit ($maxSyncUsers) reached. Unsync some users first.');
return;
}
try {
await _repository.syncUserPresence(userId, viewId: viewId);
_syncedUsers.add(userId);
print('Started syncing presence for user: $userId');
} catch (error) {
print('Failed to sync user presence for $userId: $error');
}
}
Future<void> unsyncUserPresence(String userId, {String? viewId}) async {
try {
await _repository.unsyncUserPresence(userId, viewId: viewId);
_syncedUsers.remove(userId);
print('Stopped syncing presence for user: $userId');
} catch (error) {
print('Failed to unsync user presence for $userId: $error');
}
}
Future<void> unsyncAllUserPresence() async {
try {
await _repository.unsyncAllUserPresence();
_syncedUsers.clear();
print('Stopped syncing all user presences');
} catch (error) {
print('Failed to unsync all user presences: $error');
}
}
List<String> getSyncedUsers() => _syncedUsers.toList();
bool canSyncMore() => _syncedUsers.length < maxSyncUsers;
// Smart list management
Future<void> updateVisibleUsers(List<String> userIds) async {
final newUsers = userIds.toSet();
// Unsync users no longer visible
final usersToUnsync = _syncedUsers.difference(newUsers);
for (final userId in usersToUnsync) {
await unsyncUserPresence(userId);
}
// Sync newly visible users
final availableSlots = maxSyncUsers - _syncedUsers.length;
final usersToSync = newUsers.difference(_syncedUsers).take(availableSlots);
for (final userId in usersToSync) {
await syncUserPresence(userId);
}
}
}
ViewId Parameter
The optionalviewId parameter allows binding the same user to multiple views:
// Same user in different UI components
await presenceRepo.syncUserPresence('user123', 'chat-list');
await presenceRepo.syncUserPresence('user123', 'user-profile');
// Unsync from one view doesn't affect the other
await presenceRepo.unsyncUserPresence('user123', 'chat-list');
// user123 still synced for 'user-profile' view
Observing Presence Changes
Listen to real-time presence updates for all synced users. The presence observer provides immediate notifications when users go online or offline.Real-time Updates
- TypeScript
- iOS
- Android
- React Hook
import { UserPresenceRepository, UserPresence } from '@social-plus/sdk';
import { Subscription } from 'rxjs';
class PresenceObserver {
private subscription: Subscription | null = null;
private repository: UserPresenceRepository;
constructor() {
this.repository = new UserPresenceRepository();
}
startObserving(onPresenceUpdate: (presences: UserPresence[]) => void): void {
this.subscription = this.repository.getSyncingUserPresence()
.subscribe({
next: (presences) => {
console.log(`Received presence updates for ${presences.length} users`);
onPresenceUpdate(presences);
},
error: (error) => {
console.error('Presence observation error:', error);
}
});
}
stopObserving(): void {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
// Enhanced observer with filtering and mapping
startObservingWithFilters(
onlineOnly: boolean = false,
userFilter?: (userId: string) => boolean,
onPresenceUpdate?: (presences: UserPresence[]) => void
): void {
this.subscription = this.repository.getSyncingUserPresence()
.pipe(
map(presences => {
let filtered = presences;
// Filter by online status
if (onlineOnly) {
filtered = filtered.filter(p => p.isOnline);
}
// Filter by user criteria
if (userFilter) {
filtered = filtered.filter(p => userFilter(p.userId));
}
return filtered;
})
)
.subscribe({
next: onPresenceUpdate,
error: (error) => console.error('Presence observation error:', error)
});
}
}
// Usage example
const observer = new PresenceObserver();
observer.startObserving((presences) => {
presences.forEach(presence => {
updateUserUI(presence.userId, {
isOnline: presence.isOnline,
lastSeen: presence.lastHeartbeat,
status: presence.isOnline ? 'online' : 'offline'
});
});
});
import SocialPlusSDK
import RxSwift
class PresenceObserver {
private let repository: AmityUserPresenceRepository
private var disposeBag = DisposeBag()
init(client: AmityClient) {
self.repository = AmityUserPresenceRepository(client: client)
}
func startObserving(onPresenceUpdate: @escaping ([AmityUserPresence]) -> Void) {
repository.getSyncingUserPresence()
.observe(on: MainScheduler.instance)
.subscribe(
onNext: { presences in
print("Received presence updates for \(presences.count) users")
onPresenceUpdate(presences)
},
onError: { error in
print("Presence observation error: \(error)")
}
)
.disposed(by: disposeBag)
}
func startObservingWithFilters(
onlineOnly: Bool = false,
userFilter: ((String) -> Bool)? = nil,
onPresenceUpdate: @escaping ([AmityUserPresence]) -> Void
) {
repository.getSyncingUserPresence()
.map { presences in
var filtered = presences
// Filter by online status
if onlineOnly {
filtered = filtered.filter { $0.isOnline }
}
// Filter by user criteria
if let userFilter = userFilter {
filtered = filtered.filter { userFilter($0.userId) }
}
return filtered
}
.observe(on: MainScheduler.instance)
.subscribe(
onNext: onPresenceUpdate,
onError: { error in
print("Presence observation error: \(error)")
}
)
.disposed(by: disposeBag)
}
func stopObserving() {
disposeBag = DisposeBag()
}
// SwiftUI-friendly observable
func getPresencePublisher() -> Observable<[AmityUserPresence]> {
return repository.getSyncingUserPresence()
.observe(on: MainScheduler.instance)
}
}
// SwiftUI integration
struct PresenceView: View {
@State private var presences: [AmityUserPresence] = []
private let observer: PresenceObserver
init(client: AmityClient) {
self.observer = PresenceObserver(client: client)
}
var body: some View {
VStack {
ForEach(presences, id: \.userId) { presence in
HStack {
Circle()
.fill(presence.isOnline ? Color.green : Color.gray)
.frame(width: 10, height: 10)
Text(presence.userId)
Spacer()
Text(presence.isOnline ? "Online" : "Offline")
}
}
}
.onAppear {
observer.startObserving { newPresences in
self.presences = newPresences
}
}
.onDisappear {
observer.stopObserving()
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityUserPresenceRepository
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
class PresenceObserver(private val client: AmityClient) {
private val repository = AmityUserPresenceRepository(client)
private val disposables = CompositeDisposable()
fun startObserving(onPresenceUpdate: (List<AmityUserPresence>) -> Unit) {
repository.getSyncingUserPresence()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ presences ->
println("Received presence updates for ${presences.size} users")
onPresenceUpdate(presences)
},
{ error ->
println("Presence observation error: $error")
}
)
.let(disposables::add)
}
fun startObservingWithFilters(
onlineOnly: Boolean = false,
userFilter: ((String) -> Boolean)? = null,
onPresenceUpdate: (List<AmityUserPresence>) -> Unit
) {
repository.getSyncingUserPresence()
.map { presences ->
var filtered = presences
// Filter by online status
if (onlineOnly) {
filtered = filtered.filter { it.isOnline }
}
// Filter by user criteria
userFilter?.let { filter ->
filtered = filtered.filter { filter(it.userId) }
}
filtered
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
onPresenceUpdate,
{ error -> println("Presence observation error: $error") }
)
.let(disposables::add)
}
fun stopObserving() {
disposables.clear()
}
}
// Android Compose integration
@Composable
fun PresenceList(client: AmityClient, userIds: List<String>) {
var presences by remember { mutableStateOf<List<AmityUserPresence>>(emptyList()) }
val observer = remember { PresenceObserver(client) }
LaunchedEffect(userIds) {
// Sync users
userIds.forEach { userId ->
AmityUserPresenceRepository(client).syncUserPresence(userId)
}
// Observe changes
observer.startObserving { newPresences ->
presences = newPresences
}
}
DisposableEffect(Unit) {
onDispose {
observer.stopObserving()
AmityUserPresenceRepository(client).unsyncAllUserPresence()
}
}
LazyColumn {
items(presences, key = { it.userId }) { presence ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (presence.isOnline) Color.Green else Color.Gray,
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = presence.userId,
modifier = Modifier.weight(1f)
)
Text(
text = if (presence.isOnline) "Online" else "Offline",
style = MaterialTheme.typography.caption
)
}
}
}
}
import { useEffect, useState, useCallback } from 'react';
import { UserPresenceRepository, UserPresence } from '@social-plus/sdk';
interface UsePresenceOptions {
onlineOnly?: boolean;
autoSync?: boolean;
userFilter?: (userId: string) => boolean;
}
export const useUserPresence = (
userIds: string[],
options: UsePresenceOptions = {}
) => {
const [presences, setPresences] = useState<UserPresence[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const repository = new UserPresenceRepository();
const { onlineOnly = false, autoSync = true, userFilter } = options;
const syncUsers = useCallback(async () => {
if (!autoSync) return;
try {
for (const userId of userIds) {
await repository.syncUserPresence(userId);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to sync users');
}
}, [userIds, autoSync, repository]);
const unsyncUsers = useCallback(async () => {
try {
for (const userId of userIds) {
await repository.unsyncUserPresence(userId);
}
} catch (err) {
console.error('Failed to unsync users:', err);
}
}, [userIds, repository]);
useEffect(() => {
setLoading(true);
setError(null);
// Start syncing
syncUsers();
// Subscribe to presence changes
const subscription = repository.getSyncingUserPresence()
.subscribe({
next: (allPresences) => {
let filtered = allPresences;
// Filter by online status
if (onlineOnly) {
filtered = filtered.filter(p => p.isOnline);
}
// Filter by user criteria
if (userFilter) {
filtered = filtered.filter(p => userFilter(p.userId));
}
setPresences(filtered);
setLoading(false);
},
error: (err) => {
setError(err.message || 'Presence observation failed');
setLoading(false);
}
});
return () => {
subscription.unsubscribe();
unsyncUsers();
};
}, [syncUsers, unsyncUsers, onlineOnly, userFilter]);
const manualSync = useCallback(async (userId: string) => {
await repository.syncUserPresence(userId);
}, [repository]);
const manualUnsync = useCallback(async (userId: string) => {
await repository.unsyncUserPresence(userId);
}, [repository]);
return {
presences,
loading,
error,
manualSync,
manualUnsync,
onlineCount: presences.filter(p => p.isOnline).length,
totalCount: presences.length
};
};
// Usage in React component
const UserList: React.FC<{ userIds: string[] }> = ({ userIds }) => {
const { presences, loading, error, onlineCount } = useUserPresence(userIds, {
onlineOnly: false,
autoSync: true
});
if (loading) return <div>Loading presence...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="user-list">
<h3>Users ({onlineCount} online)</h3>
{presences.map(presence => (
<div key={presence.userId} className="user-item">
<div className={`status-dot ${presence.isOnline ? 'online' : 'offline'}`} />
<span>{presence.userId}</span>
<span className="status">
{presence.isOnline ? 'Online' : `Last seen ${formatTime(presence.lastHeartbeat)}`}
</span>
</div>
))}
</div>
);
};
Accessing Presence from User Objects
When presence data is fetched, it’s also mapped toAmityUser objects when available:
// Access presence directly from user objects
const user = await getUserById('user123');
if (user.lastHeartbeat) {
const isOnline = (Date.now() - user.lastHeartbeat) < 60000;
console.log(`User ${user.userId} is ${isOnline ? 'online' : 'offline'}`);
}
Online Users Analytics
Get insights into your user base with comprehensive online user statistics and snapshots.Online Users Count
Query the current number of online users in your network:- TypeScript
- iOS
- Android
import { UserPresenceRepository } from '@social-plus/sdk';
const getOnlineUsersCount = async (): Promise<number> => {
const repository = new UserPresenceRepository();
try {
const count = await repository.getOnlineUsersCount();
console.log(`Currently ${count} users online`);
return count;
} catch (error) {
console.error('Failed to get online users count:', error);
return 0;
}
};
// Usage with periodic updates
class OnlineUsersTracker {
private intervalId: NodeJS.Timeout | null = null;
private repository: UserPresenceRepository;
constructor() {
this.repository = new UserPresenceRepository();
}
startTracking(updateInterval: number = 30000, onUpdate: (count: number) => void): void {
this.intervalId = setInterval(async () => {
try {
const count = await this.repository.getOnlineUsersCount();
onUpdate(count);
} catch (error) {
console.error('Failed to update online count:', error);
}
}, updateInterval);
}
stopTracking(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
import SocialPlusSDK
class OnlineUsersTracker {
private let repository: AmityUserPresenceRepository
private var timer: Timer?
init(client: AmityClient) {
self.repository = AmityUserPresenceRepository(client: client)
}
func getOnlineUsersCount(completion: @escaping (Result<Int, Error>) -> Void) {
repository.getOnlineUsersCount { result in
DispatchQueue.main.async {
switch result {
case .success(let count):
print("Currently \(count) users online")
case .failure(let error):
print("Failed to get online users count: \(error)")
}
completion(result)
}
}
}
func startTracking(updateInterval: TimeInterval = 30.0, onUpdate: @escaping (Int) -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { _ in
self.getOnlineUsersCount { result in
if case .success(let count) = result {
onUpdate(count)
}
}
}
}
func stopTracking() {
timer?.invalidate()
timer = nil
}
}
// SwiftUI integration
struct OnlineUsersCounter: View {
@State private var onlineCount: Int = 0
private let tracker: OnlineUsersTracker
init(client: AmityClient) {
self.tracker = OnlineUsersTracker(client: client)
}
var body: some View {
HStack {
Image(systemName: "person.2.fill")
.foregroundColor(.green)
Text("\(onlineCount) online")
.font(.caption)
}
.onAppear {
tracker.startTracking { count in
self.onlineCount = count
}
}
.onDisappear {
tracker.stopTracking()
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityUserPresenceRepository
class OnlineUsersTracker(private val client: AmityClient) {
private val repository = AmityUserPresenceRepository(client)
private val disposables = CompositeDisposable()
fun getOnlineUsersCount(): Single<Int> {
return repository.getOnlineUsersCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess { count ->
println("Currently $count users online")
}
.doOnError { error ->
println("Failed to get online users count: $error")
}
}
fun startTracking(updateInterval: Long = 30, onUpdate: (Int) -> Unit) {
Single.interval(updateInterval, TimeUnit.SECONDS)
.flatMap { getOnlineUsersCount() }
.subscribe(
onUpdate,
{ error -> println("Tracking error: $error") }
)
.let(disposables::add)
}
fun stopTracking() {
disposables.clear()
}
}
// Android Compose integration
@Composable
fun OnlineUsersCounter(client: AmityClient) {
var onlineCount by remember { mutableStateOf(0) }
val tracker = remember { OnlineUsersTracker(client) }
LaunchedEffect(Unit) {
tracker.startTracking { count ->
onlineCount = count
}
}
DisposableEffect(Unit) {
onDispose {
tracker.stopTracking()
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Online users",
tint = Color.Green
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "$onlineCount online",
style = MaterialTheme.typography.caption
)
}
}
Online Users Snapshot
Get a paginated list of currently online users with detailed information:Snapshot Limitations: Returns up to 1,000 online users with 20 users per page. Snapshots are not auto-updating and represent a point-in-time view.
- TypeScript
- iOS
- Android
import { UserPresenceRepository, OnlineUsersSnapshot, AmityUser } from '@social-plus/sdk';
class OnlineUsersManager {
private repository: UserPresenceRepository;
constructor() {
this.repository = new UserPresenceRepository();
}
async getOnlineUsersSnapshot(): Promise<OnlineUsersSnapshot> {
try {
const snapshot = await this.repository.getOnlineUsersSnapshot();
console.log(`Got ${snapshot.users.length} online users`);
return snapshot;
} catch (error) {
console.error('Failed to get online users snapshot:', error);
throw error;
}
}
async getAllOnlineUsers(): Promise<AmityUser[]> {
const allUsers: AmityUser[] = [];
let snapshot = await this.getOnlineUsersSnapshot();
allUsers.push(...snapshot.users);
// Load more pages if available
while (snapshot.canLoadMore) {
snapshot = await snapshot.loadMore();
allUsers.push(...snapshot.users);
}
return allUsers;
}
async getOnlineUsersPaginated(
pageSize: number = 20,
onPageLoaded: (users: AmityUser[], hasMore: boolean) => void
): Promise<void> {
try {
let snapshot = await this.getOnlineUsersSnapshot();
onPageLoaded(snapshot.users, snapshot.canLoadMore);
while (snapshot.canLoadMore) {
snapshot = await snapshot.loadMore();
onPageLoaded(snapshot.users, snapshot.canLoadMore);
}
} catch (error) {
console.error('Failed to load paginated online users:', error);
}
}
// Enhanced snapshot with filtering
async getFilteredOnlineUsers(
filter: (user: AmityUser) => boolean
): Promise<AmityUser[]> {
const allUsers = await this.getAllOnlineUsers();
return allUsers.filter(filter);
}
}
// Usage examples
const manager = new OnlineUsersManager();
// Get first page
const snapshot = await manager.getOnlineUsersSnapshot();
console.log('First page users:', snapshot.users);
// Get all online users
const allOnlineUsers = await manager.getAllOnlineUsers();
console.log('All online users:', allOnlineUsers.length);
// Paginated loading with callback
await manager.getOnlineUsersPaginated(20, (users, hasMore) => {
console.log(`Loaded ${users.length} users, more available: ${hasMore}`);
// Update UI with new users
});
import SocialPlusSDK
class OnlineUsersManager {
private let repository: AmityUserPresenceRepository
init(client: AmityClient) {
self.repository = AmityUserPresenceRepository(client: client)
}
func getOnlineUsersSnapshot(completion: @escaping (Result<AmityOnlineUsersSnapshot, Error>) -> Void) {
repository.getOnlineUsersSnapshot { result in
DispatchQueue.main.async {
switch result {
case .success(let snapshot):
print("Got \(snapshot.users.count) online users")
case .failure(let error):
print("Failed to get online users snapshot: \(error)")
}
completion(result)
}
}
}
func getAllOnlineUsers(completion: @escaping ([AmityUser]) -> Void) {
var allUsers: [AmityUser] = []
getOnlineUsersSnapshot { result in
guard case .success(let snapshot) = result else {
completion([])
return
}
allUsers.append(contentsOf: snapshot.users)
self.loadMorePages(snapshot: snapshot, allUsers: &allUsers) { finalUsers in
completion(finalUsers)
}
}
}
private func loadMorePages(
snapshot: AmityOnlineUsersSnapshot,
allUsers: inout [AmityUser],
completion: @escaping ([AmityUser]) -> Void
) {
if snapshot.canLoadMore {
snapshot.loadMore { result in
switch result {
case .success(let newSnapshot):
allUsers.append(contentsOf: newSnapshot.users)
self.loadMorePages(snapshot: newSnapshot, allUsers: &allUsers, completion: completion)
case .failure:
completion(allUsers)
}
}
} else {
completion(allUsers)
}
}
}
// SwiftUI integration
struct OnlineUsersList: View {
@State private var onlineUsers: [AmityUser] = []
@State private var isLoading = true
private let manager: OnlineUsersManager
init(client: AmityClient) {
self.manager = OnlineUsersManager(client: client)
}
var body: some View {
NavigationView {
List(onlineUsers, id: \.userId) { user in
HStack {
AsyncImage(url: URL(string: user.avatarUrl ?? "")) { image in
image.resizable()
} placeholder: {
Circle().fill(Color.gray.opacity(0.3))
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.displayName ?? user.userId)
.font(.headline)
Text("Online")
.font(.caption)
.foregroundColor(.green)
}
Spacer()
Circle()
.fill(Color.green)
.frame(width: 8, height: 8)
}
}
.navigationTitle("Online Users")
.onAppear {
loadOnlineUsers()
}
}
.overlay(
Group {
if isLoading {
ProgressView("Loading online users...")
}
}
)
}
private func loadOnlineUsers() {
manager.getAllOnlineUsers { users in
self.onlineUsers = users
self.isLoading = false
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityUserPresenceRepository
import io.amity.sdk.user.AmityUser
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
class OnlineUsersManager(private val client: AmityClient) {
private val repository = AmityUserPresenceRepository(client)
fun getOnlineUsersSnapshot(): Single<AmityOnlineUsersSnapshot> {
return repository.getOnlineUsersSnapshot()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess { snapshot ->
println("Got ${snapshot.users.size} online users")
}
.doOnError { error ->
println("Failed to get online users snapshot: $error")
}
}
fun getAllOnlineUsers(): Single<List<AmityUser>> {
return getOnlineUsersSnapshot()
.flatMap { snapshot ->
loadAllPages(snapshot, mutableListOf<AmityUser>().apply { addAll(snapshot.users) })
}
}
private fun loadAllPages(snapshot: AmityOnlineUsersSnapshot, allUsers: MutableList<AmityUser>): Single<List<AmityUser>> {
return if (snapshot.canLoadMore) {
snapshot.loadMore()
.flatMap { newSnapshot ->
allUsers.addAll(newSnapshot.users)
loadAllPages(newSnapshot, allUsers)
}
} else {
Single.just(allUsers.toList())
}
}
fun getOnlineUsersPaginated(
onPageLoaded: (List<AmityUser>, Boolean) -> Unit,
onError: (Throwable) -> Unit
) {
getOnlineUsersSnapshot()
.flatMap { snapshot ->
onPageLoaded(snapshot.users, snapshot.canLoadMore)
loadPagesRecursively(snapshot, onPageLoaded)
}
.subscribe({}, onError)
}
private fun loadPagesRecursively(
snapshot: AmityOnlineUsersSnapshot,
onPageLoaded: (List<AmityUser>, Boolean) -> Unit
): Single<Unit> {
return if (snapshot.canLoadMore) {
snapshot.loadMore()
.flatMap { newSnapshot ->
onPageLoaded(newSnapshot.users, newSnapshot.canLoadMore)
loadPagesRecursively(newSnapshot, onPageLoaded)
}
} else {
Single.just(Unit)
}
}
}
// Android Compose integration
@Composable
fun OnlineUsersList(client: AmityClient) {
var onlineUsers by remember { mutableStateOf<List<AmityUser>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<String?>(null) }
val manager = remember { OnlineUsersManager(client) }
LaunchedEffect(Unit) {
manager.getAllOnlineUsers()
.subscribe({ users ->
onlineUsers = users
isLoading = false
}, { throwable ->
error = throwable.message
isLoading = false
})
}
Column {
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (error != null) {
Text(
text = "Error: $error",
color = MaterialTheme.colors.error,
modifier = Modifier.padding(16.dp)
)
} else {
Text(
text = "${onlineUsers.size} users online",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(16.dp)
)
LazyColumn {
items(onlineUsers, key = { it.userId }) { user ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = user.avatarUrl,
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
placeholder = painterResource(R.drawable.placeholder_avatar)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = user.displayName ?: user.userId,
style = MaterialTheme.typography.body1
)
Text(
text = "Online",
style = MaterialTheme.typography.caption,
color = Color.Green
)
}
Box(
modifier = Modifier
.size(8.dp)
.background(
color = Color.Green,
shape = CircleShape
)
)
}
}
}
}
}
}
Advanced Use Cases
Smart List Management
Efficiently manage presence for dynamic user lists:class SmartPresenceList {
private presenceRepo: UserPresenceRepository;
private visibleUsers = new Set<string>();
private syncedUsers = new Set<string>();
private readonly MAX_SYNC = 20;
constructor() {
this.presenceRepo = new UserPresenceRepository();
}
// Called when list items become visible/invisible
updateVisibleUsers(userIds: string[]): void {
const newVisible = new Set(userIds);
// Remove users that are no longer visible
this.visibleUsers.forEach(userId => {
if (!newVisible.has(userId)) {
this.unsyncUser(userId);
}
});
// Add newly visible users
newVisible.forEach(userId => {
if (!this.visibleUsers.has(userId)) {
this.syncUser(userId);
}
});
this.visibleUsers = newVisible;
}
private syncUser(userId: string): void {
if (this.syncedUsers.size >= this.MAX_SYNC) {
// Remove least recently used
const oldestUser = this.syncedUsers.values().next().value;
this.unsyncUser(oldestUser);
}
this.presenceRepo.syncUserPresence(userId);
this.syncedUsers.add(userId);
}
private unsyncUser(userId: string): void {
this.presenceRepo.unsyncUserPresence(userId);
this.syncedUsers.delete(userId);
this.visibleUsers.delete(userId);
}
cleanup(): void {
this.presenceRepo.unsyncAllUserPresence();
this.syncedUsers.clear();
this.visibleUsers.clear();
}
}
Team Presence Dashboard
Build comprehensive team presence monitoring:interface TeamMember {
userId: string;
name: string;
role: string;
department: string;
avatar?: string;
}
interface TeamPresenceStats {
totalMembers: number;
onlineMembers: number;
byDepartment: Record<string, { online: number; total: number }>;
byRole: Record<string, { online: number; total: number }>;
}
class TeamPresenceDashboard {
private presenceRepo: UserPresenceRepository;
private teamMembers: TeamMember[] = [];
private presenceData = new Map<string, UserPresence>();
constructor(teamMembers: TeamMember[]) {
this.presenceRepo = new UserPresenceRepository();
this.teamMembers = teamMembers;
}
async initialize(): Promise<void> {
// Sync all team members
for (const member of this.teamMembers) {
await this.presenceRepo.syncUserPresence(member.userId);
}
// Listen for presence updates
this.presenceRepo.getSyncingUserPresence().subscribe(presences => {
presences.forEach(presence => {
this.presenceData.set(presence.userId, presence);
});
this.onPresenceUpdate();
});
}
private onPresenceUpdate(): void {
const stats = this.calculateStats();
this.updateDashboardUI(stats);
}
private calculateStats(): TeamPresenceStats {
const stats: TeamPresenceStats = {
totalMembers: this.teamMembers.length,
onlineMembers: 0,
byDepartment: {},
byRole: {}
};
this.teamMembers.forEach(member => {
const presence = this.presenceData.get(member.userId);
const isOnline = presence?.isOnline || false;
if (isOnline) stats.onlineMembers++;
// Department stats
if (!stats.byDepartment[member.department]) {
stats.byDepartment[member.department] = { online: 0, total: 0 };
}
stats.byDepartment[member.department].total++;
if (isOnline) stats.byDepartment[member.department].online++;
// Role stats
if (!stats.byRole[member.role]) {
stats.byRole[member.role] = { online: 0, total: 0 };
}
stats.byRole[member.role].total++;
if (isOnline) stats.byRole[member.role].online++;
});
return stats;
}
getOnlineMembers(): TeamMember[] {
return this.teamMembers.filter(member => {
const presence = this.presenceData.get(member.userId);
return presence?.isOnline || false;
});
}
getMembersByStatus(status: 'online' | 'offline'): TeamMember[] {
return this.teamMembers.filter(member => {
const presence = this.presenceData.get(member.userId);
const isOnline = presence?.isOnline || false;
return status === 'online' ? isOnline : !isOnline;
});
}
private updateDashboardUI(stats: TeamPresenceStats): void {
// Update your dashboard UI with the new stats
console.log('Team Presence Stats:', stats);
}
cleanup(): void {
this.presenceRepo.unsyncAllUserPresence();
}
}
Presence-Based Features
Build features that react to presence changes:class PresenceBasedFeatures {
private presenceRepo: UserPresenceRepository;
private notificationManager: NotificationManager;
constructor() {
this.presenceRepo = new UserPresenceRepository();
this.notificationManager = new NotificationManager();
}
// Auto-suggest active users for collaboration
async suggestActiveCollaborators(projectId: string): Promise<TeamMember[]> {
const projectMembers = await getProjectMembers(projectId);
// Sync presence for project members
for (const member of projectMembers) {
await this.presenceRepo.syncUserPresence(member.userId);
}
return new Promise(resolve => {
this.presenceRepo.getSyncingUserPresence().subscribe(presences => {
const activeMembers = projectMembers.filter(member => {
const presence = presences.find(p => p.userId === member.userId);
return presence?.isOnline;
});
resolve(activeMembers);
});
});
}
// Send notifications when VIP users come online
setupVIPNotifications(vipUserIds: string[]): void {
const previousState = new Map<string, boolean>();
vipUserIds.forEach(userId => {
this.presenceRepo.syncUserPresence(userId);
previousState.set(userId, false);
});
this.presenceRepo.getSyncingUserPresence().subscribe(presences => {
presences.forEach(presence => {
if (vipUserIds.includes(presence.userId)) {
const wasOnline = previousState.get(presence.userId) || false;
if (!wasOnline && presence.isOnline) {
this.notificationManager.show({
title: 'VIP User Online',
message: `${presence.userId} is now available`,
type: 'info'
});
}
previousState.set(presence.userId, presence.isOnline);
}
});
});
}
// Dynamic UI based on presence
getUIConfigForUser(userId: string): UIConfig {
const presence = this.getPresenceForUser(userId);
return {
showChatButton: presence?.isOnline || false,
showCallButton: presence?.isOnline || false,
statusIndicator: presence?.isOnline ? 'online' : 'offline',
responseTimeEstimate: this.estimateResponseTime(presence)
};
}
private estimateResponseTime(presence: UserPresence | null): string {
if (!presence) return 'Unknown';
if (presence.isOnline) return 'Usually responds immediately';
const timeSinceLastSeen = Date.now() - presence.lastHeartbeat;
const hours = timeSinceLastSeen / (1000 * 60 * 60);
if (hours < 1) return 'Usually responds within an hour';
if (hours < 24) return 'Usually responds within a day';
return 'May take several days to respond';
}
private getPresenceForUser(userId: string): UserPresence | null {
// Implementation to get cached presence data
return null;
}
}
Performance Optimization
Efficient Presence Caching
Implement smart caching to reduce API calls:class PresenceCache {
private cache = new Map<string, CachedPresence>();
private readonly CACHE_TTL = 60000; // 1 minute
interface CachedPresence {
presence: UserPresence;
timestamp: number;
}
getCachedPresence(userId: string): UserPresence | null {
const cached = this.cache.get(userId);
if (!cached) return null;
// Check if cache is still valid
if (Date.now() - cached.timestamp > this.CACHE_TTL) {
this.cache.delete(userId);
return null;
}
return cached.presence;
}
setCachedPresence(userId: string, presence: UserPresence): void {
this.cache.set(userId, {
presence,
timestamp: Date.now()
});
}
clearCache(): void {
this.cache.clear();
}
// Get cache statistics
getCacheStats(): { size: number; hitRate: number } {
return {
size: this.cache.size,
hitRate: this.calculateHitRate()
};
}
private calculateHitRate(): number {
// Implementation for hit rate calculation
return 0;
}
}
Batch Presence Operations
Optimize API usage with batching:class BatchPresenceManager {
private pendingQueries = new Set<string>();
private batchTimer: NodeJS.Timeout | null = null;
private readonly BATCH_DELAY = 100; // 100ms
private readonly MAX_BATCH_SIZE = 220;
async getPresence(userIds: string[]): Promise<UserPresence[]> {
// Add to pending queries
userIds.forEach(id => this.pendingQueries.add(id));
// Schedule batch execution
if (!this.batchTimer) {
this.batchTimer = setTimeout(() => {
this.executeBatch();
}, this.BATCH_DELAY);
}
// Return cached or wait for batch result
return this.waitForBatchResult(userIds);
}
private async executeBatch(): Promise<void> {
if (this.pendingQueries.size === 0) return;
const userIds = Array.from(this.pendingQueries).slice(0, this.MAX_BATCH_SIZE);
this.pendingQueries.clear();
this.batchTimer = null;
try {
const repository = new UserPresenceRepository();
const presences = await repository.getUserPresence(userIds);
// Cache results and notify waiters
this.processResults(presences);
} catch (error) {
console.error('Batch presence query failed:', error);
}
}
private async waitForBatchResult(userIds: string[]): Promise<UserPresence[]> {
// Implementation for waiting and returning results
return [];
}
private processResults(presences: UserPresence[]): void {
// Process and cache results
presences.forEach(presence => {
// Update cache and notify listeners
});
}
}
Best Practices
Sync Management
Sync Management
Optimize Sync Usage: Only sync users that are currently visible to avoid hitting limits.
// Good: Sync only visible users
const VisibleUsersList = ({ userIds }: { userIds: string[] }) => {
const { presences } = useUserPresence(userIds.slice(0, 20)); // Limit to max sync
return (
<div>
{presences.map(presence => (
<UserPresenceIndicator key={presence.userId} presence={presence} />
))}
</div>
);
};
// Bad: Syncing all users regardless of visibility
const AllUsersList = ({ userIds }: { userIds: string[] }) => {
const { presences } = useUserPresence(userIds); // Could exceed 20 users
// This will fail if userIds.length > 20
};
Memory Management
Memory Management
Clean Up Resources: Always unsubscribe and unsync when components unmount.
useEffect(() => {
const repository = new UserPresenceRepository();
// Sync users
userIds.forEach(id => repository.syncUserPresence(id));
// Subscribe to changes
const subscription = repository.getSyncingUserPresence()
.subscribe(handlePresenceUpdate);
return () => {
// Cleanup: Critical for preventing memory leaks
subscription.unsubscribe();
repository.unsyncAllUserPresence();
};
}, [userIds]);
Error Handling
Error Handling
Robust Error Recovery: Handle network failures and sync limit errors gracefully.
class RobustPresenceManager {
private readonly maxRetries = 3;
private retryCount = 0;
async syncWithRetry(userId: string): Promise<void> {
try {
await this.repository.syncUserPresence(userId);
this.retryCount = 0; // Reset on success
} catch (error) {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
const delay = Math.pow(2, this.retryCount) * 1000;
setTimeout(() => this.syncWithRetry(userId), delay);
} else {
console.error(`Failed to sync user ${userId} after ${this.maxRetries} attempts`);
throw error;
}
}
}
}
Troubleshooting
Sync Limit Exceeded
Sync Limit Exceeded
Problem: Trying to sync more than 20 users simultaneously.Solution: Implement smart sync management:
const SYNC_LIMIT = 20;
const currentSyncs = new Set<string>();
const safeSyncUser = async (userId: string) => {
if (currentSyncs.size >= SYNC_LIMIT) {
// Remove oldest sync or least important user
const oldestUser = currentSyncs.values().next().value;
await repository.unsyncUserPresence(oldestUser);
currentSyncs.delete(oldestUser);
}
await repository.syncUserPresence(userId);
currentSyncs.add(userId);
};
Presence Not Updating
Presence Not Updating
Problem: Presence information seems stale or not updating.Solution: Check presence flow:
const debugPresence = async () => {
// 1. Verify presence is enabled
const isEnabled = await client.presence.isEnabled();
console.log('Presence enabled:', isEnabled);
// 2. Check heartbeat status
const isHeartbeatActive = client.presence.isHeartbeatActive();
console.log('Heartbeat active:', isHeartbeatActive);
// 3. Verify user is synced
const syncedUsers = repository.getSyncedUsers();
console.log('Synced users:', syncedUsers);
// 4. Check for observer subscription
const observer = repository.getSyncingUserPresence();
observer.subscribe(presences => {
console.log('Presence update received:', presences);
});
};
Next Steps
Channel Presence
Monitor member activity in conversation channels
Heartbeat Sync
Understand automatic presence synchronization and lifecycle
Real-time Events
Build reactive applications with real-time event handling
Live Objects
Create collaborative experiences with live object synchronization
Production Ready: All examples include comprehensive error handling, performance optimizations, and cleanup procedures suitable for production applications.