Community : fil in-app, commentaires et profils

La page Personnaliser de Communauté dans le dashboard Appwin : titre du fil, couleur d'accent, dégradé et ombre des boutons, avec l'aperçu du fil sur un téléphone.

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. 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
    PlateformePaquetSymbole
    iOS 16produit SPM AppwinCommunityAppwinCommunity
    Android 7.0 (API 24)io.appwin:appwin-communityio.appwin.community.AppwinCommunity
    Flutter 3.3appwin_communityAppwinCommunity.instance
    React Native 0.73@appwin/react-nativeAppwinCommunity
  2. 2

    Initialise Community

    Appelle initialize() au démarrage, après Appwin Core. Le fil apparaît quand trois conditions sont réunies : configure appelé au lancement, la communauté activée au dashboard (Personnaliser → Général → Communauté activée), et initialize() qui répond ready. Conditionne ton onglet sur cette réponse : le SDK ne possède pas ta navigation, il ne peut pas le masquer à ta place.

    swift
    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

swift
import AppwinCommunity

TabView {
  HomeView()
    .tabItem { Label("Accueil", systemImage: "house") }

  AppwinCommunity.communityView()
    .tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }
}

UIKit

swift
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

swift
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é).

swift
AppwinCommunity.communityView { result in
  ComingSoonView()
}
.tabItem { Label("Communauté", systemImage: "bubble.left.and.bubble.right") }

En UIKit, on passe un constructeur de contrôleur :

swift
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 est debuggable, 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() renvoie ready, et le SDK journalise une fois :
Texte brut
[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.

swift
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 filCe 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.

swift
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énementQuand
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é.

swift
.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).

swift
@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).

swift
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.

swift
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 :

Texte brut
"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 :

  1. le fil devient visible dans le SDK
  2. un groupe par défaut est créé
  3. 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églageEffet
Nom, emoji, descriptionCe que voit le membre
Qui peut publier« Tout le monde » ou « L'équipe uniquement »
ArchiverMasqué 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

StatutSignification
PubliéeVisible dans le fil
En attente de relectureRetenue par la modération, visible de son seul auteur
ProgramméePartira à sa date
BrouillonNe partira pas tant que tu ne l'auras pas décidé
RetiréeRetiré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 :

ActionEffet
Laisser en ligneÉcarte les signalements, le contenu reste
RetirerMasqué pour tous, conservé pour l'audit
Remettre en ligneAnnule un retrait

Sur un profil :

ActionEffet
AvertirTrace au journal, aucun effet visible
Shadow banLe membre publie et voit ses contenus, personne d'autre. Jamais notifié
BannirLecture seule
Lever la sanctionRetour à la normale
Effacer le profilPseudo, 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.

ModeComportement
DésactivéeTout est publié. Les signalements arrivent quand même
AutomatiqueChaque contenu est évalué avant publication
ManuelleRien 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.

OngletContenu
GénéralActivation, fonctionnalités, traduction
ApparenceCouleur, police, taille, arrondi, thème clair/sombre, aperçu
ContenuLongueurs max, images par post, lignes avant « voir plus », anti-flood
ModérationMode, 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.

bash
# 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ômeCause 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 releaseLe 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 handlerinitialize() 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 appelsProduit Community pas activé sur l'App ID : réactive depuis Personnaliser
Fil vide sans erreurAucune publication ; publie depuis le dashboard pour amorcer
Le fil ne scrolle pas (Flutter)Un widget parent intercepte la gestuelle
Écran d'erreur natifconfigure pas appelé avant l'affichage de la vue
Compilation iOS échoueCible de déploiement sous iOS 16

Suite