Summary

Before this, the only thing shareable inside Flow was a crew invite, and only in the seconds after creating a crew, from a sheet that opened once and never came back. Everything else — an event, a venue, a profile — left the app through the system share sheet, as a link or text in WhatsApp. Flow lost the conversation at exactly the moment it mattered.

The Model: SharedContent

lib/shared/models/shared_content.dart

An enum SharedKind { event, crewInvite, crew, venue, profile, place } plus a snapshot: title, subtitle, imageUrl, latitude, longitude, inviteCode, targetId.

crewInvite is deliberately separate from crew. Sharing a crew says “look at this”, inviting says “come with us”: they read differently and act differently — one opens a page, the other joins you to a group. Collapsing them into one type would force the widget to guess which was meant.

Why the snapshot is stored, not just the id

A message is a record of what was said, at the time it was said. If only the id were stored and resolved on read:

  • a deleted event would leave a blank bubble in a conversation from three weeks ago;
  • a renamed venue would silently rewrite history — someone would read “we’re going to X” where what was sent said Y.

So title, subtitle and image are captured at send time and rendered from the snapshot. The id travels too, because tapping should open the LIVE thing: the record is historical, the destination is current. When the target is gone the card still reads correctly and says it is no longer available, instead of vanishing.

Backward compatibility with older invites

Crew invites sent before this model used a flat shape:

{ "type": "crew_invite", "crew_id": "…", "crew_title": "…", "vibe": "…" }

written by crew_invite_share_sheet.dart. Those messages are already in people’s conversations. SharedContent.fromMetadata reads that shape too, so they render as cards rather than staying plain text forever while everything sent from today looks different.

Note

A feature that only works going forward makes the app look broken backwards.

The new format lives under metadata.share, so the two shapes do not collide.

The Cards: SharedContentCard

lib/shared/widgets/shared_content_card.dart

Three shapes, not one, because the three things are read differently:

ShapeForLook
_postereventimage on top, title under — you decide by looking, the way you decide from a flyer
_invitecrew invitea verb and a button, because “come with us” is a question and the card should let you answer it without leaving the conversation
_mapplacemini map with a pin, gestures disabled; tapping opens the maps app
_referencevenue, profile, crewcompact row: circle for a profile, rounded square for a place

The join button appears only for the recipient: showing the sender a button to join a crew they are already in is a button that cannot work.

_kindLine prints the small line saying what this is (“EVENT”, “CREW INVITE”). Without it a card is just a title, and a shared venue looks like an event with a different picture.

The Sheet: ShareToChatSheet

lib/shared/widgets/share_to_chat_sheet.dart

One screen, not two. The obvious build is a picker that navigates into the conversation and leaves you there, but sharing is almost always an aside: you are looking at an event, you want a friend to see it, and you want to stay where you were. So the sheet sends in place and stays open, ticking off the chats already served: sharing one thing with three people is one action, not three trips through the same sheet.

It shows a preview of what will actually arrive. Sharing is a small public act — the sender should see the message before it is sent, not discover it in the conversation afterwards.

Conversations are ordered by updated_at descending: the person you spoke to five minutes ago is far likelier than the one from March.

The Button: ShareButton

lib/shared/widgets/share_button.dart

One widget, not a helper per screen. This session spent hours on bugs that all had the same cause: the same short piece of logic written independently in four or five places, so fixing one never fixed the others — the category label, the image fallback, the vibe filter. Sharing has exactly that shape (build a snapshot, open a sheet, call the right method), so it gets one implementation now rather than four to reconcile later.

Callers pass a SharedContent because only they know what they are looking at; the sending is resolved here from its kind.

Entry Points

ScreenFileWhere
Event detailevent_details_screen.dart_showShareChoice() — chooses between sending in Flow and the system share
Venuevenue_screen.dartSliverAppBar action, on the same dark disc as the back button, because a bare icon disappears over a cover photo
Public profilepublic_profile_screen.dartreplaces a button that already existed with onTap: () {} and a // TODO: share sheet — it looked live and did nothing
Crewcrew_detail_sheet.dart”Invite via chat” next to the CTA — a crew with two people in it on a Friday afternoon is exactly when you want a third

The Service: ChatShareService

lib/core/services/chat_share_service.dart

Warning

It is called ChatShareService, not ShareService: lib/shared/services/share_service.dart already exists for the system share (deep link out of the app). They are different things — outside Flow and inside Flow.

Typed methods: shareEvent, inviteToCrew, shareCrew, shareVenue, shareProfile, sharePlace. Each writes content (the fallback text) and metadata (the snapshot) on the message.

The fallback text is not decoration

SharedContent.fallbackText (🎟️ Title, 👥 I invited you to «X») is what a push notification shows, what a search finds, and what someone on an older app version sees. A share must stay readable with the widget stripped away.

That text is written to the database at send time, so it stays in the sender’s language: it is a record, not UI. The labels rendered on screen (sheet title, “Join”, the kind line) are localised and follow the reader’s language.

Cards do not sit on the sender’s gradient

A card is an object with its own edge and rounding. It used to go translucent white on top of the coral bubble, which made it look like a sticker on a balloon. The bubble now paints nothing behind a card and adds no padding around it; which side sent it stays obvious from the alignment and the status tick.

That made onOwnBubble always false, so it was removed rather than left as a parameter nobody varies — along with the four white-on-coral colour branches it existed to feed.

Accepting an Invite

chat_screen.dart_acceptCrewInvite(Message). Handles the “you are already in” case without an error: two people tapping the same invite must not see a failure.

l10n Keys Added

shareSheetTitle, shareSheetEmpty, shareSheetFailed, shareChatFallbackName, shareInviteToChat, shareJoinCrew, sharedKindEvent, sharedKindCrewInvite, sharedKindCrew, sharedKindVenue, sharedKindProfile, sharedKindPlace. See localization.

feature-crew-chat-lifecycle — crew chat and its lifecycle fix-crew-event-link — the routes in this feature were wrong on first pass feat-event-details-v3 — event detail screen