From 0bb7d3c68734163eaf775276cd99e478f930e083 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 8 May 2026 16:10:57 +0400 Subject: [PATCH] update --- plans/2026-05-08-public-groups-via-relays.md | 115 +++++++++---------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/plans/2026-05-08-public-groups-via-relays.md b/plans/2026-05-08-public-groups-via-relays.md index ebd0525da5..5ddce5560e 100644 --- a/plans/2026-05-08-public-groups-via-relays.md +++ b/plans/2026-05-08-public-groups-via-relays.md @@ -61,12 +61,12 @@ encoded value for the existing `GroupType` field on `PublicGroupProfile` What does change: -- **Chat protocol version bump.** Add `publicGroupsVersion :: VersionChat = - VersionChat 18` in `src/Simplex/Chat/Protocol.hs` (currently - `currentChatVersion = VersionChat 17`, line 90). The new version signals - that the peer understands `groupType = "group"` semantically. `Protocol.hs` - already has the version-bump idiom (`shortLinkDataVersion = 16`, - `memberSupportVoiceVersion = 17`). +- **Chat protocol version bump.** Add `publicGroupsVersion :: VersionChat` + set to the next available version (one higher than the current + `currentChatVersion`) in `src/Simplex/Chat/Protocol.hs`. The new version + signals that the peer understands `groupType = "group"` semantically. + `Protocol.hs` already has the version-bump idiom (`shortLinkDataVersion`, + `memberSupportVoiceVersion`). - **Older-client behaviour.** Older clients decode the channel link's `groupType` field as `GTUnknown "group"` (lossless tag preservation in `textDecode`, `Types.hs:778-781`). They must refuse to join with a clear @@ -274,15 +274,15 @@ prefs builder that mirrors this table. mContent_ rcvMsg msgTs`. Note that `xGrpDirectInv` (line 3249+) already gates on `groupFeatureMemberAllowed SGFDirectMessages`, so the DM preference is honored. - 3. The sender-side currently produces `XGrpDirectInv` with - `msgScope = Just (MSMember recipientMemberId)`. Verify that the - relay's delivery-task creation (`infoToDeliveryContext`, - `Subscriber.hs:1811`/`2394`/`2115`) yields a `DJSMemberSupport` - scope so the relay routes only to the target member, not to all - members. If the existing support-scope path is the right substrate, - reuse it; if support-scope semantics conflict, introduce a sibling - `DJSDirectInv` scope. Decide during implementation by tracing one - end-to-end DM flow with logging. + 3. Introduce a new `DJSDirectInv` job scope, parallel to + `DJSMemberSupport` but delivering only to the target member with + no moderator broadcast. Reusing `DJSMemberSupport` would leak a + DM intention to all moderators, which is the exact opposite of + the privacy property the DM-graph threat model (§6.A.1) requires. + Wire the scope into `infoToDeliveryContext` + (`Subscriber.hs:1811`, `2394`, `2115`) so an `XGrpDirectInv` + with `msgScope = Just (MSMember recipientMemberId)` resolves to + `DJSDirectInv` rather than `DJSMemberSupport`. 4. Relay-side gate: only forward `XGrpDirectInv` when `groupFeatureMemberAllowed SGFDirectMessages senderMember gInfo` holds. The DM preference is already in `groupPreferences`; the @@ -428,10 +428,11 @@ a new sibling `describe "public groups"`): 1. **Member sends content; all members receive it.** Mirror `testChannels1RelayDeliver` (line 8523), but with cath as a Public group member, not a channel subscriber. Verify dan and eve receive - cath's message attributed to cath (no "unknown member record" - line — assumes the dissemination plan has landed). -2. **Profile dissemination integration.** With dissemination on, assert - no "unknown member" lines appear. + cath's message attributed to cath (no "unknown member record" line + — profile dissemination is active per §3.5 prerequisite). +2. **Profile dissemination integration.** Assert that no "unknown + member" lines appear at any point in a multi-author Public-group + session. 3. **Member edit / delete / react.** Each forwarded by the relay, each visible to all members. 4. **Member-to-member DM creation.** Member A sends `/_create direct @@ -456,8 +457,8 @@ a new sibling `describe "public groups"`): members; verify `aChatMsgHasReceipt` does not produce a receipt request. 11. **Older-client refusal.** Channel link with `groupType = "group"`; - older client (chat version 17) sees `GTUnknown "group"` and - refuses to join with a clear message. + older client (chat version below `publicGroupsVersion`) sees + `GTUnknown "group"` and refuses to join with a clear message. 12. **Incognito member posting.** Create a Public group; have a member join with `incognito = on`; member posts a content message; verify other members receive it attributed to the incognito @@ -639,13 +640,15 @@ if it gates transport (e.g., owner-only relay-management hooks). an enum `LinkVariant { secret, publicGroup, channel }`, or (ii) pass `groupInfo` and read variant inside. Pick (ii) — fewer call sites to update. -- `GroupPreferencesView.swift` — preferences UI is unchanged for - Public groups; the directMessages preference, which is - channel-default-off, channel-Public-group-default-on, is already - controlled by `groupPreferences`. Verify that creating a Public - group sets `directMessages` to its default-on value (the existing - default for non-channel groups). Pick this default at the - `Commands.hs` create site (§3.3). +- `GroupPreferencesView.swift` — the `directMessages` preference is + `ON` by parser inheritance in both channels and Public groups + (§3.3.1), but is *dormant* in channels because `XGrpDirectInv` is + not forwarded for `GTChannel` (§3.4 sub-section on member-to-member + DM forwarding). In Public groups it becomes active via the new + forwarding arm. The toggle on `GroupPreferencesView.swift` gains + the off-state help text from §4.4 + (`direct_messages_metadata_note`); the rest of the preferences UI + is unchanged. - `chatIconName` (`ChatTypes.swift:2472-2482`): - `useRelays && isChannel` → existing antenna icon (`antenna.radiowaves.left.and.right.circle.fill`). @@ -659,8 +662,8 @@ if it gates transport (e.g., owner-only relay-management hooks). - Members view in `GroupChatInfoView`. Channels show the relay-known list which is thin (post-dissemination it grows). Public groups use the same list — once dissemination ships, the list is - populated for any member who has interacted in the group. **MVP - decision**: show all members the relay has announced (the same + populated for any member who has interacted in the group. + **Decision**: show all members the relay has announced (the same list as channels post-dissemination). Show "subscribers" for channels, "members" for Public groups in the section header. No separate "owner+moderators only" filter for the MVP; defer @@ -715,8 +718,9 @@ Mirrors §4. Same order: model → audit → create flow → views. **Use `isChannel`** except line 1578 which is "subscriber count for relay-mediated groups" — that should also display for Public groups (member count from relay-side dissemination), so generalise - to `useRelays` and rename the field `publicMemberCount` → display - it as "subscribers" for channels and "members" for Public groups. + to `useRelays`; the field name `publicMemberCount` stays as-is; + the display label varies — "subscribers" for channels, "members" + for Public groups. - `model/ChatModel.kt` line 4617, 4624, 4631 — group icons in `chatIconName`/`chatLinkText`. Add a third arm for Public groups with the chosen distinct icon (mirror iOS). @@ -992,7 +996,7 @@ both new properties: - **Existing channels are unaffected.** Channel profiles continue to carry `groupType = "channel"`; the new code path produces `GTGroup` only when explicitly requested. -- **Older clients** (chat version ≤ 17) decode `groupType = "group"` +- **Older clients** (chat version below `publicGroupsVersion`) decode `groupType = "group"` as `GTUnknown "group"`. They should not silently treat it as a channel — that would let owners post but block members and break the UX. Required client behavior: when about to join a link @@ -1001,10 +1005,11 @@ both new properties: block the join. Add this alert in `ConnectPlan.kt` / `NewChatView.swift` as part of §4.7/§5.4. The Haskell side does not need to refuse — the client decides. -- **Minimum versions.** Chat protocol version 18 (`publicGroupsVersion`) - is the new floor. Owner client must be at least 18 to *create* a - Public group. Member clients must be at least 18 to *join*. Older - clients that are already members of a channel are unaffected. +- **Minimum versions.** `publicGroupsVersion` is the new floor. + Owner client must be at least `publicGroupsVersion` to *create* a + Public group. Member clients must be at least `publicGroupsVersion` + to *join*. Older clients that are already members of a channel are + unaffected. Older relays — currently relays accept any `groupType` and forward by `useRelays`, so they will forward Public-group traffic correctly without an upgrade. The relay-side type-driven @@ -1018,68 +1023,52 @@ both new properties: to joiners — members joining via that relay cannot post, but members joining via an upgraded relay can. The owner sees a partial-functionality state. Mitigation: warn at create time if - any selected relay's chat version is < 18 (`Commands.hs` already + any selected relay's chat version is below `publicGroupsVersion` + (`Commands.hs` already has access to relay versions via the relay request flow). The warning is not a hard block — the owner may proceed knowing that some relays will reject member posts. ## 8. Open questions -1. **Forwarding scope for `XGrpDirectInv`.** The relay needs to deliver a - single-target message. Reuse `MSMember` / `DJSMemberSupport`, or - introduce a sibling scope for direct-invite delivery? The two - semantics overlap (deliver to a specific member) but support-scope - is also delivered to all moderators, which would leak a DM - intention. Likely answer: introduce a new `DJSDirectInv` scope - that delivers only to the target member. **Decide before - implementing §3.4 step 3.** -2. **Member-DM consent.** P2P groups gate `XGrpDirectInv` by the +1. **Member-DM consent.** P2P groups gate `XGrpDirectInv` by the `directMessages` group preference. Public groups inherit the same gate. Should owners get a per-channel additional toggle ("DMs between members allowed") or should the existing preference suffice? Recommend: existing preference is enough for MVP. -3. **`memberAdmission` (review/captcha) on relay-mediated join.** +2. **`memberAdmission` (review/captcha) on relay-mediated join.** Today, relay-side join short-circuits `GAAccepted` regardless of the channel's `memberAdmission` setting. This is a generic relay-side gap (channels and Public groups equally), out of scope for this feature. Surface in release notes; defer until a separate plan. -4. **Distinct icon for Public groups.** Pending design review on +3. **Distinct icon for Public groups.** Pending design review on both platforms. The set must distinguish Public groups from channels (which use the broadcast/antenna metaphor) and from secret groups (which use a plain people metaphor). A "people + wedge" composite is the obvious candidate. -5. **Removing `channelSubscriberRole` from config.** The field has no +4. **Removing `channelSubscriberRole` from config.** The field has no callers after §3.3. Tests at `tests/ChatClient.hs:214` already override it for member-posting scenarios; those tests should become Public-group tests. Confirm that no out-of-tree consumer (CLI scripts, embedded clients) reads this config. -6. **Subscribed/unsubscribed roster filter in members view.** With +5. **Subscribed/unsubscribed roster filter in members view.** With 100K+ Public-group members the relay-known list grows large. Should the client paginate / filter (e.g., "recently active only")? Out of scope for the MVP — the existing channel members view already handles this case for subscribers. -7. **Wording for connect plan**: "ok to join via relays" (Public group) +6. **Wording for connect plan**: "ok to join via relays" (Public group) vs "ok to subscribe via relays" (channel) vs "ok to connect via relays" (current, ambiguous). The CLI string in `View.hs:2105` is read by tests — update test expectations alongside the new wording. Mobile clients can derive their own. -8. **Adopt-the-prior-plan timing.** `2026-04-29-member-profile-sending-channels.md` - is approved but unmerged at the time of writing. Public groups - are *usable* without it but feel broken ("unknown member" lines - on every member-authored message). Two ship orders: - - (A) Land the dissemination plan first, then ship Public groups - when it is in. - - (B) Ship Public groups behind a feature flag while dissemination - is in flight; flip the flag once dissemination lands. - Recommend (A) — fewer states to support, less user confusion. ## 9. Sequencing 1. **Prerequisite: member-profile dissemination plan** (`2026-04-29-member-profile-sending-channels.md`). Lands first, - independently. Public groups are a soft dependency: usable without - it but UX-poor. + independently. Hard prerequisite — Public groups do not ship + until dissemination has landed. 2. **Backend types + command + role derivation** (§3.2, §3.3, §3.4 except DM forwarding). Single PR. Adds `GTGroup`-producing path, replaces config with `groupType`-based derivation, removes