Skip to content

feat(chat): restrict and remove members in groups - #1163

Merged
bmc08gt merged 17 commits into
mainfrom
feat/group-moderation-restrict-remove
Oct 29, 2026
Merged

bmc08gt merged 17 commits into
mainfrom
feat/group-moderation-restrict-remove

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Adds group moderation for hosts: Restrict stops a member from sending messages, and Remove bans them from the group. The spec and design frames are in docs/specs/group-moderation/, and the cross-platform spec is code-payments/flipcash-client-orchestrator#68.

The UI says Restrict. The code keeps the contract's Silence names, so the Swift matches the protos.

Host

  • A member's profile menu and the long-press message menu both offer Restrict and Remove from Group. Restrict opens a sheet with 1 Hour, 8 Hours, 1 Week and Forever.
  • The group menu has a Moderation menu with Restricted and Removed lists. Lift ends a restriction and the Removed list unbans.
  • A toast confirms each change once the server accepts it.

Restricted or removed member

  • While restricted, the composer becomes a read-only pill: "You can't send messages until 2:57 PM", or "in this group" for Forever. When the restriction lifts, by the host or the timer, the member gets "You are now able to chat again".
  • A removed member who tries to rejoin is told the host removed them.
  • The member list drops a removed member. The ban arrives as a membershipChanged update that names no one, which GroupRosters used to ignore.

Other changes

  • UserProfileOrigin.groupMember carries the group, so the member profile knows which group it is moderating.
  • A stored group refreshes its moderation permissions on open, and the viewer's stored state survives feed refreshes.

A chat saved before canSilence and canBan existed lost its whole viewer state on
read (the synthesized decoder throws on a missing key), and a later fetch at the
same version was dropped by the version gate, so the host never saw Silence or
Ban. Decode missing permissions as false, and refresh permissions from GetChat
when the group profile opens.
… transcript open

A metadata or feed row replaced the whole viewer state, so an older row rolled
back a mute and one that omitted the permissions reset canSilence and canBan.
Keep the stored state when the incoming version is lower, and keep held
permissions at an equal version. The transcript now re-reads permissions when
it opens, sharing one fetch with the profile within 30 seconds.
… changes

Share the Blocked Users / Trusted Websites empty state as EmptyStateMessage and use
it on the Silenced and Removed lists. Show a toast after a silence or unsilence
succeeds, never on failure.
A ban reaches the other members as an undisclosed membershipChanged
update that moves the count and names no one, and the host receives no
departure at all. GroupRosters ignored that update, so the list kept
showing the removed member while the count dropped. Re-read the roster
on membershipChanged and drop held members the full read omits (except
ones the stream delivered mid-read), and drop the member immediately
after a removal made on this device.

Also use the moderation empty-state copy from the design.
The lift menu built its rows with contextMenu(for:).children.compactMap { $0 as? UIAction }.
The context menu nests the moderation rows in an inline UIMenu section after Report, so the
compactMap skipped them and the long-press menu showed only Copy, Reply, Forward and Report.

Add liftMenuActions(for:), which flattens nested menus, and use it in presentLift. The menu
view now puts a divider before the moderation group and none before Remove, and tints
destructive rows with textError instead of systemRed.
User-facing copy changes from Silence to Restrict; code and contract names stay. The message
menu row reads "Restrict <First>" with no ellipsis, Remove keeps its ellipsis, and the profile
menu, sheet, toasts, composer pill, moderation lists and empty states follow the new wording.
MessageCapability.menuTitle is now internal so ModerationRowTitleTests can check it.
@bmc08gt bmc08gt self-assigned this Oct 29, 2026
@bmc08gt
bmc08gt merged commit 1460573 into main Oct 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant