Codons une application mobile de messagerie en temps réel avec React Native

Nous voici dans la suite de notre série d'articles sur la façon de développer une application mobile de messagerie en temps réel avec React Native et Kuzzle Mobile ! Il s'agit d'une série de 3 articles :

English version here

 

 

Voici la deuxième partie (je vous encourage vivement à lire la première partie avant de continuer). Nous allons enfin entrer dans le vif du sujet et créer notre chat en temps réel avec Kuzzle Mobile.


Kuzzle Mobile est une suite prête à l'emploi, open-source, installable sur vos propres serveurs, qui vous permet de créer des applications mobiles modernes en un rien de temps. Elle offre des capacités en temps réel et c'est ce que nous allons voir maintenant.

Précédement

Rappelons rapidement ce que nous avons fait dans la section précédente.

Tout d'abord, nous avons initialisé notre application mobile avec Expo et dans un deuxième temps, nous avons préparé notre backend en créant des profils et des utilisateurs ainsi qu'une collection pour stocker les messages de notre application de messagerie. Et enfin, nous avons mis en place la logique de d'authentification.

 

Encore une fois, n'hésitez pas à revenir en arrière et à lire quelques passages de la partie 1 si vous avez besoin de vous rafraîchir la mémoire.

Un dernier petit rappel : vous devez, si ce n'est déjà fait, redémarrer toute la pile de Kuzzle avec  docker avant de continuer. Pour ce faire, lancez votre plus beau terminale et allez à la racine du projet précédemment créé et exécutez :

 

docker-compose up

 

Aussi, toujours à la racine du projet, pour relancer la construction de l'application mobile avec Expo, il suffit d'exécuter cette commande :

 

npm start

 

Nous sommes maintenant prêts à coder !

 

Créer un nouvel écran

Il est temps de prendre votre éditeur de code ! La première chose que vous devez faire est de créer un nouvel écran pour notre application. Celui-ci montrera à nos utilisateurs tous les messages envoyés et nous permettra d'en envoyer de nouveaux.

Il suffit de créer un nouveau fichier dans le dossier "components" et de l'appeler Chat.js.


pour le moment, restons minimalistes, nous le mettrons à jour plus tard :

 

import React, { useState, useEffect } from "react";
import { Container, Toast, Input, Form, Item, Label, Text } from "native-base";
import kuzzle from "../services/kuzzle";

export default function Chat({ currentUsername }) {
return (
<Container style={{ flex: 1 }}>
<Text>Hey, here will be the list of messages</Text>
</Container>
);
}

 

Nous n'avons besoin que de quelques importations de base et, bien sûr, de la déclaration de retour qui sera utilisée pour rendre le contenu.

Notez que nous avons défini une propriété CurrentUsername pour le composant, nous en aurons besoin pour les éléments suivants.

Maintenant, allez dans votre fichier App.js et importez notre nouveau composant :

 

import Chat from "./components/Chat";

 

Nous devons en outre mettre à jour la méthode renderApp() pour afficher notre composant fraîchement importé. Trouvez la ligne avec :

 

pageContent = <Text>Hello {username}</Text>;

 

Et le remplacer par :

 

pageContent = <Chat currentUsername={username} />;

 

Maintenant, si vous lancez l'application, vous pouvez remplir le formulaire de connexion et voir le nouvel écran.

 

 

 

 

 

 

Maintenant que tout est prêt, nous pouvons passer à la recherche des messages

 

Récupérer des messages de Kuzzle

Rapidement, ouvrez le fichier Chat.js et ajoutez quelques variables d'état avec la méthode useState :

 

const [messages, setMessages] = useState([]);
const [messagesFetched, setMessagesFetched] = useState(false);

 

Le premier sera utilisé pour stocker des messages en mémoire sur notre application. Et le second pour valider l'état de l'action de récupération des messages de Kuzzle.

L'étape suivante consiste à implémenter la fonction fetchMessages(). Pour ce faire, nous utiliserons le kuzzle-sdk et plus particulièrement le document de contrôle et l'action de recherche

 

const fetchMessages = async () => {
try {
// send search request with the kuzzle-sdk
const results = await kuzzle.document.search(
"messaging-app", // index
"messages", // collection
{ sort: { "_kuzzle_info.createdAt": { order: "asc" } } }, // query body
{ size: 100 } // options
);

const fetchedMessages = results.hits.map((message) =>
formatMessage(message)
);

setMessages(fetchedMessages);
setMessagesFetched(true);
} catch {
showToast(
"danger",
"It looks like there is an error while fetching messages..."
);
}
};

 

Vous pouvez trouver la documentation complète sur l'action de recherche ici. Mais en bref, nous appelons la méthode de recherche et nous lui donnons quelques paramètres :

 

  • Le nom de l'index, dans notre cas c'est "messaging-app" (nous l'avions déjà créé dans la première partie de ce guide)
  • Le nom de la collection ou "messages" (également créés dans la première partie)
  • Le troisième paramètre est le corps de la requête, il utilise la syntaxe de la requête syntaxe de la requête Elasticsearch. Ici, nous voulons trier de manière ascendante tous les messages selon leur date de création.
  • Le dernier paramètre permet de passer des options à l'action de recherche. Par exemple, nous fixons la taille du résultat à seulement 100 documents
Si vous vous souvenez de la première partie, nous avons créé une collection de messages  avec un mapping personnalisé comme celui-ci :

 

{
"content": { "type": "text" },
"author": { "type": "keyword" }
}

 

Comme vous le voyez, pour que les messages persistent, il suffit de sauvegarder le contenu du message et bien sûr l'auteur.

Pour faire simple, le résultat de l'action de recherche est un tableau contenant des résultats. Chaque résultat représente un document de notre collection. Voici un exemple de réponse :

 

{
"hits": [
{
"_id": "BLIRFHMBBug7TUOQMXt-",
"_source": {
"author": "Luca",
"content": "Hi Kuzzle team !",
"_kuzzle_info": {
"author": "admin",
"createdAt": 1593769537906,
"updatedAt": 1593769607613,
"updater": "admin"
}
}
},
{
"_id": "BbIRFHMBBug7TUOQYnva",
"_source": {
"author": "Esteban",
"content": "Hello sir",
"_kuzzle_info": {
"author": "admin",
"createdAt": 1593769537906,
"updatedAt": 1593769607613,
"updater": "admin"
}
}
}
]
}

 

Chaque résultat (ou "hits") contient l'ID du document et les propriétés du sous objet "_source" contiennent les données du document. Vous pouvez également voir que Kuzzle ajoute des métadonnées enregistrées dans le champ _kuzzle_info

Cet exemple vous aidera à comprendre une autre fonction que vous devez ajouter dans ce composant :

 

const formatMessage = (message) => {
return {
id: message._id,
author: message._source.author,
content: message._source.content,
date: new Date(message._source._kuzzle_info.createdAt).toLocaleString(),
};
};

 

Le but de cette fonction est d'extraire des données du résultat de l'action de recherche afin d'avoir un objet qui peut être facilement exploité par la suite. Pour cela, nous avons besoin de l'identifiant unique du document, du contenu du message, de son auteur et de la date d'envoi. Notez que la date d'envoi est automatiquement gérée par Kuzzle et stockée dans les métadonnées du document ( horodatage au format "Epoch").

De plus, nous avons besoin d'une autre fonction pour afficher un message d'erreur en cas de problème avec la méthode de recherche, donc ajoutez-la au composant Chat :

 

const showToast = (type, message) => {
return Toast.show({
text: message,
duration: 8000,
type: type,
});
};

 

Pour ce qui suit, nous devons ajouter un hook useEffect dans le composants pour appeler et effectuer l'action de recherche :

 

useEffect(() => {
if (!messagesFetched) {
fetchMessages();
}
}, [messagesFetched]);

 

L'étape suivante consiste à mettre à jour la déclaration de notre composante :

 

return (
<Container style={{ flex: 1 }}>
<Container style={{ flex: 1 }}>
<MessagesList messages={messages} currentUsername={currentUsername} />
</Container>
</Container>
);

 

Si vous regardez de près, vous pouvez voir un nouveau composant appelé MessagesList. Donc... vous savez quoi faire ;)
Créez un nouveau fichier, appelez-le MessagesList.js et n'oubliez pas de l'importer dans le fichier Chat.js

 

import MessagesList from "./MessagesList";

 

Pour la prochaine étape, laissez-moi vous présenter le composant personnalisé MessageList :

 

import React, { useRef } from "react";
import { StyleSheet, View, Text, FlatList } from "react-native";

export default function MessagesList({ messages, currentUsername }) {

const flatListRef = useRef(); // used to reference the FlatList itself to perform scroll to end operation when new messages come in

const renderFlatListItem = (item) => {
return (
<View>
<View
style={
currentUsername === item.author
? styles.message__fromCurrentUser
: styles.message__fromOthers
}
>
<View style={styles.message__header}>
<Text style={styles.message__author}>{item.author}</Text>
<Text>{item.date}</Text>
</View>
<Text>{item.content}</Text>
</View>
</View>
);
};

return (
<FlatList
style={styles.messagesList}
data={messages}
onContentSizeChange={() =>
flatListRef.current.scrollToEnd({ animated: true })
}
renderItem={(item) => renderFlatListItem(item.item)}
keyExtractor={(item) => item.id} // needed for the FlatList to set an unique key for each item of the list

ref={flatListRef}
/>
);
}

const styles = StyleSheet.create({
messagesList: {
marginTop: 30,
marginBottom: 30,
alignSelf: "stretch",
},
message__fromCurrentUser: {
backgroundColor: "#9EE493",
alignSelf: "flex-end",
margin: 5,
width: 250,
padding: 5,
borderRadius: 5,
},
message__fromOthers: {
backgroundColor: "#86BBD8",
alignSelf: "flex-start",
margin: 5,
width: 250,
padding: 5,
borderRadius: 5,
},
message__header: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 5,
},
message__author: {
fontWeight: "bold",
},
});

 

Les premières lignes sont pour les importations, vous commencez à connaitre !

Et juste après, la définition de la fonction du composant. Vous pouvez voir que nous déclarons deux paramètres : messages et currentUsername. Rien de plus normal puisque c'est le composant qui se chargera d'afficher notre liste de messages. C'est aussi le composant qui aura pour mission de séparer les messages de l'utilisateur courant et ceux envoyés par les autres utilisateurs. C'est pourquoi nous avons besoin de ces deux paramètres (ou  "props").

Pour ce faire, nous utilisons une FlatList, un composant React Native très pratique dans une application mobile pour avoir des listes scrollable. La principale information à retenir est que le composant reçoit une liste de tous nos messages (propriété "data") et pour chaque message, il appellera la fonction renderFlatListItem (propriété "renderItem").

Le dernier point concerne le style pour l'affichage ! Nous avons besoin de différents styles selon que le message provient d'autres utilisateurs ou de l'utilisateur actuel. Et surtout, nous voulons que notre application ait un bon look.

Nous avons presque terminé cette partie, il ne reste qu'un tout petit détail à régler. Dans la première partie, nous avons créé un profil appelé "standard-user" associé au rôle "authentication".

{
// authentication role
"controllers": {
"auth": {
"actions": {
"*": true
}
}
}
}

 

{
// standard-user profile
"rateLimit": 0,
"policies": [
{
"roleId": "authentication"
}
]
}

 

Cela signifie que tout utilisateur associé à ce profil ne dispose que des droits sur le contrôleur d'authentification. Tout autre appel à un autre contrôleur sera rejeté par le système de permissions de Kuzzle.

Une bonne pratique consiste ici à autoriser les autorisations au fur et à mesure de l'avancement du développement. Cela peut sembler évident, mais croyez-moi, il vaut mieux ne pas avoir à gérer toutes les autorisations en même temps à la fin du développement de votre application, surtout si elle est assez grande.

Si nous voulons permettre à nos utilisateurs d'effectuer des recherches avec le contrôleur de documents, nous devons créer un autre rôle.

Pour ce faire, retournez sur l'Admin Console de Kuzzle et connectez-vous à votre environnement local.

Ensuite, allez à la page de sécurité et ensuite à la section "Rôle" en cliquant dessus dans le menu de gauche. Enfin, cliquez sur le bouton "Créer".

Donnez un nom au nouveau rôle que vous voulez créer, quelque chose comme "fetch-messages" fera l'affaire. Ensuite, remplissez la zone de texte JSON et validez le formulaire.

 

{
"controllers": {
"document": {
"actions": {
"search": true
}
}
}
}

 

 

Une fois que notre nouveau rôle est créé, nous devons l'associer au profil existant. Allez dans la section "Profils" de l'Admin Console et modifiez le profil "utilisateur standard" pour ajouter le nouveau rôle que nous venons de créer :

 

{
"rateLimit": 0,
"policies": [
{
"roleId": "authentication"
},
{
"roleId": "fetch-messages"
}
]

>

 

 

 

Une fois fait, nous allons pouvoir tester notre application. Mais avant cela, notre collection de "messages" est vide, nous utiliserons l'Admin Console pour créer certains documents dans notre collection.

Allez dans les pages "Data" de la console d'administration, sélectionnez votre index (messaging-app) puis la collection (messages). Vous voyez ici la liste des documents de la collection. Bien sûr, il n'y en a aucun pour le moment. Cliquez sur le bouton "Create a document".

Pas besoin de remplir la case "Document identifier", Kuzzle va générer un identifiant unique pour le document. Il suffit de remplir la zone de texte JSON avec un contenu factice pour tester dans l'application mobile la récupération des messages. Il suffit donc de creer un document en définissant un contenu et un auteur pour le message :

 

{
"content": "Hello sir",
"author": "Esteban"
}

 

 

 

Vous pouvez créer autant de messages que vous le souhaitez. Une fois terminé, c'est le moment de tester notre application ! Si tout fonctionne bien, vous pourrez voir sur votre appareil tous les messages fraîchement créés.

 

 

 

 

 

 

 

Envoyer des messages

L'étape suivante consiste à permettre à notre utilisateur de pouvoir envoyer des messages. La première chose à faire est d'ajouter un champ permettant la saisie de texte sur le composant de chat. Ouvrez le fichier Chat.js et nous mettrons à jour la déclaration de retour du composant comme ceci :

 

return (
<Container style={{ flex: 1 }}>
<Container style={{ flex: 1 }}>
<MessagesList messages={messages} currentUsername={currentUsername} />
</Container>
<Form>
<Item floatingLabel>
<Label>Your message</Label>
<Input
ref={messageInputRef}
onChangeText={(message) => setNewMessage(message)}
onSubmitEditing={() => sendMessage()}
value={newMessage}
/>
</Item>
</Form>
</Container>
);

 

Simplement, nous ajoutons un composant "form" (fourni par la bibliothèque Native-Base) et un composant "Input".

Ensuite, nous devons déclarer une nouvelle variable d'état, que nous appellerons "newMessage". Nous utiliserons également le mécanisme "React Refs" pour référencer le texte saisi. Il suffit d'ajouter ces 2 lignes au dessus des composants, juste après les autres variables d'état :

 

const [newMessage, setNewMessage] = useState(null);

const messageInputRef = React.createRef();

 

Une fois fait, nous devons maintenant implémenter la méthode sendMessage() pour pouvoir envoyer le nouveau message de l'utilisateur à Kuzzle Mobile.

 

const sendMessage = async () => {
try {
await kuzzle.document.create(
"messaging-app",
"messages",
{
content: newMessage,
author: currentUsername,
}
);
setNewMessage(null);
} catch {
showToast(
"danger",
"It looks like there is an error while sending a message..."
);
}
};

 

Cette fonction utilise le contrôleur "document" fourni par le Kuzzle-sdk et appelle l'action "create". On lui donne le nom de l'index, le nom de la collection et le corps du document que l'on veut créer. Bien sûr, nous vidons le texte saisi une fois l'envoi d'un nouveau message effectué, en mettant une valeur nulle à l'état newMessage.

Si vous vous souvenez de l'étape précédente, vous savez que nous devons ajouter un nouveau rôle pour autoriser les utilisateurs à créer des documents. Encore une fois, allez dans la console d'administration et créez un nouveau rôle, appelez-le "send-messages" et ajoutez-y ce contenu :

 

{
"controllers": {
"document": {
"actions": {
"create": true
}
}
}
}

 

N'oubliez pas de mettre à jour le profil "standard-user" pour l'associer à ce nouveau rôle :

 

{
"rateLimit": 0,
"policies": [
{
"roleId": "authentication"
},
{
"roleId": "fetch-messages"
},
{
"roleId": "send-messages"
}
]
}

 

Enfin, nous pouvons tester l'application et essayer d'envoyer un nouveau message à Kuzzle Mobile.

Mais... quand on envoie un nouveau message par l'application... rien ne se passe... en fait, pas vraiment...

Vous pouvez aller dans l'Admin Console et consulter la liste des documents présents sur la collection "messages", vous verrez tous les nouveaux messages que vous venez d'envoyer.

Nous allons voir comment les visualiser dans l'applications et ajouter des fonctionnalités temps réel  lors de la prochaine étape !

Temps-Réel (Enfin) !


Nous y voici... enfin... la partie où nous allons mettre en œuvre des capacités de temps réel pour notre application mobile. Avant de commencer, laissez-moi vous expliquer comment fonctionne le temps réel avec Kuzzle Mobile.

Le moteur en temps réel de Kuzzle vous permet d'utiliser le mechanisme de Pub/Sub dans des canaux de communication dédiés appelés "rooms".

Le processus est le suivant :

  • un client s'abonne à une room particulière,
  • un deuxième client poste un message dans cette salle,
  • le client qui s'abonne à la chambre reçoit une notification.

 

 

 

 

Cependant, le moteur en temps réel de Kuzzle vous permet également de vous abonner aux notifications correspondant aux changements dans la base de données, selon le même schéma, en envoyant automatiquement des notifications dans une room dédiée et c'est exactement ce dont nous avons besoin !

Si nous relions ce modèle à notre cas, nous voulons nous abonner à chaque nouveau document créé dans la collection "messages".

L'abonnement à une salle se fait par la méthode "realtime:subscribe". Elle nécessite 4 paramètres :

  • nom d'un index,
  • nom d'une collection,
  • les filtres d'abonnement contenus dans le corps de la demande.
  • une fonction de rappel, appelée chaque fois que nous recevons une nouvelle notification dans la room
Maintenant que nous savons cela, allez dans le fichier Chat.js et ajoutez la fonction de souscription :

 

const subscribeToMessages = async () => {
try {
const roomId = await kuzzle.realtime.subscribe(
"messaging-app",
"messages",
{},
async (notification) => {
if (
notification.type !== "document" ||
notification.action !== "create"
) {
return;
}
setMessages([...messages, formatMessage(notification.result)]);
}
);

setRoomId(roomId);
} catch {
showToast(
"danger",
"It looks like there is an error with the real-time messages subscription..."
);
}
};

 

Vous pouvez voir que nous vérifions le type et l'action de la notification pour être sûr d'ajouter de nouveaux messages et de ne pas être pollué par d'autres notifications (vous pouvez en savoir plus sur les notifications ici).

Maintenant, ajoutez une nouvelle variable d'état pour gérer l'état de l'abonnement :

 

const [roomId, setRoomId] = useState(null);

 

Une fois fait, il suffit de mettre à jour le hook useEffect pour gérer correctement cette souscription :

 

useEffect(() => {
if (!messagesFetched) {
fetchMessages();
}

if (messagesFetched && !roomId) {
subscribeToMessages();
}
}, [messagesFetched, roomId]);

 

Avant de tester l'application, nous devons créer un autre rôle, pour permettre aux utilisateurs de s'abonner aux notifications en temps réel.

Vous connaissez la marche à suivre, allez dans la console d'administration et créez un nouveau rôle, appelez-le "messages-subscription" et ajoutez-lui ce contenu :

 

{
"controllers": {
"realtime": {
"actions": {
"subscribe": true
}
}
}
}

 

 

Et mettez à jour le profil "utilisateur standard" comme ceci :

 

{
"rateLimit": 0,
"policies": [
{
"roleId": "authentication"
},
{
"roleId": "fetch-messages"
},
{
"roleId": "send-messages"
},
{
"roleId": "messages-subscription"
}
]
}

 

 

 

 

Il est maintenant temps de tester notre application pour s'assurer que tout fonctionne comme prévu. Si vous avez suivi toutes les étapes, vous pouvez maintenant envoyer des messages via l'application et les voir apparaître à l'écran.

Vous pouvez également vérifier la liste des documents sur la console d'administration et pourquoi ne pas créer un nouveau document pour le voir apparaître sur votre mobile.

 

 

 

Conclusion

Ainsi se termine la deuxième partie de notre série d'articles sur React Native et Kuzzle Mobile. Nous avons vu comment effectuer des actions de recherche sur des documents. Mais surtout comment s'abonner à des notifications en temps réel. Ce dernier point apporte une réelle valeur ajoutée à votre application mobile et surtout à sa simplicité de mise en place.

Une fois de plus, je tiens à remercier toute l'équipe de Kuzzle pour la révision de cet article. Retrouvons-nous pour la prochaine et dernière partie et plongeons dans les bonnes pratiques d'utilisation de Kuzzle Mobile pour enfin terminer notre application.

 

 

Nicolas Juelle

Postes associés