Skip to content

Chat on repository - #777

Merged
toddmitchell merged 38 commits into
mainfrom
chat-on-repository
Jun 26, 2026
Merged

Chat on repository#777
toddmitchell merged 38 commits into
mainfrom
chat-on-repository

Conversation

@toddmitchell

Copy link
Copy Markdown
Member

No description provided.

toddmitchell and others added 6 commits June 19, 2026 12:28
…ntry projection

Additive foundation for migrating the contact book onto ContactRepository:
- ContactName.resolveDisplayName/initials (homebase-api), shared by all consumers.
- Contact.toContactBookEntry() projection in core (server-shaped Contact -> flat
  UI model), display name via the shared resolver, image fields from ContactImageRef.

No consumers rewired yet; nothing removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewire the contact book onto the single source of truth and delete the core-side
read/write wrappers:

- ContactBookViewModel + ContactDetailViewModel inject ContactRepository instead
  of ContactBookStream + ContactBookService. They project repo.contacts
  (List<Contact>) -> ContactBookEntry via the new projection; reads use
  repo.isLoaded/ensureLoaded; writes use repo.save/delete/sync/setImage with the
  optimistic update owned by the repo (no more stream insert/remove dance).
- ContactSaveHelper saves through ContactRepository; ContactSaveResult.Success no
  longer carries an optimistic entry.
- Delete ContactBookStream + ContactBookService; AppModule registers neither and
  the post-auth bootstrap starts ContactRepository.
- ContactDetailViewModel now ensureLoaded()s on deep-link.

Main-parity (the §7/#6 fixes and shared helpers are re-applied next). Core +
Konsist + api jvmTest suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rser

- §7: contact detail "Contact details" section lists the Homebase ID, so an
  identity-only contact no longer reads "None".
- #6: no-clear merge mitigation on the repository path — saveContactDraft now
  coalesces the saved content (keeps the old value for a blanked field, matching
  the server's no-clear merge, so the optimistic entry doesn't flash empty then
  reappear) and reports clearedFieldsIgnored; both the list and detail screens
  surface a "Clearing a contact field isn't supported yet…" snackbar.
- Remove the now-dead HomebaseFile.toContactBookEntry (the repo path parses to
  Contact via toContact(); the UI projects Contact.toContactBookEntry) and its
  now-unused imports; refresh the ContactBookEntry KDoc.

Core + Konsist + api jvmTest suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry; delete DriveContactService

Migrates the chat contact read+write path onto the single ContactRepository
source of truth and removes the duplicate chat contact stack:

- ContactUiModel gains a Contact.toContactUiModel() projection (server-shaped
  Contact -> connection-oriented chat model), via the shared resolveDisplayName/
  initials helpers.
- ContactService now sources raw contacts from ContactRepository.contacts and
  layers connection state on top (unchanged enrichment); it no longer starts a
  drive reader (the repo is started by the post-auth bootstrap).
- ConnectionRequestService writes contacts on connection accept/finalize via
  contactRepository.sync(odinId) instead of DriveContactService.saveContactForOdinId.
- Delete DriveContactService and the entire chat duplicate model family
  (ContactServerFile, ContactName/Phone/Email/Location/Birthday, ContactImage,
  ContactProtocol, ContactSizer); drop its DI registration.

Now both chat and the contact book read/write through one ContactRepository:
one drive query, one event observer, one writer. Chat + core + Konsist + api
jvmTest suites pass.

NOT yet device-validated — the chat people-pickers (new conversation, select/add
members, conversation list) read through ContactService and need a smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t/last name

- Danger zone is now set off by a HorizontalDivider (clearer separation).
- "Contact details" lists labeled rows (overline label + value): First name,
  Last name, Homebase ID, Phone, Email, Location, Birthday — name parts first so
  an identity contact shows real details, not just the Homebase ID; the rest tuck
  behind the existing "More" toggle. Reuses the edit-form label strings + a new
  contactbook_detail_location.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

JVM Test Results

2 496 tests  +50   2 491 ✅ +50   1m 58s ⏱️ +5s
  307 suites + 7       5 💤 ± 0 
  307 files   + 7       0 ❌ ± 0 

Results for commit 9e11346. ± Comparison against base commit 74c8c41.

♻️ This comment has been updated with latest results.

toddmitchell and others added 23 commits June 19, 2026 18:51
…ew screen

The minimal ContactInfoScreen (avatar + name + odinId) shown for a 1:1
conversation / group-member tap is a strict subset of the contact-detail screen,
which already loads the 1:1 conversation overview (recent media, groups-in-common,
circles) plus the full contact fields, message/connect, and danger zone. Converge
on one screen:

- Both nav sites (1:1 conversation info, group-member tap) now navigate to
  Route.ContactBookDetail(uniqueId = md5(odinId), odinId) instead of Route.ContactInfo.
- Delete ContactInfoScreen/ViewModel/UiState/UiAction, Route.ContactInfo, and the
  DI registration.

The chat-side ShowContactInfo actions/events are unchanged — they just deliver an
odinId to the screen callback, which now opens the contact detail.

Core + chat + Konsist jvmTest suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bug: opening a 1:1 chat and tapping the header (or the overflow "Conversation
info", or long-pressing it in the list) fired ShowConversationSettings, which
opened the conversation-overview/settings screen — not the contact detail. The
earlier ContactInfo rewire only covered reaction taps / group members, missing
this main path.

handleShowConversationSettings now routes a 1:1 (non-group, non-self) to
NavigateToContactInfo(peerOdinId) -> the full contact-detail screen. Groups still
open group settings; note-to-self keeps the conversation-settings screen (no
contact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… settings)

RecentMediaSection now shows "See all" whenever the 1:1 has ANY shared content
(media, files, audio, dice rolls, or locations) — not just media — so non-media
items are reachable even with no media to strip; the empty state shows only when
there's truly nothing. "See all" already routes to the same ConversationMedia
screen the conversation-settings overview used, so the contact detail now fully
subsumes that overview with nothing lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire clearKeyCache through reset(), evict the stale per-uniqueId AES key
on delete, make the deletedIds resurrection guard race-safe, and lift
delete suppression before sync re-creates a contact. Adds
ContactRepositoryTest covering the load/delete/sync paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend ContactContent with the remaining header fields (shortBio,
nickname, status, link, social, isEmergencyContact). social and
isEmergencyContact are nullable so they omit-on-null and don't clobber
stored values through the server's field-level UPDATE merge.

Add the ext_data payload read path: ContactExtData (raw JsonElement map,
forward-compatible) with lazily-decoded Experience and Bio types,
disambiguating the short_bio string-vs-richtext collision by attribute
type id. Contact now carries fileId/keyHeader/hasExtData so the bios are
fetched on demand without re-reading the file header; the fetch goes
through a narrow ContactPayloadReader seam (mirrors ContactHeaderReader)
backed by DriveFileProvider.getPayloadBytesDecrypted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ContactRepository.emergencyContacts, a StateFlow subset of contacts
flagged as emergency contacts, derived from the live list so it tracks
the same optimistic writes and sync reconciliation.

Make isEmergencyContact a plain non-null Boolean (you either are one or
you're not) with @EncodeDefault(NEVER) so the false default is omitted —
keeping the field-level UPDATE merge non-destructive — while true is
written. Clearing the flag is not expressible through the merge and
needs a dedicated write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contact detail UI:
- Show the free-text status under the contact's odinId in the header.
- New "Bio" (shortBio) and "Social" sections under contact details.
- ContactAttributes.kt: well-known attribute-type GUIDs + ContactSocialNetwork
  enum; socialHandles() resolves ContactContent.social, normalizing the dashless
  32-hex stored key form against the hyphenated constants. ContactSocialTest pins it.

Per-app contact data:
- setAppData/deleteAppData (inline tier) and setAppExtData/deleteAppExtData +
  loadAppExtData (bulk ext_data tier) on ContactRepository, over the new
  ContactsProvider app-data/app-ext-data endpoints (SetContactAppDataRequest).
- ContactContent.appData inline map; Contact.payloadKeys replaces hasExtData so a
  reader can skip a guaranteed-404 fetch for any absent payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the per-app contact app-data client landed in ed2015b: the four
ContactsProvider writes (shared body with no appId, inline vs bulk tier
paths, version-gated retry, size-cap 400 -> MaxContentLengthExceeded),
the appId normalization + inline/bulk read helpers, and ContactRepository
bulk read + the too-large -> ContactAppDataTooLargeException translation.
Adds a ProblemDetails fixture for the size-cap case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Receive-side of the emergency-contact handshake:
- New StatusMessage.EmergencyContactDesignated discriminator.
- ConversationStream gains onEmergencyContactDesignated + a dispatchEmergencyDesignations
  pre-pass (live BatchReceived only, receiver-side filtered) mirroring the GroupHealRequested
  hook.
- AppModule wires it to ContactRepository.setEmergencyContact on the sender's contact
  (md5(odinId)) on our own drive: ensureLoaded, idempotent, sync-if-absent, Forbidden-safe.
- MessageMapper renders a system line "X added you as an emergency contact" (self/subject
  variants); 3 new strings.

ContactRepository.setEmergencyContact(uniqueId, versionTag): minimal-delta write that sends
only {"isEmergencyContact": true} and optimistically copies the flag onto the live contact's
existing content (set-only; the merge can't clear it). Covered by a new ContactRepositoryTest.

Also: pre-normalize ContactSocialNetwork attribute ids via normalizeId at construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ConversationService.sendEmergencyContactDesignation(recipient): gets-or-creates the 1:1
and posts a StatusMessage.EmergencyContactDesignated status (subject = recipient, so the
sender's own copy reads "You added X…"). Mirrors the ConversationStarted send pattern;
best-effort, returns the conversation id or null. Pairs with the receive-side hook in
ConversationStream/AppModule and ContactRepository.setEmergencyContact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detail screen:
- "Emergency contact" indicator under the avatar/odinId when the contact is one
  of our emergency contacts.
- Management action toggles between "Mark as emergency contact" (sets the flag +
  notifies the contact via EmergencyContactDesignated) and "Remove as emergency
  contact" (clears it, local-only). Shown for a synced Homebase identity.
- ContactDetailViewModel.handleMakeEmergencyContact / handleRemoveEmergencyContact;
  new actions/events + snackbars; ContactBookEntry.isEmergencyContact.

API:
- ContactRepository.clearEmergencyContact + setEmergencyContact refactored onto a
  shared writeEmergencyFlag helper over the new version-gated
  ContactsProvider.writeEmergencyContact.
- EmergencyContactDelta/SetEmergencyContactRequest: a dedicated wire shape that
  always emits the bool, so a clear (isEmergencyContact=false) can be expressed —
  ContactContent omits a false (@EncodeDefault NEVER). Assumes the server honors an
  explicit false as a clear. Covered by a new clearEmergencyContact test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ShowConversationSettings picked the peer via participants.firstOrNull(),
but participants is the raw recipient list that includes the owner. When
the owner sorted first, tapping the 1:1 conversation header opened the
owner's own contact detail instead of the person being chatted with.
Exclude the owner before picking the peer, mirroring handleShowContactInfo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…you"

Move the emergency-contact flag off the shared ContactContent into this app's
private inline app-data slot (keyed by AppConfig.APP_ID):
- New core EmergencyContact.kt: ChatContactAppData blob, Contact.isEmergencyContact(),
  ContactRepository.setEmergencyContact/clearEmergencyContact (clear just drops the
  slot — no merge-can't-express-false problem), and a derived emergencyContacts flow.
- Removed ContactContent.isEmergencyContact + the dedicated delta write, the api
  ContactRepository emergency methods/flow, and their tests; added a core read-path
  test. ContactBookEntry/ContactDetail/AppModule repointed to the app-data extension.

Location dashboard "who can locate you" now lists contacts marked as emergency
contacts (reactive), replacing the Emergency Location Access circle members; keeps
the owner-console manage link. Excludes the logged-in identity (you're not your own
emergency contact), and the contact-detail mark/remove action is hidden for your own
self-contact (isSelf) so it can't be flagged in the first place.

Also bundles in-progress contact address/label fields (ContactContent location/phone/
email labels, ContactSaveHelper, ContactBookEntry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anLocate

Splits the two conflated directions of the emergency-location feature. "Who
can locate you" now reads our own emergency-circle membership (the source of
truth) instead of an app-data flag; the per-contact flag is repurposed to the
reverse direction — `iCanLocate`, "people we can locate" — and backs a new
"Who you can locate" dashboard section.

- EmergencyCircleNotifier: diffs emergency-circle membership (baseline-seeded
  to avoid login re-spam, reset on logout) and notifies peers on grant/revoke.
- EmergencyContactRevoked status + sendEmergencyContactRevocation mirror the
  designation pair; MessageMapper renders the revoke system line.
- EmergencyContactReceiveService: sets/clears iCanLocate on the receiver and
  silently consumes the status message (group-heal-style soft-delete) so a
  re-delivered designate-after-revoke can't re-flip a stale flag. Unknown
  senders are synced but not consumed, so the flag still lands on a later
  delivery.
- EmergencyContactReconciler: on dashboard open, verifies each iCanLocate
  contact against verifyTemporalAccess and clears stale flags (lost revoke
  self-corrects); inconclusive checks leave the cache untouched.
- Removes the contact-detail "mark as emergency contact" toggle (designation
  now flows from circle membership, not a button).
- Moves EMERGENCY_LOCATION_CIRCLE_ID into AppConfig.

Tests cover the membership-diff baseline-seed and the receive-side consume
decision tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
toddmitchell and others added 9 commits June 25, 2026 06:06
… show data freshness

Tapping Sync on the contact detail screen now also preflights temporal read
access to the contact's location drive (verifyTemporalAccess — reads no data,
fires no peer notification). On access, sets the iCanLocate "emergency contact"
flag (adding them to the emergency list) and surfaces the newest-file timestamp
as "Location data as of <when>" under the badge. A definitive no-access clears a
stale flag, mirroring EmergencyContactReconciler; network/parse failures are
inconclusive and leave state untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…learer copy

On Android 11+ the "Allow all the time" (background) permission can't be granted
by re-firing the runtime dialog — after the first non-grant the OS silently
auto-denies (a brief GrantPermissionsActivity flash), so the Setup row kept
showing "Grant" (permanentlyDenied stays false in the "ask every time" state)
and looped, never routing to Settings.

Fix: latch alwaysRequestAttempted when the user taps Grant; once set, the row
shows "Open settings" instead of re-offering Grant. The latch clears when the
grant lands so a later revoke starts fresh.

Also clarify the copy: rename the row to "Allow all the time" (matches Android's
own label), reword the default hint, and add a settings-specific hint that tells
the user to choose "Allow all the time" in system Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ages

Disable kotlin.incremental.native: on Kotlin 2.3.21 the per-module
incremental-native bitcode cache crashes the K/N backend with
"Lowering ReturnsInsertion: phases [Enums] are required, but not
satisfied" during :homebase-core:linkDebugFrameworkIosArm64 when an
unchanged downstream module (homebase-auth) is rebuilt against changed
dependencies (homebase-api/common). It's a compiler cache-codegen phase
bug, not our code; the cache is build-speed only and framework output is
identical with it off.

MessageMapper: a soft-deleted status message (e.g. an emergency-contact
designation consumed by the receiver) should render nothing, not a
"Deleted File" tombstone — the user never authored or saw it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deleting a contact only removed the address-book record, leaving a
connected identity's connection (and the drive access it granted) live.
Delete now tears down the connection first when the contact is connected,
and the confirm prompt warns about it.

- handleConfirm() disconnects before delete for connected contacts,
  aborting (and surfacing the connection error) if disconnect fails so we
  never drop the record while the connection lingers.
- New contactbook_detail_delete_message_connected string; ConfirmDialog
  shows it when isConnected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@toddmitchell
toddmitchell merged commit 7a6934a into main Jun 26, 2026
7 of 8 checks passed
@toddmitchell
toddmitchell deleted the chat-on-repository branch June 26, 2026 18:16
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