Skip to main content
Build a custom participant list UI to display and manage call participants. Listen for participant events and use the SDK’s management methods.

Get Participants

Listen for participant list changes:
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';

CometChatCalls.addEventListener('onParticipantListChanged', (participants) => {
  console.log('Participants:', participants);
});

Participant Object

Each participant contains:
PropertyTypeDescription
pidstringParticipant ID (unique per session)
uidstringUser ID
namestringDisplay name
avatarstringAvatar URL (optional)

Participant Events

Subscribe to individual participant events:
// Join/Leave
CometChatCalls.addEventListener('onParticipantJoined', (participant) => {
  console.log(`${participant.name} joined`);
});

CometChatCalls.addEventListener('onParticipantLeft', (participant) => {
  console.log(`${participant.name} left`);
});

// Audio state
CometChatCalls.addEventListener('onParticipantAudioMuted', (participant) => {
  console.log(`${participant.name} muted`);
});

CometChatCalls.addEventListener('onParticipantAudioUnmuted', (participant) => {
  console.log(`${participant.name} unmuted`);
});

// Video state
CometChatCalls.addEventListener('onParticipantVideoPaused', (participant) => {
  console.log(`${participant.name} video paused`);
});

CometChatCalls.addEventListener('onParticipantVideoResumed', (participant) => {
  console.log(`${participant.name} video resumed`);
});

// Hand raised
CometChatCalls.addEventListener('onParticipantHandRaised', (participant) => {
  console.log(`${participant.name} raised hand`);
});

CometChatCalls.addEventListener('onParticipantHandLowered', (participant) => {
  console.log(`${participant.name} lowered hand`);
});

// Screen sharing
CometChatCalls.addEventListener('onParticipantStartedScreenShare', (participant) => {
  console.log(`${participant.name} started screen share`);
});

CometChatCalls.addEventListener('onParticipantStoppedScreenShare', (participant) => {
  console.log(`${participant.name} stopped screen share`);
});

// Dominant speaker
CometChatCalls.addEventListener('onDominantSpeakerChanged', (participant) => {
  console.log(`Dominant speaker: ${participant.name}`);
});

Participant Actions

Manage participants using these methods:
// Pin participant
CometChatCalls.pinParticipant(participantId, 'human');

// Unpin
CometChatCalls.unpinParticipant();

// Mute participant (requires moderator)
CometChatCalls.muteParticipant(participantId);

// Pause participant video (requires moderator)
CometChatCalls.pauseParticipantVideo(participantId);

Complete Example

import React, { useState, useEffect } from 'react';
import {
  View,
  FlatList,
  Text,
  TouchableOpacity,
  StyleSheet,
  Image,
  Modal,
} from 'react-native';
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';

interface Participant {
  pid: string;
  uid: string;
  name: string;
  avatar?: string;
}

interface ParticipantState extends Participant {
  isAudioMuted: boolean;
  isVideoMuted: boolean;
  isHandRaised: boolean;
  isScreenSharing: boolean;
  isDominantSpeaker: boolean;
}

function CustomParticipantList() {
  const [participants, setParticipants] = useState<Map<string, ParticipantState>>(
    new Map()
  );
  const [pinnedId, setPinnedId] = useState<string | null>(null);
  const [isVisible, setIsVisible] = useState(false);
  const [dominantSpeakerId, setDominantSpeakerId] = useState<string | null>(null);

  useEffect(() => {
    const unsubscribers: Array<() => void> = [];

    // Participant list changes
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantListChanged',
        (list: Participant[]) => {
          setParticipants((prev) => {
            const newMap = new Map(prev);
            // Add new participants
            list.forEach((p) => {
              if (!newMap.has(p.pid)) {
                newMap.set(p.pid, {
                  ...p,
                  isAudioMuted: false,
                  isVideoMuted: false,
                  isHandRaised: false,
                  isScreenSharing: false,
                  isDominantSpeaker: false,
                });
              }
            });
            // Remove participants who left
            const currentPids = new Set(list.map((p) => p.pid));
            newMap.forEach((_, pid) => {
              if (!currentPids.has(pid)) {
                newMap.delete(pid);
              }
            });
            return newMap;
          });
        }
      )
    );

    // Audio state
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantAudioMuted',
        (p: Participant) => {
          updateParticipant(p.pid, { isAudioMuted: true });
        }
      )
    );

    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantAudioUnmuted',
        (p: Participant) => {
          updateParticipant(p.pid, { isAudioMuted: false });
        }
      )
    );

    // Video state
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantVideoPaused',
        (p: Participant) => {
          updateParticipant(p.pid, { isVideoMuted: true });
        }
      )
    );

    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantVideoResumed',
        (p: Participant) => {
          updateParticipant(p.pid, { isVideoMuted: false });
        }
      )
    );

    // Hand raised
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantHandRaised',
        (p: Participant) => {
          updateParticipant(p.pid, { isHandRaised: true });
        }
      )
    );

    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantHandLowered',
        (p: Participant) => {
          updateParticipant(p.pid, { isHandRaised: false });
        }
      )
    );

    // Screen sharing
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantStartedScreenShare',
        (p: Participant) => {
          updateParticipant(p.pid, { isScreenSharing: true });
        }
      )
    );

    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onParticipantStoppedScreenShare',
        (p: Participant) => {
          updateParticipant(p.pid, { isScreenSharing: false });
        }
      )
    );

    // Dominant speaker
    unsubscribers.push(
      CometChatCalls.addEventListener(
        'onDominantSpeakerChanged',
        (p: Participant) => {
          setDominantSpeakerId(p.pid);
        }
      )
    );

    return () => {
      unsubscribers.forEach((unsub) => unsub());
    };
  }, []);

  const updateParticipant = (pid: string, updates: Partial<ParticipantState>) => {
    setParticipants((prev) => {
      const newMap = new Map(prev);
      const participant = newMap.get(pid);
      if (participant) {
        newMap.set(pid, { ...participant, ...updates });
      }
      return newMap;
    });
  };

  const handlePin = (participant: ParticipantState) => {
    if (pinnedId === participant.pid) {
      CometChatCalls.unpinParticipant();
      setPinnedId(null);
    } else {
      CometChatCalls.pinParticipant(participant.pid, 'human');
      setPinnedId(participant.pid);
    }
  };

  const handleMute = (participant: ParticipantState) => {
    CometChatCalls.muteParticipant(participant.pid);
  };

  const renderParticipant = ({ item }: { item: ParticipantState }) => (
    <View style={styles.participantItem}>
      <View style={styles.avatarContainer}>
        {item.avatar ? (
          <Image source={{ uri: item.avatar }} style={styles.avatar} />
        ) : (
          <View style={styles.avatarPlaceholder}>
            <Text style={styles.avatarText}>
              {item.name.charAt(0).toUpperCase()}
            </Text>
          </View>
        )}
        {dominantSpeakerId === item.pid && (
          <View style={styles.speakingIndicator} />
        )}
      </View>

      <View style={styles.participantInfo}>
        <Text style={styles.participantName}>{item.name}</Text>
        <View style={styles.statusIcons}>
          {item.isAudioMuted && <Text style={styles.statusIcon}>🔇</Text>}
          {item.isVideoMuted && <Text style={styles.statusIcon}>📷</Text>}
          {item.isHandRaised && <Text style={styles.statusIcon}></Text>}
          {item.isScreenSharing && <Text style={styles.statusIcon}>🖥️</Text>}
          {pinnedId === item.pid && <Text style={styles.statusIcon}>📌</Text>}
        </View>
      </View>

      <View style={styles.actions}>
        <TouchableOpacity
          style={styles.actionButton}
          onPress={() => handlePin(item)}
        >
          <Text style={styles.actionText}>
            {pinnedId === item.pid ? 'Unpin' : 'Pin'}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.actionButton}
          onPress={() => handleMute(item)}
        >
          <Text style={styles.actionText}>Mute</Text>
        </TouchableOpacity>
      </View>
    </View>
  );

  const participantList = Array.from(participants.values());

  return (
    <>
      <TouchableOpacity
        style={styles.toggleButton}
        onPress={() => setIsVisible(true)}
      >
        <Text style={styles.toggleButtonText}>
          👥 {participantList.length}
        </Text>
      </TouchableOpacity>

      <Modal
        visible={isVisible}
        transparent
        animationType="slide"
        onRequestClose={() => setIsVisible(false)}
      >
        <View style={styles.modalOverlay}>
          <View style={styles.modalContent}>
            <View style={styles.modalHeader}>
              <Text style={styles.modalTitle}>
                Participants ({participantList.length})
              </Text>
              <TouchableOpacity onPress={() => setIsVisible(false)}>
                <Text style={styles.closeButton}></Text>
              </TouchableOpacity>
            </View>
            <FlatList
              data={participantList}
              keyExtractor={(item) => item.pid}
              renderItem={renderParticipant}
              ListEmptyComponent={
                <Text style={styles.emptyText}>No participants</Text>
              }
            />
          </View>
        </View>
      </Modal>
    </>
  );
}

const styles = StyleSheet.create({
  toggleButton: {
    position: 'absolute',
    top: 60,
    right: 16,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 20,
  },
  toggleButtonText: {
    color: '#fff',
    fontSize: 16,
  },
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'flex-end',
  },
  modalContent: {
    backgroundColor: '#1a1a1a',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    maxHeight: '70%',
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#333',
  },
  modalTitle: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600',
  },
  closeButton: {
    color: '#fff',
    fontSize: 20,
    padding: 4,
  },
  participantItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#333',
  },
  avatarContainer: {
    position: 'relative',
  },
  avatar: {
    width: 44,
    height: 44,
    borderRadius: 22,
  },
  avatarPlaceholder: {
    width: 44,
    height: 44,
    borderRadius: 22,
    backgroundColor: '#6851D6',
    justifyContent: 'center',
    alignItems: 'center',
  },
  avatarText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600',
  },
  speakingIndicator: {
    position: 'absolute',
    bottom: 0,
    right: 0,
    width: 14,
    height: 14,
    borderRadius: 7,
    backgroundColor: '#22c55e',
    borderWidth: 2,
    borderColor: '#1a1a1a',
  },
  participantInfo: {
    flex: 1,
    marginLeft: 12,
  },
  participantName: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '500',
  },
  statusIcons: {
    flexDirection: 'row',
    marginTop: 4,
    gap: 4,
  },
  statusIcon: {
    fontSize: 14,
  },
  actions: {
    flexDirection: 'row',
    gap: 8,
  },
  actionButton: {
    backgroundColor: '#333',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 4,
  },
  actionText: {
    color: '#fff',
    fontSize: 12,
  },
  emptyText: {
    color: '#666',
    textAlign: 'center',
    padding: 20,
  },
});

export default CustomParticipantList;