mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 21:51:13 +00:00
* feat(ios): unify chat and voice experience * refactor(ios): finish unified chat ownership * fix(ios): unify chat and voice capture ownership * fix(ios): serialize Talk permission hydration * fix(ios): clear voice CI regressions * fix(ios): clear remaining unified chat CI failures * chore(i18n): sync native source inventory * refactor(ios): defer provider-only realtime voice * fix(ios): clean unified chat voice PR * fix(ios): localize unified voice controls * fix(ios): serialize composer audio controls * fix(ios): isolate dictation preparation lifecycle * fix(ios): key chat tab icons by appearance * fix(ios): preserve talk upgrade handshakes * fix(ios): refresh native i18n inventory * fix(ios): reconcile shared locale artifacts * fix(ios): satisfy chat view lint limit * fix(ios): reconcile shared composer implementation * test(apple): avoid XCTest actor deinit crash * fix(ios): satisfy SwiftFormat scope spacing * fix(ios): remove duplicate merged helper declarations
115 lines
3.7 KiB
Swift
115 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
/// Host-provided voice-note recording state for the shared chat composer.
|
|
public struct OpenClawChatVoiceNoteControl {
|
|
public var recorder: OpenClawVoiceNoteRecorder
|
|
public var isTalkActive: Bool
|
|
|
|
public init(recorder: OpenClawVoiceNoteRecorder, isTalkActive: Bool) {
|
|
self.recorder = recorder
|
|
self.isTalkActive = isTalkActive
|
|
}
|
|
}
|
|
|
|
struct OpenClawVoiceNoteButton: View {
|
|
let control: OpenClawChatVoiceNoteControl
|
|
let compact: Bool
|
|
let isComposerEnabled: Bool
|
|
let isAttachmentInputEnabled: Bool
|
|
|
|
var isRecordingEnabled: Bool {
|
|
self.isComposerEnabled
|
|
&& self.isAttachmentInputEnabled
|
|
&& !self.control.isTalkActive
|
|
&& !self.control.recorder.isRequestingPermission
|
|
}
|
|
|
|
var body: some View {
|
|
Button {
|
|
Task { await self.control.recorder.start() }
|
|
} label: {
|
|
if self.control.recorder.isRequestingPermission {
|
|
ProgressView()
|
|
.controlSize(.mini)
|
|
} else {
|
|
Image(systemName: "mic")
|
|
.font(OpenClawChatTypography.display(size: 15, weight: .semibold, relativeTo: .subheadline))
|
|
}
|
|
}
|
|
.help("Record Voice Note")
|
|
.accessibilityLabel("Record voice note")
|
|
.accessibilityIdentifier("chat-voice-note-record")
|
|
.modifier(VoiceNoteButtonChrome(compact: self.compact))
|
|
.controlSize(.small)
|
|
.foregroundStyle(.secondary)
|
|
.contentShape(Rectangle())
|
|
.disabled(!self.isRecordingEnabled)
|
|
}
|
|
}
|
|
|
|
struct OpenClawVoiceNoteRecordingRow: View {
|
|
let recorder: OpenClawVoiceNoteRecorder
|
|
var embedded = false
|
|
|
|
var body: some View {
|
|
HStack(spacing: 10) {
|
|
Circle()
|
|
.fill(OpenClawChatTheme.danger)
|
|
.frame(width: 9, height: 9)
|
|
|
|
// Live capture wave replaces a static "Recording" label; the level is
|
|
// real recorder metering, so silence reads flat and speech moves.
|
|
TalkWaveformView(phase: .listening(level: self.recorder.level, speechActive: false))
|
|
.frame(maxWidth: .infinity, minHeight: 26, maxHeight: 26)
|
|
.accessibilityLabel("Recording")
|
|
|
|
Text(openClawVoiceNoteDurationLabel(self.recorder.elapsedSeconds))
|
|
.font(OpenClawChatTypography.mono(size: 13, relativeTo: .footnote))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
Button {
|
|
self.recorder.cancel()
|
|
} label: {
|
|
Image(systemName: "xmark")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.small)
|
|
.accessibilityLabel("Cancel voice note")
|
|
|
|
Button {
|
|
self.recorder.finish()
|
|
} label: {
|
|
Image(systemName: "checkmark")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.small)
|
|
.accessibilityLabel("Finish voice note")
|
|
}
|
|
.padding(.horizontal, self.embedded ? 2 : 14)
|
|
.padding(.vertical, self.embedded ? 2 : 10)
|
|
.background {
|
|
if !self.embedded {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(OpenClawChatTheme.composerField)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.strokeBorder(OpenClawChatTheme.composerBorder))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct VoiceNoteButtonChrome: ViewModifier {
|
|
let compact: Bool
|
|
|
|
func body(content: Content) -> some View {
|
|
if self.compact {
|
|
content.buttonStyle(.plain)
|
|
} else {
|
|
content.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
}
|