feat(contacts): add favorite/save star to Node Details screen#98
Merged
Conversation
Mirror Android's top-right star (AnnounceDetailScreen) on the iOS NodeDetailsView so a peer can be saved/favorited — and unsaved — directly from node details, instead of only from the contact/announce lists. - NodeDetailsView: optional onToggleFavorite callback (parent-owned, like onStartChat/onBrowseSite) plus a .primaryAction toolbar star. The star's lit state is local @State seeded in .task, deliberately decoupled from liveContact whose isFavorite is re-derived from the seed on every path poll (binding to displayedContact.isFavorite would snap the star back). - ContactsView: wire the node-details destination to the ViewModel. - ContactsViewModel: Contact-typed toggleFavorite overload that removes a saved contact or falls back to addToContacts, so the star stays correct even when the peer isn't currently in the in-memory announce arrays. On iOS, favorite == contact membership in conversations.is_favorite, so a single tap reproduces Android's add/remove semantics through the existing persistence path — no new storage is introduced. Instant toggle (no confirm dialog) to match every other favorite star in the app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
…te (greptile #98 iter 1) Greptile P2s: the optimistic star could desync from what was actually persisted — (1) no rollback if addToContacts throws (star stays filled, contact never saved), and (2) a rapid double-tap could end with star empty but contact saved. - ContactsViewModel.toggleFavorite(for: Contact) is now async @discardableResult, returning the authoritative post-op membership (myContacts.contains) after awaiting addToContacts. On failure addToContacts leaves myContacts untouched, so the return reflects reality. - NodeDetailsView: onToggleFavorite is now (Contact) async -> Bool. The star flips optimistically then reconciles to the returned state, and an isTogglingFavorite guard + .disabled() blocks in-flight double-taps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a top-right star to the iOS Node Details screen so a peer can be saved/favorited (and unsaved) directly from node details — matching Android Columba's
AnnounceDetailScreen, which already has this affordance. Previously iOS could only favorite from the contact/announce lists, never from the detail view.Why
Parity gap reported in the field: the node details screen on iOS had no
.toolbarand no favorite control at all, only preservingisFavoritethrough live-merges.How
NodeDetailsView— new optionalonToggleFavoritecallback (parent-owned, mirroringonStartChat/onBrowseSite) and a.primaryActiontoolbar star. Its lit state is a local@Stateseeded in.task, deliberately decoupled fromliveContact—mergedContact/applyOfflineStatere-deriveisFavoritefrom the seed on every ~1.5s path poll, so binding the star todisplayedContact.isFavoritewould make it visually snap back.ContactsView— wires the node-details destination toContactsViewModel.toggleFavorite(for:).ContactsViewModel— aContact-typedtoggleFavoriteoverload that removes an already-saved contact or falls back toaddToContacts(_:), keeping the star correct even when the peer isn't currently in the in-memory announce arrays.Semantics
On iOS, favorite == contact membership in
conversations.is_favorite, so one tap reproduces Android's add/remove behaviour via the existing persistence path — no new storage is introduced. Toggle is instant (no confirmation dialog) to match every other favorite star in the app..primaryActionplacement (not the iOS-only.topBarTrailing) keeps the macOS target compiling.Decisions baked in
Known cosmetic caveat
Favoriting a NomadNet site or relay surfaces it under My Contacts, which renders entries as chat peers (
badgeTypehardcoded.peeron reload). Matches the all-node-types choice; can be gated later if undesired.Verification
Builds clean for both simulator and device (
Columba-Swift, Debug-Swift) and installed/sanity-checked on a physical device.🤖 Generated with Claude Code