Community : fil in-app, commentaires et profils

Un fil social dans ton app : publications, commentaires, réactions, profils. L'écran est rendu en natif par le SDK, à l'image de ton app plutôt qu'une webview greffée par-dessus.
Le contenu, la modération et les réglages se pilotent depuis Communauté dans le dashboard, après avoir choisi l'app (une communauté par app). L'identité du membre vient d'Appwin Core : un login, la même personne dans tous les produits et sur ses différents appareils.
Installation
- 1
Installe Appwin Core et Community
Community repose sur Appwin Core. Pas encore installé ? Commence par le Quickstart, qui installe Core et tes modules pour chaque plateforme.
Compatibilité et paquets par plateforme
Plateforme Paquet Symbole iOS 16 produit SPM AppwinCommunityAppwinCommunityAndroid 7.0 (API 24) io.appwin:appwin-communityio.appwin.community.AppwinCommunityFlutter 3.3 appwin_communityAppwinCommunity.instanceReact Native 0.73 @appwin/react-nativeAppwinCommunity - 2
Initialise Community
Appelle
initialize()au démarrage, après Appwin Core. Le fil apparaît quand trois conditions sont réunies :configureappelé au lancement, la communauté activée au dashboard (Personnaliser → Général → Communauté activée), etinitialize()qui répondready. Conditionne ton onglet sur cette réponse : le SDK ne possède pas ta navigation, il ne peut pas le masquer à ta place.import AppwinCommunity await AppwinCommunity.initialize()Un build debug de ton app déverrouille Community même hors plan : cf. Essayer sans le plan.
Méthodes disponibles
Rattacher le membre à ton utilisateur, c'est AppwinCore.identify /
AppwinCore.logout(), pas une fonction de Community (voir
Identité).
Afficher le fil
Ton app fournit un point d'entrée (un onglet, un bouton) et le SDK dessine le reste.
SwiftUI - dans un onglet
import AppwinCommunity
TabView {
HomeView()
.tabItem { Label("Accueil", systemImage: "house") }
AppwinCommunity.communityView()
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
}
UIKit
let communityVC = AppwinCommunity.communityViewController()
communityVC.tabBarItem = UITabBarItem(
title: "Communauté",
image: UIImage(systemName: "bubble.left.and.bubble.right"),
tag: 1
)
tabBarController.viewControllers = [homeVC, communityVC, profileVC]
Modal
AppwinCommunity.presentCommunity()
Quand Community n'est pas disponible
initialize() peut répondre autre chose que ready : le plan n'inclut pas
Community, le studio l'a coupé, configure n'a pas été appelé, ou le serveur
n'a pas pu être joint. La vue embarquée gère chacun de ces cas et suit le verdict
en direct : elle bascule entre son écran d'attente et le fil quand la réponse
change (un interrupteur basculé au dashboard, un plan qui expire), sans que ton
app ait quoi que ce soit à remonter.
Ce que voit le membre
Un écran centré « La communauté ouvre bientôt », traduit comme le reste du SDK, avec la palette de Community plutôt que ton thème (aucune configuration n'a pu être chargée).
Dans un build debug, une alerte développeur s'affiche en haut : la raison
(hors plan, coupé, configure manquant, pas de réponse du serveur,
initialize() jamais appelé), comment la corriger, le résultat journalisé (par
exemple unavailable(plan)) et un bouton vers l'endroit où ça se corrige. Les
builds release ne l'affichent jamais. La même alerte s'affiche au-dessus du fil
quand Community n'est ouvert que parce que le build est un build debug.
presentCommunity() n'affiche pas d'écran d'attente : tant que Community n'est
pas prêt, il ne fait rien et journalise pourquoi.
Ton propre écran d'attente
Passe ta propre UI pour le cas « pas prêt ». Elle reçoit le verdict courant (sur
iOS et Android, nil / null tant qu'initialize() n'a pas été appelé).
AppwinCommunity.communityView { result in
ComingSoonView()
}
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
En UIKit, on passe un constructeur de contrôleur :
let communityVC = AppwinCommunity.communityViewController { result in
ComingSoonViewController()
}
Essayer sans le plan
Un build debug obtient Community même quand le plan de ton organisation ne l'inclut pas encore : tu peux l'intégrer et le montrer avant de souscrire.
- chaque requête du SDK depuis un build debug porte
X-Appwin-Build: debug(iOS : SDK compilé en configuration Debug ; Android : ton app estdebuggable, quel que soit le build du SDK) ; - quand le plan n'inclut pas Community, le serveur répond
readyà ce build, quel que soit l'interrupteur du dashboard (masqué dans ce cas) ; initialize()renvoieready, et le SDK journalise une fois :
[Appwin] community is unlocked because this is a debug build: the organisation's plan does not include it, so release builds will get unavailable(plan).
Les builds release ont toujours besoin du plan : la même app, archivée pour
TestFlight ou le Play Store, reçoit unavailable(plan) et affiche l'écran
d'attente. Quand le plan inclut Community, un build debug suit l'interrupteur du
dashboard comme n'importe quel build.
Afficher ou masquer l'onglet en direct
initialize() donne un verdict au lancement. Pour le suivre ensuite (un
interrupteur basculé pendant que l'app est ouverte, un plan qui expire),
observe-le depuis Core : tu reçois le verdict courant tout de suite, puis chaque
changement. Il est réévalué à chaque initialize() d'un produit et au retour de
l'app au premier plan.
import AppwinCore
struct RootView: View {
@State private var showsCommunity = false
var body: some View {
TabView {
HomeView().tabItem { Label("Accueil", systemImage: "house") }
if showsCommunity {
AppwinCommunity.communityView()
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
}
}
.task {
for await result in AppwinCore.availabilityUpdates(of: .community) {
showsCommunity = result.isReady
}
}
}
}
Le flux se termine quand la tâche est annulée, ici quand la vue disparaît.
isReady vaut true quand lastResult vaut ready. lastResult
(AppwinCommunity.lastResult, .instance.lastResult en Flutter,
getLastResult() en React Native) donne le même verdict de façon synchrone,
nil / null avant initialize().
Ouvrir une publication depuis une notification
Community envoie des pushs (une réponse, une réaction, une mention). Dès
qu'initialize() a répondu ready, le SDK gère leurs taps lui-même : rien à
brancher au-delà des notifications push de Core.
Ce que fait un tap par défaut
| Où est le fil | Ce que le tap ouvre |
|---|---|
À l'écran (ton onglet Communauté est sélectionné, ou presentCommunity() est ouvert) | La publication, dans la navigation de ce fil |
| Pas à l'écran : dans un autre onglet, ou jamais affiché | La publication en plein écran par-dessus ton app (une feuille sur iOS, une activité sur Android), avec un bouton de fermeture qui ramène le membre là où il était |
Quand la notification porte sur une réponse, le fil de réponses s'ouvre par-dessus la publication. Si Community vit dans un onglet, le défaut est la deuxième ligne : la plupart des apps préfèrent basculer sur leur onglet. Pour ça, route le tap toi-même.
Community dans un onglet : router le tap toi-même
Renseigne onNotificationTap. Le SDK ne navigue plus : il te passe la cible
(postId, et commentId pour un fil de réponses), tu sélectionnes ton onglet
Communauté, puis tu appelles openPost, qui ouvre la publication dans le fil
monté dans cet onglet.
Renseigne-le avant initialize() : un tap qui a lancé l'app est rejoué juste
après qu'initialize() a répondu ready, et il doit trouver ton handler.
import AppwinCommunity
import AppwinCore
enum AppTab { case home, community, profile }
@MainActor
final class AppRouter: ObservableObject {
@Published var tab: AppTab = .home
}
@main
struct MyApp: App {
@StateObject private var router = AppRouter()
init() {
AppwinCore.configure(projectAppId: "YOUR_APP_ID")
}
var body: some Scene {
WindowGroup {
TabView(selection: $router.tab) {
HomeView()
.tabItem { Label("Accueil", systemImage: "house") }
.tag(AppTab.home)
AppwinCommunity.communityView()
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
.tag(AppTab.community)
ProfileView()
.tabItem { Label("Profil", systemImage: "person") }
.tag(AppTab.profile)
}
.task {
AppwinCommunity.onNotificationTap = { [router] target in
router.tab = .community
AppwinCommunity.openPost(target.postId, commentId: target.commentId)
}
await AppwinCommunity.initialize()
}
}
}
}
Le fil n'a pas besoin d'être monté à l'avance. Un onglet Communauté jamais
affiché n'a pas encore de fil (les onglets se construisent à la demande) :
openPost garde alors la cible environ 600 ms, et le fil que ton changement
d'onglet monte dans ce délai l'ouvre. C'est seulement si aucun fil n'apparaît
que la publication s'ouvre en plein écran.
Ouvrir une publication depuis ton propre code
openPost marche aussi hors notification, depuis un lien in-app ou un bouton
« voir la discussion » : il ouvre dans le fil monté, ou dans celui qui se monte
dans les 600 ms environ, sinon en plein écran. Tant que Community n'est pas prêt,
il ne fait rien et journalise pourquoi.
Réagir aux actions du membre
Community signale ce que fait le membre courant, une fois que le serveur l'a accepté : jamais de façon optimiste, jamais pour les autres membres. Cinq événements :
| Événement | Quand |
|---|---|
postCreated(postId) | Le membre a publié |
commentCreated(commentId, postId) | Le membre a commenté une publication |
replyCreated(replyId, commentId, postId) | Le membre a répondu à un commentaire ; commentId est le commentaire auquel il répond |
reactionModified(postId, commentId, reaction) | Le membre a posé, changé ou retiré une réaction. commentId est renseigné quand elle porte sur un commentaire ; reaction est la clé de l'API (like, love...), null quand elle est retirée |
profileUpdated(profileId) | Le profil du membre a changé, depuis l'éditeur du SDK ou via setUser |
L'usage type, c'est la gamification : une app de fitness qui donne des points pour la participation à sa communauté, comme pour une séance terminée. Les événements survenus pendant que personne n'écoute ne sont pas rejoués : abonne-toi au démarrage de l'app, pas à l'ouverture de l'onglet Communauté.
.task {
for await event in AppwinCommunity.events {
switch event {
case .postCreated(let postId):
rewards.grant(.post, id: postId)
case .commentCreated(let commentId, _):
rewards.grant(.comment, id: commentId)
case .replyCreated(let replyId, _, _):
rewards.grant(.comment, id: replyId)
case .reactionModified(let postId, let commentId, _?):
rewards.grant(.reaction, id: commentId ?? postId)
case .reactionModified, .profileUpdated:
break
}
}
}
Chaque accès à events renvoie un flux indépendant ; il se termine quand la
tâche qui le consomme est annulée.
Indexe tes récompenses sur l'id : un membre peut retirer une réaction puis la reposer, et sans l'id il cumulerait des points d'un seul bouton. Pour une récompense qui a une vraie valeur, confirme côté serveur : ces événements viennent de l'appareil.
Une pastille de non-lus en direct
unreadNotificationCount() répond une fois. Pour une pastille qui reste à jour,
écoute le compteur : tu reçois la valeur connue tout de suite, un rafraîchissement
depuis le serveur, puis chaque changement. Le SDK le tient à jour à partir des
pushs Community, du rechargement du fil, des notifications lues dans le SDK et du
retour de l'app au premier plan (au plus une requête supplémentaire toutes les
30 secondes, et aucune quand personne n'écoute).
@State private var unread = 0
AppwinCommunity.communityView()
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
.badge(unread)
.task {
for await count in AppwinCommunity.unreadNotificationCountUpdates {
unread = count
}
}
Rattacher ton membre
Sans rien faire, le profil appartient à l'appareil et reste anonyme.
AppwinCore.identify le rattache à l'utilisateur de ton app, une fois à la
connexion : l'identité est portée par Core, donc Support reconnaît la même
personne au même instant. Community n'a pas de fonction de login à lui ; il
recharge le profil quand Core le prévient que l'identité a changé.
setUser pousse le profil communautaire public que ton app connaît déjà, pour
que le membre ne le retape pas. Il est distinct de la fiche client
(AppwinCore.updateUser).
import AppwinCommunity
import AppwinCore
try await AppwinCore.identify(externalId: user.id)
try await AppwinCommunity.setUser(
nickname: user.displayName,
avatarUrl: user.avatarUrl
)
await AppwinCore.logout()
Fournir un pseudo sort le profil de l'anonymat. Le retour à l'anonyme est explicite, et il se fait depuis l'écran de profil du SDK : ce n'est pas à ton app de le décider pour le membre.
Utiliser ton propre éditeur de profil
Si ton app a déjà un écran de profil, fais-en la seule source de vérité :
renseigne onEditProfile, et chaque endroit du SDK qui ouvrirait son propre
éditeur (le bouton de modification, « Configurer mon profil » dans le composer)
appelle ton handler à la place. Ton écran enregistre, puis pousse le résultat avec
setUser : le fil monté se rafraîchit et profileUpdated est émis. Sans handler,
c'est l'éditeur du SDK qui s'ouvre.
AppwinCommunity.onEditProfile = { [router] in
router.showsProfileEditor = true
}
// Quand ton éditeur enregistre :
try await AppwinCommunity.setUser(
nickname: profile.displayName,
avatarUrl: profile.avatarUrl,
bio: profile.bio
)
Localisation
Les libellés du SDK sont résolus dans les ressources de ton app, pas dans celles du module. Tu peux donc traduire ou réécrire n'importe quel libellé sans attendre une livraison de notre part.
Ajoute un AppwinCommunity.strings à ton projet :
"community.new_post" = "Partager quelque chose";
"community.like" = "J'aime";
"community.comments" = "Réactions";
Les clés sont listées dans CommunityStrings.swift.
Sans ressource fournie, le SDK utilise ses défauts anglais, jamais une clé brute à l'écran. Deux usages : traduire dans une langue qu'on ne fournit pas encore, ou réécrire un libellé qui ne colle pas à ton ton.
Activer
Personnaliser → Général → Communauté activée. L'activation fait trois choses d'un coup :
- le fil devient visible dans le SDK
- un groupe par défaut est créé
- le produit Community est activé sur ton App ID
Tant qu'elle est éteinte, le SDK affiche un écran d'attente et aucun contenu n'est servi : on ne veut pas qu'un fil vide apparaisse dans une app en production le jour du déploiement.
Groupes
Les onglets de ton fil. Une publication appartient à exactement un groupe, ce qui rend les onglets lisibles et les compteurs honnêtes.
| Réglage | Effet |
|---|---|
| Nom, emoji, description | Ce que voit le membre |
| Qui peut publier | « Tout le monde » ou « L'équipe uniquement » |
| Archiver | Masqué dans l'app, ses publications restent en base |
« L'équipe uniquement » transforme le groupe en tableau d'annonces : les membres lisent et commentent, ils ne publient pas. C'est le mécanisme derrière les groupes de bienvenue façon « À propos de… ».
Le groupe par défaut reçoit les publications sans groupe explicite. Il ne peut être ni supprimé ni archivé. Un groupe ne se supprime que vide ; sinon, archive-le.
Publications
Le tableau de tout ce qui est publié, par ton équipe comme par les membres. Filtrable par groupe, statut et recherche plein texte.
Publier
Une publication du studio porte le badge « équipe » et ne passe pas par la modération. La longueur maximale configurée s'applique aux membres, pas à tes annonces. Options : épingler en tête de groupe, afficher ou masquer le badge équipe, programmer.
Programmer
Renseigne une date future : la publication part automatiquement à l'heure dite (granularité d'une minute). Une date future implique toujours une programmation, même si tu demandes « publier maintenant » : sinon une publication datée de demain apparaîtrait en tête de fil aujourd'hui.
Créer en lot
Jusqu'à 50 publications d'un coup, pour amorcer une communauté ou dérouler un calendrier éditorial. Un bloc par publication, les options communes en tête. C'est tout ou rien : si une entrée est refusée, aucune n'est créée. Une série à moitié publiée serait plus pénible à rattraper qu'un échec franc.
Statuts
| Statut | Signification |
|---|---|
| Publiée | Visible dans le fil |
| En attente de relecture | Retenue par la modération, visible de son seul auteur |
| Programmée | Partira à sa date |
| Brouillon | Ne partira pas tant que tu ne l'auras pas décidé |
| Retirée | Retirée par la modération, conservée pour l'audit |
Supprimer ou retirer
Deux gestes différents :
- Supprimer (tableau Publications) efface définitivement, avec commentaires et réactions. Pour du contenu qui n'aurait jamais dû exister (données personnelles publiées par erreur, demande d'effacement).
- Retirer (file de modération) passe en « Retirée » et journalise la décision. C'est le geste pour sanctionner.
Membres
Tous ceux qui ont ouvert la communauté. Un profil est créé à la première ouverture du fil, sans inscription.
Modifiables en ligne :
- Rôle - membre, modérateur, admin
- Badge équipe - affiche un badge sous le pseudo dans le fil
Tu ne peux modifier ni le pseudo ni la bio d'un membre. Modérer un contenu est légitime, réécrire l'identité de quelqu'un ne l'est pas. Les sanctions passent par le bouton Décision, qui les journalise.
Modération
File
Les signalements des membres, enrichis de l'extrait du contenu visé, de son auteur et du nombre de signalements sur la même cible. Un membre ne signale un contenu qu'une fois : sinon le compteur qui déclenche le masquage automatique serait trivial à manipuler.
Décisions
Une seule route pour toutes les actions, ce qui garantit qu'aucune décision n'échappe au journal.
Sur un contenu :
| Action | Effet |
|---|---|
| Laisser en ligne | Écarte les signalements, le contenu reste |
| Retirer | Masqué pour tous, conservé pour l'audit |
| Remettre en ligne | Annule un retrait |
Sur un profil :
| Action | Effet |
|---|---|
| Avertir | Trace au journal, aucun effet visible |
| Shadow ban | Le membre publie et voit ses contenus, personne d'autre. Jamais notifié |
| Bannir | Lecture seule |
| Lever la sanction | Retour à la normale |
| Effacer le profil | Pseudo, bio et avatar effacés (RGPD). Les publications restent, signées du nom d'alors |
Ban et shadow ban acceptent une durée ; sans durée, la sanction est permanente. Une sanction temporaire est levée automatiquement au retour du membre.
Le shadow ban est le seul outil qui ne s'annonce pas. Un membre banni le sait et va se plaindre ailleurs ; un membre shadow banni continue de parler dans le vide. À réserver aux cas où le bruit est le problème.
Modes
Personnaliser → Modération.
| Mode | Comportement |
|---|---|
| Désactivée | Tout est publié. Les signalements arrivent quand même |
| Automatique | Chaque contenu est évalué avant publication |
| Manuelle | Rien n'est publié sans validation. Tenable seulement à faible volume |
En mode automatique, deux seuils entre 0 et 1 : au-delà du seuil de relecture, le contenu part en file au lieu d'être publié ; au-delà du seuil de refus, il est refusé d'emblée et l'auteur le sait.
Les mots interdits sont comparés sur des mots entiers, sans tenir compte de la casse ni des accents. Le masquage automatique après N signalements met le contenu de côté en attendant l'arbitrage (zéro le désactive).
En cas de panne du classifieur, le contenu est publié. Une communauté qui refuse tout parce qu'un fournisseur est indisponible est cassée de façon bien plus visible qu'une heure de contenu non filtré. Les signalements restent le filet de sécurité.
Journal
Toutes les décisions, humaines et automatiques, avec leur motif. Jamais modifié, jamais purgé : c'est ce qui permet de répondre « pourquoi ce membre a-t-il été banni ? » six mois plus tard.
Statistiques
Chaque compteur est affiché avec sa valeur sur la période précédente de même durée. « 240 publications » ne dit rien ; « 240 contre 180 » dit tout.
Audience
Membres, nouveaux membres, membres actifs, actifs par jour, ouvertures. « Membres actifs » compte les personnes distinctes ayant ouvert la communauté, pas le nombre d'ouvertures.
Contribution
Publications, commentaires, réactions, vues, et le taux de participation : la part des membres actifs qui publient ou commentent. C'est la métrique qui distingue une communauté d'un mur d'annonces. Les vues sont uniques par membre : rescroller un post ne regonfle pas le compteur.
Santé
Signalements en attente, signalements sur la période, contenus retirés, membres bannis. Ces métriques sont inversées à l'affichage : une hausse est en rouge.
Rétention
Part de chaque cohorte hebdomadaire encore active N semaines plus tard. L'intensité de la couleur porte l'information. C'est le seul indicateur qui dit si la communauté tient : une courbe d'activité peut monter uniquement parce que tu acquiers, pendant que chaque cohorte s'évapore.
Classements
Meilleurs contributeurs et publications les plus vues, sur la période et non en cumul. Un classement figé sur le total récompenserait éternellement les anciens et masquerait qui porte la communauté ce mois-ci.
Personnaliser
Quatre onglets, chacun enregistrable indépendamment : régler la modération n'écrase pas le thème.
| Onglet | Contenu |
|---|---|
| Général | Activation, fonctionnalités, traduction |
| Apparence | Couleur, police, taille, arrondi, thème clair/sombre, aperçu |
| Contenu | Longueurs max, images par post, lignes avant « voir plus », anti-flood |
| Modération | Mode, seuils, masquage auto, règles, mots interdits |
Tout changement est repris par le SDK à la prochaine ouverture, sans republication de l'app.
Utiliser ta propre police
Dans Apparence, choisis « Personnalisée » et saisis le nom PostScript d'une police déjà embarquée dans ton app, pas le nom du fichier.
# Trouver le nom PostScript d'un fichier de police
mdls -name com_apple_ats_name_postscript MaPolice.ttf
Si le SDK ne trouve pas la police, il retombe silencieusement sur la police système. Réglage iOS uniquement : sur Android, « personnalisée » et « arrondie » retombent sur la police par défaut du système.
Traduction instantanée
Les contenus sont traduits dans la langue de l'appareil du lecteur. Le premier lecteur d'une langue paie l'appel au traducteur, les suivants lisent le cache : c'est ce qui rend la fonctionnalité soutenable. Le membre peut toujours revenir au texte original.
Ce que le membre peut faire
Selon ce qui est activé dans le dashboard :
- lire le fil, filtrer par groupe, tirer pour rafraîchir
- publier, éditer et supprimer ses propres contenus
- commenter, répondre à un commentaire (un seul niveau)
- réagir aux publications et commentaires
- consulter les profils, éditer le sien, redevenir anonyme
- signaler une publication, un commentaire ou un profil
- lire dans sa langue si la traduction est activée
Chacun de ces points a un interrupteur côté studio. Une fonctionnalité désactivée disparaît de l'interface, elle n'est pas grisée.
Dépannage
| Symptôme | Cause probable |
|---|---|
| Écran « bientôt disponible » | Community pas prêt : lance un build debug, l'alerte en haut donne la raison et la correction |
| Le fil marche en debug, « bientôt disponible » en release | Le plan n'inclut pas Community : les builds debug sont déverrouillés (une alerte au-dessus du fil le signale), pas les builds release |
| Un tap de notification ouvre une feuille au lieu de l'onglet Communauté | Comportement par défaut quand le fil n'est pas à l'écran : renseigne onNotificationTap |
| Un tap qui a lancé l'app est perdu, ou traité par le SDK au lieu de ton handler | initialize() pas appelé au lancement (les taps sont rejoués quand il répond ready), ou onNotificationTap renseigné trop tard : renseigne-le avant initialize() |
| 403 sur tous les appels | Produit Community pas activé sur l'App ID : réactive depuis Personnaliser |
| Fil vide sans erreur | Aucune publication ; publie depuis le dashboard pour amorcer |
| Le fil ne scrolle pas (Flutter) | Un widget parent intercepte la gestuelle |
| Écran d'erreur natif | configure pas appelé avant l'affichage de la vue |
| Compilation iOS échoue | Cible de déploiement sous iOS 16 |