Skip to content

v5: [Composer Redesign] Voice Recording#1267

Merged
nuno-vieira merged 47 commits intov5from
add/v5-composer-voice-recording
Mar 5, 2026
Merged

v5: [Composer Redesign] Voice Recording#1267
nuno-vieira merged 47 commits intov5from
add/v5-composer-voice-recording

Conversation

@nuno-vieira
Copy link
Member

@nuno-vieira nuno-vieira commented Mar 5, 2026

πŸ”— Issue Links

IOS-1385

🎯 Goal

Redesign the voice recording flow in the message composer to match the new v5 design system.

πŸ“ Summary

  • Redesign the active recording state with mic indicator, duration label, slide-to-cancel animation, and lock gesture
  • Redesign the locked/stopped recording states with waveform playback, play/pause, trash, stop, and confirm controls
  • Redesign the voice recording attachment card in the composer (when a recording has been added) with play/pause, waveform, duration, playback speed toggle, and discard button
  • Add a floating lock button that morphs between unlocked (capsule) and locked (circle) states with smooth animations
  • Add shimmering effect to the slide-to-cancel label
  • Add snack bar notifications for recording tips, recording stopped, and voice message deleted events
  • Add custom rounded slider thumb with accent color fill, white border, and shadow to all waveform views
  • Use ViewFactory.makeConfirmEditButton for the publish/confirm recording action for consistency and customizability
  • Consolidate shared audio playback logic (togglePlayback, cycleRate, seek, isPlaying, rateTitle) into VoiceRecordingHandler to reduce duplication across views
  • Move and rename files to follow consistent naming conventions across the Attachments and VoiceRecording folders
  • Add comprehensive snapshot tests for all recording states (recording, slide-to-cancel, locked, stopped, tip snackbar, added voice recording) using AssertSnapshot with all variants

Important

The images added/referenced in this PR (e.g. SF Symbols used via Image(systemName:)) still need to be moved to the common module (StreamChat).

πŸ›  Implementation

Voice Recording Input (active recording):

  • ComposerVoiceRecordingInputView handles both the active recording UI and the locked/stopped UI with seamless transitions.
  • VoiceRecordingGestureOverlay captures drag gestures on the composer to start/stop recording and detect lock/cancel gestures.
  • LockView is a floating capsule/circle that morphs based on drag progress.

Voice Recording Attachment (added to composer):

  • ComposerVoiceRecordingAttachmentView follows the same naming pattern as ComposerFileAttachmentView, ComposerImageAttachmentView, etc.
  • ComposerVoiceRecordingContainerView wraps a VStack of attachment cards, matching the container pattern of ComposerAttachmentsContainerView.

Shared playback logic:

  • VoiceRecordingHandler (already the AudioPlayingDelegate) now also owns isPlaying, rate, and exposes togglePlayback(for:), cycleRate(), seek(to:loadingFrom:), updatePlaybackState(for:), and isActive(for:). This eliminated duplicated logic in ComposerVoiceRecordingInputView, ComposerVoiceRecordingAttachmentView, and VoiceRecordingView.

Waveform views:

  • RecordingWaveform (used during active recording) and WaveformViewSwiftUI (used for completed recordings) are both UIViewRepresentable wrappers around WaveformView and now live together in WaveformView.swift. Both share a custom slider thumb via WaveformView.applyCustomSliderThumb().

🎨 Showcase

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-03-05.at.01.37.13.mov

πŸ§ͺ Manual Testing Notes

  1. Open a channel and long-press the mic button to start recording
  2. Drag up to lock, drag left to cancel, or release to auto-lock
  3. Once locked, verify trash/stop/confirm controls appear
  4. Tap stop to preview with waveform playback (play/pause, seek, speed toggle)
  5. Confirm to add as attachment β€” verify the card shows in the composer
  6. Send the message and verify the voice recording appears in the message list
  7. Short-tap the mic button to see the recording tip snackbar

β˜‘οΈ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

@nuno-vieira nuno-vieira requested a review from a team as a code owner March 5, 2026 00:13
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cb5ba6f-d8b4-4907-84ea-083e11a75b68

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add/v5-composer-voice-recording

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

several AI slops, good to do self-review first. Also, the checks are failing.


// MARK: - Slider Thumb

static func roundedSliderThumbImage() -> UIImage {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

Copy link
Member Author

@nuno-vieira nuno-vieira Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To have the figma slider in the audio waveform

// MARK: - Swipe to Reply

/// Areas that should not trigger swipe-to-reply (e.g. waveform sliders).
struct SwipeToReplyExcludedFrameKey: PreferenceKey {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this is not possible to scrub the audio in the message list, it will trigger the swipe to reply

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

1 Warning
⚠️ Big PR
1 Message
πŸ“– There seems to be app changes but CHANGELOG wasn't modified.
Please include an entry if the PR includes user-facing changes.
You can find it at CHANGELOG.md.

Generated by 🚫 Danger

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, just 2 things from my side and then we're good

@Injected(\.utils) private var utils
@Injected(\.tokens) private var tokens

var viewModel: MessageComposerViewModel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we need an @ObservedObject here?

/// Options for creating the composer input view.
public final class ComposerInputViewOptions: Sendable {
/// The view model for the message composer.
public let viewModel: MessageComposerViewModel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also good to revisit if we can avoid sending the whole VM, but just the properties it needs? Since everything else in this class is pointless with the VM - they are all coming from it already.

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

Public Interface

+ public enum VoiceRecordingState: Equatable, Sendable  
+ 
+   case initial
+   case recording
+   case locked
+   case stopped

+ public struct PlayPauseButton: View  
+ 
+   public var body: some View
+   
+ 
+   public init(isPlaying: Bool,onTap: @escaping () -> Void)

+ public struct ComposerVoiceRecordingContainerView: View  
+ 
+   public var body: some View
+   
+ 
+   public init(addedVoiceRecordings: [AddedVoiceRecording],onDiscardAttachment: @escaping (String) -> Void)

+ public final class ComposerVoiceRecordingInputViewOptions: @unchecked Sendable  
+ 
+   public let recordingState: VoiceRecordingState
+   public let audioRecordingInfo: AudioRecordingInfo
+   public let pendingAudioRecordingURL: URL?
+   public let gestureLocation: CGPoint
+   public let stopRecording: @MainActor () -> Void
+   public let confirmRecording: @MainActor () -> Void
+   public let discardRecording: @MainActor () -> Void
+   public let previewRecording: @MainActor () -> Void
+   
+ 
+   public init(recordingState: VoiceRecordingState,audioRecordingInfo: AudioRecordingInfo,pendingAudioRecordingURL: URL?,gestureLocation: CGPoint,stopRecording: @escaping @MainActor () -> Void,confirmRecording: @escaping @MainActor () -> Void,discardRecording: @escaping @MainActor () -> Void,previewRecording: @escaping @MainActor () -> Void)

- public struct AddedVoiceRecordingsView: View  
- 
-   public var body: some View
-   
- 
-   public init(addedVoiceRecordings: [AddedVoiceRecording],onDiscardAttachment: @escaping (String) -> Void)

- public final class ComposerRecordingLockedViewOptions: Sendable  
- 
-   public let viewModel: MessageComposerViewModel
-   
- 
-   public init(viewModel: MessageComposerViewModel)

- public final class ComposerRecordingViewOptions: Sendable  
- 
-   public let viewModel: MessageComposerViewModel
-   public let gestureLocation: CGPoint
-   
- 
-   public init(viewModel: MessageComposerViewModel,gestureLocation: CGPoint)

- public enum RecordingState: Equatable, Sendable  
- 
-   case initial
-   case showingTip
-   case recording(CGPoint)
-   case locked
-   case stopped

- public final class ComposerRecordingTipViewOptions: Sendable  
- 
-   public init()

 extension ViewFactory  
-   public func makeComposerRecordingView(options: ComposerRecordingViewOptions)-> some View
+   public func makeComposerVoiceRecordingInputView(options: ComposerVoiceRecordingInputViewOptions)-> some View
-   public func makeComposerRecordingLockedView(options: ComposerRecordingLockedViewOptions)-> some View
+   public func makeAttachmentPickerView(options: AttachmentPickerViewOptions)-> some View
-   public func makeComposerRecordingTipView(options: ComposerRecordingTipViewOptions)-> some View
+   public func makeAttachmentCommandsPickerView(options: AttachmentCommandsPickerViewOptions)-> some View
-   public func makeAttachmentPickerView(options: AttachmentPickerViewOptions)-> some View
+   public func makeVoiceRecordingView(options: VoiceRecordingViewOptions)-> some View
-   public func makeAttachmentCommandsPickerView(options: AttachmentCommandsPickerViewOptions)-> some View
+   public func makeCustomAttachmentPickerView(options: CustomAttachmentPickerViewOptions)-> some View
-   public func makeVoiceRecordingView(options: VoiceRecordingViewOptions)-> some View
+   public func makeCustomAttachmentPreviewView(options: CustomAttachmentPreviewViewOptions)-> some View
-   public func makeCustomAttachmentPickerView(options: CustomAttachmentPickerViewOptions)-> some View
+   public func makeAttachmentTypePickerView(options: AttachmentTypePickerViewOptions)-> some View
-   public func makeCustomAttachmentPreviewView(options: CustomAttachmentPreviewViewOptions)-> some View
+   public func makeAttachmentMediaPickerView(options: AttachmentMediaPickerViewOptions)-> some View
-   public func makeAttachmentTypePickerView(options: AttachmentTypePickerViewOptions)-> some View
+   public func makeAttachmentFilePickerView(options: AttachmentFilePickerViewOptions)-> some View
-   public func makeAttachmentMediaPickerView(options: AttachmentMediaPickerViewOptions)-> some View
+   public func makeAttachmentCameraPickerView(options: AttachmentCameraPickerViewOptions)-> some View
-   public func makeAttachmentFilePickerView(options: AttachmentFilePickerViewOptions)-> some View
+   public func makeAttachmentPollPickerView(options: AttachmentPollPickerViewOptions)-> some View
-   public func makeAttachmentCameraPickerView(options: AttachmentCameraPickerViewOptions)-> some View
+   public func makeSendInChannelView(options: SendInChannelViewOptions)-> some View
-   public func makeAttachmentPollPickerView(options: AttachmentPollPickerViewOptions)-> some View
+   public func makeMessageActionsView(options: MessageActionsViewOptions)-> some View
-   public func makeSendInChannelView(options: SendInChannelViewOptions)-> some View
+   public func makeBottomReactionsView(options: ReactionsBottomViewOptions)-> some View
-   public func makeMessageActionsView(options: MessageActionsViewOptions)-> some View
+   public func makeMessageReactionView(options: MessageReactionViewOptions)-> some View
-   public func makeBottomReactionsView(options: ReactionsBottomViewOptions)-> some View
+   public func makeReactionsOverlayView(options: ReactionsOverlayViewOptions)-> some View
-   public func makeMessageReactionView(options: MessageReactionViewOptions)-> some View
+   public func makeReactionsContentView(options: ReactionsContentViewOptions)-> some View
-   public func makeReactionsOverlayView(options: ReactionsOverlayViewOptions)-> some View
+   public func makeReactionsBackgroundView(options: ReactionsBackgroundOptions)-> some View
-   public func makeReactionsContentView(options: ReactionsContentViewOptions)-> some View
+   public func makeMoreReactionsView(options: MoreReactionsViewOptions)-> some View
-   public func makeReactionsBackgroundView(options: ReactionsBackgroundOptions)-> some View
+   public func makeReactionsDetailView(options: ReactionsDetailViewOptions)-> some View
-   public func makeMoreReactionsView(options: MoreReactionsViewOptions)-> some View
+   public func makeComposerQuotedMessageView(options: ComposerQuotedMessageViewOptions)-> some View
-   public func makeReactionsDetailView(options: ReactionsDetailViewOptions)-> some View
+   public func makeChatQuotedMessageView(options: ChatQuotedMessageViewOptions)-> some View
-   public func makeComposerQuotedMessageView(options: ComposerQuotedMessageViewOptions)-> some View
+   public func makeQuotedMessageView(options: QuotedMessageViewOptions)-> some View
-   public func makeChatQuotedMessageView(options: ChatQuotedMessageViewOptions)-> some View
+   public func makeComposerEditedMessageView(options: ComposerEditedMessageViewOptions)-> some View
-   public func makeQuotedMessageView(options: QuotedMessageViewOptions)-> some View
+   public func makeMessageAttachmentPreviewThumbnailView(options: MessageAttachmentPreviewViewOptions)-> some View
-   public func makeComposerEditedMessageView(options: ComposerEditedMessageViewOptions)-> some View
+   public func makeMessageAttachmentPreviewIconView(options: MessageAttachmentPreviewIconViewOptions)-> some View
-   public func makeMessageAttachmentPreviewThumbnailView(options: MessageAttachmentPreviewViewOptions)-> some View
+   public func makeSuggestionsContainerView(options: SuggestionsContainerViewOptions)-> some View
-   public func makeMessageAttachmentPreviewIconView(options: MessageAttachmentPreviewIconViewOptions)-> some View
+   public func makeMessageReadIndicatorView(options: MessageReadIndicatorViewOptions)-> some View
-   public func makeSuggestionsContainerView(options: SuggestionsContainerViewOptions)-> some View
+   public func makeNewMessagesIndicatorView(options: NewMessagesIndicatorViewOptions)-> some View
-   public func makeMessageReadIndicatorView(options: MessageReadIndicatorViewOptions)-> some View
+   public func makeJumpToUnreadButton(options: JumpToUnreadButtonOptions)-> some View
-   public func makeNewMessagesIndicatorView(options: NewMessagesIndicatorViewOptions)-> some View
+   public func makeComposerPollView(options: ComposerPollViewOptions)-> some View
-   public func makeJumpToUnreadButton(options: JumpToUnreadButtonOptions)-> some View
+   public func makePollView(options: PollViewOptions)-> some View
-   public func makeComposerPollView(options: ComposerPollViewOptions)-> some View
+   public func makeThreadDestination(options: ThreadDestinationOptions)-> @MainActor (ChatThread) -> ChatChannelView<Self>
-   public func makePollView(options: PollViewOptions)-> some View
+   public func makeThreadListItem(options: ThreadListItemOptions<ThreadDestination>)-> some View
-   public func makeThreadDestination(options: ThreadDestinationOptions)-> @MainActor (ChatThread) -> ChatChannelView<Self>
+   public func makeNoThreadsView(options: NoThreadsViewOptions)-> some View
-   public func makeThreadListItem(options: ThreadListItemOptions<ThreadDestination>)-> some View
+   public func makeThreadListLoadingView(options: ThreadListLoadingViewOptions)-> some View
-   public func makeNoThreadsView(options: NoThreadsViewOptions)-> some View
+   public func makeThreadListContainerViewModifier(options: ThreadListContainerModifierOptions)-> some ViewModifier
-   public func makeThreadListLoadingView(options: ThreadListLoadingViewOptions)-> some View
+   public func makeThreadListHeaderViewModifier(options: ThreadListHeaderViewModifierOptions)-> some ViewModifier
-   public func makeThreadListContainerViewModifier(options: ThreadListContainerModifierOptions)-> some ViewModifier
+   public func makeThreadListHeaderView(options: ThreadListHeaderViewOptions)-> some View
-   public func makeThreadListHeaderViewModifier(options: ThreadListHeaderViewModifierOptions)-> some ViewModifier
+   public func makeThreadListFooterView(options: ThreadListFooterViewOptions)-> some View
-   public func makeThreadListHeaderView(options: ThreadListHeaderViewOptions)-> some View
+   public func makeThreadListBackground(options: ThreadListBackgroundOptions)-> some View
-   public func makeThreadListFooterView(options: ThreadListFooterViewOptions)-> some View
+   public func makeThreadListItemBackground(options: ThreadListItemBackgroundOptions)-> some View
-   public func makeThreadListBackground(options: ThreadListBackgroundOptions)-> some View
+   public func makeThreadListDividerItem(options: ThreadListDividerItemOptions)-> some View
-   public func makeThreadListItemBackground(options: ThreadListItemBackgroundOptions)-> some View
+   public func makeAddUsersView(options: AddUsersViewOptions)-> some View
-   public func makeThreadListDividerItem(options: ThreadListDividerItemOptions)-> some View
+   public func makeAttachmentTextView(options: AttachmentTextViewOptions)-> some View
-   public func makeAddUsersView(options: AddUsersViewOptions)-> some View
-   public func makeAttachmentTextView(options: AttachmentTextViewOptions)-> some View

- public final class ComposerInputViewOptions: Sendable  
+ public final class ComposerInputViewOptions: @unchecked Sendable  
-   public let recordingState: Binding<RecordingState>
+   public let recordingState: Binding<VoiceRecordingState>
-   public let composerAssets: [ComposerAsset]
+   public let recordingGestureLocation: Binding<CGPoint>
-   public let addedCustomAttachments: [CustomAttachment]
+   public let composerAssets: [ComposerAsset]
-   public let addedVoiceRecordings: [AddedVoiceRecording]
+   public let addedCustomAttachments: [CustomAttachment]
-   public let quotedMessage: Binding<ChatMessage?>
+   public let addedVoiceRecordings: [AddedVoiceRecording]
-   public let editedMessage: Binding<ChatMessage?>
+   public let quotedMessage: Binding<ChatMessage?>
-   public let maxMessageLength: Int?
+   public let editedMessage: Binding<ChatMessage?>
-   public let cooldownDuration: Int
+   public let maxMessageLength: Int?
-   public let hasContent: Bool
+   public let cooldownDuration: Int
-   public let canSendMessage: Bool
+   public let hasContent: Bool
-   public let onCustomAttachmentTap: @MainActor (CustomAttachment) -> Void
+   public let canSendMessage: Bool
-   public let removeAttachmentWithId: @MainActor (String) -> Void
+   public let audioRecordingInfo: AudioRecordingInfo
-   public let sendMessage: @MainActor () -> Void
+   public let pendingAudioRecordingURL: URL?
-   public let onImagePasted: @MainActor (UIImage) -> Void
+   public let onCustomAttachmentTap: @MainActor (CustomAttachment) -> Void
-   public let startRecording: @MainActor () -> Void
+   public let removeAttachmentWithId: @MainActor (String) -> Void
-   public let stopRecording: @MainActor () -> Void
+   public let sendMessage: @MainActor () -> Void
-   public let sendInChannelShown: Bool
+   public let onImagePasted: @MainActor (UIImage) -> Void
-   public let showReplyInChannel: Binding<Bool>
+   public let startRecording: @MainActor () -> Void
-   
+   public let stopRecording: @MainActor () -> Void
- 
+   public let confirmRecording: @MainActor () -> Void
-   public init(channelController: ChatChannelController,text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,recordingState: Binding<RecordingState>,composerAssets: [ComposerAsset],addedCustomAttachments: [CustomAttachment],addedVoiceRecordings: [AddedVoiceRecording],quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,maxMessageLength: Int?,cooldownDuration: Int,hasContent: Bool,canSendMessage: Bool,onCustomAttachmentTap: @escaping @MainActor (CustomAttachment) -> Void,removeAttachmentWithId: @escaping @MainActor (String) -> Void,sendMessage: @escaping @MainActor () -> Void,onImagePasted: @escaping @MainActor (UIImage) -> Void,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,sendInChannelShown: Bool,showReplyInChannel: Binding<Bool>)
+   public let discardRecording: @MainActor () -> Void
+   public let previewRecording: @MainActor () -> Void
+   public let showRecordingTip: @MainActor () -> Void
+   public let sendInChannelShown: Bool
+   public let showReplyInChannel: Binding<Bool>
+   
+ 
+   public init(channelController: ChatChannelController,text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,recordingState: Binding<VoiceRecordingState>,recordingGestureLocation: Binding<CGPoint>,composerAssets: [ComposerAsset],addedCustomAttachments: [CustomAttachment],addedVoiceRecordings: [AddedVoiceRecording],quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,maxMessageLength: Int?,cooldownDuration: Int,hasContent: Bool,canSendMessage: Bool,audioRecordingInfo: AudioRecordingInfo,pendingAudioRecordingURL: URL?,onCustomAttachmentTap: @escaping @MainActor (CustomAttachment) -> Void,removeAttachmentWithId: @escaping @MainActor (String) -> Void,sendMessage: @escaping @MainActor () -> Void,onImagePasted: @escaping @MainActor (UIImage) -> Void,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,confirmRecording: @escaping @MainActor () -> Void,discardRecording: @escaping @MainActor () -> Void,previewRecording: @escaping @MainActor () -> Void,showRecordingTip: @escaping @MainActor () -> Void,sendInChannelShown: Bool,showReplyInChannel: Binding<Bool>)

 open class WaveformView: UIView  
-     public var duration: TimeInterval
+     public var isPlaying: Bool
-     public var currentTime: TimeInterval
+     public var duration: TimeInterval
-     public var waveform: [Float]
+     public var currentTime: TimeInterval
-     public static let initial
+     public var waveform: [Float]
-     
+     public static let initial
-   
+     
-     public init(isRecording: Bool,duration: TimeInterval,currentTime: TimeInterval,waveform: [Float])
+   
+     public init(isRecording: Bool,isPlaying: Bool = false,duration: TimeInterval,currentTime: TimeInterval,waveform: [Float])

 @MainActor open class MessageComposerViewModel: ObservableObject  
-   @Published public var recordingState: RecordingState
+   @Published public var recordingState: VoiceRecordingState
-   @Published public var audioRecordingInfo
+   @Published public var recordingGestureLocation: CGPoint
-   @Published public var snackBarText: String?
+   @Published public var audioRecordingInfo
-   public let channelController: ChatChannelController
+   @Published public var snackBarText: String?
-   public var messageController: ChatMessageController?
+   public let channelController: ChatChannelController
-   public let eventsController: EventsController
+   public var messageController: ChatMessageController?
-   public var quotedMessage: Binding<ChatMessage?>?
+   public let eventsController: EventsController
-   public var waveformTargetSamples: Int
+   public var quotedMessage: Binding<ChatMessage?>?
-   public internal var pendingAudioRecording: AddedVoiceRecording?
+   public var waveformTargetSamples: Int
-   public var canSendPoll: Bool
+   public internal var pendingAudioRecording: AddedVoiceRecording?
-   public lazy var commandsHandler
+   public var canSendPoll: Bool
-   public var instantCommands: [CommandHandler]
+   public lazy var commandsHandler
-   public var mentionedUsers
+   public var instantCommands: [CommandHandler]
-   public var canSendMessage: Bool
+   public var mentionedUsers
-   public var hasContent: Bool
+   public var canSendMessage: Bool
-   public var sendInChannelShown: Bool
+   public var hasContent: Bool
-   public var isDirectChannel: Bool
+   public var shouldShowRecordingGestureOverlay: Bool
-   public var showSuggestionsOverlay: Bool
+   public var sendInChannelShown: Bool
-   
+   public var isDirectChannel: Bool
- 
+   public var showSuggestionsOverlay: Bool
-   public init(channelController: ChatChannelController,messageController: ChatMessageController?,eventsController: EventsController? = nil,quotedMessage: Binding<ChatMessage?>? = nil)
+   
-   
+ 
- 
+   public init(channelController: ChatChannelController,messageController: ChatMessageController?,eventsController: EventsController? = nil,quotedMessage: Binding<ChatMessage?>? = nil)
-   public func addFileURLs(_ urls: [URL])
+   
-   public func fillEditedMessage(_ editedMessage: ChatMessage?)
+ 
-   public func fillDraftMessage()
+   public func addFileURLs(_ urls: [URL])
-   public func updateDraftMessage(quotedMessage: ChatMessage?,isSilent: Bool = false,extraData: [String: RawJSON] = [:])
+   public func fillEditedMessage(_ editedMessage: ChatMessage?)
-   public func deleteDraftMessage()
+   public func fillDraftMessage()
-   open func sendMessage(quotedMessage: ChatMessage?,editedMessage: ChatMessage?,isSilent: Bool = false,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: @escaping @MainActor () -> Void)
+   public func updateDraftMessage(quotedMessage: ChatMessage?,isSilent: Bool = false,extraData: [String: RawJSON] = [:])
-   public func change(pickerState: AttachmentPickerState)
+   public func deleteDraftMessage()
-   public func imageTapped(_ addedAsset: AddedAsset)
+   open func sendMessage(quotedMessage: ChatMessage?,editedMessage: ChatMessage?,isSilent: Bool = false,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: @escaping @MainActor () -> Void)
-   public func imagePasted(_ image: UIImage)
+   public func change(pickerState: AttachmentPickerState)
-   public func removeAttachment(with id: String)
+   public func imageTapped(_ addedAsset: AddedAsset)
-   public func cameraImageAdded(_ image: AddedAsset)
+   public func imagePasted(_ image: UIImage)
-   public func isImageSelected(with id: String)-> Bool
+   public func removeAttachment(with id: String)
-   public func customAttachmentTapped(_ attachment: CustomAttachment)
+   public func cameraImageAdded(_ image: AddedAsset)
-   public func isCustomAttachmentSelected(_ attachment: CustomAttachment)-> Bool
+   public func isImageSelected(with id: String)-> Bool
-   public func askForPhotosPermission()
+   public func customAttachmentTapped(_ attachment: CustomAttachment)
-   public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
+   public func isCustomAttachmentSelected(_ attachment: CustomAttachment)-> Bool
-   open func convertAddedAssetsToPayloads()throws -> [AnyAttachmentPayload]
+   public func askForPhotosPermission()
-   public func checkForMentionedUsers(commandId: String?,extraData: [String: Any])
+   public func handleCommand(for text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,extraData: [String: Any])
-   public func clearRemovedMentions()
+   open func convertAddedAssetsToPayloads()throws -> [AnyAttachmentPayload]
-   public func clearInputData()
+   public func checkForMentionedUsers(commandId: String?,extraData: [String: Any])
-   public func checkChannelCooldown()
+   public func clearRemovedMentions()
-   public func updateAddedAssets(_ assets: [AddedAsset])
+   public func clearInputData()
+   public func checkChannelCooldown()
+   public func updateAddedAssets(_ assets: [AddedAsset])

 public final class ComposerInputTrailingViewOptions: @unchecked Sendable  
-   @Binding public var recordingState: RecordingState
+   @Binding public var recordingState: VoiceRecordingState
-   public let sendMessage: @MainActor () -> Void
+   public let showRecordingTip: @MainActor () -> Void
-   
+   public let sendMessage: @MainActor () -> Void
- 
+   
-   public init(text: Binding<String>,recordingState: Binding<RecordingState>,composerInputState: MessageComposerInputState,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,sendMessage: @escaping @MainActor () -> Void)
+ 
+   public init(text: Binding<String>,recordingState: Binding<VoiceRecordingState>,composerInputState: MessageComposerInputState,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,showRecordingTip: @escaping @MainActor () -> Void,sendMessage: @escaping @MainActor () -> Void)

 public struct ComposerInputView: View, KeyboardReadable  
-   public init(factory: Factory,channelController: ChatChannelController,text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,recordingState: Binding<RecordingState>,composerAssets: [ComposerAsset],addedCustomAttachments: [CustomAttachment],addedVoiceRecordings: [AddedVoiceRecording],quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,maxMessageLength: Int? = nil,cooldownDuration: Int,hasContent: Bool,canSendMessage: Bool,onCustomAttachmentTap: @escaping @MainActor (CustomAttachment) -> Void,removeAttachmentWithId: @escaping (String) -> Void,sendMessage: @escaping @MainActor () -> Void,onImagePasted: @escaping @MainActor (UIImage) -> Void,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,sendInChannelShown: Bool,showReplyInChannel: Binding<Bool>)
+   public init(factory: Factory,channelController: ChatChannelController,text: Binding<String>,selectedRangeLocation: Binding<Int>,command: Binding<ComposerCommand?>,recordingState: Binding<VoiceRecordingState>,recordingGestureLocation: Binding<CGPoint>,composerAssets: [ComposerAsset],addedCustomAttachments: [CustomAttachment],addedVoiceRecordings: [AddedVoiceRecording],quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,maxMessageLength: Int? = nil,cooldownDuration: Int,hasContent: Bool,canSendMessage: Bool,audioRecordingInfo: AudioRecordingInfo,pendingAudioRecordingURL: URL?,onCustomAttachmentTap: @escaping @MainActor (CustomAttachment) -> Void,removeAttachmentWithId: @escaping (String) -> Void,sendMessage: @escaping @MainActor () -> Void,onImagePasted: @escaping @MainActor (UIImage) -> Void,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,confirmRecording: @escaping @MainActor () -> Void,discardRecording: @escaping @MainActor () -> Void,previewRecording: @escaping @MainActor () -> Void,showRecordingTip: @escaping @MainActor () -> Void,sendInChannelShown: Bool,showReplyInChannel: Binding<Bool>)

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title v5 branch diff status
StreamChatSwiftUI 9.97 MB 10.07 MB +104 KB 🟒

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
77.7% Coverage on New Code (required β‰₯ 80%)

See analysis details on SonarQube Cloud

@Stream-SDK-Bot
Copy link
Collaborator

StreamChatSwiftUI XCSize

Object Diff (bytes)
ComposerVoiceRecordingInputView.o +42841
LockedView.o -17540
VoiceRecordingLockView.o +15003
ComposerVoiceRecordingAttachmentView.o +13448
VoiceRecordingGestureOverlay.o +10675
Show 46 more objects
Object Diff (bytes)
RecordingView.o -9645
MessageComposerView.o +9094
TrailingComposerView.o -8599
AddedVoiceRecordingsView.o -5947
WaveformView.o +4996
ComposerViewFactoryOptions.o +2919
Modifiers.o -2560
MessageItemView.o +2254
Shimmer.o +2134
RecordingWaveform.o -2119
VoiceRecordingContainerView.o +2033
RecordingTipView.o -1992
VoiceRecordingDurationView.o +1735
PlayPauseButton.o +1716
RecordingDurationView.o -1496
MessageComposerViewModel+Recording.o +1136
RecordingState.o -1048
MessageComposerViewModel.o +920
VoiceRecordingState.o +833
DefaultViewFactory.o +750
TrailingInputComposerView.o -655
EmptyViewFactoryOptions.o -511
ReactionsDetailView.o -414
LoadingView.o +302
ChatChannelInfoViewModel.o +294
DefaultMessageActions.o +232
ViewFactory.o -192
ChatThreadListView.o -192
ReactionsIconProvider.o -137
ChannelAvatar.o -136
StreamButton.o -128
SwiftUICore.tbd +104
MessageAttachmentPreviewThumbnail.o -92
Symbols.tbd +88
MessageRepliesView.o -88
StreamLazyImage.o -80
ChatThreadListLoadingView.o -72
MessageActionsView.o +72
MessageAttachmentPreviewIconView.o -72
ComposerTextInputView.o +70
ReactionsOverlayView.o +64
SlowModeView.o -60
AudioSessionFeedbackGenerator.o +54
BadgeCountView.o -52
ComposerAttachmentPickerButton.o -46
AlertBannerViewModifier.o -44

@nuno-vieira nuno-vieira merged commit 68932f0 into v5 Mar 5, 2026
11 of 12 checks passed
@nuno-vieira nuno-vieira deleted the add/v5-composer-voice-recording branch March 5, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants