consulting1195) tandis que le SDK mobile générait des IDs différents (#AbWyGIH9S)UserRegister retournait des erreurs de format (code 660000002)saveGroupId devait être appelé depuis le mobile pour synchroniserEaseUIKit) avec interface complète de chatsaveGroupId depuis le mobile n'est plus nécessaire| Concept | Description |
|---|---|
room_name |
Identifiant unique de la consultation, format : "consulting" + consulting.id.Exemple : consulting1380.Utilisé comme nom de groupe Agora Chat et comme channel_name pour les appels RTC. Généré par Laravel lors de la création de la consultation. |
group_id |
Identifiant du groupe Agora Chat, auto-généré par Agora. Exemple : 315697874927617.Retourné par l'API REST Agora lors de la création du groupe. Sauvegardé dans consultings.group_id par Laravel. |
username |
Identifiant utilisateur Agora Chat, dérivé de l'ID utilisateur. Règle : minuscules + préfixe u si commence par un chiffre.Exemple : ID 346 → username u346 |
storeConsultingScheduled(). Laravel crée la consultation avec doctor_uuid déjà assigné.room_name = "consulting" + consulting.id (ex: consulting1380).group_id auto-généré (ex: 315697874927617).group_id est sauvegardé dans la base de données (consultings.group_id).group_id via getConsulting() et ouvre le chat directement.store(). Pas de médecin assigné — doctor_uuid = null.acceptConsulting(). Laravel assigne doctor_uuid.acceptConsulting(). Le groupe est créé côté serveur.group_id et rejoint le même groupe.saveGroupId depuis le mobile n'est plus utilisé pour la création. Le backend Laravel gère tout automatiquement.
agora/chat-tokenAuthentification : Bearer Token (Sanctum)
Entrées : Aucune — l'utilisateur est déduit de l'authentification.
Utilisation : Appelé par le mobile avant d'ouvrir l'écran de chat. Le token retourné est passé à AgoraChatHelper.login().
{
"status": true,
"message": "success",
"data": {
"token": "007eJxTYOhdrv0y6GdG0CVlX6WC...",
"username": "u346"
},
"errors": []
}
| Champ | Description |
|---|---|
data.token | Token Agora Chat généré par Laravel. Durée : 1 heure. Passé à ChatClient.loginWithAgoraToken(username, token). |
data.username | Le nom d'utilisateur Agora (sanitized). L'ID 346 devient "u346" car Agora interdit les usernames commençant par un chiffre. |
agora/rtc-tokenAuthentification : Bearer Token (Sanctum)
| Paramètre | Type | Description |
|---|---|---|
channel_name |
String (requis) | Le nom du canal RTC. Correspond à consulting.room_name.Exemple : "consulting1380"Les deux utilisateurs (patient + médecin) doivent utiliser le même channel_name pour se voir/entendre. Android : consulting.getRoom_name()iOS : consulting.roomName |
uid |
int (optionnel) | Identifiant unique dans le canal. Valeur recommandée : 0 (Agora assigne automatiquement).Le même uid doit être utilisé dans rtcEngine.joinChannel(token, channelName, uid, options). |
{
"status": true,
"message": "success",
"data": {
"token": "007eJxTYMg66/4v/P6ZxBwPRd3I...",
"channel_name": "consulting1380",
"uid": 0,
"app_id": "cdc6c95ed75d45cda0a45ee86a4c6c64"
},
"errors": []
}
| Champ | Description |
|---|---|
data.token | Token RTC. Durée : 1 heure. Passé à rtcEngine.joinChannel(token, channelName, uid, options). |
data.channel_name | Echo du channel_name envoyé. = room_name de la consultation. |
data.uid | UID (0 = auto-assigné par Agora lors du joinChannel). |
data.app_id | L'App ID Agora, utile si le mobile ne le stocke pas en dur. |
channel_name est toujours consulting.room_name (ex: "consulting1380"). C'est le même pour le patient et le médecin. Chaque consultation a un canal unique.
AgoraChatHelperLa classe AgoraChatHelper gère toute la logique de connexion à Agora Chat. Elle remplace ZIMKit.connectUser().
login(userId, token, callback)sanitizeUsername("346") → "u346". Minuscules, préfixe u si commence par un chiffre. Max 64 caractères.onSuccess() immédiatement. Pas besoin de re-login.loginWithAgoraToken()Connexion réelle au serveur Agora. Toujours effectuée si pas déjà connecté.200 = déjà connecté → traité comme succès218 = autre utilisateur encore connecté → force logout + retry// 1. Sanitize
String username = sanitizeUsername(userId); // "346" → "u346"
// 2. Autre utilisateur connecté ? → logout d'abord
if (isLoggedIn && currentUser != username) {
ChatClient.logout() → then doLogin(username, token);
return;
}
// 3. Même utilisateur ET connecté ? → succès immédiat
if (isLoggedIn && currentUser == username && isConnected()) {
callback.onSuccess();
return;
}
// 4. Login réel
ChatClient.loginWithAgoraToken(username, token, callback);
// 5. Si erreur 218 → force logout + retry une fois
sanitizeUsername()| Entrée (user ID) | Sortie (username Agora) | Règle appliquée |
|---|---|---|
346 | u346 | Préfixe u (commence par chiffre) |
330 | u330 | Préfixe u |
abc-123 | abc-123 | Déjà valide |
Dr. Ahmed | dr._ahmed | Minuscules + caractères spéciaux → _ |
| Écran | Quand | Pourquoi |
|---|---|---|
AgoraChatActivity | onChatTokenReceived() | Connexion au Chat pour envoyer/recevoir des messages |
ConsultingAdapter | N/A — supprimé | Avant : ZIMKit.connectUser() dans startChatActivity(). Maintenant supprimé. Le login se fait dans AgoraChatActivity. |
ConsultingDoctorAdapter | N/A — supprimé | Avant : ZIMKit.connectUser() + ZIMKit.createGroup() dans le bouton Accepter. Maintenant supprimé. |
AgoraChatActivityQuand l'utilisateur (patient ou médecin) appuie sur le bouton "Entrer dans la session" :
onCreate()Lance deux appels API en parallèle :
presenter.getConsulting(consulting.getId()) — récupère les données fraîches (dont group_id)presenter.getAgoraChatToken() — récupère le token Agora ChatonChatTokenReceived()Le token arrive → appelle AgoraChatHelper.login() → connexion réelle → isLoggedIn = true → appelle tryOpenChat()setConsulting()Les données de la consultation arrivent → extrait group_id → isGroupIdReady = true → appelle tryOpenChat()tryOpenChat()Vérifie les 3 conditions :
isLoggedIn == true (token reçu + login réussi)isGroupIdReady == true (group_id récupéré du backend)ChatClient.isConnected() == true (connexion réelle au serveur)showChatFragment().isConnected() est false → retry après 500ms.
showChatFragment(groupId)Affiche le fragment UIKit Agora Chat.setConsulting() — Callback du backendCette méthode est appelée automatiquement par le CallPresenter quand la réponse de getConsulting() arrive :
@Override
public void setConsulting(Consulting cons) {
// cons contient les données fraîches de la consultation
// y compris group_id créé par Laravel
if (cons.getGroup_id() != null && !cons.getGroup_id().trim().isEmpty()) {
// ✅ group_id existe → le sauvegarder localement
this.groupId = cons.getGroup_id();
isGroupIdReady = true;
tryOpenChat(); // Tente d'ouvrir le chat
} else {
// ❌ Pas de group_id → le backend n'a pas encore créé le groupe
// Cas possible : consultation immédiate, médecin pas encore assigné
Toast.makeText(this, "المحادثة غير جاهزة", Toast.LENGTH_SHORT).show();
}
}
group_id peut avoir été créé entre le moment où la liste des consultations a été chargée et le moment où l'utilisateur ouvre le chat. Récupérer les données fraîches garantit que le group_id est à jour.
showChatFragment() — Affichage du UIKitprivate void showChatFragment(String groupId) {
EaseChatFragment fragment = new EaseChatFragment.Builder(
groupId, // "315697874927617"
EaseChatType.GROUP_CHAT // Type : groupe (pas 1-to-1)
)
.useHeader(false) // On utilise notre propre toolbar
.build();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fra_message, fragment) // FrameLayout dans le layout XML
.commit();
}
| Paramètre | Description |
|---|---|
groupId | L'ID du groupe Agora Chat (ex: "315697874927617"). Pas le room_name. |
EaseChatType.GROUP_CHAT | Indique que c'est une conversation de groupe (pas un chat 1-to-1). |
useHeader(false) | Désactive le header intégré du UIKit car on a notre propre toolbar avec le nom + avatar + boutons appel. |
R.id.fra_message | Le FrameLayout dans le layout XML où le fragment est inséré. |
Le EaseChatFragment fournit automatiquement :
<!-- Votre toolbar personnalisée (nom, avatar, boutons appel) -->
<Toolbar android:id="@+id/chatToolbar" ... />
<!-- Le UIKit fragment remplit tout l'espace restant -->
<FrameLayout
android:id="@+id/fra_message"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Plus besoin de RecyclerView, EditText, ou bouton Send -->
<!-- Le EaseChatFragment fournit tout ça automatiquement -->
room (channel_name), chat (0=audio, 1=vidéo).presenter.getAgoraRtcToken(channelName, 0) → appel API Laravel → retourne le token.onRtcTokenReceived()Token reçu → initialise le moteur RTC → configure audio/vidéo → rejoint le canal.CallForegroundService avec notification "مكالمة جارية" pour garder l'audio actif en arrière-plan.onUserJoined() affiche sa vidéo (mode vidéo) ou garde juste l'audio (mode audio).Audio (chat=0) | Vidéo (chat=1) | |
|---|---|---|
| Caméra | Désactivée | Activée |
| Preview local | Caché | Affiché (150×220dp) |
| Remote view | Caché (fond blanc) | Plein écran |
| Boutons camera/switch | Cachés | Visibles |
| Boutons mute/speaker | Visibles | Visibles |
.envAGORA_APP_ID=cdc6c95ed75d45cda0a45ee86a4c6c64
AGORA_APP_CERTIFICATE=votre_certificat
AGORA_CHAT_ORG_NAME=41200031602
AGORA_CHAT_APP_NAME=200043726
AGORA_CHAT_DOMAIN=a41.chat.agora.io
AgoraChatService.php| Méthode | Description |
|---|---|
getAppToken() | Token REST API. Caché 55 min. |
getChatUserToken(userId) | Token utilisateur pour le SDK mobile. |
getRtcToken(channel, uid) | Token pour les appels vidéo/audio. |
registerUser(userId, nickname) | Enregistre un utilisateur. 409 = déjà existant = OK. |
createGroup(name, owner, members) | Crée un groupe. ID auto-généré par Agora. |
updateUserInfo(userId, name, avatar) | Met à jour nom + avatar sur Agora. |
createGroupAgora() est appelé| Méthode | Contrôleur | Quand |
|---|---|---|
storeConsultingScheduled() | ConsultingController | Après sauvegarde du room_name |
addConsultingFromPack() | ConsultingController | Après sauvegarde du room_name |
addConsultingForSameDoctor() | ConsultingController | Après sauvegarde du room_name |
addConsultingForAnotherDoctor() | ConsultingController | Après sauvegarde du room_name |
acceptConsulting() | DoctorController | Après assignation du doctor_uuid |
composer require boogiefromzk/agora-token
| Fichier | Remplacé par |
|---|---|
app/Services/ZegoZimService.php Supprimé | AgoraChatService.php |
| Supprimer (ZegoCloud) Supprimé | Ajouter (Agora) Nouveau |
|---|---|
im.zego:zim:x.x.x | io.agora.rtc:chat-uikit:1.3.0 |
im.zego:express-video:x.x.x | io.agora.rtc:full-sdk:4.3.0 |
| ZIMKit dependency | (UIKit inclut le Chat SDK) |
| Fichier Nouveau | Description |
|---|---|
App.java | Initialise EaseUIKit + UserProfileProvider pour les avatars et noms |
AgoraChatHelper.java | Login Agora Chat avec gestion multi-utilisateur, retry, et logout automatique |
AgoraUserCache.java | Cache mémoire des profils (nom + avatar) pour le UserProfileProvider |
AgoraChatActivity.java | Écran de chat. Utilise EaseChatFragment pour l'interface complète |
AgoraCallActivity.java | Appels vidéo/audio. Utilise Agora RTC SDK |
CallForegroundService.java | Service en premier plan pour garder l'audio actif en arrière-plan |
AgoraChatToken.java | Modèle réponse API (token + username) |
AgoraRtcToken.java | Modèle réponse API (token + channel_name + uid + app_id) |
CallPresenter.java | getAgoraChatToken() et getAgoraRtcToken() sans progress dialog |
CallView.java | onChatTokenReceived() et onRtcTokenReceived() |
| Fichier Modifié | Changement |
|---|---|
ConsultingAdapter.java | Supprimer ZIMKit.connectUser() de startChatActivity(). Ouvrir AgoraChatActivity avec group_id |
ConsultingDoctorAdapter.java | Changement majeur : Bouton Accepter → supprimer ZIMKit.createGroup() + saveGroupId(). Garder uniquement presenter.acceptConsulting() |
ApiHelper.java | Ajouter getAgoraChatToken() et getAgoraRtcToken() |
ApiEndPoint.java | Ajouter AGORA_CHAT_TOKEN et AGORA_RTC_TOKEN |
AndroidManifest.xml | FileProvider, permissions foreground service, CallForegroundService |
| Supprimé Supprimé | Remplacé par |
|---|---|
ZimKitActivity.java | AgoraChatActivity.java |
ZegoCallActivity.java | AgoraCallActivity.java |
ChatService.java | AgoraChatHelper.java |
activity_zim_kit.xml | activity_agora_chat.xml |
# Podfile
pod 'AgoraChat', '~> 1.3.0'
pod 'AgoraChat_iOS', '~> 1.3.0' # UIKit
pod 'AgoraRtcEngine_iOS', '~> 4.3.0'
ZIMKit.createGroup() de partoutsaveGroupId depuis le mobilePOST /api/agora/chat-token avant le loginPOST /api/agora/rtc-token avant les appelsAgoraChatClient.shared.login(username, agoraToken)group_id reçu du backendAgoraRtcEngineKit avec le token du backendsanitizeUsername() convertit l'ID en minuscules et préfixe avec u si l'ID commence par un chiffre. Exemple : ID 346 → u346.
| Paramètre | Valeur |
|---|---|
| App ID | cdc6c95ed75d45cda0a45ee86a4c6c64 |
| App Key (Chat) | 41200031602#200043726 |
| Console | https://console.agora.io |
.env Laravel pour la génération de tokens côté serveur.
| Composant | Avant (ZegoCloud) | Après (Agora) |
|---|---|---|
| Chat SDK | ZIMKit | Agora Chat UIKit (EaseUIKit) |
| Appels vidéo/audio | ZegoExpress SDK | Agora RTC SDK |
| Création groupe | Mobile (ZIMKit.createGroup) | Backend Laravel (API REST Agora) |
| Group ID | Custom ou auto (#AbWyGIH9S) | Auto-généré par Agora (315697874927617) |
saveGroupId (mobile) | Appelé depuis Android/iOS | Supprimé — Laravel sauvegarde directement |
| Auth tokens | AppSign dans le code mobile | Tokens générés par Laravel (/api/agora/*) |
| Interface chat | ZIMKitMessageFragment | EaseChatFragment (EaseUIKit) |
| Profils utilisateurs | Non géré | EaseUserProfileProvider + AgoraUserCache |
| Appels en arrière-plan | Non supporté | CallForegroundService + WakeLock |
room_name | Utilisé comme GroupId (échouait) | Utilisé comme nom de groupe + channel RTC |