Files
buzz/mobile/lib/features/channels/compose_bar/voice_note.dart
klopez4212 6f5ec4a731 Add mobile voice notes (#7121)
## Summary
- add iOS and Android voice-note recording and preview directly in the
mobile composer
- add waveform playback, scrubbing, speed controls, haptics, and
one-shot playback in chat
- package recordings in a canonical H.264/AAC MP4 envelope on both
platforms so existing relays accept them
- preserve the shared composer interaction and attachment-card treatment
across mobile platforms

Mobile counterpart to #6978.

## Testing
- `just ci`
- `just mobile-check`
- `just mobile-test` (2,026 tests)
- Android debug build compiled, installed, launched, and Voice note
verified in the attachment menu on Pixel 10
- signed iOS device build installed on iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-09-03 08:07:04 +00:00

109 lines
3.2 KiB
Dart

part of '../compose_bar.dart';
class _ComposerVoiceNote {
const _ComposerVoiceNote({
required this.start,
required this.onKeyboardHidden,
required this.onDraftIdentityChanged,
required ValueNotifier<bool> isPreparing,
required ValueNotifier<bool> isRecording,
required ValueChanged<VoiceNoteRecording> onRecorded,
}) : _isPreparing = isPreparing,
_isRecording = isRecording,
_onRecorded = onRecorded;
final VoidCallback start;
final VoidCallback onKeyboardHidden;
final VoidCallback onDraftIdentityChanged;
final ValueNotifier<bool> _isPreparing;
final ValueNotifier<bool> _isRecording;
final ValueChanged<VoiceNoteRecording> _onRecorded;
bool get isPreparing => _isPreparing.value;
bool get isRecording => _isRecording.value;
Widget? get recorder => isRecording
? VoiceNoteComposerRecorder(
onCancel: () => _isRecording.value = false,
onRecorded: _onRecorded,
)
: null;
}
bool _voiceNoteFullWidth(
_ComposerVoiceNote voiceNote,
List<_PendingAttachment> attachments,
) =>
voiceNote.isPreparing ||
voiceNote.isRecording ||
attachments.any((item) => item.kind == _PendingAttachmentKind.voiceNote);
_ComposerVoiceNote _useComposerVoiceNote({
required BuildContext context,
required WidgetRef ref,
required FocusNode focusNode,
required ValueNotifier<bool> isComposerExpanded,
required ValueNotifier<bool> showFormatting,
required ValueNotifier<_AttachmentSurface> attachmentSurface,
required ValueNotifier<String?> uploadError,
required ObjectRef<int> draftRevision,
required ValueNotifier<List<_PendingAttachment>> attachments,
}) {
final isPreparing = useState(false);
final isRecording = useState(false);
final resetForDraftIdentityChange = useCallback(() {
isPreparing.value = false;
isRecording.value = false;
}, [isPreparing, isRecording]);
void beginRecording() {
if (!isPreparing.value) return;
isPreparing.value = false;
isRecording.value = true;
}
void start() {
if (attachments.value.isNotEmpty) {
uploadError.value = 'A voice note must be the only attachment.';
return;
}
attachmentSurface.value = _AttachmentSurface.closed;
showFormatting.value = false;
isComposerExpanded.value = false;
_dismissComposerKeyboard(focusNode);
if (ref.read(huddleSessionProvider).isInSession) {
uploadError.value = 'Leave the Huddle before recording a voice note.';
return;
}
uploadError.value = null;
draftRevision.value += 1;
isPreparing.value = true;
if (View.of(context).viewInsets.bottom == 0) beginRecording();
}
void complete(VoiceNoteRecording recording) {
draftRevision.value += 1;
uploadError.value = null;
attachments.value = [
...attachments.value,
_PendingAttachment(
file: recording.file,
kind: _PendingAttachmentKind.voiceNote,
deleteAfterUse: true,
duration: recording.duration,
waveform: recording.waveform,
),
];
isRecording.value = false;
}
return _ComposerVoiceNote(
start: start,
onKeyboardHidden: beginRecording,
onDraftIdentityChanged: resetForDraftIdentityChange,
isPreparing: isPreparing,
isRecording: isRecording,
onRecorded: complete,
);
}