Skip to main content

Channel Presence Monitoring

Monitor member activity and engagement within conversation channels to create dynamic, responsive chat experiences. Track who’s active in channels, show typing indicators, and build engaging group communication features.

Overview

Channel presence enables you to see which members are currently active in conversation channels, creating more engaging and responsive chat experiences. Perfect for team collaboration, group messaging, and community platforms where member engagement visibility drives interaction.

Quick Start

Start monitoring channel presence in minutes:
import { ChannelPresenceRepository, ChannelPresence } from '@social-plus/sdk';

// Initialize channel presence repository
const channelPresence = new ChannelPresenceRepository();

// Start monitoring a conversation channel
await channelPresence.syncChannelPresence('channel-123');

// Listen for presence updates
channelPresence.getSyncingChannelPresence().subscribe(updates => {
  updates.forEach(channelPresenceData => {
    console.log(`Channel ${channelPresenceData.channelId}:`);
    console.log(`- Active members: ${channelPresenceData.userPresences.length}`);
    console.log(`- Anyone online: ${channelPresenceData.isAnyMemberOnline}`);
  });
});

Channel Presence Object

The ChannelPresence object provides comprehensive channel activity information:
PropertyTypeDescription
channelIdstringUnique identifier of the conversation channel
userPresencesUserPresence[]Array of presence objects for synced channel members
isAnyMemberOnlinebooleanQuick check if any member (excluding current user) is online
activeUserCountnumberCount of currently active members in the channel
lastActivitynumberTimestamp of the most recent member activity
Channel Type Support: Only conversation channels support presence monitoring. Community and broadcast channels do not provide presence information.

Real-time Channel Synchronization

Monitor member presence in conversation channels with efficient real-time updates.

Sync Channel Presence

Start monitoring presence for specific conversation channels:
Sync Limits: Maximum of 20 channels can be synced simultaneously. Use unsyncChannelPresence() to remove channels no longer needed.
import { ChannelPresenceRepository, ChannelPresence } from '@social-plus/sdk';

class ChannelPresenceManager {
  private repository: ChannelPresenceRepository;
  private syncedChannels = new Set<string>();
  private readonly MAX_SYNC_CHANNELS = 20;

  constructor() {
    this.repository = new ChannelPresenceRepository();
  }

  async syncChannelPresence(channelId: string, viewId?: string): Promise<void> {
    if (this.syncedChannels.size >= this.MAX_SYNC_CHANNELS) {
      console.warn(`Maximum sync limit (${this.MAX_SYNC_CHANNELS}) reached. Unsync some channels first.`);
      return;
    }

    try {
      await this.repository.syncChannelPresence(channelId, viewId);
      this.syncedChannels.add(channelId);
      console.log(`Started syncing presence for channel: ${channelId}`);
    } catch (error) {
      console.error(`Failed to sync channel presence for ${channelId}:`, error);
    }
  }

  async unsyncChannelPresence(channelId: string, viewId?: string): Promise<void> {
    try {
      await this.repository.unsyncChannelPresence(channelId, viewId);
      this.syncedChannels.delete(channelId);
      console.log(`Stopped syncing presence for channel: ${channelId}`);
    } catch (error) {
      console.error(`Failed to unsync channel presence for ${channelId}:`, error);
    }
  }

  async unsyncAllChannelPresence(): Promise<void> {
    try {
      await this.repository.unsyncAllChannelPresence();
      this.syncedChannels.clear();
      console.log('Stopped syncing all channel presences');
    } catch (error) {
      console.error('Failed to unsync all channel presences:', error);
    }
  }

  getSyncedChannels(): string[] {
    return Array.from(this.syncedChannels);
  }

  canSyncMore(): boolean {
    return this.syncedChannels.size < this.MAX_SYNC_CHANNELS;
  }

  // Smart channel management for chat lists
  updateVisibleChannels(channelIds: string[]): void {
    const newChannels = new Set(channelIds);
    
    // Unsync channels no longer visible
    this.syncedChannels.forEach(channelId => {
      if (!newChannels.has(channelId)) {
        this.unsyncChannelPresence(channelId);
      }
    });

    // Sync newly visible channels
    newChannels.forEach(channelId => {
      if (!this.syncedChannels.has(channelId) && this.canSyncMore()) {
        this.syncChannelPresence(channelId);
      }
    });
  }
}

ViewId Parameter

Use the optional viewId parameter to bind channels to specific UI views:
// Same channel in different UI components
await channelRepo.syncChannelPresence('channel-123', 'chat-list');
await channelRepo.syncChannelPresence('channel-123', 'channel-detail');

// Unsync from one view doesn't affect the other
await channelRepo.unsyncChannelPresence('channel-123', 'chat-list');
// channel-123 still synced for 'channel-detail' view

Observing Channel Presence Changes

Listen to real-time presence updates for all synced channels. Get immediate notifications when members become active or leave channels.
import { ChannelPresenceRepository, ChannelPresence } from '@social-plus/sdk';
import { Subscription } from 'rxjs';

class ChannelPresenceObserver {
  private subscription: Subscription | null = null;
  private repository: ChannelPresenceRepository;

  constructor() {
    this.repository = new ChannelPresenceRepository();
  }

  startObserving(onPresenceUpdate: (channelPresences: ChannelPresence[]) => void): void {
    this.subscription = this.repository.getSyncingChannelPresence()
      .subscribe({
        next: (channelPresences) => {
          console.log(`Received presence updates for ${channelPresences.length} channels`);
          onPresenceUpdate(channelPresences);
        },
        error: (error) => {
          console.error('Channel presence observation error:', error);
        }
      });
  }

  stopObserving(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
      this.subscription = null;
    }
  }

  // Enhanced observer with filtering
  startObservingWithFilters(
    activeOnly: boolean = false,
    channelFilter?: (channelId: string) => boolean,
    onPresenceUpdate?: (channelPresences: ChannelPresence[]) => void
  ): void {
    this.subscription = this.repository.getSyncingChannelPresence()
      .pipe(
        map(channelPresences => {
          let filtered = channelPresences;
          
          // Filter by activity
          if (activeOnly) {
            filtered = filtered.filter(cp => cp.isAnyMemberOnline);
          }
          
          // Filter by channel criteria
          if (channelFilter) {
            filtered = filtered.filter(cp => channelFilter(cp.channelId));
          }
          
          return filtered;
        })
      )
      .subscribe({
        next: onPresenceUpdate,
        error: (error) => console.error('Channel presence observation error:', error)
      });
  }
}

// Usage example with comprehensive channel monitoring
class ChatChannelManager {
  private presenceObserver: ChannelPresenceObserver;
  private channelPresenceData = new Map<string, ChannelPresence>();

  constructor() {
    this.presenceObserver = new ChannelPresenceObserver();
  }

  startMonitoring(channelIds: string[]): void {
    // Sync channels
    channelIds.forEach(id => this.repository.syncChannelPresence(id));

    // Observe presence changes
    this.presenceObserver.startObserving((channelPresences) => {
      channelPresences.forEach(cp => {
        this.channelPresenceData.set(cp.channelId, cp);
        this.updateChannelUI(cp);
      });
    });
  }

  private updateChannelUI(channelPresence: ChannelPresence): void {
    const channelElement = document.getElementById(`channel-${channelPresence.channelId}`);
    if (channelElement) {
      // Update active member count
      const memberCountElement = channelElement.querySelector('.member-count');
      if (memberCountElement) {
        memberCountElement.textContent = `${channelPresence.activeUserCount} active`;
      }

      // Update activity indicator
      const activityIndicator = channelElement.querySelector('.activity-indicator');
      if (activityIndicator) {
        activityIndicator.classList.toggle('active', channelPresence.isAnyMemberOnline);
      }

      // Show/hide online members
      this.updateOnlineMembers(channelPresence);
    }
  }

  private updateOnlineMembers(channelPresence: ChannelPresence): void {
    const onlineMembers = channelPresence.userPresences.filter(up => up.isOnline);
    
    // Update UI with online members
    const onlineMembersElement = document.getElementById(`online-members-${channelPresence.channelId}`);
    if (onlineMembersElement) {
      onlineMembersElement.innerHTML = onlineMembers
        .map(member => `<span class="online-member">${member.userId}</span>`)
        .join('');
    }
  }

  getChannelActivity(channelId: string): ChannelActivityInfo | null {
    const presence = this.channelPresenceData.get(channelId);
    if (!presence) return null;

    return {
      channelId,
      isActive: presence.isAnyMemberOnline,
      activeCount: presence.activeUserCount,
      onlineMembers: presence.userPresences.filter(up => up.isOnline),
      lastActivity: presence.lastActivity
    };
  }

  cleanup(): void {
    this.presenceObserver.stopObserving();
    this.repository.unsyncAllChannelPresence();
  }
}

interface ChannelActivityInfo {
  channelId: string;
  isActive: boolean;
  activeCount: number;
  onlineMembers: UserPresence[];
  lastActivity: number;
}

Advanced Channel Features

Multi-Channel Activity Monitor

Track activity across multiple channels with intelligent prioritization:
interface ChannelPriority {
  channelId: string;
  priority: 'high' | 'medium' | 'low';
  category: 'work' | 'social' | 'project';
}

class MultiChannelActivityMonitor {
  private channelPresenceRepo: ChannelPresenceRepository;
  private channelPriorities: Map<string, ChannelPriority> = new Map();
  private activityCallbacks: Map<string, (activity: ChannelActivity) => void> = new Map();

  constructor() {
    this.channelPresenceRepo = new ChannelPresenceRepository();
  }

  addChannels(channels: ChannelPriority[]): void {
    channels.forEach(channel => {
      this.channelPriorities.set(channel.channelId, channel);
      this.channelPresenceRepo.syncChannelPresence(channel.channelId);
    });

    // Set up monitoring
    this.channelPresenceRepo.getSyncingChannelPresence()
      .subscribe(channelPresences => {
        this.processChannelActivities(channelPresences);
      });
  }

  private processChannelActivities(channelPresences: ChannelPresence[]): void {
    channelPresences.forEach(cp => {
      const priority = this.channelPriorities.get(cp.channelId);
      if (!priority) return;

      const activity: ChannelActivity = {
        channelId: cp.channelId,
        priority: priority.priority,
        category: priority.category,
        isActive: cp.isAnyMemberOnline,
        activeCount: cp.activeUserCount,
        onlineMembers: cp.userPresences.filter(up => up.isOnline),
        timestamp: Date.now(),
        urgencyScore: this.calculateUrgencyScore(cp, priority)
      };

      this.notifyActivity(activity);
    });
  }

  private calculateUrgencyScore(cp: ChannelPresence, priority: ChannelPriority): number {
    let score = 0;

    // Base score by priority
    switch (priority.priority) {
      case 'high': score += 100; break;
      case 'medium': score += 50; break;
      case 'low': score += 10; break;
    }

    // Boost for active members
    score += cp.activeUserCount * 10;

    // Category modifiers
    switch (priority.category) {
      case 'work': score *= 1.5; break;
      case 'project': score *= 1.3; break;
      case 'social': score *= 1.0; break;
    }

    return score;
  }

  private notifyActivity(activity: ChannelActivity): void {
    const callback = this.activityCallbacks.get(activity.channelId);
    if (callback) {
      callback(activity);
    }

    // Send notifications for high-priority channels
    if (activity.priority === 'high' && activity.isActive) {
      this.sendNotification(activity);
    }
  }

  onChannelActivity(channelId: string, callback: (activity: ChannelActivity) => void): void {
    this.activityCallbacks.set(channelId, callback);
  }

  getTopActiveChannels(limit: number = 5): ChannelActivity[] {
    const activities: ChannelActivity[] = [];
    // Implementation to return top active channels by urgency score
    return activities.slice(0, limit);
  }

  private sendNotification(activity: ChannelActivity): void {
    // Implementation for sending notifications
    console.log(`High priority activity in ${activity.channelId}: ${activity.activeCount} members active`);
  }
}

interface ChannelActivity {
  channelId: string;
  priority: 'high' | 'medium' | 'low';
  category: string;
  isActive: boolean;
  activeCount: number;
  onlineMembers: UserPresence[];
  timestamp: number;
  urgencyScore: number;
}

Smart Channel Recommendations

Suggest active channels to users based on presence patterns:
class ChannelRecommendationEngine {
  private presenceRepo: ChannelPresenceRepository;
  private userInteractions: Map<string, ChannelInteraction[]> = new Map();

  constructor() {
    this.presenceRepo = new ChannelPresenceRepository();
  }

  async getRecommendedChannels(
    userId: string, 
    availableChannels: string[]
  ): Promise<ChannelRecommendation[]> {
    const recommendations: ChannelRecommendation[] = [];

    // Sync all available channels to get presence data
    for (const channelId of availableChannels) {
      await this.presenceRepo.syncChannelPresence(channelId);
    }

    return new Promise(resolve => {
      this.presenceRepo.getSyncingChannelPresence()
        .subscribe(channelPresences => {
          channelPresences.forEach(cp => {
            const score = this.calculateRecommendationScore(userId, cp);
            
            recommendations.push({
              channelId: cp.channelId,
              score,
              reason: this.getRecommendationReason(cp, score),
              activeMembers: cp.userPresences.filter(up => up.isOnline),
              activityLevel: this.getActivityLevel(cp)
            });
          });

          // Sort by score and return top recommendations
          recommendations.sort((a, b) => b.score - a.score);
          resolve(recommendations);
        });
    });
  }

  private calculateRecommendationScore(userId: string, cp: ChannelPresence): number {
    let score = 0;

    // Active members boost
    score += cp.activeUserCount * 20;

    // Recent activity boost
    if (cp.isAnyMemberOnline) {
      score += 50;
    }

    // User interaction history
    const interactions = this.userInteractions.get(userId) || [];
    const channelInteractions = interactions.filter(i => i.channelId === cp.channelId);
    score += channelInteractions.length * 10;

    // Mutual connections (users you've interacted with before)
    const mutualConnections = cp.userPresences.filter(up => 
      interactions.some(i => i.withUserId === up.userId)
    );
    score += mutualConnections.length * 15;

    return score;
  }

  private getRecommendationReason(cp: ChannelPresence, score: number): string {
    if (cp.activeUserCount > 5) return 'Very active conversation';
    if (cp.isAnyMemberOnline) return 'Members are online now';
    if (score > 100) return 'High engagement potential';
    return 'Moderate activity';
  }

  private getActivityLevel(cp: ChannelPresence): 'high' | 'medium' | 'low' {
    if (cp.activeUserCount > 10) return 'high';
    if (cp.activeUserCount > 3) return 'medium';
    return 'low';
  }

  trackUserInteraction(userId: string, channelId: string, withUserId?: string): void {
    const interactions = this.userInteractions.get(userId) || [];
    interactions.push({
      channelId,
      withUserId,
      timestamp: Date.now(),
      type: 'message'
    });
    this.userInteractions.set(userId, interactions);
  }
}

interface ChannelRecommendation {
  channelId: string;
  score: number;
  reason: string;
  activeMembers: UserPresence[];
  activityLevel: 'high' | 'medium' | 'low';
}

interface ChannelInteraction {
  channelId: string;
  withUserId?: string;
  timestamp: number;
  type: 'message' | 'reaction' | 'mention';
}

Channel Presence Analytics

Track and analyze channel engagement patterns:
class ChannelPresenceAnalytics {
  private presenceData: Map<string, ChannelPresenceHistory[]> = new Map();
  private analyticsInterval: NodeJS.Timeout | null = null;

  interface ChannelPresenceHistory {
    timestamp: number;
    activeCount: number;
    onlineMembers: string[];
    isAnyMemberOnline: boolean;
  }

  startAnalytics(channelIds: string[], intervalMs: number = 60000): void {
    const presenceRepo = new ChannelPresenceRepository();
    
    // Sync all channels
    channelIds.forEach(id => presenceRepo.syncChannelPresence(id));

    // Collect data periodically
    this.analyticsInterval = setInterval(() => {
      presenceRepo.getSyncingChannelPresence()
        .subscribe(channelPresences => {
          this.recordPresenceData(channelPresences);
        });
    }, intervalMs);
  }

  private recordPresenceData(channelPresences: ChannelPresence[]): void {
    channelPresences.forEach(cp => {
      const history = this.presenceData.get(cp.channelId) || [];
      
      history.push({
        timestamp: Date.now(),
        activeCount: cp.activeUserCount,
        onlineMembers: cp.userPresences.filter(up => up.isOnline).map(up => up.userId),
        isAnyMemberOnline: cp.isAnyMemberOnline
      });

      // Keep only last 24 hours of data
      const cutoff = Date.now() - (24 * 60 * 60 * 1000);
      const filteredHistory = history.filter(h => h.timestamp > cutoff);
      
      this.presenceData.set(cp.channelId, filteredHistory);
    });
  }

  getChannelAnalytics(channelId: string, timeRange: TimeRange = '24h'): ChannelAnalytics {
    const history = this.presenceData.get(channelId) || [];
    const cutoff = this.getTimeCutoff(timeRange);
    const relevantData = history.filter(h => h.timestamp > cutoff);

    return {
      channelId,
      timeRange,
      totalDataPoints: relevantData.length,
      averageActiveMembers: this.calculateAverage(relevantData.map(d => d.activeCount)),
      peakActiveMembers: Math.max(...relevantData.map(d => d.activeCount)),
      activityPercentage: this.calculateActivityPercentage(relevantData),
      mostActiveHours: this.getMostActiveHours(relevantData),
      uniqueActiveMembers: this.getUniqueActiveMembers(relevantData),
      activityTrend: this.calculateTrend(relevantData)
    };
  }

  private getTimeCutoff(timeRange: TimeRange): number {
    const now = Date.now();
    switch (timeRange) {
      case '1h': return now - (60 * 60 * 1000);
      case '6h': return now - (6 * 60 * 60 * 1000);
      case '24h': return now - (24 * 60 * 60 * 1000);
      case '7d': return now - (7 * 24 * 60 * 60 * 1000);
      default: return now - (24 * 60 * 60 * 1000);
    }
  }

  private calculateAverage(numbers: number[]): number {
    return numbers.length > 0 ? numbers.reduce((a, b) => a + b, 0) / numbers.length : 0;
  }

  private calculateActivityPercentage(data: ChannelPresenceHistory[]): number {
    const activeDataPoints = data.filter(d => d.isAnyMemberOnline).length;
    return data.length > 0 ? (activeDataPoints / data.length) * 100 : 0;
  }

  private getMostActiveHours(data: ChannelPresenceHistory[]): number[] {
    const hourCounts = new Array(24).fill(0);
    
    data.forEach(d => {
      const hour = new Date(d.timestamp).getHours();
      hourCounts[hour] += d.activeCount;
    });

    // Return top 3 most active hours
    return hourCounts
      .map((count, hour) => ({ hour, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 3)
      .map(item => item.hour);
  }

  private getUniqueActiveMembers(data: ChannelPresenceHistory[]): string[] {
    const uniqueMembers = new Set<string>();
    data.forEach(d => {
      d.onlineMembers.forEach(member => uniqueMembers.add(member));
    });
    return Array.from(uniqueMembers);
  }

  private calculateTrend(data: ChannelPresenceHistory[]): 'increasing' | 'decreasing' | 'stable' {
    if (data.length < 2) return 'stable';
    
    const firstHalf = data.slice(0, Math.floor(data.length / 2));
    const secondHalf = data.slice(Math.floor(data.length / 2));
    
    const firstHalfAvg = this.calculateAverage(firstHalf.map(d => d.activeCount));
    const secondHalfAvg = this.calculateAverage(secondHalf.map(d => d.activeCount));
    
    const diff = secondHalfAvg - firstHalfAvg;
    
    if (diff > 0.5) return 'increasing';
    if (diff < -0.5) return 'decreasing';
    return 'stable';
  }

  stopAnalytics(): void {
    if (this.analyticsInterval) {
      clearInterval(this.analyticsInterval);
      this.analyticsInterval = null;
    }
  }
}

type TimeRange = '1h' | '6h' | '24h' | '7d';

interface ChannelAnalytics {
  channelId: string;
  timeRange: TimeRange;
  totalDataPoints: number;
  averageActiveMembers: number;
  peakActiveMembers: number;
  activityPercentage: number;
  mostActiveHours: number[];
  uniqueActiveMembers: string[];
  activityTrend: 'increasing' | 'decreasing' | 'stable';
}

Performance Optimization

Efficient Channel Sync Management

Optimize channel presence monitoring for large-scale applications:
class EfficientChannelSyncManager {
  private readonly MAX_CONCURRENT_SYNCS = 20;
  private syncQueue: string[] = [];
  private activeSyncs = new Set<string>();
  private presenceRepo: ChannelPresenceRepository;

  constructor() {
    this.presenceRepo = new ChannelPresenceRepository();
  }

  async requestSync(channelId: string, priority: 'high' | 'medium' | 'low' = 'medium'): Promise<void> {
    if (this.activeSyncs.has(channelId)) {
      return; // Already syncing
    }

    if (this.activeSyncs.size >= this.MAX_CONCURRENT_SYNCS) {
      this.queueSync(channelId, priority);
      return;
    }

    await this.performSync(channelId);
  }

  private queueSync(channelId: string, priority: 'high' | 'medium' | 'low'): void {
    // Remove if already queued
    this.syncQueue = this.syncQueue.filter(id => id !== channelId);
    
    // Add based on priority
    if (priority === 'high') {
      this.syncQueue.unshift(channelId);
    } else {
      this.syncQueue.push(channelId);
    }
  }

  private async performSync(channelId: string): Promise<void> {
    try {
      this.activeSyncs.add(channelId);
      await this.presenceRepo.syncChannelPresence(channelId);
      console.log(`Synced channel: ${channelId}`);
    } catch (error) {
      console.error(`Failed to sync channel ${channelId}:`, error);
    }
  }

  async unsyncChannel(channelId: string): Promise<void> {
    if (this.activeSyncs.has(channelId)) {
      await this.presenceRepo.unsyncChannelPresence(channelId);
      this.activeSyncs.delete(channelId);
      
      // Process queue
      this.processQueue();
    }
  }

  private async processQueue(): Promise<void> {
    if (this.syncQueue.length > 0 && this.activeSyncs.size < this.MAX_CONCURRENT_SYNCS) {
      const nextChannelId = this.syncQueue.shift()!;
      await this.performSync(nextChannelId);
    }
  }

  // Smart sync based on UI visibility
  updateVisibleChannels(visibleChannelIds: string[]): void {
    const currentlyVisible = new Set(visibleChannelIds);
    
    // Unsync channels that are no longer visible
    this.activeSyncs.forEach(channelId => {
      if (!currentlyVisible.has(channelId)) {
        this.unsyncChannel(channelId);
      }
    });

    // Sync newly visible channels
    visibleChannelIds.forEach(channelId => {
      if (!this.activeSyncs.has(channelId)) {
        this.requestSync(channelId, 'high'); // High priority for visible items
      }
    });
  }

  getStats(): { active: number; queued: number; capacity: number } {
    return {
      active: this.activeSyncs.size,
      queued: this.syncQueue.length,
      capacity: this.MAX_CONCURRENT_SYNCS
    };
  }
}

Best Practices

Smart Sync Management: Only sync channels that are currently visible or relevant to the user.
// Good: Sync only visible channels
const ChannelList = ({ channels }: { channels: Channel[] }) => {
  const visibleChannels = channels.slice(0, 20); // Limit to viewport
  const { channelPresences } = useChannelPresence(
    visibleChannels.map(c => c.id)
  );
  
  return (
    <div>
      {visibleChannels.map(channel => (
        <ChannelItem 
          key={channel.id} 
          channel={channel}
          presence={channelPresences.find(cp => cp.channelId === channel.id)}
        />
      ))}
    </div>
  );
};

// Bad: Syncing all channels regardless of visibility
const AllChannelsList = ({ channels }: { channels: Channel[] }) => {
  const { channelPresences } = useChannelPresence(
    channels.map(c => c.id) // Could be hundreds of channels
  );
  // This will fail if channels.length > 20
};
Cleanup Resources: Always unsubscribe and unsync when components unmount or navigate away.
useEffect(() => {
  const repository = new ChannelPresenceRepository();
  
  // Sync channels
  channelIds.forEach(id => repository.syncChannelPresence(id));
  
  // Subscribe to changes
  const subscription = repository.getSyncingChannelPresence()
    .subscribe(handlePresenceUpdate);
  
  return () => {
    // Critical cleanup
    subscription.unsubscribe();
    repository.unsyncAllChannelPresence();
  };
}, [channelIds]);
Progressive Loading: Show presence information as it becomes available.
const ChannelPresenceIndicator = ({ channelId }: { channelId: string }) => {
  const [presence, setPresence] = useState<ChannelPresence | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const repo = new ChannelPresenceRepository();
    repo.syncChannelPresence(channelId);
    
    const subscription = repo.getSyncingChannelPresence()
      .subscribe(presences => {
        const channelPresence = presences.find(cp => cp.channelId === channelId);
        setPresence(channelPresence || null);
        setLoading(false);
      });
    
    return () => {
      subscription.unsubscribe();
      repo.unsyncChannelPresence(channelId);
    };
  }, [channelId]);
  
  if (loading) {
    return <div className="presence-skeleton">Loading...</div>;
  }
  
  return (
    <div className="presence-indicator">
      <div className={`activity-dot ${presence?.isAnyMemberOnline ? 'active' : 'inactive'}`} />
      <span className="member-count">
        {presence?.activeUserCount || 0} active
      </span>
    </div>
  );
};

Troubleshooting

Problem: Trying to sync presence for community or broadcast channels.Solution: Only conversation channels support presence monitoring:
const syncChannelPresenceIfSupported = async (channel: Channel) => {
  if (channel.type !== 'conversation') {
    console.warn(`Channel presence not supported for type: ${channel.type}`);
    return false;
  }
  
  try {
    await repository.syncChannelPresence(channel.id);
    return true;
  } catch (error) {
    console.error('Failed to sync channel presence:', error);
    return false;
  }
};
Problem: Attempting to sync more than 20 channels simultaneously.Solution: Implement intelligent sync management:
class SyncLimitManager {
  private readonly MAX_SYNCS = 20;
  private activeSyncs = new Set<string>();
  
  async safeSync(channelId: string): Promise<boolean> {
    if (this.activeSyncs.size >= this.MAX_SYNCS) {
      // Remove least important channel
      const channelToRemove = this.selectChannelToUnsync();
      if (channelToRemove) {
        await this.unsyncChannel(channelToRemove);
      } else {
        return false; // Cannot sync more
      }
    }
    
    await repository.syncChannelPresence(channelId);
    this.activeSyncs.add(channelId);
    return true;
  }
  
  private selectChannelToUnsync(): string | null {
    // Implement priority-based selection
    // Return least important channel ID
    return this.activeSyncs.values().next().value || null;
  }
}
Problem: Channel presence information appears stale.Solution: Debug the presence flow:
const debugChannelPresence = async (channelId: string) => {
  const repository = new ChannelPresenceRepository();
  
  // 1. Verify channel type
  const channel = await getChannel(channelId);
  console.log('Channel type:', channel.type);
  
  // 2. Check if syncing
  await repository.syncChannelPresence(channelId);
  console.log('Started syncing channel:', channelId);
  
  // 3. Monitor updates
  repository.getSyncingChannelPresence()
    .subscribe({
      next: (presences) => {
        const channelPresence = presences.find(cp => cp.channelId === channelId);
        console.log('Channel presence update:', channelPresence);
      },
      error: (error) => {
        console.error('Presence observation error:', error);
      }
    });
};

Next Steps

User Presence

Learn about individual user presence tracking and management

Heartbeat Sync

Understand automatic presence synchronization and lifecycle

Chat Integration

Build real-time chat experiences with presence integration

Real-time Events

Create reactive applications with comprehensive event handling
Production Ready: All examples include comprehensive error handling, performance optimizations, and cleanup procedures suitable for production applications.