Files
roadwave/docs/domains/moderation/rules/autres-comportements.md
jpgiannetti 35aaa105d0 docs: améliorer rendu markdown et navigation mkdocs
- Ajouter ADR-018 (librairies Go) dans TECHNICAL.md
- Transformer Shared en menu dépliable dans mkdocs (cohérence avec autres domaines)
- Corriger listes markdown (ajout lignes vides avant listes)
- Corriger line breaks dans génération BDD (étapes "Et" sur nouvelles lignes)
- Ajouter script fix-markdown-lists.sh pour corrections futures

Impacte 86 fichiers de documentation et 164 fichiers BDD générés.
2026-02-09 20:49:52 +01:00

658 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 15. Autres comportements
### 15.1 Partage de contenu
**Décision** : Système de partage complet avec web player
#### 15.1.1 Bouton "Partager"
**Disponibilité** : Partout dans l'application
**Emplacements** :
- Player en lecture (bouton dans contrôles)
- Page profil créateur (sur chaque contenu)
- Liste de recherche (menu contextuel)
- Historique personnel
**Icône** : ⬆️ (universelle iOS/Android)
**Menu options** :
- Copier le lien
- WhatsApp
- Email
- SMS
- Plus... (sheet natif OS)
**Justification** :
- Viralité = croissance organique gratuite
- Aucune friction, partage universel
---
#### 15.1.2 Comportement du lien partagé
**Format URL** : `https://roadwave.fr/share/c/[content_id]`
**Comportement multi-plateforme** :
```
User clique lien partagé
Page web responsive
┌─────────────────────────────────┐
│ Si app installée │
│ → Deep link (ouverture directe) │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Si app non installée │
│ → Web player + CTA téléchargement│
└─────────────────────────────────┘
```
**Contenu de la page web** :
```html
┌───────────────────────────────────────┐
│ RoadWave │
├───────────────────────────────────────┤
│ [Image cover 16:9] │
│ │
│ 📻 Titre du contenu │
│ Par @créateur · 12 min · 🎧 2.3K │
│ │
│ 📍 Paris 5e · Ancré │
│ 🏷️ #Voyage #Histoire │
│ │
│ Description : Lorem ipsum... │
│ │
│ [▶️ Écouter maintenant] │
│ (Player HTML5 si contenu public) │
│ │
│ ────────────────────────────────── │
│ │
│ 📱 Télécharger l'app RoadWave │
│ [App Store] [Google Play] │
│ │
│ [Voir le profil de @créateur] │
└───────────────────────────────────────┘
```
**Métadonnées Open Graph (SEO)** :
```html
<meta property="og:title" content="[Titre contenu] - RoadWave">
<meta property="og:description" content="[Description ou extrait]">
<meta property="og:image" content="[URL cover image]">
<meta property="og:audio" content="[URL audio si public]">
<meta property="og:type" content="music.song">
<meta property="og:site_name" content="RoadWave">
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://roadwave.fr/player/[content_id]">
```
**Deep linking** :
- iOS : Universal Links (configuration `apple-app-site-association`)
- Android : App Links (configuration `assetlinks.json`)
- URL scheme : `roadwave://content/[content_id]`
**Justification** :
- Meilleure viralité (partage social optimisé)
- SEO (contenus indexés Google)
- UX optimale (web + app)
- Coût : 0€ (backend simple + CDN existant)
---
#### 15.1.3 Contenus Premium partagés
**Décision** : Preview 30 secondes + paywall
**Comportement** :
1. User clique lien contenu Premium partagé
2. Page web affiche badge "👑 Contenu Premium"
3. Player démarre automatiquement
4. Après **30 secondes exactement** :
- Fade out audio (2 secondes)
- Overlay apparaît :
```
┌─────────────────────────────────┐
│ 👑 Contenu réservé Premium │
│ │
│ Profitez de ce contenu complet │
│ et de milliers d'autres │
│ sans publicité │
│ │
│ [Passer Premium - 4.99€/mois] │
│ [Télécharger l'app] │
└─────────────────────────────────┘
```
5. Utilisateur peut :
- S'abonner Premium (redirection web Mangopay)
- Télécharger l'app (redirection stores)
- Rejouer les 30 premières secondes (illimité)
**Tracking** :
- Métriques créateur : "Partages Premium" + "Conversions Premium"
- Créateur touche sa part si conversion (70%)
**Justification** :
- Équilibre viralité / monétisation
- 30s = assez pour donner envie, pas assez pour satisfaire
- Protège revenus créateurs
---
### 15.2 Profil créateur
**Décision** : Profil public complet et transparent
#### 15.2.1 Structure de la page profil
**URL** : `https://roadwave.fr/@[pseudo]`
**Layout** :
```
┌────────────────────────────────────────┐
│ [Photo profil 120×120] │
│ @pseudo ✓ │
│ [Badge vérifié si applicable] │
│ │
│ Bio : Lorem ipsum dolor sit amet... │
│ (300 caractères max) │
│ │
│ 🎧 1.2K abonnés │
│ 📻 42 contenus │
│ ⏱️ 18h de contenu créé │
│ 🔊 54K écoutes totales │
│ │
│ [S'abonner] [Partager profil] [•••] │
│ │
│ ──────────────────────────────────── │
│ │
│ Contenus ▼ [Plus récents ▼] │
│ │
│ ┌──────────────────────────────────┐ │
│ │ [Cover] Titre contenu 1 │ │
│ │ 12 min · 🎧 2.3K · 📍 Paris │ │
│ │ [▶️] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ [Cover] Titre contenu 2 │ │
│ │ 8 min · 🎧 5.1K · 📍 Lyon │ │
│ │ [▶️] │ │
│ └──────────────────────────────────┘ │
│ │
│ [Charger plus] │
└────────────────────────────────────────┘
```
**Informations affichées** :
| Élément | Visibilité | Détails |
|---------|------------|---------|
| **Photo + pseudo** | ✅ Public | Identité visuelle |
| **Badge vérifié ✓** | ✅ Public (si applicable) | Compte authentique |
| **Bio** | ✅ Public | 0-300 caractères, markdown basique (gras, italique, liens) |
| **Nombre abonnés** | ✅ Public | Arrondi si >1000 (ex: 1.2K, 54K) |
| **Nombre contenus** | ✅ Public | Exact |
| **Durée totale créée** | ✅ Public | Arrondi en heures (ex: 18h, 142h) |
| **Écoutes totales** | ✅ Public | Arrondi (ex: 54K, 1.2M) |
| **Liste abonnés** | ❌ Privé | Protection vie privée (RGPD) |
| **Revenus** | ❌ Privé | Confidentialité financière |
| **Localisation précise** | ❌ Privé | Sécurité |
| **Email** | ❌ Privé | Anti-spam |
**Tri des contenus** :
| Option | Comportement |
|--------|--------------|
| **Plus récents** | Date publication DESC (défaut) |
| **Plus populaires** | Écoutes complètes × (1 + (date_publication - now) / 90 jours) |
| **Plus anciens** | Date publication ASC |
| **Par tag** | Filtre multi-sélection tags |
**Recherche locale** :
- Barre recherche dans profil : "Rechercher dans les contenus de @pseudo"
- Recherche full-text sur titres + descriptions
**Actions menu [•••]** :
- Partager profil
- Signaler profil (spam, usurpation)
- Bloquer créateur (masque tous ses contenus)
---
#### 15.2.2 Statistiques publiques
**Décision** : Stats arrondies et motivantes
**Affichage public** :
| Métrique | Format affichage | Exemple |
|----------|------------------|---------|
| **Abonnés** | Exact si <1000, arrondi sinon | 342 / 1.2K / 54K / 1.2M |
| **Écoutes totales** | Arrondi dès 1000 | 842 / 5.4K / 142K / 2.1M |
| **Contenus publiés** | Exact | 42 contenus |
| **Durée totale** | Arrondi en heures | 18h / 142h de contenu |
**Métriques PRIVÉES (créateur uniquement)** :
| Métrique | Disponible dans dashboard créateur |
|----------|-------------------------------------|
| **Taux complétion moyen** | 78% (écoutes >80% / écoutes totales) |
| **Évolution abonnés** | Graphique 30j / 90j / 1 an |
| **Écoutes par contenu** | Tableau détaillé |
| **Revenus** | Dashboard monétisation dédié |
| **Taux conversion Premium** | Partages → conversions |
| **Démographie** | Âge / zone géo (agrégée, anonymisée) |
**Justification** :
- Arrondi = évite comparaisons anxiogènes
- Preuve sociale pour nouveaux auditeurs (trust)
- Gamification douce (motivation créateurs)
- Privacy by design
---
#### 15.2.3 Badge vérifié
**Décision** : Badge unique ✓ (vérifié officiel)
**Critères d'attribution** (au moins UN des critères) :
1. **KYC monétisation validé** : identité vérifiée via Mangopay KYC
2. **Célébrité / Média officiel** : validation manuelle équipe RoadWave
3. **Communauté significative** : ≥10K abonnés + compte actif >6 mois
**Affichage** :
- Badge bleu **✓** accolé au pseudo (partout : profil, player, recherche)
- Tooltip au survol/appui long : "Compte vérifié"
**Processus d'obtention** :
| Type | Processus |
|------|-----------|
| **Automatique (KYC)** | Badge attribué dès validation documents Mangopay |
| **Manuel (célébrité)** | Formulaire demande → équipe vérifie identité → validation 48-72h |
| **Automatique (10K)** | Badge attribué automatiquement à 10K abonnés si compte >6 mois |
**Retrait du badge** :
- Suspension monétisation → badge retiré temporairement
- Strikes multiples → badge retiré définitivement
- Usurpation identité détectée → ban + retrait
**Justification** :
- Combat usurpations d'identité
- Trust auditeurs (surtout pour médias/personnalités)
- Simplicité (1 seul badge, pas de gamification excessive)
- Coût : 0€ (champ boolean `verified` en DB)
---
### 15.3 Recherche
**Décision** : Recherche full-text + géo + filtres avancés
#### 15.3.1 Recherche par mot-clé
**Implémentation** : PostgreSQL full-text search (français)
**Configuration technique** :
```sql
-- Index full-text optimisé français
CREATE INDEX idx_content_search ON contents
USING GIN(
to_tsvector('french',
coalesce(title, '') || ' ' ||
coalesce(description, '') || ' ' ||
coalesce(creator_pseudo, '')
)
);
-- Recherche avec ranking
SELECT
c.*,
ts_rank(
to_tsvector('french', c.title || ' ' || c.description),
plainto_tsquery('french', $search_query)
) AS rank
FROM contents c
WHERE to_tsvector('french', c.title || ' ' || c.description)
@@ plainto_tsquery('french', $search_query)
ORDER BY rank DESC, listen_count DESC
LIMIT 20;
```
**Champs indexés** :
- Titre du contenu (poids × 3)
- Description (poids × 1)
- Pseudo créateur (poids × 2)
- Tags (poids × 1.5)
**Fonctionnalités** :
| Feature | Description |
|---------|-------------|
| **Stemming français** | "voyages" trouve "voyage", "voyager", etc. |
| **Correction auto** | Suggestion si 0 résultat |
| **Recherches populaires** | "Essayez plutôt : balade paris, audio-guide louvre" |
| **Historique personnel** | 10 dernières recherches sauvegardées |
| **Autocomplete** | Suggestions pendant frappe (top 5) |
**Coût** : 0€ (PostgreSQL natif)
**Migration future** :
- Si >100K contenus : Meilisearch (typo-tolerance avancée, ~20-50€/mois)
- Si >1M contenus : Elasticsearch cluster
**Justification** :
- PostgreSQL full-text = performant jusqu'à 500K contenus
- Stemming français natif
- 0€, aucune dépendance externe
---
#### 15.3.2 Recherche géographique
**Décision** : Recherche lieu + rayon paramétrable
**Interface utilisateur** :
```
┌─────────────────────────────────────┐
│ 🔍 Recherche contenu... │
├─────────────────────────────────────┤
<20><> Lieu │
│ [Paris, France ▼] │
│ · Autour de moi (GPS actuel) │
│ · Entrer une adresse/ville │
│ │
│ 📏 Rayon de recherche │
│ [●─────────────────] 50 km │
│ (curseur 5 km → 500 km) │
│ │
│ 🗺️ [Afficher sur carte] │
└─────────────────────────────────────┘
```
**Géocodage** :
| Service | Usage | Coût |
|---------|-------|------|
| **Nominatim (OSM)** | MVP (API publique) | 0€ (rate limit 1 req/s) |
| **Nominatim self-hosted** | Scale (Docker) | 20-50€/mois VPS |
| **Mapbox Geocoding** | Fallback premium | 0.50€ / 1000 requêtes |
**Processus de recherche géo** :
1. User tape "Louvre" ou "Paris"
2. Autocomplete via Nominatim → liste suggestions
3. User sélectionne → récupération coordonnées (lat, lon)
4. Requête PostGIS :
```sql
SELECT c.*,
ST_Distance(c.location::geography, ST_Point($lon, $lat)::geography) AS distance
FROM contents c
WHERE ST_DWithin(
c.location::geography,
ST_Point($lon, $lat)::geography,
$radius_meters
)
ORDER BY distance ASC;
```
**Affichage résultats** :
- Tri par défaut : distance croissante
- Indication distance : "À 2.3 km" / "À 15 km" / "À 142 km"
- Option carte : markers cliquables (clustering si >50 résultats)
**Coût** :
- MVP : 0€ (Nominatim public)
- Scale : 20-50€/mois (Nominatim self-hosted Docker)
**Justification** :
- Essentiel pour tourisme / planification trajet
- OpenStreetMap = pas de dépendance Google
- PostGIS = performant (index GIST natif)
---
#### 15.3.3 Filtres avancés
**Décision** : 7 catégories de filtres combinables
**Interface filtres** :
```
┌─────────────────────────────────────┐
│ Filtres [×] │
├─────────────────────────────────────┤
│ Type de contenu │
│ ☐ Contenu court (<5 min) │
│ ☐ Podcast (>5 min) │
│ ☐ Radio live │
│ ☐ Audio-guide │
│ │
│ Durée │
│ ○ Toutes durées │
│ ○ <5 min │
│ ○ 5-15 min │
│ ○ 15-30 min │
│ ○ >30 min │
│ │
│ Classification âge │
│ ☐ Tout public │
│ ☐ 13+ │
│ ☐ 16+ │
│ ☐ 18+ │
│ │
│ Géo-pertinence │
│ ☐ Ancré (lieu précis) │
│ ☐ Contextuel (zone large) │
│ ☐ Neutre (national) │
│ │
│ Tags (multi-sélection) │
│ ☐ Automobile ☐ Voyage │
│ ☐ Famille ☐ Histoire │
│ ☐ Économie ☐ Sciences │
│ ... (liste complète tags) │
│ │
│ Date de publication │
│ ○ Toutes dates │
│ ○ Dernières 24h │
│ ○ Cette semaine │
│ ○ Ce mois │
│ ○ Cette année │
│ │
│ Abonnement │
│ ○ Tous les contenus │
│ ○ Gratuits uniquement │
│ ○ Premium uniquement 👑 │
│ │
│ ────────────────────────────── │
│ [Réinitialiser] [Appliquer] │
└─────────────────────────────────────┘
```
**Options de tri** :
| Tri | Algorithme |
|-----|-----------|
| **Pertinence** | Score recherche × (1 + log(listen_count + 1)) |
| **Popularité** | Écoutes complètes derniers 30j DESC |
| **Récent** | Date publication DESC |
| **Proximité** | Distance GPS ASC (si recherche géo active) |
| **Durée** | Durée audio ASC ou DESC |
**Sauvegarde de recherches** :
- Bouton "💾 Sauvegarder cette recherche"
- Nom personnalisable : "Podcasts voyage Paris"
- Maximum **5 recherches sauvegardées**
- Accès rapide : onglet "Recherches sauvegardées" dans page recherche
- Notifications optionnelles : "3 nouveaux contenus dans 'Podcasts voyage Paris'"
**Performances** :
```sql
-- Index composites pour filtres
CREATE INDEX idx_content_filters ON contents (
content_type,
duration,
age_rating,
geo_type,
published_at
);
-- Index GIN pour tags
CREATE INDEX idx_content_tags ON contents USING GIN(tags);
```
**Coût** : 0€ (PostgreSQL + index standards)
**Justification** :
- Filtres essentiels pour découvrabilité
- Combinables = puissance maximale
- Sauvegarde = gain temps utilisateurs réguliers
---
#### 15.3.4 Page de résultats
**Décision** : Liste avec previews enrichies
**Layout résultats** :
```
┌─────────────────────────────────────────┐
│ 🔍 "voyage paris" │
│ 42 résultats · Tri : Pertinence ▼ │
│ [Filtres] [Carte] │
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ [Cover ] Balade à Paris │ │
│ │ [16:9 ] @paris_stories ✓ │ │
│ │ [Image ] 12 min · 🎧 2.3K │ │
│ │ 📍 Paris 5e · Ancré │ │
│ │ 🏷️ #Voyage #Histoire │ │
│ │ [▶️ Écouter] [⋮] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ [Cover ] Secrets Montmartre │ │
│ │ [16:9 ] @explore_paris │ │
│ │ [Image ] 8 min · 🎧 5.1K │ │
│ │ 📍 Paris 18e · Guide │ │
│ │ 🏷️ #Voyage #Art │ │
│ │ [▶️ Écouter] [⋮] │ │
│ └─────────────────────────────────────┘ │
│ │
│ [Charger plus] (20 suivants) │
└─────────────────────────────────────────┘
```
**Informations par résultat** :
| Élément | Affichage |
|---------|-----------|
| **Cover image** | 16:9, 120×68 px, lazy loading |
| **Titre** | Tronqué 2 lignes max |
| **Créateur** | @pseudo + badge ✓ si vérifié, cliquable → profil |
| **Durée** | Format : "3 min" / "12 min" / "1h 24 min" |
| **Écoutes** | Arrondi : "2.3K" / "54K" / "1.2M" |
| **Localisation** | Ville + type géo (Ancré/Contextuel/Neutre) |
| **Tags** | Maximum 3 premiers tags |
| **Badge Premium** | 👑 si contenu premium |
| **Distance** | Si recherche géo : "À 2.3 km" |
**Actions contextuelles [⋮]** :
- Partager
- Ajouter à une playlist (future feature)
- Télécharger (offline)
- Signaler
**Pagination** :
- **20 résultats** par page
- Infinite scroll (charger automatiquement si scroll >80%)
- Bouton "Charger 20 suivants" en bas (fallback si scroll auto désactivé)
**Vue carte (alternative)** :
- Bouton toggle "Liste / Carte"
- Map Leaflet (OpenStreetMap)
- Markers cliquables → popup avec preview
- Clustering si >50 résultats proches
**Coût** : 0€ (Leaflet open source + OSM tiles gratuit)
**Justification** :
- Équilibre information / compacité
- Lazy loading = performances
- Infinite scroll = UX moderne
---
## Récapitulatif Section 15
| Point | Décision | Coût | Complexité |
|-------|----------|------|------------|
| **15.1.1** Bouton partager | Disponible partout (⬆️), menu natif OS | 0€ | Faible |
| **15.1.2** Lien partagé | Web player + deep link + Open Graph SEO | 0€ | Moyenne |
| **15.1.3** Premium partagé | Preview 30s + paywall overlay | 0€ | Faible |
| **15.2.1** Page profil | Profil public complet (stats + bio + contenus + tri) | 0€ | Faible |
| **15.2.2** Stats publiques | Arrondies (abonnés, écoutes, durée totale) | 0€ | Faible |
| **15.2.3** Badge vérifié | ✓ si KYC/célébrité/>10K abonnés | 0€ | Faible |
| **15.3.1** Recherche texte | PostgreSQL full-text french + stemming | 0€ | Moyenne |
| **15.3.2** Recherche géo | Lieu + rayon (Nominatim OSM) | 0-50€/mois | Moyenne |
| **15.3.3** Filtres | 7 catégories combinables + sauvegarde recherches | 0€ | Moyenne |
| **15.3.4** Page résultats | Liste enrichie + vue carte Leaflet + infinite scroll | 0€ | Moyenne |
**Coût total MVP : 0-50€/mois** (Nominatim self-hosted optionnel)
---
## Points d'attention pour Gherkin
- Tester partage contenu public vs Premium (preview 30s)
- Tester deep linking iOS/Android (ouverture app si installée)
- Tester Open Graph (aperçu correct sur WhatsApp, Twitter, Facebook)
- Tester profil public (stats arrondies, badge vérifié)
- Tester recherche full-text français (stemming, accents)
- Tester recherche géo + rayon (PostGIS distance)
- Tester combinaison filtres multiples (AND logic)
- Tester sauvegarde recherches (max 5)
- Tester pagination infinite scroll + fallback bouton
- Tester vue carte Leaflet (clustering, markers cliquables)