mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +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>
129 lines
4.5 KiB
Dart
129 lines
4.5 KiB
Dart
part of '../compose_bar.dart';
|
|
|
|
Future<void> _sendTextOnlyDraft({
|
|
required BuildContext context,
|
|
required _MarkdownEditingController controller,
|
|
required ObjectRef<Map<String, MentionCandidate>> mentionMap,
|
|
required ObjectRef<int> draftRevision,
|
|
required int submittedDraftRevision,
|
|
required FocusNode focusNode,
|
|
required VoidCallback clearComposer,
|
|
required Future<void> Function() addMentionedNonMembers,
|
|
required _ComposeDraftPayload payload,
|
|
required _OutgoingMentions outgoing,
|
|
required ComposeBarOnSend onSend,
|
|
required ScaffoldMessengerState? messenger,
|
|
}) async {
|
|
TextEditingValue? clearedDraftText;
|
|
Map<String, MentionCandidate>? clearedDraftMentions;
|
|
int? clearedDraftRevision;
|
|
|
|
void restoreClearedDraft() {
|
|
if (!context.mounted ||
|
|
clearedDraftText == null ||
|
|
clearedDraftMentions == null ||
|
|
clearedDraftRevision == null ||
|
|
draftRevision.value != clearedDraftRevision) {
|
|
return;
|
|
}
|
|
controller.value = clearedDraftText;
|
|
mentionMap.value
|
|
..clear()
|
|
..addAll(clearedDraftMentions);
|
|
focusNode.requestFocus();
|
|
}
|
|
|
|
try {
|
|
await addMentionedNonMembers();
|
|
// Clear before optimistic insertion so the outgoing row and draft never
|
|
// appear simultaneously during the send transition. If the user edited
|
|
// while membership changes were pending, preserve that newer draft.
|
|
if (context.mounted && draftRevision.value == submittedDraftRevision) {
|
|
clearedDraftText = controller.value;
|
|
clearedDraftMentions = Map<String, MentionCandidate>.of(mentionMap.value);
|
|
clearComposer();
|
|
clearedDraftRevision = draftRevision.value;
|
|
}
|
|
await onSend(
|
|
payload.content,
|
|
outgoing.pubkeys,
|
|
mediaTags: [...payload.mediaTags, ...outgoing.referenceTags],
|
|
);
|
|
} on StateError {
|
|
restoreClearedDraft();
|
|
_reportSendCancelledByCommunitySwitch(messenger);
|
|
} catch (error) {
|
|
// The caller runs unawaited, so surface publish failures and restore the
|
|
// sent draft unless the user has already started a new one.
|
|
restoreClearedDraft();
|
|
messenger?.showSnackBar(
|
|
SnackBar(content: Text(_composeSendErrorMessage(error))),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _useComposeDraftLifecycle({
|
|
required WidgetRef ref,
|
|
required _MarkdownEditingController controller,
|
|
required String draftKey,
|
|
required String channelId,
|
|
required String? threadHeadId,
|
|
required String draftIdentity,
|
|
required ObjectRef<int> draftRevision,
|
|
required ValueNotifier<List<_PendingAttachment>> attachments,
|
|
required ObjectRef<int> uploadGeneration,
|
|
required ObjectRef<UploadCancellationToken?> activeUploadCancellation,
|
|
required ValueNotifier<int> uploadingCount,
|
|
required ValueNotifier<bool> isSending,
|
|
required ValueNotifier<_AttachmentSurface> attachmentSurface,
|
|
required ValueNotifier<String?> uploadError,
|
|
required _IOSAttachmentPopoverController iosAttachmentPopover,
|
|
required VoidCallback onDraftIdentityChanged,
|
|
}) {
|
|
final lastDraftIdentity = useRef<String?>(null);
|
|
useEffect(() {
|
|
final identityChanged =
|
|
lastDraftIdentity.value != null &&
|
|
lastDraftIdentity.value != draftIdentity;
|
|
lastDraftIdentity.value = draftIdentity;
|
|
final saved = ref.read(composeDraftsProvider.notifier).textFor(draftKey);
|
|
if (identityChanged) {
|
|
draftRevision.value += 1;
|
|
onDraftIdentityChanged();
|
|
uploadGeneration.value += 1;
|
|
activeUploadCancellation.value?.cancel();
|
|
activeUploadCancellation.value = null;
|
|
uploadingCount.value = 0;
|
|
isSending.value = false;
|
|
attachmentSurface.value = _AttachmentSurface.closed;
|
|
uploadError.value = null;
|
|
unawaited(iosAttachmentPopover.dispose());
|
|
final staleAttachments = attachments.value;
|
|
attachments.value = const [];
|
|
unawaited(_deleteOwnedAttachments(staleAttachments));
|
|
controller.text = saved ?? '';
|
|
} else if (saved != null && controller.text.isEmpty) {
|
|
controller.text = saved;
|
|
}
|
|
|
|
var lastPersistedText = controller.text;
|
|
void persistDraft() {
|
|
final text = controller.text;
|
|
if (text == lastPersistedText) return;
|
|
lastPersistedText = text;
|
|
draftRevision.value += 1;
|
|
ref
|
|
.read(composeDraftsProvider.notifier)
|
|
.save(
|
|
key: draftKey,
|
|
channelId: channelId,
|
|
threadHeadId: threadHeadId,
|
|
text: text,
|
|
);
|
|
}
|
|
|
|
controller.addListener(persistDraft);
|
|
return () => controller.removeListener(persistDraft);
|
|
}, [controller, draftKey, draftIdentity, onDraftIdentityChanged]);
|
|
}
|