Repository navigation
feat(chat): join private groups from invite links - #1152
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 28, 2026 18:47
f5cef84 to
4ec4ffe
Compare
bmc08gt
marked this pull request as ready for review
October 28, 2026 18:47
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 28, 2026 18:52
4ec4ffe to
db15d17
Compare
bmc08gt
marked this pull request as draft
October 28, 2026 19:02
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
4 times, most recently
from
October 28, 2026 20:16
c9f4228 to
afc7c72
Compare
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 28, 2026 21:33
afc7c72 to
33d3cfc
Compare
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 29, 2026 00:18
33d3cfc to
952ac8a
Compare
bmc08gt
marked this pull request as ready for review
October 29, 2026 00:35
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 29, 2026 00:52
952ac8a to
7baf8b2
Compare
Route /chat/invite#s=… ahead of /chat/{uuid} and read the fragment
as the invite secret, whose description is redacted. A link with no
usable secret routes to the not-found state instead of the browser.
The invite preview (3a) resolves the link with GetInvite and
GetRoster, joins with the invite token and sealed secret, and sends a
member straight to the chat.
The card resolves an invite link with GetInvite by its secret instead of GetChat, and shows the link's status in place of the entry rule. The resolver keys it on a digest of the link, so the secret is never a memo key. Tapping it opens the invite preview.
The join notice parsed and stored invited_by but still worded an invite
join as a plain "joined the group". It now reads "{name} joined with
{inviter}'s invite link", "You joined with …" or "… with your invite
link" (nodes 12846:374, 12846:427, 12846:480), as Android #1870 does. The
inviter's name is bold and opens their profile.
A non-member sees Encryption and Report, as on Android's invite landing; Mute needs viewer state the preview doesn't have.
bmc08gt
force-pushed
the
feat/group-invites-joiner
branch
from
October 29, 2026 01:16
7baf8b2 to
3146017
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third of three PRs for v1 invite links to private groups: the joiner side. It stacks on #1149, which stacks on #1144.
Routing
app.flipcash.com/chat/invite#s=…now opens in the app wherever it's tapped: message text, the link card, a QR code, or a universal link. It used to fall through to Safari.Routeasks shared-core'sSharedInviteCipher.route(url:)before the generic/chat/{uuid}match, so/chat/inviteis never read as a chat id. A missing or malformed secret goes to the in-app "not found" state, not to Safari.The secret stays out of logs.
InviteLinkSecretprints as redacted, the newDestinationcase leaves it out of its description, and the existing log lines already strip the fragment throughsanitizedForAnalytics. The route firesgroupInviteFollowed(source: .link), matching Android.Landing (3a, 3c, 3e, 3f, 4a–4c)
GroupInviteLandingfollows Android's from #1870: derive the token,GetInvitewith the owner so a ban is reported, then either open the chat for a member or show the preview. The preview fetches the roster with the invite token. Join sendsinviteIdplus the secret sealed to the joiner, and the group is seated the same way a request-to-join approval seats it.GetInvitealso fails, the dead-invite screen shows without a reason line.GroupInviteScreenreusesProfileHeaderViewandGroupChattingGridfrom the group profile.Link card (surface 6)
A message with an invite link shows the group card, resolved through
GetInviterather thanGetChat: name, photo, member count and status ("Private group · Invite link"). View opens 3a, or the chat for a member. Invite URLs get no metadata unfurl (Decision 6). Android #1870 has no card for invite links yet.Join notice (surface 5)
An invite join used to read "Alex joined the group":
invited_bywas parsed and stored, butSystemNoticeStringsworded.userJoinedViaInviteas a plain join (a placeholder from #1141). It now reads "Alex joined with Jeff Yanta’s invite link", "You joined with Jeff Yanta’s invite link" or "Alex joined with your invite link" (nodes 12846:374, 12846:480, 12846:427), as Android #1870 does. The inviter’s name is bold and opens their profile through the same handler as a gutter face.Tests
Route, link-card classifier and deep-link suites cover
/chat/invite#s=…→ invite, a missing or bad fragment → invalid invite, and/chat/{uuid}and/chat/{uuid}/m/{n}unchanged.SystemNoticeStringsTestscovers the three wordings and that only the inviter’s name is a link, including when the joiner’s name contains the inviter’s.