# 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