feature/chat-sharing — condivisione in chat (eventi, locali, profili, crew)
Data: 31 luglio 2026 Priorità: 3 — Feature nuove
Prima di questa modifica l’unica cosa condivisibile dentro Flow era l’invito a una crew, e solo nei secondi successivi alla creazione, da uno sheet che si apriva una volta e non tornava più. Tutto il resto — un evento, un locale, un profilo — usciva dall’app tramite lo share di sistema, cioè come link o testo in WhatsApp. L’app perdeva la conversazione proprio nel momento in cui serviva.
Il modello: SharedContent
lib/shared/models/shared_content.dart
Un enum SharedKind { event, crewInvite, crew, venue, profile, place } più uno
snapshot: title, subtitle, imageUrl, latitude, longitude,
inviteCode, targetId.
crewInvite è volutamente separato da crew. Condividere una crew dice
“guarda qui”, invitare dice “vieni con noi”: si leggono diversamente e agiscono
diversamente — uno apre una pagina, l’altro ti aggiunge a un gruppo. Fonderli
in un tipo solo costringerebbe il widget a indovinare quale dei due era.
Perché salviamo lo snapshot e non solo l’id
Un messaggio è il verbale di quello che è stato detto, nel momento in cui è stato detto. Se salvassimo solo l’id e risolvessimo il contenuto in lettura:
- un evento cancellato lascerebbe una bolla vuota in una conversazione di tre settimane fa;
- un locale rinominato riscriverebbe la storia in silenzio — qualcuno leggerebbe “andiamo da X” dove quello che era stato inviato diceva Y.
Quindi titolo, sottotitolo e immagine sono catturati all’invio e renderizzati dallo snapshot. L’id viaggia comunque, perché il tap deve aprire la cosa VIVA: il record è storico, la destinazione è attuale. Quando il target non esiste più la card resta leggibile e dice che non è più disponibile, invece di sparire.
Retrocompatibilità con gli inviti vecchi
Gli inviti crew inviati prima di questo modello usavano una forma piatta:
{ "type": "crew_invite", "crew_id": "…", "crew_title": "…", "vibe": "…" }scritta da crew_invite_share_sheet.dart. Quei messaggi sono già nelle
conversazioni delle persone. SharedContent.fromMetadata legge anche quella
forma, così diventano card pure loro invece di restare testo semplice per
sempre mentre tutto quello inviato da oggi ha un aspetto diverso: una feature
che funziona solo in avanti fa sembrare l’app rotta all’indietro.
Il nuovo formato vive sotto metadata.share, quindi le due forme non
collidono.
Le card: SharedContentCard
lib/shared/widgets/shared_content_card.dart
Tre forme, non una, perché le tre cose si leggono in modo diverso:
| Forma | Per | Aspetto |
|---|---|---|
_poster | evento | immagine sopra, titolo sotto — si decide guardando, come da un volantino |
_invite | invito crew | verbo + pulsante “Unisciti”, perché “vieni con noi” è una domanda e la card deve permettere di rispondere senza uscire dalla conversazione |
_map | posizione | mini-mappa con pin, gesture disattivate; il tap apre l’app mappe |
_reference | locale, profilo, crew | riga compatta: cerchio per il profilo, quadrato arrotondato per il luogo |
Il pulsante “Unisciti” compare solo al destinatario: mostrare a chi ha inviato l’invito un pulsante per unirsi a una crew di cui fa già parte è un bottone che non può funzionare.
_kindLine stampa la riga piccola che dice cos’è (“EVENTO”, “INVITO CREW”).
Senza, una card è solo un titolo e un locale condiviso sembra un evento con una
foto diversa.
Lo sheet: ShareToChatSheet
lib/shared/widgets/share_to_chat_sheet.dart
Una schermata sola, non due. La costruzione ovvia è un picker che naviga dentro la conversazione e ti lascia lì, ma condividere è quasi sempre un inciso: stai guardando un evento, vuoi che un amico lo veda, e vuoi restare dov’eri. Quindi lo sheet invia sul posto e resta aperto, segnando con una spunta le chat già servite: condividere una cosa con tre persone è un’azione sola, non tre giri nello stesso sheet.
Mostra un’anteprima di quello che arriverà davvero. Condividere è un piccolo atto pubblico — chi invia deve vedere il messaggio prima che parta, non scoprirlo nella conversazione dopo.
Le conversazioni sono ordinate per updated_at discendente: la persona con cui
hai parlato cinque minuti fa è molto più probabile di quella di marzo.
Il pulsante: ShareButton
lib/shared/widgets/share_button.dart
Un widget, non un helper per schermata. Questa sessione ha speso ore su bug che avevano tutti la stessa causa: lo stesso pezzo di logica breve scritto indipendentemente in quattro o cinque punti, così sistemarne uno non sistemava gli altri — l’etichetta della categoria, il fallback dell’immagine, il filtro vibe. La condivisione ha esattamente quella forma (costruisci uno snapshot, apri uno sheet, chiama il metodo giusto), quindi ha una implementazione sola adesso invece di quattro da riconciliare dopo.
Il chiamante passa un SharedContent perché solo lui sa cosa sta guardando;
l’invio viene risolto qui dal suo kind.
Punti d’ingresso
| Schermata | File | Dove |
|---|---|---|
| Dettaglio evento | event_details_screen.dart | _showShareChoice() — sceglie tra invio in Flow e share di sistema |
| Locale | venue_screen.dart | azione nella SliverAppBar, sullo stesso disco scuro del back, perché un’icona nuda sparisce sopra una copertina |
| Profilo pubblico | public_profile_screen.dart | sostituisce un pulsante che esisteva già con onTap: () {} e un // TODO: share sheet — sembrava vivo e non faceva niente |
| Crew | squad_detail_sheet.dart | ”Invita in chat” vicino alla CTA — una crew con due persone dentro il venerdì pomeriggio è esattamente quando ne vuoi una terza |
Il servizio: ChatShareService
lib/core/services/chat_share_service.dart
⚠️ Si chiama
ChatShareService, nonShareService: esiste giàlib/shared/services/share_service.dartper lo share di sistema (deep link fuori dall’app). Sono due cose diverse — fuori da Flow e dentro Flow.
Metodi tipizzati: shareEvent, inviteToCrew, shareCrew, shareVenue,
shareProfile, sharePlace. Ognuno scrive content (il testo di fallback) e
metadata (lo snapshot) sul messaggio.
Il testo di fallback non è decorazione
SharedContent.fallbackText (🎟️ Titolo, 👥 Ti ho invitato in «X») è quello
che vede una notifica push, quello che trova una ricerca, e quello che vede chi
ha una versione vecchia dell’app. Una condivisione deve restare leggibile con
il widget tolto di mezzo.
Questo testo è scritto nel DB al momento dell’invio, quindi resta nella lingua di chi ha inviato: è un record, non UI. Le etichette renderizzate a schermo (titolo dello sheet, “Unisciti”, la riga del tipo) sono invece localizzate e seguono la lingua di chi legge.
Accettare un invito
chat_screen.dart → _acceptCrewInvite(Message). Gestisce il caso “sei già
dentro” senza errore: due persone che toccano lo stesso invito non devono
vedere un fallimento.
Chiavi l10n aggiunte
shareSheetTitle, shareSheetEmpty, shareSheetFailed,
shareChatFallbackName, shareInviteToChat, shareJoinCrew,
sharedKindEvent, sharedKindCrewInvite, sharedKindCrew, sharedKindVenue,
sharedKindProfile, sharedKindPlace — in app_en.arb, app_it.arb,
app_es.arb. Vedi localization.
Related
feature-crew-chat — chat di crew e collegamento crew → chat feat-event-details-v3 — schermata dettaglio evento localization — regole e indice delle chiavi