[feature] the microphone says so on the lock screen, and in your hand - #393
Merged
Merged
Conversation
Parley can hold the microphone for up to an hour, and the only thing that said so was iOS's own orange dot plus three places you had to go and look at: the Settings picker footer, the Record tab's bar, and the keyboard's "Mic ready" chip. Lock the screen and all three are gone. One Live Activity with three modes — meeting, dictation, standby — because there is one microphone. `MeetingRecorder.start` already takes it from `DictationCoordinator`, so a card per subsystem would have been a way for two cards to disagree about a fact the app has only one of. Every clock on the card is a `Text(timerInterval:)`, ticked by the system inside the widget process. That is defensive: it is unresolved whether `activity.update(...)` reaches a backgrounded app holding a recording audio session, and it is exactly then that the card matters most. An update is an enrichment here, never the thing that keeps the card true. `MicActivityPolicy.staleAfter` is the one lever the device spike flips. Three of the card's four buttons needed no app-side work: they write the same App Group mailboxes the keyboard has always written, so the card is a second keyboard as far as the app is concerned. Only the meeting's Stop is new, and it copies `MicWindowControl` exactly — a timestamp, not a flag, so a control file left behind by a crash cannot stop the next recording. Haptics: the mic opening becomes a rising two-beat, and swiping the keyboard away mid-dictation — which until now was silent, and takes the live transcript off screen with it — becomes a falling one. Touch carries direction reliably and texture badly, and out of sight is the whole situation. The start's first beat stays `.medium`: the report was "I did not feel it", so nothing about the moment of the press may get quieter. Nothing that destroys recorded audio is reachable from a locked screen, and the transcript is not on the card in either mode — a lock screen is a public surface and the person opposite can read it. See docs/design/ios-live-activity.md.
✅ SonarQube Quality Gate passed — pathorsAI_parley0 open issues on this PR. |
Lanznx
added a commit
that referenced
this pull request
Sep 18, 2026
For iOS this release is #393 and nothing else — the four other commits since ios-v1.14 are desktop, Android, the website, and 1.14's own release notes. What's New says so in both locales, and no screenshots were re-captured because nothing inside the app moved: the card is a lock-screen surface. The part that would otherwise have cost a twenty-minute CI run: #393 embedded a third target, and `asc_signing.py` had the two bundle ids written into it twice over — once as a tuple to iterate and once as a pair of outputs. A target with no provisioning profile does not fail at the point it is added. It fails at export, saying `No profiles for '…' were found`, about a bundle id nobody was thinking about. That list is now `SIGNED_BUNDLES`, and RELEASING.md says out loud that adding an embedded target means adding it there and registering it on the portal first. `com.pathors.parley.ios.activities` still has to be registered on the developer portal, with App Groups enabled and joined to group.com.pathors.parley.ios, before a tag will build. All three targets move to 1.15 (27) together; a mismatch is rejected at upload rather than at build. Co-authored-by: YJack0000 <jack@pathors.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.
Parley can hold the microphone open for up to an hour. Until now the only thing that said so was iOS's own orange dot, plus three surfaces you had to go and look at: the Settings picker footer, the Record tab's bar, and the keyboard's "Mic ready" chip. Lock the screen and all three are gone.
This adds the Live Activity that fills the gap — Dynamic Island and lock screen — and the haptics that go with it.
Design:
docs/design/ios-live-activity.md.One microphone, one card
Three modes, never three activities:
.meeting(red),.dictation(blue),.standby(iOS's own privacy-indicator orange, the same value the keyboard's chip uses).MeetingRecorder.startalready callsDictationCoordinator.yieldMicrophone()— there is one microphone, and a card per subsystem would only have been a way for two cards to disagree about it.MicActivityState.deriveis the single place that precedence is written, as a free function in ParleyKit so it is testable without a main actor or a device.Every clock is a
Text(timerInterval:)The most load-bearing decision here, and it is defensive. It is not established that
activity.update(...)reaches a backgrounded app holding a recording audio session — reports say background updates work under background location and PiP and not under theaudiobackground mode, and the simulator gives a false pass. Parley holds a recording session rather than playing audio, which may or may not be the same case.So the card is built to be correct with no update at all: the system ticks every clock inside the widget process. An update changes the mode; it is never what keeps the card true.
MicActivityPolicy.staleAfteris the one constant the device spike flips, and both behaviours are already written.This needs measuring on a device before it ships — see the spike note in the design doc. Everything else is verified.
The buttons were mostly free
Three of the four already had a listener: they write the same App Group mailboxes the keyboard has always written, so the card is a second keyboard as far as the app is concerned. Only the meeting's Stop is new, and
MeetingControlcopiesMicWindowControlexactly — a timestamp, not a flag, so a control file left behind by a crash cannot stop the next recording.None of the four touches the microphone, which is why they work from a locked screen over a backgrounded app.
Haptics
.medium.medium→.heavy, rising.heavy→.light, falling.success.rigidTouch carries direction reliably and texture badly, and out of sight is the entire situation. The falling beat fires only when a session is live — dismissing the keyboard is a constant action and buzzing every time is how a signal becomes noise. The rise starts at
.mediumrather than.lighton purpose: the report was I did not feel it, so nothing about the millisecond of the press is allowed to get quieter.Two platform rules found the hard way
Both cost a build failure, both are now commented at the code and in the design doc:
#if os(iOS), never#if canImport(ActivityKit). The macOS SDK ships ActivityKit and AppIntents, socanImportis true there while the types are@available(macOS, unavailable). ParleyKit builds for macOS so its logic can be tested without a simulator, which is what makes this reachable.titlemust come from the main bundle.ExtractAppIntentsMetadatarejects aLocalizedStringResourcenaming another bundle, and ParleyKit's strings are inBundle.module. The intent titles are therefore bare literals (Shortcuts metadata, all fourisDiscoverable = false); the card's button words come fromMicActivityCopy. Labelling from the titles instead would compile, look right in English, and ship a Chinese-less card.Deliberately not here
"Continue recording" is not in this PR. If the app is swiped away the recording stops — no iOS API brings it back — so the card says so rather than pretending. Resuming means stitching a new audio segment onto an existing recording, which is a change to the file model and not to anything on a lock screen; it is worth costing separately. The local notification that would go with it is held back too, because it would be this app's first notification-permission prompt and that should arrive attached to something that works.
No Delete on the card, and no transcript on it. Dictation's ✕ throws away text that was never inserted and can be said again; a meeting's delete removes an audio file, and nothing that destroys recorded audio is reachable from a locked screen. The transcript is off the card in both modes because a lock screen is a public surface — the phone is on the table and the person opposite can read it.
Verification
swift test --package-path ios/ParleyKit→ 422 tests, 1 skipped, 0 failures (20 new).xcodegen generate+xcodebuild -scheme Parley -configuration Debug -destination generic/platform=iOS→ BUILD SUCCEEDED, all three targets, both.appexes embedded.One pre-existing wart left alone rather than swept into this diff:
Haptics.swiftcarriesnonisolated(unsafe)annotations that the current language mode calls unnecessary. They were there before (4 of them) and this change follows the file's established style rather than opening an unrelated cleanup.🤖 Generated with Claude Code