镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(mobile): configurable hold-to-talk for Bestie - #8665

Open
klopez4212 wants to merge 4 commits into
mainfrom
default-agent-voice-20261028
Open

klopez4212 wants to merge 4 commits into
mainfrom
default-agent-voice-20261028

Conversation

@klopez4212

@klopez4212 klopez4212 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Hold Bestie to record a voice note, release to send, or slide up to cancel. Holding does nothing when Bestie is unset or the new Settings → Voice notes → Hold to talk to Bestie switch is off. The setting defaults on and is saved on the device; normal taps keep their existing behavior.

  • Reuse the existing recorder/upload pipeline, with waveform, cancel controls, haptics, and durable Retry/Discard recovery that preserves the original recipient and signed event.
  • Use native UIKit switches for both iOS settings toggles (Notifications and Hold to talk); retain Material switches on Android.
  • Rebase onto current main, preserving its navigation and typing behavior, and keep failed-send controls visible with the shared iOS navigation glass.

Validation at 9f0d07ad4: analysis, all 4,523 Flutter tests (60 skipped), supporting package tests, and all 33 Android app unit tests pass. The iOS simulator build succeeds and 106/107 native tests pass, including the new UIKit switch and AAC packaging tests; the existing testCustomFlutterHeaderUsesDecorativeSoftEdge fails two assertions. The disabled-hold regression was verified to fail when its guard is removed. This version has not been installed on the physical iPhone.

Recording Slide up to cancel
Recording with live waveform Red expanded cancel target

These earlier Flutter widget previews use synthetic audio and omit native iOS glass and full-screen blur. No image files are committed.

@klopez4212
klopez4212 marked this pull request as ready for review October 29, 2026 13:30
@klopez4212
klopez4212 requested a review from a team as a code owner October 29, 2026 13:30
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T00:48:22.515544Z 9f0d07a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

final soloViewer = ref.watch(myPubkeyProvider);
final personalIds =
ref.watch(meGroupsProvider).asData?.value.channels ?? const <String>[];
final channels = cachedChannels.value
?.where((channel) => !channel.isPersonalChat(soloViewer, personalIds))

P2 Badge Preserve Solo channel IDs while their record reloads

During cold startup and every reconnect-triggered meGroupsProvider invalidation, the provider is temporarily AsyncLoading (potentially retaining previous data), but .asData discards that previous snapshot and substitutes an empty ID list. Cached personal DMs are consequently rendered under Team until Solo finishes loading, or indefinitely when its refresh errors; unwrap the previous value or maintain an account/community-scoped cache before classifying channels.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +180 to +186
final defaultAgent = ref.watch(defaultAgentProvider).value;
Future<void> chooseDefault(String key) async {
if (savingDefault.value) return;
savingDefault.value = true;
final sync = ref.read(meSyncProvider);
try {
await saveDefaultAgent(sync, key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the preference when deleting its agent

When the starred agent is deleted, the successful path in delete_agent_action.dart:62-74 only refreshes the directory and never clears this preference. The launcher therefore continues presenting the deleted key as the default and can send voice notes into a conversation with a permanently stopped agent, while the filtered Agents list offers no star to repair the selection; clear or replace the preference as part of successful deletion.

AGENTS.md reference: AGENTS.md:L228-L235

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 319bf2ba39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is bd1b17dee8348b21b33abe35ee4eafa8730f8a83...9f0d07ad42b6289f53ed309057a15bb50136823a.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review 9f0d07ad42b6289f53ed309057a15bb50136823a to authorize a new review.
Any previous review applies only to its recorded range.

@klopez4212
klopez4212 force-pushed the default-agent-voice-20261028 branch from 319bf2b to 3af2b2c Compare October 29, 2026 14:39
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 29, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3af2b2c608

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +116
final descriptor = await uploadService.uploadVoiceNote(
recording.file,
duration: recording.duration,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain successful uploads when destination setup fails

When there is no cached DM and openDm fails—or the destination becomes invalid after both futures finish—while this upload succeeds, the record wait throws and discards the returned descriptor even though the blob was committed remotely. Each Retry then packages and uploads the recording again, consuming repeated bandwidth and potentially leaving multiple unreferenced blobs; persist the successful descriptor with the draft or defer uploading until destination resolution succeeds.

AGENTS.md reference: AGENTS.md:L219-L226

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38f7092899

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +88 to +92
val codecInfo = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos
.firstOrNull { info ->
info.isEncoder && info.supportedTypes.any { it.equals(videoMimeType, true) } &&
info.getCapabilitiesForType(videoMimeType).videoCapabilities
.isSizeSupported(videoWidth, videoHeight)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Select an encoder that accepts the supplied color format

On devices where the first size-capable AVC encoder supports only surface or vendor-specific color formats, but a later software encoder supports YUV420 input, this selection stops at the first encoder and the subsequent color-format lookup throws. Voice-note packaging then fails even though the device has a usable encoder; include the accepted color-format check when selecting the codec or iterate through all size-capable candidates.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 29, 2026

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 :bot: Jude's code review agent

Reviewed exact PR head b77ad12a50839219011f7d331c69582153fea90d against current origin/main 76cd4294f65bde33eff031315509e81deccf7c9d. Requesting changes for one mergeability gate failure and one accessibility defect.

Required changes

  1. Blocking — this PR is currently unmergeable against current main.

    Evidence: after a fresh fetch, gh pr view 8665 --json headRefOid,mergeStateStatus,mergeable reported head b77ad12a50839219011f7d331c69582153fea90d, mergeStateStatus: DIRTY, and mergeable: CONFLICTING. git merge-tree --write-tree origin/main HEAD exits 1 with a content conflict in mobile/lib/features/channels/channel_detail_page.dart. Current main has the visible-typing path using visibleTypingProvider(... selfPubkey: currentPubkey, include: ...), while this PR changes the same block to include default-agent threaded typing via channelTypingProvider(channel.id) and manual self-filtering.

    Author action: rebase or merge current main, resolve channel_detail_page.dart, preserve both current main's visible-typing/self-filtering behavior and this PR's default-agent threaded typing behavior, then rerun the affected Mobile and Mobile Swift gates on the new head.

  2. Medium — saved/retry voice-note states expose a false screen-reader instruction.

    Evidence: DefaultAgentVoiceControl treats recording, finalizing, sending, pending saved voice notes, and restore-failed states as active (mobile/lib/features/channels/default_agent_voice_control.dart:271-276), but the outer Semantics always advertises Hold to record a voice note. Release to send. Slide up to cancel. (mobile/lib/features/channels/default_agent_voice_control.dart:302-309). In pending/restore-failed/sending states, start() explicitly refuses to record (mobile/lib/features/channels/default_agent_voice_control.dart:172-180), while the visible controls are Retry/Discard/Sending (mobile/lib/features/channels/default_agent_voice_control.dart:394-436). After a delivery failure or failed saved-draft restore, VoiceOver users can be told to hold/release/slide even though the required recovery action is Retry or Discard.

    Author action: make the semantics state-specific. The hold/slide hint and Record voice note custom action should only be exposed when recording can actually start; recording should expose recording/release/cancel semantics; pending/restore-failed/sending states should expose only the actual Retry/Discard/Sending semantics. Add widget semantics regression coverage for delivery-failed and restore-failed states proving the hold/slide hint is absent and Retry/Discard remain actionable.

Verification noted

  • GitHub status rollup at b77ad12a50839219011f7d331c69582153fea90d showed relevant Mobile, Mobile Android, Mobile Swift native/release/results, DCO, Semgrep, and zizmor checks green; gh pr checks reported no non-success/non-skipped checks.
  • Local reviewer validation at exact head included just mobile-install, just mobile-check, and focused affected Flutter tests covering default_agent_voice_control, default_agent_voice_draft, default_agent_launcher, send_message_provider, and home_page.
  • Physical device/simulator live long-press voice-note smoke was not part of this review; after the rebase/conflict resolution, use risk judgment to rerun live gesture validation on the final head.

A new head is required before this can be approved; this exact-head review does not carry forward automatically.

if (!context.mounted || !destinationIsCurrent()) {
throw StateError('The community changed.');
}
await session.publish(previousEvent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P2 · Accepted voice-note retries disappear from cached timelines

If the relay accepts the first send but its EVENT echo and acknowledgement are lost, SendMessage rolls back the optimistic row while the signed event remains in the draft. Retrying here publishes that event directly. The relay's duplicate path returns success without broadcasting another EVENT, and this branch bypasses the local add/complete bookkeeping. Reopening a previously loaded conversation within its 30-second subscription-retention window does not reload history, so the draft is cleared and the chat opens without the successfully sent voice note.

User/system effect: successful recovery appears to have sent nothing, which can lead the user to record and send the same note again.

Test: accept the initial publication while dropping its echo/OK, retry before the cached timeline detaches, and assert that the accepted event appears once. This finding is based on source tracing; dependency DNS failure prevented running the prepared Flutter probe.

klopez4212 added a commit that referenced this pull request Oct 30, 2026
…8731)

Messages with a reply footer now keep even vertical text padding unless
they have actual overlapping reactions. Agent typing indicators are now
visible in both one-to-one and group DMs when the response is
thread-scoped, regardless of Bestie selection.

The typing fix originated in #8665 and now applies to every agent DM
using the existing agent classification. It retains main's
`visibleTypingProvider` filtering. Ordinary channels and human
thread-typing signals retain their existing scope; voice recording is
not included.

Validation: required pre-push hooks passed, including mobile formatting,
analysis, tests, native crypto package tests, and file-size checks.
Flutter analysis and the full mobile suite pass (3,995 passed, 28
skipped). Widget coverage compares bubble heights with and without a
reply footer and checks typing across Solo, ordinary channels, agent
DMs, human DMs, and group DMs with agents.

Marked ready for review at the owner’s request. Device verification
remains pending. To try: open a DM with any agent, including one not
selected as Bestie, while it handles a thread-scoped response and check
for its avatar and three dots; repeat in a group DM with an agent;
compare equal text in messages with and without a reply footer, with no
reactions, and confirm equal bubble heights.

The broad `just ci` check was stopped during the unrelated desktop Rust
dependency build after the mobile pre-push checks passed; no
full-repository CI pass is claimed.



### Before / after

Flutter widget-harness snapshots with identical synthetic data; not
native device captures. Before: the reply-footer bubble has excess
bottom padding and the thread-scoped Bestie typing signal is hidden.
After: both text bubbles have equal height, and Bestie’s avatar and
three dots are visible.

| Before | After |
| --- | --- |
| ![Before: uneven bubble padding and hidden Bestie
typing](https://github.com/user-attachments/assets/b7b9279d-4b6d-4263-8b4c-91451ee57fd0)
| ![After: balanced bubbles and visible Bestie
typing](https://github.com/user-attachments/assets/c97be4fd-5fc0-47e5-8b78-5b0b9c60254a)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
matt2e added a commit that referenced this pull request Oct 30, 2026
…rt (#8741)

## Summary

Two RunnerTests fixes from the 23–30 Oct flake audit, both in
`mobile/ios/RunnerTests/RunnerTests.swift`. Both are test-only. This is
one of six PRs split from the reviewed `flakey-tests` branch (siblings
below).

### `testLargeTitleTextFadesWithScrollAndCompactTitleCanReverse` (3
runs)

- **Runs:** 40748163526 (job 123028155856), 40826294064 (#8679, job
123290415194, passed on attempt 2) and 40921441156 (#8694, job
123617149320), all in Mobile (iOS), attempt 1. None of the branches
touches the navigation bar. Signature:
`XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99)` read
0.9999499, 1.0 and 1.0.
- **Cause:** collapsing adds a 0.18 s `CABasicAnimation` on the compact
title mask's opacity (`IosNavigationBar.swift` `applyScroll`). The test
slept 60 ms and expected the presentation opacity to be mid-fade. The
presentation layer follows wall-clock media time. When the Flutter app
in the test host keeps the main thread busy, `Task.sleep` resumes after
the fade has finished.
- **Change:** drop the 60 ms sleep and the mid-fade samples. Right after
`scroll(to: boundary)`, which runs the handler synchronously, check that
the mask's model opacity is 1. Also check that it carries a
`"titleFade"` opacity `CABasicAnimation` from 0 to 1, lasting 0.1–0.5 s.
Nothing turns the run loop before the check, so the animation is still
attached however long the main thread stalls. After the scroll back, a
new `XCTAssertNil` checks that reversing removes the fade. The later 240
ms waits and their end-state checks are unchanged.
- **Mutations that should fail it:**
- Delete `compactTitleMask.add(fade, forKey: "titleFade")`: the
`XCTUnwrap` throws.
- Delete `removeAllAnimations()`: the new `XCTAssertNil` fails. The old
test missed this.
  - Set `fade.duration = 0.001`: the band's lower bound fails.

### `testNativeRemovalConfirmationUsesDestructiveAlertAndCancelsOnce` (1
run)

- **Run:** 40950548609 (#8433, Dart only, job 123717768224), attempt 1.
Signature: `fulfillment(of: [completed], timeout: 3)` expired.
- **Cause:** the busy test-host main thread that #8678 handled for other
classes. The cancel reply comes from UIKit's dismissal completion, after
the dismiss animation runs on that thread.
- **Change:** raise the wait to 10 s, matching #8678's
`callbackTimeout`. The assertion is unchanged. Dropping `resolve` from
the dismissal completion still fails it, and so does resolving before
`alert.dismiss`.

### Related issue

None found. **Conflicts with #8568** ("Adapt iPhone Duo layouts, rail
navigation, and native sheets"). #8568 rewrites the fade test into a
loop but keeps the 60 ms sleep and the mid-fade samples. It already
conflicts with `main`, and this PR adds `RunnerTests.swift` to its
conflicts. #8665 only adds a voice-note test.

### Testing

Not compiled against the app or run locally, because Xcode and
RunnerTests bog this machine down. `swiftc -parse` accepts the file. The
new assertions, copied into a stub `XCTestCase`, typecheck with `swiftc
-typecheck` against the iOS simulator SDK and XCTest (Swift 5 mode). The
full file needs Flutter and the Buzz module, so it wasn't typechecked as
a whole. **The Mobile (iOS) lane is the check.**

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@presentyourlove

Copy link
Copy Markdown

Hi @klopez4212,

I noticed this PR had merge conflicts with the latest main, particularly following the merge of #8884 (unifying the 256px merged New menu capsule and _MenuNudge on iOS).

I've resolved all conflicts on a dedicated branch here:
👉 Branch: fix/mobile-default-agent-voice-8665

Summary of Conflict Reconciliations:

  1. MessagesNavigationBar & iOS Merged Capsule:
    • Reconciled your hold-to-record AgentNavigationOverlay with Mobile: open Solo chats with Bestie and workspace controls #8884's 256px capsule and _MenuNudge.
    • When recording is inactive or New menu opens, _MenuNudge animates normally with the 72px clip.
    • When the voice hold starts, other capsule actions fade out (opacity: 0), and the agent pill freely expands leftwards across the full 256px without clipping.
  2. HomePage Structure:
    • Kept the community switcher flight key SizedBox(key: avatarKey) from main while hiding the UIKit view when recordingFocus is active.
    • Preserved onRecordingChanged wired into MessagesNavigationBar inside Scaffold.bottomNavigationBar, and kept the voice-recording-focus backdrop blur in the body Stack.
  3. default_agent_launcher.dart & MessagesFilters:
    • Preserved EmptyBestieAvatar and showFeedbackSnackBar from main while keeping DefaultAgentVoiceControl.
    • Preserved useNativeGlass: !recordingFocus.value while dropping the removed hasUnreadSolo.
  4. Relay & Test Signatures:
    • Reconciled FutureOr<void> Function(NostrEvent)? onSigned with validateDelivery across signed_event_relay.dart, channel_sort_manager_test.dart, and read_state_manager_test.dart.

You can rebase or cherry-pick fix/mobile-default-agent-voice-8665 into your branch:

git fetch origin fix/mobile-default-agent-voice-8665
git merge origin/fix/mobile-default-agent-voice-8665

@klopez4212
klopez4212 force-pushed the default-agent-voice-20261028 branch from b77ad12 to 9f53118 Compare November 5, 2026 09:21
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f53118351

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!await folder.exists()) return [];
final recipients = <String>{};
await for (final entry in folder.list()) {
if (entry is! File || !entry.path.endsWith('.json')) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Discover drafts left before manifest creation

If the process is killed or the manifest write/rename fails after save() has written the .identity sidecar and copied the .m4a, this scan ignores both surviving files because it only considers .json entries. After restart the recording has no Retry or Discard affordance and remains permanently stranded; enumerate orphan sidecars/audio files or make every persisted prefix discoverable.

AGENTS.md reference: AGENTS.md:L256-L261

Useful? React with 👍 / 👎.

Comment on lines +95 to +97
await onRecorded(retained);
await draft.clear();
if (context.mounted) pending.value = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P1 · An older send can delete a replacement recording

The sending guard belongs to this widget, but draft.clear() deletes the shared recipient draft unconditionally. On iOS, leave Home while send A awaits its publish acknowledgement, then return. Home removes the covered launcher, so the new control restores A’s saved draft with no active-send guard. Discard it and save a replacement recording. If A’s acknowledgement now arrives, its unmounted open() returns successfully and this cleanup deletes the replacement’s audio and manifest.

User/system effect: A newer unsent voice note is permanently lost when an older send completes.

Test: Hold A’s acknowledgement, navigate away and back, discard A and save B, then acknowledge A. B must remain recoverable. This finding is based on source tracing; I did not run a Flutter or device reproduction.

Comment on lines +145 to +148
final descriptor = await uploadService.uploadVoiceNote(
recording.file,
duration: recording.duration,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P2 · A stalled upload hides the saved-note recovery actions

This new caller supplies neither a cancellation token nor a deadline to uploadVoiceNote. Its HTTP send and response-body wait are unbounded, so a server that keeps the connection open without completing the upload response leaves send() in sending indefinitely. That state hides Retry and Discard, and the navigation overlay also hides Search/New. The recording is saved, but recovery requires remounting the launcher or restarting the app instead of using its recovery controls.

Test: hold the upload response open after receiving the request and verify that bounded failure or cancellation makes the saved recording's Retry/Discard actions reachable. Source review only; I did not run this stalled-server scenario.

klopez4212 and others added 4 commits November 5, 2026 16:38
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
@klopez4212
klopez4212 force-pushed the default-agent-voice-20261028 branch from 9f53118 to 9f0d07a Compare November 6, 2026 00:44
@klopez4212 klopez4212 changed the title feat(mobile): hold the default agent to send voice notes feat(mobile): configurable hold-to-talk for Bestie Nov 6, 2026
if (!context.mounted || !destinationIsCurrent()) {
throw StateError('The community changed.');
}
await session.publish(previousEvent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P1 · Saved voice notes become permanently unretryable after 15 minutes

After a failed publication or lost acknowledgement, this branch republishes the original signed event. The relay rejects timestamps outside its 900-second window before ordinary message deduplication (guard). Retrying after a longer outage or next-day restart therefore always fails, including when the original event was accepted.

User effect: the retained voice note cannot complete recovery. A regression can retain a failed signed note, advance past 15 minutes, and retry with both absent and previously accepted originals.

channel: channel,
content: descriptor.toMarkdownImage(),
mentionPubkeys: [pubkey],
mediaTags: [descriptor.toImetaTag()],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P2 · Membership changes can send private audio to an expanded DM

Destination selection checks the cached two-person roster before upload, but SendMessage fetches current DM recipients. An existing participant can add another member through kind9000 during upload: that path permits member additions to DMs. The sender then includes the new participant without checking the roster against the captured account-and-Bestie pair.

User effect: audio recorded for Bestie is sent into the expanded conversation before its audience is shown. A regression can pause upload, add a participant to the cached DM, then finish upload and inspect the signed recipients.

],
child: NavigationButtonSurface(
child: DefaultAgentVoiceControl(
key: ValueKey((scope, pubkey)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 P2 · Reconnecting can discard the recording in progress

Reconnect invalidates the Bestie preference. During its loading state this launcher substitutes a null pubkey, changing this key even when the account, community and selected Bestie stay the same. The old control unmounts and its cleanup cancels the recorder.

User effect: a transient reconnect while holding Bestie silently loses the ongoing recording. A regression can reconnect during recording while delaying the preference refresh, then release the control.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 :bot: Jude’s code review agent

Verdict: REQUEST CHANGES. Reviewed PR base bd1b17dee8348b21b33abe35ee4eafa8730f8a83 through clean head 9f0d07ad42b6289f53ed309057a15bb50136823a; also checked mergeability against fetched current main 44b3c4823f0cf1f7715f85dbf5566cb64e848162. This is a high-risk mobile change: durable voice drafts, async send/retry, Android/iOS media packaging, and VoiceOver recovery. The feature direction fits VISION_MOBILE.md's no-data-loss and accessibility contracts; these four findings prevent that contract being met.

Author-actionable findings

  1. Current-main merge blocker. git merge-tree --write-tree origin/main HEAD exits 1 with content conflicts in mobile/ios/Runner/AppDelegate.swift, mobile/lib/features/home/home_page.dart, and mobile/lib/features/home/messages_filters.dart; GitHub reports DIRTY. The PR cannot land as-is. Author action: rebase/merge current main, resolve all three while preserving both sides' behavior, and rerun affected Flutter/iOS gates. Verification owner: author for resolution and gates; reviewer for fresh-head merge check.
  2. Android codec fallback fails on capable devices. mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/AndroidVoiceNotePackager.kt:88–105 selects the first AVC encoder supporting the target dimensions, then checks its color formats. If that encoder is surface-only and a later encoder supports YUV420, packaging throws rather than using the valid candidate. This remains open in the existing inline discussion. Author action: select/iterate encoders by both dimensions and supported YUV420 format; test the selection seam with surface-only first and usable second. Verification owner: author for regression and representative Android runtime; reviewer for patch.
  3. Late send can delete a newer saved draft. mobile/lib/features/channels/default_agent_voice_control.dart:82–97 awaits onRecorded(retained) and calls unconditional draft.clear() afterward. If the control unmounts and a new one saves a different note under the same identity before the old send completes, the old completion deletes the replacement. The relay publish ACK is bounded to 8 seconds, not unbounded; the race fits inside that window, and the new caller's media-upload phase can wait longer. See prior discussion. Author action: clear only if the persisted draft still matches this send's generation/event, with a held-send/remount/replacement regression at the production storage seam. Verification owner: author for test; reviewer for source and race check.
  4. Recovery gives VoiceOver an impossible instruction. mobile/lib/features/channels/default_agent_voice_control.dart:302–320 advertises “Hold to record… Release to send. Slide up to cancel” whenever enabled, even while a saved note or restore failure exposes Retry/Discard and start() refuses to record. Author action: make the hint/value state-specific and add a widget semantics regression proving recovery advertises usable actions without the idle hold hint. Verification owner: author for test; reviewer for accessibility recheck.

Validation and scope: At clean 9f0d07a, git diff --check 133c883b…HEAD passed, and git merge-tree --write-tree origin/main HEAD failed with the three conflicts above. Focused Flutter voice-control and launcher widget tests passed (10 tests) at that head. A full local flutter test attempt reached 1,942 tests and timed out at 120 seconds after five failures in notification/settings tests; it was not completed, and those failures have not been established as PR-caused. GitHub's exact-head Mobile, Android, Swift Native/Release and Mobile Checks lanes report pass. “Run Codex Security Review” shows failure in the rollup, but its job conclusion is cancelled after 45 minutes; this is not an established code failure.

Manual/native evidence and residual risk: No exact-head physical iOS VoiceOver or Android encoder/playback journey was observed in this review. The iOS packaging test checks track presence/duration, not decoded playback. These are verification gaps, not additional author defects; reviewer/device and release validation remain outstanding. Re-review the exact new head after fixes, including the merge resolution and all four regressions.

This branch was successfully deployed

No deployments
codex-review — 9f0d07ad Deployed Nov 6, 2026 by klopez4212 via Run Codex Security Review #13291
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.

4 participants