mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
## Summary - add foreground mobile Huddles on Android and iOS with native Opus capture/playback, mute, speaker routing, participants, lifecycle, and minimized drawer UI - keep mobile Huddle cards and roster state live, including ended rooms, relay-resolved profiles, and agents - broadcast desktop agent TTS through the existing Huddle audio protocol ## Scope Foreground human-to-human voice MVP only. Agent setup/transcripts, background calling, recording, and advanced device controls remain out of scope. ## Validation - `just mobile-check` - `just mobile-test` — 1,500 passed - `just desktop-check` and `just desktop-test` — 4,957 passed - desktop typecheck, strict Clippy, and Tauri tests — 2,445 passed, 15 ignored - mobile worktree identity contract checks - physical Pixel/iPhone behavior reviewed during development --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
92 lines
3.0 KiB
Dart
92 lines
3.0 KiB
Dart
part of 'channel_management_provider.dart';
|
|
|
|
const _huddleReactionNameMax = 48;
|
|
|
|
String _clampHuddleReactionName(String name) {
|
|
final trimmed = name.trim();
|
|
if (trimmed.length <= _huddleReactionNameMax) return trimmed;
|
|
return '${trimmed.substring(0, _huddleReactionNameMax - 1).trimRight()}…';
|
|
}
|
|
|
|
/// Canonical Huddle lifecycle operations built on the shared channel actions.
|
|
extension HuddleChannelActions on ChannelActions {
|
|
/// Creates the private, one-hour stream used only by Huddle media.
|
|
Future<String> createHuddleBackingChannel() async {
|
|
final channelId = _newUuidV4();
|
|
await _signedEventRelay.submit(
|
|
kind: 9007,
|
|
content: '',
|
|
tags: buildCreateChannelTags(
|
|
channelId: channelId,
|
|
name: 'huddle-${channelId.substring(0, 8)}',
|
|
channelType: 'stream',
|
|
visibility: 'private',
|
|
ttlSeconds: 3600,
|
|
),
|
|
);
|
|
// The accepted kind:9007 write has already committed creator membership
|
|
// on the relay. Avoid refreshing the global channel list here: the backing
|
|
// channel is intentionally hidden, and racing its membership fan-out can
|
|
// temporarily make the parent channel appear read-only.
|
|
return channelId;
|
|
}
|
|
|
|
/// Publishes the canonical creator-signed Huddle start event in its parent.
|
|
Future<NostrEvent> announceHuddleStarted({
|
|
required String parentChannelId,
|
|
required String ephemeralChannelId,
|
|
}) async {
|
|
NostrEvent? signed;
|
|
await _signedEventRelay.submit(
|
|
kind: EventKind.huddleStarted,
|
|
content: jsonEncode({'ephemeral_channel_id': ephemeralChannelId}),
|
|
tags: [
|
|
['h', parentChannelId],
|
|
],
|
|
onSigned: (event) => signed = event,
|
|
);
|
|
return signed ??
|
|
(throw StateError('Huddle start event was not signed locally'));
|
|
}
|
|
|
|
/// Publishes the canonical creator-signed Huddle end event in its parent.
|
|
Future<void> announceHuddleEnded({
|
|
required String parentChannelId,
|
|
required String ephemeralChannelId,
|
|
}) async {
|
|
await _signedEventRelay.submit(
|
|
kind: EventKind.huddleEnded,
|
|
content: jsonEncode({'ephemeral_channel_id': ephemeralChannelId}),
|
|
tags: [
|
|
['h', parentChannelId],
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Sends an ephemeral emoji burst to the active Huddle backing channel.
|
|
Future<void> sendHuddleReaction({
|
|
required String channelId,
|
|
required String emoji,
|
|
required String senderName,
|
|
}) async {
|
|
final normalizedEmoji = emoji.trim();
|
|
if (normalizedEmoji.isEmpty) return;
|
|
final shortcode = normalizeShortcode(normalizedEmoji);
|
|
final emojiUrl = reactionEmojiUrl(
|
|
normalizedEmoji,
|
|
_ref.read(customEmojiListProvider),
|
|
);
|
|
await _signedEventRelay.submit(
|
|
kind: EventKind.huddleReaction,
|
|
content: normalizedEmoji,
|
|
tags: [
|
|
['h', channelId],
|
|
['reaction', normalizedEmoji],
|
|
['sender_name', _clampHuddleReactionName(senderName)],
|
|
if (shortcode != null && emojiUrl != null)
|
|
['emoji', shortcode, emojiUrl],
|
|
],
|
|
);
|
|
}
|
|
}
|