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
TheChannelPresence object provides comprehensive channel activity information:
| Property | Type | Description |
|---|---|---|
channelId | string | Unique identifier of the conversation channel |
userPresences | UserPresence[] | Array of presence objects for synced channel members |
isAnyMemberOnline | boolean | Quick check if any member (excluding current user) is online |
activeUserCount | number | Count of currently active members in the channel |
lastActivity | number | Timestamp 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.- TypeScript
- iOS
- Android
- React Native
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);
}
});
}
}
import SocialPlusSDK
class ChannelPresenceManager {
private let repository: AmityChannelPresenceRepository
private var syncedChannels: Set<String> = []
private let maxSyncChannels = 20
init(client: AmityClient) {
self.repository = AmityChannelPresenceRepository(client: client)
}
func syncChannelPresence(channelId: String, viewId: String? = nil) {
guard syncedChannels.count < maxSyncChannels else {
print("Maximum sync limit (\(maxSyncChannels)) reached. Unsync some channels first.")
return
}
repository.syncChannelPresence(channelId: channelId, viewId: viewId)
syncedChannels.insert(channelId)
print("Started syncing presence for channel: \(channelId)")
}
func unsyncChannelPresence(channelId: String, viewId: String? = nil) {
repository.unsyncChannelPresence(channelId: channelId, viewId: viewId)
syncedChannels.remove(channelId)
print("Stopped syncing presence for channel: \(channelId)")
}
func unsyncAllChannelPresence() {
repository.unsyncAllChannelPresence()
syncedChannels.removeAll()
print("Stopped syncing all channel presences")
}
func getSyncedChannels() -> [String] {
return Array(syncedChannels)
}
func canSyncMore() -> Bool {
return syncedChannels.count < maxSyncChannels
}
// Efficient management for chat lists
func updateVisibleChannels(_ channelIds: [String]) {
let newChannels = Set(channelIds)
// Unsync channels no longer visible
for channelId in syncedChannels {
if !newChannels.contains(channelId) {
unsyncChannelPresence(channelId: channelId)
}
}
// Sync newly visible channels
for channelId in newChannels {
if !syncedChannels.contains(channelId) && canSyncMore() {
syncChannelPresence(channelId: channelId)
}
}
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityChannelPresenceRepository
class ChannelPresenceManager(private val client: AmityClient) {
private val repository = AmityChannelPresenceRepository(client)
private val syncedChannels = mutableSetOf<String>()
private val maxSyncChannels = 20
fun syncChannelPresence(channelId: String, viewId: String? = null) {
if (syncedChannels.size >= maxSyncChannels) {
println("Maximum sync limit ($maxSyncChannels) reached. Unsync some channels first.")
return
}
try {
repository.syncChannelPresence(channelId, viewId)
syncedChannels.add(channelId)
println("Started syncing presence for channel: $channelId")
} catch (error: Exception) {
println("Failed to sync channel presence for $channelId: $error")
}
}
fun unsyncChannelPresence(channelId: String, viewId: String? = null) {
try {
repository.unsyncChannelPresence(channelId, viewId)
syncedChannels.remove(channelId)
println("Stopped syncing presence for channel: $channelId")
} catch (error: Exception) {
println("Failed to unsync channel presence for $channelId: $error")
}
}
fun unsyncAllChannelPresence() {
try {
repository.unsyncAllChannelPresence()
syncedChannels.clear()
println("Stopped syncing all channel presences")
} catch (error: Exception) {
println("Failed to unsync all channel presences: $error")
}
}
fun getSyncedChannels(): List<String> = syncedChannels.toList()
fun canSyncMore(): Boolean = syncedChannels.size < maxSyncChannels
// Batch operations for efficiency
fun updateVisibleChannels(channelIds: List<String>) {
val newChannels = channelIds.toSet()
// Remove channels no longer visible
val channelsToUnsync = syncedChannels - newChannels
channelsToUnsync.forEach { channelId ->
unsyncChannelPresence(channelId)
}
// Add newly visible channels
val channelsToSync = (newChannels - syncedChannels).take(maxSyncChannels - syncedChannels.size)
channelsToSync.forEach { channelId ->
syncChannelPresence(channelId)
}
}
}
import { AmityChannelPresenceRepository, AmityChannelPresence } from '@amityco/react-native-sdk';
import { useEffect, useState, useCallback } from 'react';
export const useChannelPresence = (channelIds: string[]) => {
const [channelPresences, setChannelPresences] = useState<AmityChannelPresence[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const repository = new AmityChannelPresenceRepository();
const syncChannels = useCallback(async () => {
try {
for (const channelId of channelIds) {
await repository.syncChannelPresence(channelId);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to sync channels');
}
}, [channelIds, repository]);
const unsyncChannels = useCallback(async () => {
try {
for (const channelId of channelIds) {
await repository.unsyncChannelPresence(channelId);
}
} catch (err) {
console.error('Failed to unsync channels:', err);
}
}, [channelIds, repository]);
useEffect(() => {
setLoading(true);
setError(null);
// Start syncing
syncChannels();
// Subscribe to presence changes
const subscription = repository.getSyncingChannelPresence()
.subscribe({
next: (presences) => {
setChannelPresences(presences);
setLoading(false);
},
error: (err) => {
setError(err.message || 'Channel presence observation failed');
setLoading(false);
}
});
return () => {
subscription.unsubscribe();
unsyncChannels();
};
}, [syncChannels, unsyncChannels]);
return {
channelPresences,
loading,
error,
activeChannels: channelPresences.filter(cp => cp.isAnyMemberOnline),
totalActiveMembers: channelPresences.reduce((sum, cp) => sum + cp.activeUserCount, 0)
};
};
ViewId Parameter
Use the optionalviewId 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.- TypeScript
- iOS
- Android
- React Hook
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;
}
import SocialPlusSDK
import RxSwift
class ChannelPresenceObserver {
private let repository: AmityChannelPresenceRepository
private var disposeBag = DisposeBag()
init(client: AmityClient) {
self.repository = AmityChannelPresenceRepository(client: client)
}
func startObserving(onPresenceUpdate: @escaping ([AmityChannelPresence]) -> Void) {
repository.getSyncingChannelPresence()
.observe(on: MainScheduler.instance)
.subscribe(
onNext: { channelPresences in
print("Received presence updates for \(channelPresences.count) channels")
onPresenceUpdate(channelPresences)
},
onError: { error in
print("Channel presence observation error: \(error)")
}
)
.disposed(by: disposeBag)
}
func startObservingWithFilters(
activeOnly: Bool = false,
channelFilter: ((String) -> Bool)? = nil,
onPresenceUpdate: @escaping ([AmityChannelPresence]) -> Void
) {
repository.getSyncingChannelPresence()
.map { channelPresences in
var filtered = channelPresences
// Filter by activity
if activeOnly {
filtered = filtered.filter { $0.isAnyMemberOnline }
}
// Filter by channel criteria
if let channelFilter = channelFilter {
filtered = filtered.filter { channelFilter($0.channelId) }
}
return filtered
}
.observe(on: MainScheduler.instance)
.subscribe(
onNext: onPresenceUpdate,
onError: { error in
print("Channel presence observation error: \(error)")
}
)
.disposed(by: disposeBag)
}
func stopObserving() {
disposeBag = DisposeBag()
}
}
// SwiftUI integration
struct ChannelPresenceView: View {
@State private var channelPresences: [AmityChannelPresence] = []
private let observer: ChannelPresenceObserver
private let channelIds: [String]
init(client: AmityClient, channelIds: [String]) {
self.observer = ChannelPresenceObserver(client: client)
self.channelIds = channelIds
}
var body: some View {
VStack {
Text("Channel Activity")
.font(.headline)
ForEach(channelPresences, id: \.channelId) { channelPresence in
ChannelPresenceRow(channelPresence: channelPresence)
}
}
.onAppear {
startMonitoring()
}
.onDisappear {
observer.stopObserving()
}
}
private func startMonitoring() {
// Sync channels
channelIds.forEach { channelId in
AmityChannelPresenceRepository().syncChannelPresence(channelId)
}
// Observe changes
observer.startObserving { presences in
self.channelPresences = presences
}
}
}
struct ChannelPresenceRow: View {
let channelPresence: AmityChannelPresence
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Channel \(channelPresence.channelId)")
.font(.body)
Text("\(channelPresence.activeUserCount) active members")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Circle()
.fill(channelPresence.isAnyMemberOnline ? Color.green : Color.gray)
.frame(width: 8, height: 8)
}
.padding()
}
}
import io.amity.sdk.AmityClient
import io.amity.sdk.presence.AmityChannelPresenceRepository
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
class ChannelPresenceObserver(private val client: AmityClient) {
private val repository = AmityChannelPresenceRepository(client)
private val disposables = CompositeDisposable()
fun startObserving(onPresenceUpdate: (List<AmityChannelPresence>) -> Unit) {
repository.getSyncingChannelPresence()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ channelPresences ->
println("Received presence updates for ${channelPresences.size} channels")
onPresenceUpdate(channelPresences)
},
{ error ->
println("Channel presence observation error: $error")
}
)
.let(disposables::add)
}
fun startObservingWithFilters(
activeOnly: Boolean = false,
channelFilter: ((String) -> Boolean)? = null,
onPresenceUpdate: (List<AmityChannelPresence>) -> Unit
) {
repository.getSyncingChannelPresence()
.map { channelPresences ->
var filtered = channelPresences
// Filter by activity
if (activeOnly) {
filtered = filtered.filter { it.isAnyMemberOnline }
}
// Filter by channel criteria
channelFilter?.let { filter ->
filtered = filtered.filter { filter(it.channelId) }
}
filtered
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
onPresenceUpdate,
{ error -> println("Channel presence observation error: $error") }
)
.let(disposables::add)
}
fun stopObserving() {
disposables.clear()
}
}
// Android Compose integration
@Composable
fun ChannelPresenceList(client: AmityClient, channelIds: List<String>) {
var channelPresences by remember { mutableStateOf<List<AmityChannelPresence>>(emptyList()) }
val observer = remember { ChannelPresenceObserver(client) }
LaunchedEffect(channelIds) {
// Sync channels
val repository = AmityChannelPresenceRepository(client)
channelIds.forEach { channelId ->
repository.syncChannelPresence(channelId)
}
// Observe changes
observer.startObserving { presences ->
channelPresences = presences
}
}
DisposableEffect(Unit) {
onDispose {
observer.stopObserving()
AmityChannelPresenceRepository(client).unsyncAllChannelPresence()
}
}
LazyColumn {
items(channelPresences, key = { it.channelId }) { channelPresence ->
ChannelPresenceItem(channelPresence = channelPresence)
}
}
}
@Composable
fun ChannelPresenceItem(channelPresence: AmityChannelPresence) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Channel ${channelPresence.channelId}",
style = MaterialTheme.typography.body1
)
Text(
text = "${channelPresence.activeUserCount} active members",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
)
// Show online members
if (channelPresence.userPresences.isNotEmpty()) {
Text(
text = "Online: ${channelPresence.userPresences.filter { it.isOnline }.joinToString { it.userId }}",
style = MaterialTheme.typography.caption,
color = Color.Green
)
}
}
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (channelPresence.isAnyMemberOnline) Color.Green else Color.Gray,
shape = CircleShape
)
)
}
}
import { useEffect, useState, useCallback } from 'react';
import { ChannelPresenceRepository, ChannelPresence } from '@social-plus/sdk';
interface UseChannelPresenceOptions {
activeOnly?: boolean;
autoSync?: boolean;
channelFilter?: (channelId: string) => boolean;
}
export const useChannelPresence = (
channelIds: string[],
options: UseChannelPresenceOptions = {}
) => {
const [channelPresences, setChannelPresences] = useState<ChannelPresence[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const repository = new ChannelPresenceRepository();
const { activeOnly = false, autoSync = true, channelFilter } = options;
const syncChannels = useCallback(async () => {
if (!autoSync) return;
try {
for (const channelId of channelIds) {
await repository.syncChannelPresence(channelId);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to sync channels');
}
}, [channelIds, autoSync, repository]);
const unsyncChannels = useCallback(async () => {
try {
for (const channelId of channelIds) {
await repository.unsyncChannelPresence(channelId);
}
} catch (err) {
console.error('Failed to unsync channels:', err);
}
}, [channelIds, repository]);
useEffect(() => {
setLoading(true);
setError(null);
// Start syncing
syncChannels();
// Subscribe to presence changes
const subscription = repository.getSyncingChannelPresence()
.subscribe({
next: (allChannelPresences) => {
let filtered = allChannelPresences;
// Filter by activity
if (activeOnly) {
filtered = filtered.filter(cp => cp.isAnyMemberOnline);
}
// Filter by channel criteria
if (channelFilter) {
filtered = filtered.filter(cp => channelFilter(cp.channelId));
}
setChannelPresences(filtered);
setLoading(false);
},
error: (err) => {
setError(err.message || 'Channel presence observation failed');
setLoading(false);
}
});
return () => {
subscription.unsubscribe();
unsyncChannels();
};
}, [syncChannels, unsyncChannels, activeOnly, channelFilter]);
const getChannelPresence = useCallback((channelId: string) => {
return channelPresences.find(cp => cp.channelId === channelId) || null;
}, [channelPresences]);
return {
channelPresences,
loading,
error,
getChannelPresence,
activeChannelsCount: channelPresences.filter(cp => cp.isAnyMemberOnline).length,
totalActiveMembers: channelPresences.reduce((sum, cp) => sum + cp.activeUserCount, 0)
};
};
// Usage in React component
const ChannelActivityDashboard: React.FC<{ channelIds: string[] }> = ({ channelIds }) => {
const {
channelPresences,
loading,
error,
activeChannelsCount,
totalActiveMembers
} = useChannelPresence(channelIds, {
activeOnly: false,
autoSync: true
});
if (loading) return <div>Loading channel presence...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="channel-activity-dashboard">
<div className="stats-summary">
<div className="stat">
<span className="stat-value">{activeChannelsCount}</span>
<span className="stat-label">Active Channels</span>
</div>
<div className="stat">
<span className="stat-value">{totalActiveMembers}</span>
<span className="stat-label">Total Active Members</span>
</div>
</div>
<div className="channel-list">
{channelPresences.map(channelPresence => (
<div key={channelPresence.channelId} className="channel-item">
<div className="channel-info">
<h4>Channel {channelPresence.channelId}</h4>
<p>{channelPresence.activeUserCount} members active</p>
{channelPresence.userPresences.length > 0 && (
<div className="online-members">
<strong>Online:</strong>
{channelPresence.userPresences
.filter(up => up.isOnline)
.map(up => up.userId)
.join(', ')}
</div>
)}
</div>
<div className={`activity-indicator ${channelPresence.isAnyMemberOnline ? 'active' : 'inactive'}`} />
</div>
))}
</div>
</div>
);
};
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
Sync Optimization
Sync Optimization
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
};
Memory Management
Memory Management
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]);
User Experience
User Experience
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
Channel Type Not Supported
Channel Type Not Supported
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;
}
};
Sync Limit Exceeded
Sync Limit Exceeded
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;
}
}
Presence Data Not Updating
Presence Data Not Updating
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.