22 keys carried the English text in every locale. Copied English does not
look unfinished — it looks shipped: the key is present and non-empty, so
`validate` passes and it reaches users as a French or Russian build that
silently speaks English. 21 came in with the saved-devices work; one was
added alongside the send-cancellation fix by copying its neighbours.
Translations follow the vocabulary already established in the file —
appareil enregistré, dispositivo guardado, Kopplung, Mémoriser/Ricorda/
Zapamiętaj, and European Portuguese ficheiros/guardado.
Only format_separated_pair and format_separated_triple still read the same
in every locale. They are pure punctuation ("{first} · {second}") with no
words to translate.
Documents the rule in the README, including why it is worse than a missing
key and the trap that spread it here: copying a neighbour's shape without
checking the neighbour was translated.
Adds the native SwiftUI Saved Devices experience on top of the production
saved-device core, as a top-level destination in the iOS tab bar and the
macOS sidebar.
Core seam:
- App-facing saved-device domain models mirroring core/SavedDeviceModels.kt,
with lifecycle helpers (canReceive/canResume/canCancel/canDelete) so views
never hand-roll state checks.
- 21 gateway methods through CoreGateway/CoreRepository with UniFFI mapping.
cancelTargetedTransfer, forgetSavedDevice and blockDevice run off the serial
lane: each must reach the core while a targeted receive is blocking it.
- Payload-free pairingChanged/targetedTransferChanged signals, dispatched
before the numeric-transferId guard since saved-device events identify
their subject by peer endpoint or a string transfer id.
Experience:
- Screen lists saved devices and outstanding consent requests only; the
global targeted-transfer history stays out, reachable per device.
- Details as a sheet with detents on compact layouts and a native inspector
on macOS, owning Send, label, forget/block and that device's transfers.
- Label editing is transactional: the draft and editor survive a failed
write, conflicting actions are refused while saving, and the editor closes
only after the core confirms.
- Pairing and targeted-offer consent hosted at the app root, answerable from
any tab and suppressed while a transfer approval is up. Dismissing a
pairing prompt suppresses locally without consuming the single-use
eligibility; dismissing an offer declines it, since an unanswered offer
holds a slot in the core's bounded per-sender queue.
- Targeted send reuses the invitation composer's affordances with file,
folder, rename, replace and cleanup parity. Picker copies are released on
replace/remove/clear/cancel and after a successful create, but kept after a
failure so retry does not require re-picking.
- Notifications for pairing requests and offers (withdrawn once answered) and
for terminal targeted transfers. Wording follows direction: on the sending
device the peer finished receiving, not us.
Localization:
- Widens 52 saved-device keys from kmp-only to both platforms.
- Five keys carried a literal %1$s with no declared args, which Compose
renders positionally but the Apple generator emits as a plain constant,
leaking the placeholder into the UI. They now use named args; Compose
output is byte-identical.
- Adds targeted_offer_title/body. Reusing the invitation approval copy stated
the roles backwards, announcing the sender as the receiver.
Also surfaces core startup failures: the startup overlay is drawn above the
snackbar host, so a failed initialize() was indistinguishable from an app
that never finished loading. AppModel now keeps the reason, logs it, and the
overlay shows it with a retry, plus the technical detail in DEBUG builds.
Send and receive between two devices is verified only partially; a missing
endpoint-identity credential currently blocks startup on the test device.
Add the VniDrop-specific Compose architecture skill, unify invitation and targeted transfer drafts, and promote Saved devices to an adaptive first-class destination.
Two causes. The connect step had no timeout, so an unreachable device was
retried indefinitely instead of falling through to hold-for-later; it now
gives up after 15s and holds the offer as designed.
The picker also waited on the whole exchange, which includes a person on the
other device deciding — up to two minutes. It now closes on tap and reports
the outcome as a message, and a decline or an unanswered offer is shown as
information rather than an error, since the offer did arrive.
Send to a device now sits alongside the QR code, NFC, and export actions,
since an offer is another way to deliver the same invitation. Picking a
device pushes the existing transfer rather than re-sharing the files.
The picker lists only devices holding a live grant, so nothing offered there
can fail on tap, and it distinguishes accepted from waiting for that device
to open the app.
Also fixes the deprecated SF Symbol and the two Sendable warnings introduced
with the contacts screen: the sections now talk to the model directly rather
than storing view callbacks that a Binding setter has to convert.
Adds the opt-in foreground check and an explicit Check now, the waiting-to-
be-delivered list on the sender side, and honest reporting when a send could
not be delivered: a closed app is a delay, not a success nobody received.
The setting is off by default and its footer states that checking reveals
app-open times to remembered devices, since that is the reason it is a
setting at all.
Records in the design doc that this shipped as one global toggle rather than
the per-contact opt-in originally specified.
Contacts list and detail, pairing consent prompts, incoming offer prompt,
and the grant lifetime setting, in all nine supported languages. Added to
strings.json and regenerated; targeted at both platforms so the Compose
resources exist when the KMP side is built.
Delete the TelemetryRecorder, CrashReporter, PendingCrashStore and platform
crash hooks along with their models, JSON encoders and the diagnostics opt-in
preference. The DiagnosticsTransport interface is narrowed to sendBugReport, and
DiagnosticsCoordinator now only wires the bug-report service and install id.
Bug reporting, the breadcrumb buffer, log redaction and the diagnostics endpoint
config are kept. Regenerate localization after dropping the diagnostics_* keys.
The merge kept this branch's reworded notifications_description and
storage_delete_transfers_description over master's, but the merged KMP code is
master's, so its FoundationComposeTest assertions (and the shipped KMP copy) expect
master's wording. Restore both to master's committed text (pulling the storage one
from master's XML, since master's own strings.json was stale for it). Verified the
two failing KMP Compose tests pass locally.
Another key master referenced from Kotlin but kept only in the generated Compose
XML, so regeneration dropped it. Verified exhaustively this time: every
Res.string.* reference in shared/src/commonMain/kotlin now resolves against the
regenerated values/strings.xml, so no further keys are missing.
Master referenced storage_clear_transfer_cache(_description) and
storage_transfer_cache_cleared from Kotlin but never added them to strings.json —
they lived only in the committed Compose XML. Regenerating l10n from the merged
strings.json dropped them, breaking the shared-kmp build. Add them (kmp target,
all 9 languages, text carried over from master) so generation restores them.
Brings in custom relays, relay connection policies, storage cache clearing, and
receiver-failure reporting. Apple-side conflict resolutions:
- CoreRepository: keep CoreDispatcher, adopt master's relay factory + network
transition guard, drop the now-unused serial queue.
- TransferDetailsView: keep the toolbar-share layout; adopt master's
invitationPresentation-based QR panel and the new .failed receiver case (typed).
- SettingsModel/SettingsScreen: typed L10n titleKey with master's .network case;
relay controls and the Free up space / storage redesign coexist.
- Add the missing transfer_receiver_failed localization key.
- Regenerate l10n from the merged strings.json; keep the Apple catalog untracked.
- Drop the notificationsEnabled test assertion (notifications preference was
intentionally removed on this branch).
The core initializes asynchronously at launch, so for a moment the transfer lists
look empty and the app feels stalled. Show a full-window overlay (centered spinner
+ "Starting…") as the top layer of the root stack while coreState.isInitialized is
false; it fades out once the core is ready.
Redesign the Storage screen for clarity: an "On this device" usage header with a
manual Refresh control, symbol-led action buttons, and a caption under each action
spelling out exactly what it does (Free up space = temp + trash, non-destructive;
Delete all transfers = clears history + cached share content, keeps received
files).
Fix the summary sticking on "Calculating…": it loaded only on .onAppear and bailed
when opened before the core finished its async launch, leaving the loading branch
showing with nothing running (only a manual refresh recovered it). loadStorageUsage
now waits for the core to become ready before reading usage, distinguishes a real
failure (retry) from loading, loads via .task, and can be refreshed on demand. Use
plain button styling with explicit tints so pressing an action no longer flips the
label to the white selection highlight.
Delete all transfers only clears core records, and the blob-store cache is
reclaimed by the core's own timer. Neither touches the app's temporary directory
(leftover picker/staging copies — hundreds of MB on macOS) or the stray .Trash
folders that accumulate in app-owned directories and can't be removed via
Files/Finder. Add a non-destructive Free up space button that empties the temp
directory and removes .Trash folders under the core data dir (and, on iOS, the
fixed Documents receive folder), reporting the bytes reclaimed. Guarded against
running while a transfer is in flight; never touches received files, the core
database, or user-chosen macOS receive folders.
Adds background notifications for the "the thing you were waiting for is
done" moments, alongside the existing incoming-approval-request one:
- a receive finished downloading (receive -> done)
- a receive failed / was interrupted (receive -> failed)
- a share you own failed (send -> failed)
- a receiver finished downloading your share (receiver status completed)
A new TransferNotificationCoordinator observes core state + signals and
publishes these; the decision of which moments notify is a pure function
(plannedTransferNotifications / plannedReceiverNotifications), unit-tested
independently. The first state snapshot only primes existing history as seen
so launch doesn't spam.
Notification permission is now the single source of truth. The in-app
notifications toggle and its decoupled UserDefaults preference are gone;
the Settings section shows an "Allow notifications" button that requests the
OS permission (or deep-links to Settings once decided), and notifications
gate purely on `permission == .granted`.
macOS delivery fixes:
- add a UNUserNotificationCenterDelegate so banners present even while the
app is active (the app window is usually open on macOS)
- present-when-active on macOS, suppress-when-foregrounded on iOS
- reserve the notification id before awaiting publish: the CombineLatest
fired several times and re-added the same identifier, which macOS
coalesces into a silent update with no banner
- LocalNotificationService seeds its permission at init so gating can't
race a not-yet-refreshed .notDetermined
Eight localized title/body strings added (apple-only); the shared
notifications_description copy is generalized from "receive requests" to
"transfer activity".
Add strict custom Iroh relay profiles with safe restart and rollback across the Rust core, Compose apps, and Apple apps. Preserve multi-relay invitations and fail closed on configuration or recovery mismatches.
Translate all 253 user-facing strings to Spanish (formal "usted"), including
the transfer_file_count plural. Generates values-es/strings.xml for KMP, es
localizations in the Apple catalog, and adds es to CFBundleLocalizations.