Skip to content

Commit 68932f0

Browse files
authored
v5: [Composer Redesign] Voice Recording (#1267)
* Add snack bar notifications on voice recording flow * Recording view redesign * Handle voice recording gestures * Improve gesture animations * Fix shimmering effect in Slide to cancel label * Redesign recording state * Improve lock animation * Do some cleanup and refactoring * Fix keyboard and attachment picker edge cases * Merge `LockedView` and `RecordingView` into `ComposerVoiceRcordingInputView` * Improve hold recording UX * Fix microphone icon to not being filled * Use the ConfirmEditButton when publishing voice recording * Hide lock view after the recording is locked * Hide microphone when recording is stopped * Update trash and stop icon sizes * Fix additional spacing in playback controls * Add accent color to the audio duration text when it is playing * Add custom slider thumb * Add test coverage * Redesign the voice recording composer attachment view * Fix waveform not having the same slider thumb * Overall refactoring and cleanup * Rename composer voice recording attachment view to follow the rest of the patterns * Move VoiceRecording attachment folder * Fix dismiss overlay in composer recording attachment view * Update Message View voice recording attachment * Fix current user voice recording attachment view not having blue borders * Reuse the play-pause button * Fix color of control border colors * Fix swipe to reply being triggered when sliding voice recording progress view * Minor cleanups * PR feedback cleanup * Some more PR cleanups * Refactor gesture location state * Cache the roundedSliderThumbImage * Add active and inactive slider thumb versions * Do not use env object for the view model * Add audio playing snapshots * Re record snapshots * Remove recording label and fix ForEach index * Reuse composer voice recording animation * Fix demo app not compiling * Fix not able to send regular messages * Remove view model from CompoerVoiceRecordingInputVIew * Fix E2E Test for edit button
1 parent 5f003f8 commit 68932f0

File tree

94 files changed

+1651
-707
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+1651
-707
lines changed

DemoAppSwiftUI/AppleMessageComposerView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ struct AppleMessageComposerView<Factory: ViewFactory>: View, KeyboardReadable {
8686
selectedRangeLocation: $viewModel.selectedRangeLocation,
8787
command: $viewModel.composerCommand,
8888
recordingState: $viewModel.recordingState,
89+
recordingGestureLocation: $viewModel.recordingGestureLocation,
8990
composerAssets: viewModel.composerAssets,
9091
addedCustomAttachments: viewModel.addedCustomAttachments,
9192
addedVoiceRecordings: viewModel.addedVoiceRecordings,
@@ -95,12 +96,18 @@ struct AppleMessageComposerView<Factory: ViewFactory>: View, KeyboardReadable {
9596
cooldownDuration: viewModel.cooldownDuration,
9697
hasContent: viewModel.hasContent,
9798
canSendMessage: viewModel.canSendMessage,
99+
audioRecordingInfo: viewModel.audioRecordingInfo,
100+
pendingAudioRecordingURL: viewModel.pendingAudioRecording?.url,
98101
onCustomAttachmentTap: viewModel.customAttachmentTapped(_:),
99102
removeAttachmentWithId: viewModel.removeAttachment(with:),
100103
sendMessage: {},
101104
onImagePasted: viewModel.imagePasted,
102105
startRecording: viewModel.startRecording,
103106
stopRecording: viewModel.stopRecording,
107+
confirmRecording: viewModel.confirmRecording,
108+
discardRecording: viewModel.discardRecording,
109+
previewRecording: viewModel.previewRecording,
110+
showRecordingTip: viewModel.showRecordingTip,
104111
sendInChannelShown: viewModel.sendInChannelShown,
105112
showReplyInChannel: $viewModel.showReplyInChannel
106113
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// Copyright © 2026 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
public struct ComposerVoiceRecordingContainerView: View {
9+
@Injected(\.tokens) private var tokens
10+
@Injected(\.utils) private var utils
11+
12+
@StateObject var voiceRecordingHandler = VoiceRecordingHandler()
13+
14+
var addedVoiceRecordings: [AddedVoiceRecording]
15+
var onDiscardAttachment: (String) -> Void
16+
17+
public init(addedVoiceRecordings: [AddedVoiceRecording], onDiscardAttachment: @escaping (String) -> Void) {
18+
self.addedVoiceRecordings = addedVoiceRecordings
19+
self.onDiscardAttachment = onDiscardAttachment
20+
}
21+
22+
private var player: AudioPlaying {
23+
utils.audioPlayer
24+
}
25+
26+
public var body: some View {
27+
VStack(spacing: tokens.spacingXxs) {
28+
ForEach(addedVoiceRecordings) { recording in
29+
ComposerVoiceRecordingAttachmentView(
30+
handler: voiceRecordingHandler,
31+
recording: recording,
32+
onDiscardAttachment: onDiscardAttachment
33+
)
34+
}
35+
}
36+
.onAppear {
37+
player.subscribe(voiceRecordingHandler)
38+
}
39+
}
40+
}
41+
42+
// MARK: - Single Voice Recording Attachment
43+
44+
struct ComposerVoiceRecordingAttachmentView: View {
45+
@Injected(\.colors) private var colors
46+
@Injected(\.fonts) private var fonts
47+
@Injected(\.tokens) private var tokens
48+
@Injected(\.utils) private var utils
49+
50+
@ObservedObject var handler: VoiceRecordingHandler
51+
52+
let recording: AddedVoiceRecording
53+
var onDiscardAttachment: (String) -> Void
54+
55+
private var isActive: Bool { handler.isActive(for: recording.url) }
56+
private var showContextDuration: Bool { isActive && handler.context.currentTime > 0 }
57+
58+
var body: some View {
59+
HStack(spacing: tokens.spacingXs) {
60+
playButton
61+
contentArea
62+
playbackSpeedToggle
63+
}
64+
.padding(.top, tokens.spacingMd)
65+
.padding(.leading, tokens.spacingSm)
66+
.padding(.bottom, tokens.spacingMd)
67+
.padding(.trailing, tokens.spacingSm)
68+
.frame(height: 72)
69+
.background(Color(colors.backgroundElevationElevation1))
70+
.cornerRadius(tokens.radiusLg)
71+
.overlay(
72+
RoundedRectangle(cornerRadius: tokens.radiusLg)
73+
.strokeBorder(Color(colors.borderCoreOpacity10), lineWidth: 1)
74+
)
75+
.dismissButtonOverlayModifier {
76+
onDiscardAttachment(recording.url.absoluteString)
77+
}
78+
.onReceive(handler.$context) { _ in
79+
handler.updatePlaybackState(for: recording.url)
80+
}
81+
}
82+
83+
// MARK: - Play Button
84+
85+
private var playButton: some View {
86+
PlayPauseButton(isPlaying: handler.isPlaying && isActive) {
87+
handler.togglePlayback(for: recording.url)
88+
}
89+
}
90+
91+
// MARK: - Content Area
92+
93+
private var contentArea: some View {
94+
HStack(spacing: tokens.spacingXs) {
95+
Text(utils.videoDurationFormatter.format(showContextDuration ? handler.context.currentTime : recording.duration) ?? "")
96+
.font(fonts.footnote.monospacedDigit())
97+
.foregroundColor(Color(colors.textSecondary))
98+
99+
WaveformViewSwiftUI(
100+
audioContext: isActive ? handler.context : nil,
101+
addedVoiceRecording: recording,
102+
isPlaying: handler.isPlaying && isActive,
103+
onSliderChanged: { timeInterval in
104+
handler.seek(to: timeInterval, loadingFrom: isActive ? nil : recording.url)
105+
},
106+
onSliderTapped: {
107+
handler.togglePlayback(for: recording.url)
108+
}
109+
)
110+
.frame(height: 20)
111+
}
112+
}
113+
114+
// MARK: - Speed Toggle
115+
116+
private var playbackSpeedToggle: some View {
117+
PlaybackSpeedToggle(handler: handler)
118+
}
119+
}

Sources/StreamChatSwiftUI/ChatComposer/VoiceRecording/AudioSessionFeedbackGenerator.swift renamed to Sources/StreamChatSwiftUI/ChatComposer/Attachments/VoiceRecording/AudioSessionFeedbackGenerator.swift

File renamed without changes.

0 commit comments

Comments
 (0)