Skip to content

feat(chat): let an eligible non-member read a group before joining - #1537

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/unblur-eligible-groups
Sep 23, 2026
Merged

bmc08gt merged 1 commit into
code/cashfrom
feat/unblur-eligible-groups

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

A viewer outside a group who meets its balance rule (GroupAccess.Eligible) saw the same blurred transcript as one who didn't. isGatedPreview answered two questions at once: whether to blur the transcript and whether the Join gate replaces the composer. This splits it into obscuresTranscript and replacesComposer, the same split iOS makes in ConversationGatePresentation.

When the blur comes off

readsFromOutside is true only when all of these hold:

  • the viewer is a known non-member (isMember == false)
  • groupAccess is Eligible
  • the group has at least one listener rule

The last condition comes from messaging.v1.ViewMode. The server gives a non-member the full transcript only when they satisfy a listener rule. A group with no listener rules also computes Eligible, but there is nothing to read.

Anything unknown keeps the blur on. A null groupAccess or a null membership stays blurred, so an eligible viewer sees the blur fade once the balance arrives. It never goes from sharp to blurred. BlurredContent already snaps on and fades off. Blocked stays blurred. If the balance drops while the chat is open, access goes back to Blocked and the blur returns.

The gate still replaces the composer for every viewer outside the group, eligible or not.

Loading the transcript

Before this change, nothing fetched an eligible viewer's messages. openTranscript skips groups the viewer is outside of, and the group feed only includes joined groups. The view model now calls loadMessages when readsFromOutside turns true. The call is keyed on the chat, so a balance that dips and recovers triggers a new fetch.

Guards

  • The read pointer is not advanced for a viewer outside the group, because the server refuses it.
  • OnGroupResolved resets groupAccess to null when membership or rules change. Without this, a stale Eligible could unblur a group whose rules just tightened until the balance flow caught up.

Join success

joinProgress.success no longer holds the blur, because only an eligible viewer can join and that viewer already sees the transcript sharp. It still keeps the composer back until the checkmark finishes.

Tests

ChatViewModelStateTest covers: Eligible is unblurred with the gate shown, null access is blurred, Blocked is blurred, Eligible → Blocked brings the blur back, a group with no listener rules stays blurred, and the join hold. GroupTranscriptScreenshotTest.rendersTranscriptFromOutsideTheGroup draws Eligible, Blocked and pending access side by side. Robolectric can't draw Haze or Modifier.blur, so a heavy fade stands in for the blur there.

A viewer outside a group who meets its balance rule saw the same blurred
transcript as one who didn't, because isGatedPreview answered both "blur
the transcript" and "replace the composer". It is now two flags, named
after iOS's ConversationGatePresentation: obscuresTranscript and
replacesComposer.

The blur comes off only when the viewer is a known non-member, the access
is Eligible, and the group has listener rules. messaging.v1.ViewMode only
serves a non-member the full transcript when they satisfy a listener rule;
a group with no listener rules also computes Eligible but has nothing to
serve. A null access or null membership keeps the blur, so the transcript
fades in after the balance arrives rather than showing and then blurring.
A balance that drops re-emits Blocked and the blur returns.

openTranscript skips groups the viewer is outside of, and the group feed
only carries joined groups, so nothing loaded an eligible viewer's
messages. They are now fetched when readsFromOutside turns true, keyed on
the chat so a dip and recovery refetches.

Two guards follow from showing messages to a non-member: the read pointer
is not advanced for someone outside the group, since the server refuses
it, and OnGroupResolved clears groupAccess when membership or rules
change, so a stale Eligible cannot unblur a group whose rules tightened.

A succeeded join no longer holds the blur. The only viewer who can join
is already reading the transcript sharp. It still holds the composer back
through the checkmark.
@bmc08gt bmc08gt self-assigned this Sep 23, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 23, 2026
@bmc08gt
bmc08gt merged commit 94acbe4 into code/cash Sep 23, 2026
3 checks passed
bmc08gt added a commit that referenced this pull request Sep 23, 2026
#1540)

With #1537 an eligible non-member reads a group's transcript under the
Join gate, but resolveCapabilities looks only at the message, so every
other person's message offered Reply. Swiping or picking Reply set
replyingTo behind the gate, and the composer later opened quoting it once
the viewer joined. A former member's own messages offered Edit and Delete
the same way.

resolveCapabilities takes canPost, and a viewer who cannot post keeps
only Copy and Report. The transcript mapping re-runs when the viewer
joins or leaves, and the selection bar narrows its set with readOnly() if
they left after the row was mapped, as it already does with the edit and
delete windows.
bmc08gt added a commit that referenced this pull request Sep 23, 2026
* feat(chat): let an eligible non-member read a group before joining

A viewer outside a group who meets its balance rule saw the same blurred
transcript as one who didn't, because isGatedPreview answered both "blur
the transcript" and "replace the composer". It is now two flags, named
after iOS's ConversationGatePresentation: obscuresTranscript and
replacesComposer.

The blur comes off only when the viewer is a known non-member, the access
is Eligible, and the group has listener rules. messaging.v1.ViewMode only
serves a non-member the full transcript when they satisfy a listener rule;
a group with no listener rules also computes Eligible but has nothing to
serve. A null access or null membership keeps the blur, so the transcript
fades in after the balance arrives rather than showing and then blurring.
A balance that drops re-emits Blocked and the blur returns.

openTranscript skips groups the viewer is outside of, and the group feed
only carries joined groups, so nothing loaded an eligible viewer's
messages. They are now fetched when readsFromOutside turns true, keyed on
the chat so a dip and recovery refetches.

Two guards follow from showing messages to a non-member: the read pointer
is not advanced for someone outside the group, since the server refuses
it, and OnGroupResolved clears groupAccess when membership or rules
change, so a stale Eligible cannot unblur a group whose rules tightened.

A succeeded join no longer holds the blur. The only viewer who can join
is already reading the transcript sharp. It still holds the composer back
through the checkmark.

* fix(chat): stop a viewer outside a group from replying to its messages

With #1537 an eligible non-member reads a group's transcript under the
Join gate, but resolveCapabilities looks only at the message, so every
other person's message offered Reply. Swiping or picking Reply set
replyingTo behind the gate, and the composer later opened quoting it once
the viewer joined. A former member's own messages offered Edit and Delete
the same way.

resolveCapabilities takes canPost, and a viewer who cannot post keeps
only Copy and Report. The transcript mapping re-runs when the viewer
joins or leaves, and the selection bar narrows its set with readOnly() if
they left after the row was mapped, as it already does with the edit and
delete windows.
bmc08gt added a commit that referenced this pull request Sep 23, 2026
#1539 replaced AppRoute.Sheets.Tips with AppRoute.Tabs.Chats, and #1537
added the scanner's GroupChatInvite branch against the old route in
parallel, so :features:scanner no longer compiles on code/cash.

Use the back stack AppRouter builds for a tapped /chat/{uuid} link:
Tabs.Chats, then the chat.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant