From 6a5061959f90890ac12f8ddd068e06e763b7b56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:07:53 +0200 Subject: [PATCH 1/2] Move basic views --- .../UI/Buttons/AsyncButton.swift | 63 +++++++++++++++++++ .../UI/Chat/ChatScrollView.swift | 62 ++++++++++++++++++ .../UI/Visualizer/BarAudioVisualizer.swift | 11 ++++ 3 files changed, 136 insertions(+) create mode 100644 Sources/LiveKitComponents/UI/Buttons/AsyncButton.swift create mode 100644 Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift diff --git a/Sources/LiveKitComponents/UI/Buttons/AsyncButton.swift b/Sources/LiveKitComponents/UI/Buttons/AsyncButton.swift new file mode 100644 index 00000000..4a528158 --- /dev/null +++ b/Sources/LiveKitComponents/UI/Buttons/AsyncButton.swift @@ -0,0 +1,63 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +/// A drop-in replacement `Button` that executes an async action and shows a busy label when in progress. +/// +/// - Parameters: +/// - action: The async action to execute. +/// - label: The label to show when not busy. +/// - busyLabel: The label to show when busy. Defaults to an empty view. +public struct AsyncButton: View { + private let action: () async -> Void + + @ViewBuilder private let label: Label + @ViewBuilder private let busyLabel: BusyLabel + + @State private var isBusy = false + + public init( + action: @escaping () async -> Void, + @ViewBuilder label: () -> Label, + @ViewBuilder busyLabel: () -> BusyLabel = EmptyView.init + ) { + self.action = action + self.label = label() + self.busyLabel = busyLabel() + } + + public var body: some View { + Button { + isBusy = true + Task { + await action() + isBusy = false + } + } label: { + if isBusy { + if busyLabel is EmptyView { + label + } else { + busyLabel + } + } else { + label + } + } + .disabled(isBusy) + } +} diff --git a/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift b/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift new file mode 100644 index 00000000..c31113c8 --- /dev/null +++ b/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift @@ -0,0 +1,62 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import LiveKit +import SwiftUI + +public struct ChatScrollView: View { + public typealias MessageBuilder = (ReceivedMessage) -> Content + + @LKConversation private var conversation + @ViewBuilder private let messageBuilder: MessageBuilder + + public init(messageBuilder: @escaping MessageBuilder) { + self.messageBuilder = messageBuilder + } + + public var body: some View { + ScrollViewReader { scrollView in + ScrollView { + LazyVStack { + ForEach(conversation.messages.values.reversed()) { message in + messageBuilder(message) + .upsideDown() + .id(message.id) + } + } + } + .onChange(of: conversation.messages.count) { _ in + scrollView.scrollTo(conversation.messages.keys.last) + } + .upsideDown() + .animation(.default, value: conversation.messages) + } + } +} + +private struct UpsideDown: ViewModifier { + func body(content: Content) -> some View { + content + .rotationEffect(.radians(Double.pi)) + .scaleEffect(x: -1, y: 1, anchor: .center) + } +} + +private extension View { + func upsideDown() -> some View { + modifier(UpsideDown()) + } +} diff --git a/Sources/LiveKitComponents/UI/Visualizer/BarAudioVisualizer.swift b/Sources/LiveKitComponents/UI/Visualizer/BarAudioVisualizer.swift index e274d0f6..296859a9 100644 --- a/Sources/LiveKitComponents/UI/Visualizer/BarAudioVisualizer.swift +++ b/Sources/LiveKitComponents/UI/Visualizer/BarAudioVisualizer.swift @@ -108,6 +108,17 @@ public struct BarAudioVisualizer: View { animationProperties = PhaseAnimationProperties(barCount: barCount) } + public init(agent: Agent?, + barColor: Color = .primary, + barCount: Int = 5, + barCornerRadius: CGFloat = 100, + barSpacingFactor: CGFloat = 0.015, + barMinOpacity: CGFloat = 0.16, + isCentered: Bool = true) + { + self.init(audioTrack: agent?.audioTrack, agentState: agent?.state ?? .listening, barColor: barColor, barCount: barCount, barCornerRadius: barCornerRadius, barSpacingFactor: barSpacingFactor, barMinOpacity: barMinOpacity, isCentered: isCentered) + } + public var body: some View { GeometryReader { geometry in let highlightingSequence = animationProperties.highlightingSequence(agentState: agentState) From f030a03f4dde24675fdc2227541e03699395c556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:07:25 +0200 Subject: [PATCH 2/2] Rename --- Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift b/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift index c31113c8..0e5a5e11 100644 --- a/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift +++ b/Sources/LiveKitComponents/UI/Chat/ChatScrollView.swift @@ -20,7 +20,7 @@ import SwiftUI public struct ChatScrollView: View { public typealias MessageBuilder = (ReceivedMessage) -> Content - @LKConversation private var conversation + @LiveKitConversation private var conversation @ViewBuilder private let messageBuilder: MessageBuilder public init(messageBuilder: @escaping MessageBuilder) {