Skip to main content

Presence & Real-time Status

Create engaging, real-time experiences by tracking user availability, activity status, and channel presence. The social.plus SDK provides comprehensive presence management with automatic heartbeat synchronization, smart offline detection, and scalable real-time updates.

Overview

Transform your application into a live, interactive platform where users can see who’s online, active in channels, and available for engagement. Whether building chat applications, collaborative tools, or social platforms, presence state drives user engagement and creates vibrant communities.

Core Capabilities

  • User Presence - Track online/offline status with automatic detection
  • Channel Activity - Monitor member activity in conversation channels
  • Smart Heartbeat - Automatic synchronization with configurable intervals
  • Real-time Updates - Live presence changes across all connected clients
  • Scalable Sync - Efficient batch processing for large user lists
Network Configuration: Presence features require network-level enablement. Contact the social.plus team to activate presence tracking for your application.

Quick Start

Get started with presence tracking in under 5 minutes:
import { PresenceManager, UserPresenceRepository } from '@social-plus/sdk';

// Initialize presence tracking
const presenceManager = new PresenceManager();

// Enable presence for current user
await presenceManager.enable();

// Start automatic heartbeat
presenceManager.startHeartbeat();

// Track other users' presence
const userPresence = new UserPresenceRepository();
await userPresence.syncUserPresence(['user-1', 'user-2']);

// Listen for presence updates
userPresence.onPresenceChange((updates) => {
  updates.forEach(update => {
    console.log(`${update.userId} is ${update.isOnline ? 'online' : 'offline'}`);
  });
});

Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Client Apps   │    │  Presence Hub   │    │  social.plus Core   │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │Heartbeat    │ │───▶│ │Sync Manager │ │───▶│ │Presence     │ │
│ │Manager      │ │    │ │& Batching   │ │    │ │Database     │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │Presence     │ │◀───│ │Real-time    │ │◀───│ │Event        │ │
│ │Observer     │ │    │ │Broadcaster  │ │    │ │Processor    │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Presence States

User Status

  • Online: Active within the last 60 seconds
  • Away: Last activity 1-15 minutes ago
  • Offline: No activity for 15+ minutes
  • Invisible: User chose to appear offline

Channel Activity

  • Active: Currently viewing/participating in channel
  • Idle: Channel open but not actively engaged
  • Away: Not currently in channel but recently active

Platform Guides

User Presence

Track individual user online status and activity

Channel Presence

Monitor member activity in conversation channels

Heartbeat Sync

Automatic presence synchronization and lifecycle management

Implementation Examples

Basic Presence Setup

import { AmityClient, PresenceManager } from '@social-plus/sdk';

class AppPresenceManager {
  private presence: PresenceManager;
  private isEnabled: boolean = false;

  constructor(private client: AmityClient) {
    this.presence = new PresenceManager(client);
  }

  async initialize() {
    // Check if presence is available
    const isAvailable = await this.presence.isEnabled();
    if (!isAvailable) {
      console.log('Presence not available for this network');
      return;
    }

    // Enable presence tracking
    try {
      await this.presence.enable();
      this.isEnabled = true;
      
      // Start heartbeat
      this.presence.startHeartbeat();
      
      console.log('Presence tracking enabled');
    } catch (error) {
      console.error('Failed to enable presence:', error);
    }
  }

  async cleanup() {
    if (this.isEnabled) {
      this.presence.stopHeartbeat();
      await this.presence.disable();
      this.isEnabled = false;
    }
  }
}

Online Users Display

import React, { useEffect, useState } from 'react';
import { UserPresenceRepository, UserPresence } from '@social-plus/sdk';

interface OnlineUsersProps {
  userIds: string[];
}

export const OnlineUsersList: React.FC<OnlineUsersProps> = ({ userIds }) => {
  const [userPresences, setUserPresences] = useState<UserPresence[]>([]);
  const [onlineCount, setOnlineCount] = useState(0);

  useEffect(() => {
    const presenceRepo = new UserPresenceRepository();
    
    // Sync presence for visible users
    userIds.forEach(userId => {
      presenceRepo.syncUserPresence(userId);
    });

    // Listen for presence updates
    const subscription = presenceRepo.getSyncingUserPresence()
      .subscribe(presences => {
        setUserPresences(presences);
        setOnlineCount(presences.filter(p => p.isOnline).length);
      });

    return () => {
      subscription.unsubscribe();
      presenceRepo.unsyncAllUserPresence();
    };
  }, [userIds]);

  return (
    <div className="online-users">
      <h3>Online Users ({onlineCount})</h3>
      <div className="users-list">
        {userPresences.map(presence => (
          <div 
            key={presence.userId} 
            className={`user-item ${presence.isOnline ? 'online' : 'offline'}`}
          >
            <div className={`status-indicator ${presence.isOnline ? 'online' : 'offline'}`} />
            <span className="user-id">{presence.userId}</span>
            <span className="last-seen">
              {presence.isOnline 
                ? 'Online' 
                : `Last seen ${formatTime(presence.lastHeartbeat)}`
              }
            </span>
          </div>
        ))}
      </div>
    </div>
  );
};

const formatTime = (timestamp: number): string => {
  const date = new Date(timestamp);
  const now = new Date();
  const diff = now.getTime() - date.getTime();
  
  if (diff < 60000) return 'Just now';
  if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
  if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
  return date.toLocaleDateString();
};

Presence Settings Management

The SDK provides comprehensive presence configuration options:
class PresenceSettings {
  private presence: PresenceManager;

  constructor(presence: PresenceManager) {
    this.presence = presence;
  }

  async checkAvailability(): Promise<boolean> {
    return await this.presence.isEnabled();
  }

  async enablePresence(): Promise<void> {
    const isAvailable = await this.checkAvailability();
    if (!isAvailable) {
      throw new Error('Presence not available for this network');
    }

    await this.presence.enable();
    console.log('Presence enabled');
  }

  async disablePresence(): Promise<void> {
    await this.presence.disable();
    console.log('Presence disabled');
  }

  async togglePresence(): Promise<boolean> {
    const isCurrentlyEnabled = await this.presence.isEnabled();
    
    if (isCurrentlyEnabled) {
      await this.disablePresence();
      return false;
    } else {
      await this.enablePresence();
      return true;
    }
  }

  startHeartbeat(): void {
    this.presence.startHeartbeat();
    console.log('Heartbeat started');
  }

  stopHeartbeat(): void {
    this.presence.stopHeartbeat();
    console.log('Heartbeat stopped');
  }
}

Advanced Features

Bulk Presence Operations

Handle large-scale presence operations efficiently:
class BulkPresenceManager {
  private readonly MAX_SYNC_USERS = 20;
  private readonly MAX_SYNC_CHANNELS = 20;
  
  async syncMultipleUsers(userIds: string[]): Promise<void> {
    const batches = this.createBatches(userIds, this.MAX_SYNC_USERS);
    const presenceRepo = new UserPresenceRepository();
    
    for (const batch of batches) {
      await Promise.all(
        batch.map(userId => presenceRepo.syncUserPresence(userId))
      );
    }
  }
  
  async getOnlineUsersSnapshot(limit: number = 100): Promise<OnlineUsersSnapshot> {
    const presenceRepo = new UserPresenceRepository();
    const snapshot = await presenceRepo.getOnlineUsersSnapshot();
    
    return {
      users: snapshot.users.slice(0, limit),
      totalCount: await presenceRepo.getOnlineUsersCount(),
      canLoadMore: snapshot.canLoadMore,
      loadMore: () => snapshot.loadMore()
    };
  }
  
  private createBatches<T>(items: T[], batchSize: number): T[][] {
    const batches: T[][] = [];
    for (let i = 0; i < items.length; i += batchSize) {
      batches.push(items.slice(i, i + batchSize));
    }
    return batches;
  }
}

Smart Presence Detection

Implement intelligent presence detection with activity monitoring:
class SmartPresenceDetector {
  private activityTimer: NodeJS.Timeout | null = null;
  private isUserActive = true;
  private readonly IDLE_THRESHOLD = 5 * 60 * 1000; // 5 minutes

  constructor(private presenceManager: PresenceManager) {
    this.setupActivityListeners();
  }

  private setupActivityListeners(): void {
    // Browser/Web environment
    if (typeof window !== 'undefined') {
      ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => {
        document.addEventListener(event, () => this.resetActivityTimer(), { passive: true });
      });
    }
  }

  private resetActivityTimer(): void {
    if (this.activityTimer) {
      clearTimeout(this.activityTimer);
    }

    // If user was idle, mark as active again
    if (!this.isUserActive) {
      this.isUserActive = true;
      this.presenceManager.startHeartbeat();
    }

    // Set new idle timer
    this.activityTimer = setTimeout(() => {
      this.isUserActive = false;
      this.presenceManager.stopHeartbeat();
    }, this.IDLE_THRESHOLD);
  }

  getActivityStatus(): { isActive: boolean; lastActivity: Date } {
    return {
      isActive: this.isUserActive,
      lastActivity: new Date(Date.now() - (this.isUserActive ? 0 : this.IDLE_THRESHOLD))
    };
  }

  cleanup(): void {
    if (this.activityTimer) {
      clearTimeout(this.activityTimer);
    }
  }
}

Presence Analytics

Track presence patterns and user engagement:
class PresenceAnalytics {
  private sessionStart: Date = new Date();
  private presenceEvents: PresenceEvent[] = [];

  trackPresenceChange(userId: string, isOnline: boolean, context?: string): void {
    this.presenceEvents.push({
      userId,
      isOnline,
      timestamp: new Date(),
      context,
      type: 'presence_change'
    });
  }

  getSessionStats(): SessionStats {
    const now = new Date();
    const sessionDuration = now.getTime() - this.sessionStart.getTime();
    
    return {
      sessionDuration,
      totalPresenceChanges: this.presenceEvents.length,
      uniqueUsersTracked: new Set(this.presenceEvents.map(e => e.userId)).size,
      averageOnlineTime: this.calculateAverageOnlineTime(),
      peakOnlineUsers: this.calculatePeakOnlineUsers()
    };
  }

  private calculateAverageOnlineTime(): number {
    const userSessions = new Map<string, { start: Date, end?: Date }[]>();
    
    this.presenceEvents.forEach(event => {
      if (!userSessions.has(event.userId)) {
        userSessions.set(event.userId, []);
      }
      
      const sessions = userSessions.get(event.userId)!;
      
      if (event.isOnline) {
        sessions.push({ start: event.timestamp });
      } else {
        const lastSession = sessions[sessions.length - 1];
        if (lastSession && !lastSession.end) {
          lastSession.end = event.timestamp;
        }
      }
    });

    // Calculate average session duration
    let totalDuration = 0;
    let sessionCount = 0;

    userSessions.forEach(sessions => {
      sessions.forEach(session => {
        if (session.end) {
          totalDuration += session.end.getTime() - session.start.getTime();
          sessionCount++;
        }
      });
    });

    return sessionCount > 0 ? totalDuration / sessionCount : 0;
  }

  private calculatePeakOnlineUsers(): number {
    const timeSlots = new Map<number, Set<string>>();
    const SLOT_DURATION = 60000; // 1 minute slots

    this.presenceEvents.forEach(event => {
      const slot = Math.floor(event.timestamp.getTime() / SLOT_DURATION);
      if (!timeSlots.has(slot)) {
        timeSlots.set(slot, new Set());
      }
      
      if (event.isOnline) {
        timeSlots.get(slot)!.add(event.userId);
      }
    });

    return Math.max(...Array.from(timeSlots.values()).map(users => users.size));
  }
}

interface PresenceEvent {
  userId: string;
  isOnline: boolean;
  timestamp: Date;
  context?: string;
  type: string;
}

interface SessionStats {
  sessionDuration: number;
  totalPresenceChanges: number;
  uniqueUsersTracked: number;
  averageOnlineTime: number;
  peakOnlineUsers: number;
}

Best Practices

Efficient Sync Management: Only sync presence for visible users to minimize resource usage.
class OptimizedPresenceManager {
  private visibleUsers = new Set<string>();
  private syncedUsers = new Set<string>();
  
  // Only sync users that are actually visible
  updateVisibleUsers(userIds: string[]): void {
    const newVisibleUsers = new Set(userIds);
    
    // Unsync users no longer visible
    this.syncedUsers.forEach(userId => {
      if (!newVisibleUsers.has(userId)) {
        this.unsyncUserPresence(userId);
        this.syncedUsers.delete(userId);
      }
    });
    
    // Sync newly visible users
    newVisibleUsers.forEach(userId => {
      if (!this.syncedUsers.has(userId)) {
        this.syncUserPresence(userId);
        this.syncedUsers.add(userId);
      }
    });
    
    this.visibleUsers = newVisibleUsers;
  }
  
  // Batch operations for better performance
  private async syncUserPresence(userId: string): Promise<void> {
    // Implementation with batching logic
  }
}
Smart Heartbeat Management: Adjust heartbeat frequency based on app state.
class AdaptiveHeartbeat {
  private heartbeatInterval: number = 30000; // Default 30s
  
  adjustHeartbeatFrequency(appState: 'active' | 'background' | 'inactive'): void {
    switch (appState) {
      case 'active':
        this.heartbeatInterval = 20000; // 20s for active usage
        break;
      case 'background':
        this.heartbeatInterval = 60000; // 60s for background
        break;
      case 'inactive':
        this.heartbeatInterval = 120000; // 2min for inactive
        break;
    }
  }
}
Progressive Loading: Show presence information as it becomes available.
interface PresenceUIState {
  loading: boolean;
  users: UserWithPresence[];
  error?: string;
}

const PresenceList: React.FC = () => {
  const [state, setState] = useState<PresenceUIState>({
    loading: true,
    users: []
  });
  
  useEffect(() => {
    const presenceRepo = new UserPresenceRepository();
    
    // Show loading state
    setState(prev => ({ ...prev, loading: true }));
    
    // Subscribe to presence updates
    const subscription = presenceRepo.getSyncingUserPresence()
      .subscribe({
        next: (presences) => {
          setState(prev => ({
            ...prev,
            loading: false,
            users: mergePresenceData(prev.users, presences)
          }));
        },
        error: (error) => {
          setState(prev => ({
            ...prev,
            loading: false,
            error: error.message
          }));
        }
      });
    
    return () => subscription.unsubscribe();
  }, []);
  
  return (
    <div>
      {state.loading && <LoadingSkeleton />}
      {state.users.map(user => (
        <UserPresenceItem key={user.id} user={user} />
      ))}
    </div>
  );
};
Robust Error Recovery: Handle network interruptions and service errors gracefully.
class ResilientPresenceManager {
  private retryAttempts = 0;
  private readonly maxRetries = 3;
  private readonly retryDelays = [1000, 3000, 5000];
  
  async enablePresenceWithRetry(): Promise<void> {
    try {
      await this.presenceManager.enable();
      this.retryAttempts = 0; // Reset on success
    } catch (error) {
      if (this.retryAttempts < this.maxRetries) {
        const delay = this.retryDelays[this.retryAttempts];
        await this.delay(delay);
        this.retryAttempts++;
        return this.enablePresenceWithRetry();
      }
      throw new Error('Failed to enable presence after multiple attempts');
    }
  }
  
  private handleNetworkError(error: any): void {
    if (error.code === 'NETWORK_ERROR') {
      // Implement exponential backoff
      this.scheduleRetry();
    } else if (error.code === 'UNAUTHORIZED') {
      // Handle auth issues
      this.handleAuthError();
    }
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
Privacy Controls: Respect user privacy preferences and provide granular controls.
class PrivacyAwarePresence {
  private privacySettings: PresencePrivacySettings;
  
  constructor(settings: PresencePrivacySettings) {
    this.privacySettings = settings;
  }
  
  shouldShowPresence(userId: string, context: string): boolean {
    // Check user privacy settings
    if (!this.privacySettings.allowPresenceSharing) {
      return false;
    }
    
    // Check context-specific settings
    if (context === 'channel' && !this.privacySettings.allowChannelPresence) {
      return false;
    }
    
    // Check blocked users
    if (this.privacySettings.blockedUsers.includes(userId)) {
      return false;
    }
    
    return true;
  }
  
  getFilteredPresence(presences: UserPresence[]): UserPresence[] {
    return presences.filter(presence => 
      this.shouldShowPresence(presence.userId, 'general')
    );
  }
}

interface PresencePrivacySettings {
  allowPresenceSharing: boolean;
  allowChannelPresence: boolean;
  blockedUsers: string[];
  invisibleMode: boolean;
}

Common Use Cases

Chat Applications

Show who’s online and active in conversations:
const ChatPresenceIndicator: React.FC<{ userId: string }> = ({ userId }) => {
  const [presence, setPresence] = useState<UserPresence | null>(null);
  
  useEffect(() => {
    const repo = new UserPresenceRepository();
    repo.syncUserPresence(userId);
    
    const subscription = repo.getSyncingUserPresence()
      .subscribe(presences => {
        const userPresence = presences.find(p => p.userId === userId);
        setPresence(userPresence || null);
      });
    
    return () => {
      subscription.unsubscribe();
      repo.unsyncUserPresence(userId);
    };
  }, [userId]);
  
  return (
    <div className="presence-indicator">
      <div className={`status-dot ${presence?.isOnline ? 'online' : 'offline'}`} />
      <span className="status-text">
        {presence ? (presence.isOnline ? 'Online' : 'Offline') : 'Unknown'}
      </span>
    </div>
  );
};

Team Collaboration

Monitor team member availability:
const TeamPresenceBoard: React.FC<{ teamId: string }> = ({ teamId }) => {
  const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
  const [presenceData, setPresenceData] = useState<Map<string, UserPresence>>(new Map());
  
  const organizedMembers = useMemo(() => {
    const online = teamMembers.filter(member => 
      presenceData.get(member.userId)?.isOnline
    );
    const offline = teamMembers.filter(member => 
      !presenceData.get(member.userId)?.isOnline
    );
    
    return { online, offline };
  }, [teamMembers, presenceData]);
  
  return (
    <div className="team-presence-board">
      <section className="online-section">
        <h3>Online ({organizedMembers.online.length})</h3>
        {organizedMembers.online.map(member => (
          <TeamMemberCard key={member.userId} member={member} isOnline={true} />
        ))}
      </section>
      
      <section className="offline-section">
        <h3>Offline ({organizedMembers.offline.length})</h3>
        {organizedMembers.offline.map(member => (
          <TeamMemberCard key={member.userId} member={member} isOnline={false} />
        ))}
      </section>
    </div>
  );
};

Live Events

Track participant engagement in live sessions:
class LiveEventPresence {
  private participantPresence = new Map<string, ParticipantStatus>();
  
  async trackEventParticipants(eventId: string, participantIds: string[]): Promise<void> {
    const channelRepo = new ChannelPresenceRepository();
    
    // Sync channel presence for the event
    await channelRepo.syncChannelPresence(eventId);
    
    // Track individual participants
    const userRepo = new UserPresenceRepository();
    participantIds.forEach(id => userRepo.syncUserPresence(id));
    
    // Listen for presence changes
    userRepo.getSyncingUserPresence().subscribe(presences => {
      presences.forEach(presence => {
        this.participantPresence.set(presence.userId, {
          isOnline: presence.isOnline,
          lastSeen: presence.lastHeartbeat,
          engagementLevel: this.calculateEngagement(presence)
        });
      });
      
      this.notifyPresenceUpdate();
    });
  }
  
  private calculateEngagement(presence: UserPresence): 'high' | 'medium' | 'low' {
    const timeSinceLastSeen = Date.now() - presence.lastHeartbeat;
    
    if (timeSinceLastSeen < 30000) return 'high';    // < 30s
    if (timeSinceLastSeen < 120000) return 'medium'; // < 2min
    return 'low';
  }
  
  getEventStats(): EventPresenceStats {
    const participants = Array.from(this.participantPresence.values());
    
    return {
      totalParticipants: participants.length,
      onlineCount: participants.filter(p => p.isOnline).length,
      highEngagement: participants.filter(p => p.engagementLevel === 'high').length,
      mediumEngagement: participants.filter(p => p.engagementLevel === 'medium').length,
      lowEngagement: participants.filter(p => p.engagementLevel === 'low').length
    };
  }
}

Troubleshooting

Network Configuration: Ensure presence is enabled at the network level.
const debugPresence = async () => {
  const presence = new PresenceManager();
  
  // Check if feature is available
  const isAvailable = await presence.isEnabled();
  console.log('Presence available:', isAvailable);
  
  if (!isAvailable) {
    console.error('Presence not enabled for this network. Contact social.plus support.');
    return;
  }
  
  // Check user-level settings
  try {
    await presence.enable();
    console.log('Presence enabled successfully');
  } catch (error) {
    console.error('Failed to enable presence:', error);
  }
};
Connection Problems: Debug heartbeat synchronization failures.
class HeartbeatDiagnostics {
  private heartbeatCount = 0;
  private failureCount = 0;
  
  startDiagnostics(presence: PresenceManager): void {
    const originalStartHeartbeat = presence.startHeartbeat.bind(presence);
    const originalStopHeartbeat = presence.stopHeartbeat.bind(presence);
    
    presence.startHeartbeat = () => {
      console.log('Starting heartbeat...');
      try {
        originalStartHeartbeat();
        this.heartbeatCount++;
        console.log(`Heartbeat started (count: ${this.heartbeatCount})`);
      } catch (error) {
        this.failureCount++;
        console.error(`Heartbeat start failed (failures: ${this.failureCount}):`, error);
      }
    };
    
    presence.stopHeartbeat = () => {
      console.log('Stopping heartbeat...');
      originalStopHeartbeat();
    };
  }
  
  getStats(): { heartbeats: number; failures: number; successRate: number } {
    const total = this.heartbeatCount + this.failureCount;
    return {
      heartbeats: this.heartbeatCount,
      failures: this.failureCount,
      successRate: total > 0 ? (this.heartbeatCount / total) * 100 : 0
    };
  }
}
Sync Limit Exceeded: Handle too many concurrent presence syncs.
class PresenceSyncManager {
  private syncedUsers = new Set<string>();
  private syncQueue: string[] = [];
  private readonly MAX_CONCURRENT_SYNCS = 20;
  
  async queueUserSync(userId: string): Promise<void> {
    if (this.syncedUsers.size >= this.MAX_CONCURRENT_SYNCS) {
      this.syncQueue.push(userId);
      console.warn(`Sync queue full. Queued user ${userId}`);
      return;
    }
    
    await this.syncUserPresence(userId);
  }
  
  private async syncUserPresence(userId: string): Promise<void> {
    if (this.syncedUsers.has(userId)) return;
    
    try {
      const repo = new UserPresenceRepository();
      await repo.syncUserPresence(userId);
      this.syncedUsers.add(userId);
    } catch (error) {
      console.error(`Failed to sync user ${userId}:`, error);
    }
  }
  
  async unsyncUserPresence(userId: string): Promise<void> {
    if (!this.syncedUsers.has(userId)) return;
    
    const repo = new UserPresenceRepository();
    await repo.unsyncUserPresence(userId);
    this.syncedUsers.delete(userId);
    
    // Process queue if space available
    if (this.syncQueue.length > 0 && this.syncedUsers.size < this.MAX_CONCURRENT_SYNCS) {
      const nextUserId = this.syncQueue.shift()!;
      await this.syncUserPresence(nextUserId);
    }
  }
}

Next Steps

User Presence

Deep dive into individual user presence tracking and management

Channel Presence

Monitor member activity and engagement in conversation channels

Heartbeat Sync

Understand automatic presence synchronization and lifecycle management

Real-time Events

Learn about real-time event handling and reactive programming patterns
Production Ready: All examples include comprehensive error handling, performance optimizations, and privacy considerations suitable for production applications.