Skip to content

feat(tabgroup): remove people from a group & handle being removed from a group - #339

Merged
DennisBauer merged 5 commits into
mainfrom
feat/remove-group-participant
Aug 7, 2026
Merged

feat(tabgroup): remove people from a group & handle being removed from a group #339
DennisBauer merged 5 commits into
mainfrom
feat/remove-group-participant

Conversation

@DennisBauer

@DennisBauer DennisBauer commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Group members and placeholders can be removed with confirmation dialogs, balance warnings, and permission checks.
    • Former members remain visible in relevant expenses, balances, and settlements with clear labeling.
    • Groups removed externally now update navigation and display a notification automatically.
  • Bug Fixes
    • Improved handling of unavailable groups and former-member names.
    • Added specific messages for restricted removal attempts.
  • Localization
    • Added German and English translations for removal flows and group status messages.

Screens resolved participants from the active member list, so anyone
removed from a group vanished from the maths even though their expenses
stay. Worst case: editing an expense dropped a non-member's split on
save, deleting it for good.

Union active members with the participants the entries reference, and
resolve names through the global participant table.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds participant removal with known error handling, real-time group-removal notifications, navigation cleanup, and former-member support across group data, settlement, entry, and presentation flows.

Changes

Group participant removal

Layer / File(s) Summary
Removal contract and service flow
core/..., features/tabgroup/domain/..., features/tabgroup/data/...
Participant removal now has domain and repository contracts, HTTP error mapping, group refresh behavior, permanent outbox failures, and wire-level tests.
Real-time removal and navigation cleanup
features/tabgroup/data/network/..., features/tabgroup/presentation/navigation/..., composeApp/...
WebSocket removal events notify the application, delete local group data, clear group-scoped routes, and show a snackbar.
Former-member data and display flow
features/tabgroup/presentation/...
Historical participants remain available in edited entries, group details, balances, and settlements. The UI labels former or unavailable participants.
Member removal confirmation and state
features/tabgroup/presentation/.../grouppeople/..., features/tabgroup/presentation/.../values*, core/presentation/...
The group-people flow checks permissions and balances, confirms removal, prevents duplicate submissions, and maps refusal errors to localized messages.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • TabMates/app#337: Both changes modify the shared group-people management flow and participant-management behavior.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: removing people from groups and handling users who are removed from groups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the feature New feature or request label Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryViewModel.kt (1)

180-231: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve unresolved former split IDs before enabling save.

If getAllParticipants() does not return a referenced former participant, Lines 191-199 omit that participant from splitParticipants. Lines 205-206 also omit the participant from participantsById.

buildSplits() uses only state.splitInputs. Saving the edited entry then deletes the unresolved participant's existing split. paidByDisplay() already treats a missing participant record as a valid historical state.

Keep unresolved split participants in state with a removed-member placeholder, or block saving until their identity data resolves. Do not build an update request from an incomplete participant lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryViewModel.kt`
around lines 180 - 231, Preserve every referenced former participant with an
existing split when loading the edit state, even if getAllParticipants() cannot
resolve its identity. Update the flow around formerParticipants,
splitParticipants, participantsById, and buildSplits() to retain unresolved IDs
via a removed-member placeholder or prevent saving until all identities resolve;
do not construct an update request from incomplete participant data.
🧹 Nitpick comments (1)
features/tabgroup/data/src/desktopTest/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSyncTest.kt (1)

84-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Turbine to collect the removal event.

Line 91 starts a manual background collector and stores events in mutable state. Collect removalNotifier.removals with Turbine and assert the RemovedFromGroup value with awaitItem().

This makes event delivery explicit and removes collector lifecycle handling from the test.

As per coding guidelines, **/*Test.kt requires kotlin-test, Turbine, and hand-written fakes; do not use a mocking library.

Also applies to: 165-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/tabgroup/data/src/desktopTest/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSyncTest.kt`
around lines 84 - 107, Replace the manual background collector and mutable
removals list in the test `being removed reports the group by its local name and
then deletes it` with Turbine collection on `removalNotifier.removals`. Use
Turbine’s `awaitItem()` to assert the expected `RemovedFromGroup` value after
emitting the message, while retaining the existing deletion assertion and
hand-written fake repository.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt`:
- Around line 8-9: Rename RemoveParticipantErrorDto to
RemoveParticipantErrorResponse and rename its file accordingly, preserving the
nullable code field and serialization. Update the response decode at the
referenced usage to use RemoveParticipantErrorResponse, and adjust all
references to the old type name.
- Around line 18-20: Update removeParticipantErrorOrNull so
CancellationException thrown while decoding RemoveParticipantErrorDto is
rethrown instead of converted to null by runCatching. Preserve the existing null
fallback for other decoding failures and the current status handling.

In
`@features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSync.kt`:
- Around line 69-70: Update the handle flow surrounding handleRemovedFromGroup
so CancellationException is caught before the broad Throwable handler and
rethrown unchanged. Preserve the existing logging and handling for
non-cancellation failures.

In
`@features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupRemovalNotifier.kt`:
- Around line 13-24: The GroupRemovalNotifier currently drops removals emitted
before the app shell begins collecting. Update GroupRemovalNotifier to retain
pending RemovedFromGroup events until ObserveAsEvents consumes or acknowledges
them, while preserving non-suspending notification behavior; add a regression
test that emits a removal before collection starts and verifies the event is
later delivered.

In
`@features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleScreen.kt`:
- Around line 259-271: Update the AlertDialog dismissal handling in
GroupPeopleScreen so both onDismissRequest and the dismissButton are disabled
while isRemoving is true. Preserve normal dismissal behavior once removal
completes, while preventing removeTarget from being cleared during the active
removal request.

---

Outside diff comments:
In
`@features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryViewModel.kt`:
- Around line 180-231: Preserve every referenced former participant with an
existing split when loading the edit state, even if getAllParticipants() cannot
resolve its identity. Update the flow around formerParticipants,
splitParticipants, participantsById, and buildSplits() to retain unresolved IDs
via a removed-member placeholder or prevent saving until all identities resolve;
do not construct an update request from incomplete participant data.

---

Nitpick comments:
In
`@features/tabgroup/data/src/desktopTest/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSyncTest.kt`:
- Around line 84-107: Replace the manual background collector and mutable
removals list in the test `being removed reports the group by its local name and
then deletes it` with Turbine collection on `removalNotifier.removals`. Use
Turbine’s `awaitItem()` to assert the expected `RemovedFromGroup` value after
emitting the message, while retaining the existing deletion assertion and
hand-written fake repository.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 40113f97-b979-4b8a-8f15-ff7886c7cb5e

📥 Commits

Reviewing files that changed from the base of the PR and between cfccfb3 and 4e15557.

📒 Files selected for processing (49)
  • composeApp/src/commonMain/kotlin/de/tabmates/composeapp/App.kt
  • core/data/src/commonMain/kotlin/de/tabmates/core/data/networking/HttpClientExt.kt
  • core/domain/src/commonMain/kotlin/de/tabmates/core/domain/util/DataError.kt
  • core/presentation/src/commonMain/composeResources/values-de/string.xml
  • core/presentation/src/commonMain/composeResources/values/string.xml
  • core/presentation/src/commonMain/kotlin/de/tabmates/core/presentation/util/DataErrorToUiText.kt
  • features/tabgroup/data/build.gradle.kts
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/di/TabgroupDataModule.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/group/KtorGroupService.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/group/OfflineFirstGroupRepository.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/network/dto/TabEntryWsMessages.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryOutbox.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSync.kt
  • features/tabgroup/data/src/commonTest/kotlin/de/tabmates/features/tabgroup/data/group/KtorGroupServiceTest.kt
  • features/tabgroup/data/src/desktopTest/kotlin/de/tabmates/features/tabgroup/data/group/FakeGroupService.kt
  • features/tabgroup/data/src/desktopTest/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSyncTest.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/activity/ActivityEventType.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupRemovalNotifier.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupRepository.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupService.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/models/ReferencedParticipants.kt
  • features/tabgroup/presentation/src/commonMain/composeResources/values-de/string.xml
  • features/tabgroup/presentation/src/commonMain/composeResources/values/string.xml
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/MainGraph.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/MainNavKeys.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryScreen.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryState.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/SplitEditorScreen.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/editsettlement/EditSettlementViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/entrydetail/EntryDetailViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/groupdetail/GroupDetailRoot.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/groupdetail/GroupDetailViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/groupoverview/GroupDetailPane.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleScreen.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleState.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/settlementdetail/SettlementDetailViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/settleup/SettleUpScreen.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/settleup/SettleUpState.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/settleup/SettleUpViewModel.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/util/GroupWithParticipants.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/addentry/AddEntryViewModelTest.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/creategroup/FakeGroupRepository.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/entrydetail/EntryDetailViewModelTest.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/groupdetail/GroupDetailViewModelTest.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleViewModelTest.kt
  • features/tabgroup/presentation/src/commonTest/kotlin/de/tabmates/features/tabgroup/presentation/navigation/settleup/SettleUpViewModelTest.kt

@coderabbitai coderabbitai Bot removed the feature New feature or request label Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt (1)

28-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the intentionally ignored exception explicit.

Because this fallback accepts malformed or non-JSON error bodies, keep the broad catch and rename e to _. Keep the CancellationException rethrow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt`
at line 28, In the exception fallback around RemoveParticipantErrorDto parsing,
rename the intentionally ignored catch parameter from e to _ while preserving
the broad Exception catch and the existing CancellationException rethrow.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt`:
- Line 28: In the exception fallback around RemoveParticipantErrorDto parsing,
rename the intentionally ignored catch parameter from e to _ while preserving
the broad Exception catch and the existing CancellationException rethrow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b9e3ea9-108e-46c2-926c-ca01d73acb94

📥 Commits

Reviewing files that changed from the base of the PR and between 4e15557 and e2afdc8.

📒 Files selected for processing (4)
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/dto/RemoveParticipantErrorDto.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSync.kt
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupRemovalNotifier.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleScreen.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • features/tabgroup/domain/src/commonMain/kotlin/de/tabmates/features/tabgroup/domain/group/GroupRemovalNotifier.kt
  • features/tabgroup/data/src/commonMain/kotlin/de/tabmates/features/tabgroup/data/tabentry/TabEntryRealtimeSync.kt
  • features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/grouppeople/GroupPeopleScreen.kt

DELETE /api/group/{groupId}/participants/{userId} answers with an empty
body, so success refetches the group to sync the participant cross-refs.

The server states two refusals by code, each sharing a status with a
different meaning: 400 CANNOT_REMOVE_SELF and 403
CANNOT_REMOVE_GROUP_CREATOR. Reading them needs mapKnownError on delete,
which only post had.
A trailing X on every row but your own and the creator's, behind a
confirm dialog. Membership is the only permission, so placeholders and
members read the same.

The dialog names an unsettled balance without blocking on it: removal
drops membership only, and the money stays exactly where it was.
The server unicasts REMOVED_FROM_GROUP to whoever it just removed, with
the group id alone — so the title for the snackbar is read from the local
mirror before the group is deleted.

Popping that group's screens now goes through a GroupScoped marker
rather than a hand-listed predicate: leaving can only happen from the
group settings, being removed can land on any of the nine.
GroupDetailRoot rendered the same placeholder while loading and when the
group is not there locally, so a push deep link for a group you were
removed from — or one that was deleted — looked like an endless spinner.
@DennisBauer
DennisBauer force-pushed the feat/remove-group-participant branch from e2afdc8 to 48348c2 Compare August 7, 2026 14:04
@DennisBauer
DennisBauer enabled auto-merge August 7, 2026 14:09
@DennisBauer
DennisBauer merged commit b43cef2 into main Aug 7, 2026
4 checks passed
@DennisBauer
DennisBauer deleted the feat/remove-group-participant branch August 7, 2026 14:14
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