mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-28 17:49:07 +00:00
229 lines
11 KiB
Markdown
229 lines
11 KiB
Markdown
# Group Lifecycle Flow
|
|
|
|
> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/database.md](../../spec/database.md)
|
|
|
|
## Overview
|
|
|
|
Complete group management in SimpleX Chat iOS: creating groups, inviting members, joining via links, managing roles and admission, and group deletion. Groups use the same E2E encryption as direct messages -- each member pair has independent encrypted channels. Group metadata (name, image, preferences) is distributed via the group protocol.
|
|
|
|
## Prerequisites
|
|
|
|
- User profile created and chat engine running
|
|
- At least one established contact (to invite to a group)
|
|
- For joining via link: a valid group link or invitation
|
|
|
|
## Step-by-Step Processes
|
|
|
|
### 1. Create Group
|
|
|
|
1. User taps "+" in `ChatListView` -> `NewChatMenuButton` -> "Create group".
|
|
2. `AddGroupView` is presented for entering group name, optional image, and description.
|
|
3. User fills in `GroupProfile(displayName:fullName:image:description:)` and taps "Create".
|
|
4. Calls `apiNewGroup(incognito:groupProfile:)`:
|
|
```swift
|
|
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo
|
|
```
|
|
5. Sends `ChatCommand.apiNewGroup(userId:incognito:groupProfile:)` to core (synchronous).
|
|
6. Core returns `ChatResponse2.groupCreated(user, groupInfo)`.
|
|
7. `GroupInfo` contains the new group's ID, profile, and the creator as owner.
|
|
8. User is navigated to `AddGroupMembersView` to optionally invite contacts.
|
|
9. User can also create a group link at this stage.
|
|
|
|
### 1a. Create Public Group (Channel)
|
|
|
|
1. Alternative to standard group creation for relay-backed channels.
|
|
2. Calls `apiNewPublicGroup(incognito:relayIds:groupProfile:)`:
|
|
```swift
|
|
func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> (GroupInfo, GroupLink, [GroupRelay])
|
|
```
|
|
3. Sends `ChatCommand.apiNewPublicGroup(userId:incognito:relayIds:groupProfile:)` to core.
|
|
4. Core returns `ChatResponse2.publicGroupCreated(user, groupInfo, groupLink, groupRelays)`.
|
|
5. The resulting `GroupInfo` has `useRelays == true` and includes a group link.
|
|
6. Channel relay members (with role `.relay`) are managed by the core.
|
|
|
|
### 2. Invite Members
|
|
|
|
1. From `GroupChatInfoView`, user taps "Add members" -> `AddGroupMembersView`.
|
|
2. `filterMembersToAdd` filters contacts already in the group.
|
|
3. User selects contacts and assigns roles (default: `.member`).
|
|
4. For each selected contact, calls `apiAddMember(groupId:contactId:memberRole:)`:
|
|
```swift
|
|
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember
|
|
```
|
|
5. Core sends group invitation to the contact and returns `ChatResponse2.sentGroupInvitation(user, _, _, member)`.
|
|
6. The invited contact receives a `CIGroupInvitationView` in their chat.
|
|
7. Invited member's status is `.invited` until they accept.
|
|
|
|
### 3. Join via Link
|
|
|
|
1. User receives a group link (scanned or pasted).
|
|
2. `apiConnectPlan` validates the link and identifies it as a group link.
|
|
3. For prepared groups (short links): `apiPrepareGroup(connLink:directLink:groupShortLinkData:)` shows group info before joining. `directLink` is `true` for standard group links, `false` for channel relay links.
|
|
4. `apiConnectPreparedGroup(groupId:incognito:msg:)` or `apiConnect(incognito:connLink:)` initiates joining.
|
|
5. Core processes the join request. Depending on group admission settings:
|
|
- **Auto-join**: Member is added immediately.
|
|
- **Approval required**: Member enters pending admission queue.
|
|
6. `apiJoinGroup(groupId:)` is called for invitation-based joins:
|
|
```swift
|
|
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult?
|
|
```
|
|
7. Returns one of:
|
|
- `.joined(groupInfo:)` -- successfully joined
|
|
- `.invitationRemoved` -- invitation was revoked (SMP AUTH error)
|
|
- `.groupNotFound` -- group no longer exists
|
|
|
|
### 4. Member Admission
|
|
|
|
1. Group has admission settings configured via `MemberAdmissionView`.
|
|
2. When a new member joins a group requiring approval, admins see pending members.
|
|
3. Admin reviews pending member in the member list.
|
|
4. To accept: `apiAcceptMember(groupId:groupMemberId:memberRole:)`:
|
|
```swift
|
|
func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> (GroupInfo, GroupMember)
|
|
```
|
|
5. Core returns `ChatResponse2.memberAccepted(user, groupInfo, member)`.
|
|
6. To reject: remove the pending member (same as member removal).
|
|
7. Member support chat (`MemberSupportView`, `MemberSupportChatToolbar`) allows admins to communicate with pending members.
|
|
|
|
### 5. Change Member Roles
|
|
|
|
1. Admin/owner navigates to member info in `GroupChatInfoView`.
|
|
2. Selects new role for the member.
|
|
3. Calls `apiMembersRole(groupId:memberIds:memberRole:)`:
|
|
```swift
|
|
func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember]
|
|
```
|
|
4. Core returns `ChatResponse2.membersRoleUser(user, _, members, _)`.
|
|
5. Available roles (in hierarchy order):
|
|
- `.owner` -- full control, can delete group
|
|
- `.admin` -- can manage members, change roles (below admin)
|
|
- `.moderator` -- can delete messages, moderate content
|
|
- `.member` -- standard participant, can send messages
|
|
- `.observer` -- read-only access
|
|
6. Role changes are broadcast to all group members as group events.
|
|
|
|
### 6. Remove Member
|
|
|
|
1. Admin/owner navigates to member info -> taps "Remove".
|
|
2. Calls `apiRemoveMembers(groupId:memberIds:withMessages:)`:
|
|
```swift
|
|
func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool) async throws -> (GroupInfo, [GroupMember])
|
|
```
|
|
3. `withMessages: true` also deletes all messages from that member.
|
|
4. Core returns `ChatResponse2.userDeletedMembers(user, updatedGroupInfo, members, withMessages)`.
|
|
5. Removed member receives notification and loses access.
|
|
|
|
### 7. Block Member for All
|
|
|
|
1. Admin can block a member's messages from being visible to all group members.
|
|
2. Calls `apiBlockMembersForAll(groupId:memberIds:blocked:)`:
|
|
```swift
|
|
func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember]
|
|
```
|
|
3. Core returns `ChatResponse2.membersBlockedForAllUser(user, _, members, _)`.
|
|
|
|
### 8. Leave Group
|
|
|
|
1. User navigates to `GroupChatInfoView` -> taps "Leave group".
|
|
2. Confirmation dialog is presented.
|
|
3. Calls `leaveGroup(groupId:)` which wraps `apiLeaveGroup(groupId:)`:
|
|
```swift
|
|
func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo
|
|
```
|
|
4. Core returns `ChatResponse2.leftMemberUser(user, groupInfo)`.
|
|
5. `ChatModel.shared.updateGroup(groupInfo)` updates the UI.
|
|
6. User retains local chat history but can no longer send/receive.
|
|
|
|
### 9. Delete Group
|
|
|
|
1. Owner navigates to `GroupChatInfoView` -> taps "Delete group".
|
|
2. Calls `apiDeleteChat(type: .group, id: groupId)`:
|
|
```swift
|
|
func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws
|
|
```
|
|
3. Core notifies all members and removes the group.
|
|
4. Chat is removed from `ChatModel.shared.chats`.
|
|
|
|
### 10. Group Link Management
|
|
|
|
**Create group link:**
|
|
1. From `GroupLinkView` (accessible via `GroupChatInfoView`).
|
|
2. Calls `apiCreateGroupLink(groupId:memberRole:)`:
|
|
```swift
|
|
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink?
|
|
```
|
|
3. Returns `GroupLink` containing the link URI and member role.
|
|
4. Optional: `apiAddGroupShortLink(groupId:)` generates an additional short link.
|
|
|
|
**Update link role:**
|
|
- `apiGroupLinkMemberRole(groupId:memberRole:)` changes the default role for new joiners.
|
|
|
|
**Delete group link:**
|
|
- `apiDeleteGroupLink(groupId:)` invalidates the link.
|
|
|
|
**Get existing link:**
|
|
- `apiGetGroupLink(groupId:)` retrieves the current link (returns `nil` if none exists).
|
|
|
|
### 11. Group Preferences
|
|
|
|
1. `GroupPreferencesView` allows configuring per-feature preferences.
|
|
2. Features controlled include:
|
|
- Timed/disappearing messages
|
|
- Message reactions
|
|
- Voice messages
|
|
- File sharing
|
|
- Direct messages between members
|
|
- Full message deletion
|
|
- Message history visibility for new members
|
|
3. Changes are saved via `apiUpdateGroup(groupId:groupProfile:)` with updated preferences.
|
|
4. `GroupWelcomeView` manages the welcome message shown to new joiners.
|
|
|
|
## Data Structures
|
|
|
|
| Type | Location | Description |
|
|
|------|----------|-------------|
|
|
| `GroupInfo` | `SimpleXChat/ChatTypes.swift` | Full group model: ID, profile, membership, preferences, business chat info |
|
|
| `GroupProfile` | `SimpleXChat/ChatTypes.swift` | Name, full name, image, description, preferences |
|
|
| `GroupMember` | `SimpleXChat/ChatTypes.swift` | Member model: role, status, profile, connection info |
|
|
| `GroupMemberRole` | `SimpleXChat/ChatTypes.swift` | `.owner`, `.admin`, `.moderator`, `.member`, `.observer`, `.relay` |
|
|
| `GroupMemberStatus` | `SimpleXChat/ChatTypes.swift` | Member lifecycle: `.invited`, `.accepted`, `.connected`, `.complete`, etc. |
|
|
| `GroupLink` | `Shared/Model/AppAPITypes.swift` | Group link with URI, member role, and short link data |
|
|
| `BusinessChatInfo` | `SimpleXChat/ChatTypes.swift` | Business chat metadata for commercial group chats |
|
|
| `JoinGroupResult` | `Shared/Model/SimpleXAPI.swift` | `.joined(groupInfo)`, `.invitationRemoved`, `.groupNotFound` |
|
|
| `GMember` | `Shared/Views/Chat/Group/` | View-layer wrapper around `GroupMember` for list display |
|
|
|
|
## Error Cases
|
|
|
|
| Error | Cause | Handling |
|
|
|-------|-------|----------|
|
|
| `errorStore(.groupNotFound)` | Group deleted or not accessible | `JoinGroupResult.groupNotFound` |
|
|
| `errorAgent(.SMP(_, .AUTH))` | Invitation revoked | `JoinGroupResult.invitationRemoved` |
|
|
| `errorStore(.groupLinkNotFound)` | No group link exists | `apiGetGroupLink` returns `nil` |
|
|
| `duplicateGroupLink` | Link already exists for group | Show alert |
|
|
| `errorAgent(.NOTICE(server, preset, expires))` | Server notice during link creation | `showClientNotice` alert |
|
|
| Network errors | SMP/XFTP server unreachable | Retryable via `chatApiSendCmdWithRetry` |
|
|
|
|
## Key Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `Shared/Views/NewChat/AddGroupView.swift` | Group creation UI |
|
|
| `Shared/Views/Chat/Group/AddGroupMembersView.swift` | Member invitation UI |
|
|
| `Shared/Views/Chat/Group/GroupLinkView.swift` | Group link management UI |
|
|
| `Shared/Views/Chat/Group/GroupProfileView.swift` | Group profile editing |
|
|
| `Shared/Views/Chat/Group/GroupPreferencesView.swift` | Feature preferences UI |
|
|
| `Shared/Views/Chat/Group/GroupWelcomeView.swift` | Welcome message editing |
|
|
| `Shared/Views/Chat/Group/MemberAdmissionView.swift` | Admission settings UI |
|
|
| `Shared/Views/Chat/Group/MemberSupportView.swift` | Admin-to-pending-member chat |
|
|
| `Shared/Views/Chat/Group/MemberSupportChatToolbar.swift` | Support chat accept/reject toolbar |
|
|
| `Shared/Views/Chat/Group/SecondaryChatView.swift` | Secondary chat view for member support |
|
|
| `Shared/Model/SimpleXAPI.swift` | All group API functions |
|
|
| `Shared/Model/AppAPITypes.swift` | `GroupLink`, `ConnectionPlan` |
|
|
| `SimpleXChat/ChatTypes.swift` | `GroupInfo`, `GroupProfile`, `GroupMember`, `GroupMemberRole` |
|
|
|
|
## Related Specifications
|
|
|
|
- `apps/ios/product/README.md` -- Product overview: Groups capability map
|
|
- `apps/ios/product/flows/connection.md` -- Connection flow (group links use the same connect mechanism)
|
|
- `apps/ios/product/flows/messaging.md` -- Messaging within groups
|