Files
buzz/mobile/ios/Runner/ConcentricSheetSurface.swift
klopez4212 8c0f42e187 Add mobile Huddles voice MVP (#6056)
## 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>
2026-08-22 05:28:16 -07:00

135 lines
3.9 KiB
Swift

import Flutter
import UIKit
final class ConcentricSheetSurfaceFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
FlutterStandardMessageCodec.sharedInstance()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
ConcentricSheetSurfacePlatformView(
frame: frame,
viewIdentifier: viewId,
messenger: messenger,
arguments: args
)
}
}
final class ConcentricSheetSurfacePlatformView: NSObject, FlutterPlatformView {
private let rootView: UIView
private let surfaceView: UIView
private let channel: FlutterMethodChannel
init(
frame: CGRect,
viewIdentifier viewId: Int64,
messenger: FlutterBinaryMessenger,
arguments args: Any?
) {
let arguments = args as? [String: Any]
let colorValue = (arguments?["color"] as? NSNumber)?.uint32Value ?? 0xFFFF_FFFF
let backdropColorValue = (arguments?["backdropColor"] as? NSNumber)?.uint32Value
let minimumRadius = (arguments?["minimumRadius"] as? NSNumber)?.doubleValue ?? 24
let corners = arguments?["corners"] as? String ?? "all"
rootView = UIView(frame: frame)
rootView.isOpaque = backdropColorValue != nil
rootView.backgroundColor = backdropColorValue.map { Self.color(from: $0) } ?? .clear
surfaceView = UIView(frame: rootView.bounds)
surfaceView.isOpaque = true
surfaceView.backgroundColor = Self.color(from: colorValue)
surfaceView.clipsToBounds = true
surfaceView.layer.cornerCurve = .continuous
surfaceView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
rootView.addSubview(surfaceView)
channel = FlutterMethodChannel(
name: "buzz/concentric_sheet_surface/\(viewId)",
binaryMessenger: messenger
)
if #available(iOS 26.0, *) {
let radius = UICornerRadius.containerConcentric(minimum: minimumRadius)
surfaceView.cornerConfiguration =
corners == "bottom"
? .uniformBottomRadius(
radius,
topLeftRadius: nil,
topRightRadius: nil
)
: .uniformCorners(radius: radius)
} else {
surfaceView.layer.cornerRadius = minimumRadius
if corners == "bottom" {
surfaceView.layer.maskedCorners = [
.layerMinXMaxYCorner,
.layerMaxXMaxYCorner,
]
}
}
super.init()
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == "updateColors" else {
result(FlutterMethodNotImplemented)
return
}
guard
let arguments = call.arguments as? [String: Any],
let colorValue = arguments["color"] as? NSNumber
else {
result(
FlutterError(
code: "invalid_arguments",
message: "Expected a surface color.",
details: nil
)
)
return
}
self?.updateColors(
colorValue: colorValue.uint32Value,
backdropColorValue: (arguments["backdropColor"] as? NSNumber)?.uint32Value
)
result(nil)
}
}
deinit {
channel.setMethodCallHandler(nil)
}
func view() -> UIView {
rootView
}
private func updateColors(colorValue: UInt32, backdropColorValue: UInt32?) {
surfaceView.backgroundColor = Self.color(from: colorValue)
rootView.isOpaque = backdropColorValue != nil
rootView.backgroundColor = backdropColorValue.map { Self.color(from: $0) } ?? .clear
}
private static func color(from value: UInt32) -> UIColor {
let alpha = CGFloat((value >> 24) & 0xFF) / 255
let red = CGFloat((value >> 16) & 0xFF) / 255
let green = CGFloat((value >> 8) & 0xFF) / 255
let blue = CGFloat(value & 0xFF) / 255
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}