Skip to main content

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

The UserPresence object contains comprehensive presence information:
PropertyTypeDescription
userIdstringUnique identifier of the user
lastHeartbeatnumberUnix timestamp of the user’s last heartbeat sync
isOnlinebooleanComputed property indicating if user is online (heartbeat within 60 seconds)
status'online' | 'away' | 'offline'Detailed presence status based on activity patterns
lastActivitynumberTimestamp 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:
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 [];
};

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.
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;
  }
}

ViewId Parameter

The optional viewId 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

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'
    });
  });
});

Accessing Presence from User Objects

When presence data is fetched, it’s also mapped to AmityUser 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:
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;
    }
  }
}

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.
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
});

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

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
};
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]);
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

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);
};
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.