Skip to content

Commit e2100f7

Browse files
joeblauclaude
andauthored
feat: mute the mic from a bottom-right FAB while live (#26)
Add a floating mic mute/unmute button pinned to the bottom-trailing corner, shown only while a broadcast is live (when muting actually matters). It toggles micVolume between 0 and the pre-mute level and posts micVolumeSignal, reusing the exact live-apply path the Settings mic-volume slider already drives, so a running broadcast responds instantly. Muted state swaps to a red mic.slash glyph. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fe7c954 commit e2100f7

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

Stream/ContentView.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,40 @@ struct ContentView: View {
2020

2121
@State private var showingSettings = false
2222

23+
/// The mic gain to restore when the user un-mutes from the FAB. Captured the
24+
/// moment they mute so toggling back returns to their chosen level, not unity.
25+
@State private var preMuteVolume: Double = 1.0
26+
2327
/// Persists `settings` into the shared App Group suite. Called on every edit.
2428
private func persist() {
2529
SettingsStore().save(settings)
2630
}
2731

32+
/// A gain at/below this reads as muted — matches the Settings "Muted" label.
33+
private var isMicMuted: Bool { settings.micVolume <= 0.0001 }
34+
35+
/// Toggles the microphone between muted (gain 0) and the last chosen level.
36+
/// Persists and posts the live-apply signal so a running broadcast responds
37+
/// immediately, exactly like the Settings mic-volume slider does.
38+
private func toggleMicMute() {
39+
Haptics.tap()
40+
if isMicMuted {
41+
settings.micVolume = preMuteVolume > 0.0001 ? preMuteVolume : 1.0
42+
} else {
43+
preMuteVolume = settings.micVolume
44+
settings.micVolume = 0
45+
}
46+
persist()
47+
BroadcastControl.post(BroadcastControl.micVolumeSignal)
48+
}
49+
2850
var body: some View {
2951
NavigationStack {
3052
ChatFeedView(chat: chat)
53+
.overlay(alignment: .bottomTrailing) {
54+
if capture.isLive { micFAB }
55+
}
56+
.animation(.spring(duration: 0.3, bounce: 0.2), value: capture.isLive)
3157
.navigationTitle("Stream")
3258
.navigationBarTitleDisplayMode(.inline)
3359
.toolbar {
@@ -79,6 +105,26 @@ struct ContentView: View {
79105
.accessibilityLabel(capture.isLive ? "Stop broadcast" : "Start broadcast")
80106
}
81107

108+
// MARK: - Mute FAB
109+
110+
/// Floating mic mute/unmute control, pinned to the bottom-trailing corner while
111+
/// a broadcast is live. Uses the platform glass so it reads as a control sitting
112+
/// above the chat feed; turns red-tinted and swaps to `mic.slash` when muted.
113+
private var micFAB: some View {
114+
Button(action: toggleMicMute) {
115+
Image(systemName: isMicMuted ? "mic.slash.fill" : "mic.fill")
116+
.font(.system(size: 22, weight: .semibold))
117+
.foregroundStyle(isMicMuted ? Color.red : Color.primary)
118+
.frame(width: 60, height: 60)
119+
.contentTransition(.symbolEffect(.replace))
120+
}
121+
.glassEffect(.regular.interactive(), in: .circle)
122+
.padding(.trailing, 20)
123+
.padding(.bottom, 20)
124+
.accessibilityLabel(isMicMuted ? "Unmute microphone" : "Mute microphone")
125+
.transition(.scale.combined(with: .opacity))
126+
}
127+
82128
// MARK: - Setup banner
83129

84130
/// Slim call-to-action shown when the connection isn't ready to publish. Tapping

0 commit comments

Comments
 (0)