mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
## 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>
109 lines
3.2 KiB
Dart
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,
|
|
);
|
|
}
|