Skip to content

Add contact profile pictures#373

Merged
Avi0n merged 3 commits into
Avi0n:devfrom
romeomont:feature/contact-avatar-and-context-menu-message
Jul 20, 2026
Merged

Add contact profile pictures#373
Avi0n merged 3 commits into
Avi0n:devfrom
romeomont:feature/contact-avatar-and-context-menu-message

Conversation

@romeomont

@romeomont romeomont commented Jul 19, 2026

Copy link
Copy Markdown

Description

Adds custom contact profile pictures, discussed in #372. Split out of the original combined PR into its own PR so it can be reviewed independently from the Nodes context-menu change (now in #374).

Contacts currently only get an auto-generated initials avatar (ContactAvatar). This adds the ability to set a real profile picture from Photos or Files.

Overview of Changes

  • MC1Services/Sources/MC1Services/Models/Contact.swift: added avatarImageData: Data? to the Contact SwiftData model and ContactDTO, threaded through all inits, apply(_:), and the with(...) copy helpers (also fixed with(isMuted:)/with(isFavorite:), which previously dropped any field not explicitly listed — would have silently wiped the avatar on those calls).
  • MC1Services/Sources/MC1Services/Services/PersistenceStore.swift: documented the new field as a v5→v6 lightweight schema migration (no VersionedSchema, matching the project's existing convention).
  • MC1Services/Sources/MC1Services/Services/ContactService.swift: added updateContactAvatar(contactID:imageData:).
  • MC1/Views/Components/ContactAvatar.swift: renders the stored image when present, falls back to the existing initials circle otherwise.
  • MC1/Views/Contacts/ContactDetailView.swift: tapping the avatar (chat-type contacts only) opens a confirmation dialog — Choose Photo (PhotosPicker), Choose File... (.fileImporter), Remove Photo. Picked images are downscaled to a 512pt max dimension and re-encoded as JPEG before saving. Purely local metadata — not synced to the radio/mesh.
  • New localized strings in Contacts.strings / regenerated L10n.swift (via SwiftGen).
  • New test: MC1Services/Tests/MC1ServicesTests/Services/ContactServiceTests.swiftupdateContactAvatar sets and clears avatarImageData.

The Nodes long-press "Send Message" item that was originally bundled here has been moved to #374.

Testing

  • xcodebuild build -project MC1.xcodeproj -scheme MC1 -destination 'platform=iOS Simulator,name=iPhone 17e,OS=26.5'BUILD SUCCEEDED, isolated on this branch.
  • swiftlint lint — clean, no warnings/errors.
  • Manually installed and launched the built app in the iOS 26.5 simulator to confirm it runs (onboarding/pairing screen renders correctly).
  • Full make test-app / MC1Tests run hit a pre-existing macro-expansion compile error in MessageLinkTokenizerTests.swift unrelated to this change — reproduced identically on a clean main checkout with these changes stashed, so it's an environment/toolchain issue, not something this PR introduces. MC1ServicesTests (where the new avatar test lives) isn't wired into any Xcode scheme in this environment and I wasn't able to execute it directly; the new test follows the exact pattern of the adjacent passing tests in that file and the file parses/compiles cleanly.
  • Have not yet manually exercised the photo/file picker against a live connected radio — no physical MeshCore hardware available in this environment. Would appreciate a manual pass on-device before merge.

Tested on

  • iOS 26.5 (Simulator, build + launch only, no paired radio)
  • iPadOS
  • macOS

Checklist

  • This PR was discussed with the maintainer either via GitHub issue or other means (Also check the box if this PR is small enough not to need discussion e.g. typo fix) — issue Contact profile pictures + "Send Message" in Nodes context menu #372 was filed but the maintainer has not yet responded
  • I have read CONTRIBUTING.md
  • Testing steps are documented above.
  • This change is not low effort and I took the time to test it

@romeomont
romeomont force-pushed the feature/contact-avatar-and-context-menu-message branch from 49272dd to 46cdd3c Compare July 19, 2026 15:40
@romeomont romeomont changed the title Add contact profile pictures and Send Message in Nodes context menu Add contact profile pictures Jul 19, 2026
@Avi0n
Avi0n changed the base branch from main to dev July 20, 2026 05:31

@Avi0n Avi0n left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the PR! I really like this idea. Please see the AI review below and have Claude address them. Also in the future, please make sure you're creating your branches from dev and pointing your PRs into dev as well. Thanks!

Requesting changes for one data-loss bug, then backup, test, and locale follow-ups.

Blocking: photo is cleared by everyday edits

updateContactAvatar correctly uses existing.with(avatarImageData:). Other ContactService updates rebuild a DTO from a temporary Contact(...) and omit avatarImageData, so it defaults to nil. saveContact then runs apply and overwrites the stored image.

Affected paths (unchanged by this PR, but they wipe the new field once it exists):

  • updateContactPreferences (ContactService.swift ~463–488) — nickname / block
  • updateContactOCVSettings (~513–537)
  • setContactFavorite (~596–620)
  • setTelemetryPermissions (~671–695)

Repro: set a photo, then save a nickname, toggle favorite, change OCV, or change telemetry flags. The photo is gone.

Note: the same OCV / favorite / telemetry rebuilds also drop isMuted and unreadMentionCount (pre-existing silent-drop pattern; worth fixing while touching those call sites).

Minimal fix: pass avatarImageData: existing.avatarImageData on every rebuild, or stop building temporary Contact(...) values and use with-style copies that always keep local-only fields.

Please add a regression test: set avatar → updateContactPreferences / setContactFavorite → assert the avatar is still present. The new set/clear test is not enough.

Also before merge

  1. mergeBackupMetadata (PersistenceStore+BackupHelpers.swift ~184–227): adopt avatarImageData when local is nil (same pattern as nickname / OCV). Fresh insert is fine; merge onto an existing contact drops the backup photo.
  2. BackupIntegrationTests: encode/decode with non-nil data, legacy key absent → nil, export→import, merge onto existing.
  3. Locales: add the five contacts.detail.avatar.* keys to de, es, fr, it, nl, pl, ru, uk, and zh-Hans (all non-English project locales; English placeholders are fine).

Strongly recommended

  • Cache decoded avatars (or list thumbnails); UIImage(data:) in ContactAvatar.body runs in scrolling lists
  • format.scale = 1 and off-main processing in processAvatarImage (default screen scale can turn “512 pt” into 1536 px)
  • Disable the avatar control while saving; surface an error when PhotosPicker returns nil data
  • Richer VoiceOver label (name + photo state + saving)

What already looks good: local-only scope, chat-only edit, security-scoped file access with defer, picker re-arm, optional Codable for legacy backups, and with(isMuted:) / with(isFavorite:) already forward the new field.

Comment thread MC1Services/Sources/MC1Services/Models/Contact.swift
Comment thread MC1Services/Sources/MC1Services/Services/ContactService.swift
Comment thread MC1Services/Sources/MC1Services/Models/Contact.swift
Comment thread MC1/Views/Components/ContactAvatar.swift Outdated
Comment thread MC1/Views/Contacts/ContactDetailView.swift Outdated
Comment thread MC1/Views/Contacts/ContactDetailView.swift Outdated
Comment thread MC1/Views/Contacts/ContactDetailView.swift
Comment thread MC1/Resources/Localization/en.lproj/Contacts.strings
romeo and others added 2 commits July 20, 2026 09:19
Contacts can now have a custom profile picture set from Photos or
Files, overriding the auto-generated initials avatar.

See discussion: Avi0n#372

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a data-loss bug: updateContactPreferences, updateContactOCVSettings,
setContactFavorite, and setTelemetryPermissions rebuilt contacts via a
temporary Contact(...) that omitted avatarImageData (and, in some cases,
isMuted/unreadMentionCount), silently wiping the photo on any of those
everyday edits. All four now forward every field from the existing contact.

Also:
- mergeBackupMetadata adopts a backup's avatarImageData when local is nil.
- Added regression tests for the avatar-preservation fix, a backup
  encode/decode/export/import/merge round-trip test, and locale placeholder
  strings (contacts.detail.avatar.*) for the 8 non-English project locales.
- Cache decoded avatar UIImages (NSCache) so ContactAvatar doesn't
  re-decode JPEG data on every scroll redraw.
- Downscale off the main thread (Task.detached) and force format.scale = 1
  so "512pt" doesn't balloon to 1536px at 3x screen scale.
- Disable the avatar button while saving and surface an error when
  PhotosPicker returns nil data instead of failing silently.
- Richer VoiceOver label combining contact name and saving state.
@romeomont
romeomont force-pushed the feature/contact-avatar-and-context-menu-message branch from 46cdd3c to ce805a4 Compare July 20, 2026 13:23
@romeomont

Copy link
Copy Markdown
Author

Thanks for the thorough review! Rebased onto dev and pushed a follow-up commit addressing everything:

Blocking: data-loss bug — fixed. updateContactPreferences, updateContactOCVSettings, setContactFavorite, and setTelemetryPermissions all rebuilt contacts via a temporary Contact(...) that omitted avatarImageData (and in some cases isMuted/unreadMentionCount). All four now forward every field from existing. Added regression tests (avatarImageData survives updateContactPreferences, avatarImageData survives setContactFavorite) that set an avatar, run the mutation, and assert the avatar is still present.

Before merge:

  1. mergeBackupMetadata now adopts avatarImageData when local is nil, same pattern as nickname/OCV.
  2. Added Contact avatarImageData survives encode decode export import and merge to BackupIntegrationTests — covers encode/decode round-trip, legacy payload with the key absent decoding to nil, a fresh-insert export→import round-trip, and merge onto an existing contact with no local avatar.
  3. Added the five contacts.detail.avatar.* keys (plus a new savingAnnouncement key for VoiceOver, see below) as English placeholders to all 8 non-English project locales (de, es, fr, nl, pl, ru, uk, zh-Hans — the repo doesn't have an it locale).

Strongly recommended — all done:

  • ContactAvatar now caches decoded UIImages in an NSCache keyed by the raw JPEG Data, so scrolling a contact list doesn't re-run UIImage(data:) on every redraw.
  • processAvatarImage is nonisolated and now runs off the main thread via Task.detached; the renderer uses an explicit UIGraphicsImageRendererFormat with scale = 1, so "512pt" stays 512px instead of ballooning to 1536px at 3x.
  • The avatar button is now .disabled(isSavingAvatar) while a save is in flight, and a PhotosPicker returning nil data now surfaces the existing "invalid image" error instead of failing silently.
  • VoiceOver label now combines the contact's name with either the "change profile picture" or "saving photo" state.

All changes verified on this branch (based on current dev):

  • xcodebuild build -scheme MC1BUILD SUCCEEDED
  • swiftlint lint — 0 violations
  • swift test --package-path MC1Services --filter "ContactServiceTests|BackupIntegrationTests" — 125/125 passed
  • xcodebuild test -scheme MC1 still hits the pre-existing MessageLinkTokenizerTests.swift macro-expansion compile error, confirmed present on a clean dev checkout with no relation to this change.

CI's swiftformat --lint --strict caught two issues not covered by
swiftlint: nonisolated must precede static in modifier order, and a
regular // comment on a private declaration should be a doc /// comment
per this repo's docComments rule.

@Avi0n Avi0n left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Looks great, thank you for your contribution!

@Avi0n
Avi0n merged commit a00841a into Avi0n:dev Jul 20, 2026
4 checks passed
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.

2 participants