Fix scroll to bottom button not working when message list is actively scrolling#1379
Closed
nuno-vieira wants to merge 33 commits intomainfrom
Closed
Fix scroll to bottom button not working when message list is actively scrolling#1379nuno-vieira wants to merge 33 commits intomainfrom
nuno-vieira wants to merge 33 commits intomainfrom
Conversation
* Show "Pinned by you" when the current user pinned a message When the current user pins a message, the pinned annotation label now shows "Pinned by you" instead of "Pinned by [user name]". Made-with: Cursor * Update CHANGELOG with pinned-by-you fix Made-with: Cursor
* Preserve voice when editing messages Allow voice attachments to map into the composer when duration or waveform metadata is missing, and always leave the conversion dispatch group. Tests: MessageAttachmentsConverter_Tests.test_attachmentsToAssets_voiceRecordingWithoutWaveformOrDuration * Send voice immediately on hold-release Stop treating finger-up as lock: finalize recording and auto-send once the file is ready. Wire the composer to invoke the same path as manual send. Tests: MessageComposerViewModel_Tests hold-gesture auto-send flag coverage Made-with: Cursor * Align voice waveform scrubber with duration Use consistent duration/currentTime for the locked preview bar and clamp message-list waveform content to the reported asset length. Made-with: Cursor * Reset voice time label after playback ends Show total duration again when the player reports a stopped state (including after natural finish) in the message list and composer preview. Made-with: Cursor * Move sendMessage() from MessageComposerView to the view model * Fix voice recording showing elapsed time and not remaining time * Update Agents File * Run swiftformat * Update Changelog * Fix the slider thumb not starting in the actual beginning of the wave form * Fix playback rate state not kept when restarting audio recording * Fix the changing rate affecting other audios * Update changelog * Add animations when releasing or discarding voice recording * Reduce changelog entries * Fix swiftformat * Improve the namings of the voice recording closures * FIx snapshot tests * Update CHANGELOG.md
* Reapply "Fix navigation issues on iPad (#1305)" * Do not use NavigationLink within channel list item when presenting channel list with NavigationSplitView
β¦caption (#1330) * Render sharp tail corner on single media attachments without caption When a message has a single image or video attachment with no text caption, the bottom corner on the sender's side is now rendered sharp (0 radius) to visually distinguish caption-less media from captioned media, matching the Figma design spec. - Add per-corner clipping support to MessageMediaAttachmentContentView - Detect single-media-without-caption in MessageMediaAttachmentsContainerView and apply bubble corners + border overlay - Pass isFirst to the container from the factory - Add snapshot tests for outgoing/incoming, first/non-first, portrait, video, and captioned variants - Re-record affected existing snapshot references [IOS-1518] Made-with: Cursor * Update CHANGELOG for IOS-1518 sharp tail corner fix Made-with: Cursor * Update CHANGELOG.md * Consolidate no-caption corner logic into mediaCell per review feedback Move the sharp-tail-corner and border overlay logic directly into mediaCell instead of a separate singleMediaCellWithoutCaption branch, so a single code path handles both cases. Re-record all affected multi-item gallery snapshot references. Made-with: Cursor * Some minor cleanup
* Fix quoting giphy shows camera icon and "Photo" text Giphy attachments were incorrectly grouped with image attachments in the attachment preview resolver, causing quoted giphy messages to display a camera icon with "Photo" text instead of a document icon with "Giphy" text. - Add `.giphy` case to `MessageAttachmentPreviewKind` - Handle giphy attachments separately from images in the resolver - Map giphy preview icon to `.document` and description to "Giphy" - Add snapshot tests for quoted giphy in message list and composer [IOS-1530] Made-with: Cursor * Fix channel list giphy preview shows "/giphy" text The channel list preview for giphy messages was showing the raw "/giphy" command text instead of a localized "Giphy" label. Updated the message preview formatter to use a proper localized string and added the `channel.item.giphy` localization key. - Change giphy case in `MessagePreviewFormatter` to return "Giphy" - Add `channel.item.giphy` to `Localizable.strings` and `L10n.swift` - Add channel list snapshot tests for giphy preview messages [IOS-1531] Made-with: Cursor * Update CHANGELOG for giphy preview fixes Made-with: Cursor * Update CHANGELOG.md * test: re-record thread attachments snapshot
β¦ign (#1338) * Redesign channel preview for deleted message * Add channel preview for pending messages * Update channel preview error message * Update channel preview deleted message for current user * Fix showing ephemeral giphy messages as part of the preview * Remove `InjectedChannelInfo` * Update empty mesasages localization according to Figma * Add archive UI to swipe actions * Fix demo app channel list item * Revert "Add archive UI to swipe actions" This reverts commit c2482f8. * Switch to remote SDK dependency * Re-record ChatChannelListView snapshot tests Update reference images to reflect channel list item changes (deleted message display, swipe actions redesign, empty state text). * Fix xcodeproject * Update changelog * Fix redudant if-statement * [CI] Snapshots (#1339) Co-authored-by: Stream Bot <ci@getstream.io> * Update E2E tests for deleted message channel preview Deleted messages now intentionally show in the channel preview as "Message deleted" instead of falling back to the previous message. Update assertions and remove the XCTSkip. * Fix typo in deleted message for E2E Test * Fix message deleted E2E Test * Change stream-chat-swift branch to 'develop' * Fix missing previewMessage parameter in test ChatChannel initializer * Fix previewMessage parameter in test ChatChannel initializer --------- Co-authored-by: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com> Co-authored-by: Stream Bot <ci@getstream.io>
* Rename all empty views with Empty prefix * Update CHANGELOG with empty views renaming entry * Update CHANGELOG.md
* Update deleted message design to match Figma spec - Add nosign icon before "Message deleted" text - Use standard message text color instead of low emphasis gray - Remove "Only visible to you" footer from deleted messages - Show delivery status (read indicator + timestamp) for deleted messages - Update textColor(for:) to no longer special-case deleted messages - Add snapshot test for deleted message container with delivery status - Re-record deleted message snapshot reference image Closes IOS-1505 * Update CHANGELOG with deleted message design fix * Update CHANGELOG.md * Fix E2E Tests
* Redesign `JumpToUnreadButton` * Move jump-to-unread layout wrapper from factory into MessageListView The VStack/Spacer/padding that positions the button belongs to the message list layout, not the factory-provided component itself. * Add animation when showing and hiding the unread button overlay * Cleanup the code a bit * Use regular weight for the arrow-up icon and re-record snapshots * Update changelog * Add Accessibility label * Introduce `JumpToUnreadButtonOverlayModifier` to make it more flexible to customize * Re-record unread indicator snapshot after overlay refactoring * Fix snapshot tests
* Redesign `NewMessagesIndicator` to match Figma spec - Use design tokens for spacing, colors, and typography - Add top/bottom border with borderCoreSubtle - Update text from "New messages" to "Unread messages" - Add standalone snapshot test for the indicator * Add thread replies separator and extract `MessageListDivider` - Extract reusable `MessageListDivider` from `NewMessagesIndicator` - Refactor `NewMessagesIndicator` to use `MessageListDivider` - Add thread replies separator between parent message and replies - Update `message.threads.count` localization to use lowercase format - Add snapshot tests for divider, indicator, and thread separator * Update CHANGELOG * Apply suggestion from @nuno-vieira * Apply suggestion from @nuno-vieira * Hide thread replies separator when not all replies are loaded * Rename `NewMessagesIndicator` to `NewMessagesDivider` and extract `ThreadRepliesDivider` * Rename factory methods and add `makeThreadRepliesDividerView` * Remove deprecated typealiases * Fix accessivlity trait * Fix snapshot tests * Fix swift format * Fix snapshot tests --------- Co-authored-by: Martin Mitrevski <martinmitrevski.oh@gmail.com>
#1359) * Fix media gallery attachment not opening the correct item on first tap * Update changelog with media gallery fix PR link
* Fix timestamp snapping back faster than delivery indicator on swipe-to-reply The deprecated `.animation(nil)` modifier on `MessageDateView` was stripping the animation transaction for all property changes, causing the timestamp to jump instantly while the rest of the message animated with the swipe-to-reply spring curve. Scoping it to `.animation(nil, value: text)` only suppresses animation on text content changes while allowing positional animations to propagate. * Update CHANGELOG.md
β¦1362) * Add `ComposerConfig.isVoiceRecordingAutoSendEnabled` and disable it by default * Add voice recording auto-send toggle to Demo App configuration Allows toggling `isVoiceRecordingAutoSendEnabled` from the app configuration screen so the behavior can be tested at runtime. * Show recording tip depending on if auto send is enabled or not * Add test coverage * Update CHANGELOG for #1362 * Apply suggestion from @nuno-vieira * Fix snapshot test
|
Generated by π« Danger |
Public Interface+ public final class EmptyChannelsViewOptions: Sendable
+
+ public init()
+ public final class ThreadRepliesDividerViewOptions: Sendable
+
+ public let replyCount: Int
+
+
+ public init(replyCount: Int)
+ public struct NewMessagesDivider: View
+
+ public var body: some View
+
+
+ public init(newMessagesStartId: Binding<String?>,count: Int)
+ public final class EmptyThreadsViewOptions: Sendable
+
+ public init()
+ public struct JumpToUnreadButtonOverlayModifier: ViewModifier
+
+ public func body(content: Content)-> some View
+ public struct EmptyThreadsView: View
+
+ public var body: some View
+
+
+ public init()
+ public struct MessageListDivider: View
+
+ public var body: some View
+
+
+ public init(title: String)
+ public final class NewMessagesDividerViewOptions: Sendable
+
+ public let newMessagesStartId: Binding<String?>
+ public let count: Int
+
+
+ public init(newMessagesStartId: Binding<String?>,count: Int)
+ public struct EmptyChannelsView: View
+
+ public var body: some View
+ public struct ThreadRepliesDivider: View
+
+ public var body: some View
+
+
+ public init(replyCount: Int)
- public struct NoThreadsView: View
-
- public var body: some View
-
-
- public init()
- public final class InjectedChannelInfo: Sendable
-
- public let subtitle: String?
- public let unreadCount: Int
- public let timestamp: String?
- public let lastMessageAt: Date?
- public let latestMessages: [ChatMessage]?
-
-
- public init(subtitle: String? = nil,unreadCount: Int,timestamp: String? = nil,lastMessageAt: Date? = nil,latestMessages: [ChatMessage]? = nil)
- public struct NoChannelsView: View
-
- public var body: some View
- public struct NewMessagesIndicator: View
-
- public var body: some View
-
-
- public init(newMessagesStartId: Binding<String?>,count: Int)
- public final class NoThreadsViewOptions: Sendable
-
- public init()
- public final class NewMessagesIndicatorViewOptions: Sendable
-
- public let newMessagesStartId: Binding<String?>
- public let count: Int
-
-
- public init(newMessagesStartId: Binding<String?>,count: Int)
- public final class NoChannelsViewOptions: Sendable
-
- public init()
public struct ChatChannelSwipeableListItem: View
- public init(factory: Factory,channelListItem: ChannelListItem,swipedChannelId: Binding<String?>,channel: ChatChannel,numberOfTrailingItems: Int = 2,widthOfTrailingItem: CGFloat = 60,trailingRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
+ public init(factory: Factory,channelListItem: ChannelListItem,swipedChannelId: Binding<String?>,channel: ChatChannel,numberOfTrailingItems: Int = 2,widthOfTrailingItem: CGFloat = 80,trailingRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
public final class PollViewOptions: Sendable
-
+ public let availableWidth: CGFloat
-
+
- public init(message: ChatMessage,poll: Poll,isFirst: Bool)
+
+ public init(message: ChatMessage,poll: Poll,isFirst: Bool,availableWidth: CGFloat)
public final class MessageComposerViewTypeOptions: Sendable
- public let onMessageSent: @MainActor () -> Void
+ public let willSendMessage: @MainActor () -> Void
- public init(channelController: ChatChannelController,messageController: ChatMessageController?,quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,onMessageSent: @escaping @MainActor () -> Void)
+ public init(channelController: ChatChannelController,messageController: ChatMessageController?,quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,willSendMessage: @escaping @MainActor () -> Void)
public struct MessageMediaAttachmentsContainerView: View
- public init(factory: Factory,message: ChatMessage,width: CGFloat)
+ public init(factory: Factory,message: ChatMessage,width: CGFloat,isFirst: Bool = true)
public class AttachmentTextViewOptions
-
+ public let availableWidth: CGFloat
-
+
- public init(message: ChatMessage)
+
+ public init(message: ChatMessage,availableWidth: CGFloat)
public struct AttachmentTextView: View
- public init(factory: Factory = DefaultViewFactory.shared,message: ChatMessage,injectedBackgroundColor: UIColor? = nil)
+ public init(factory: Factory = DefaultViewFactory.shared,message: ChatMessage,availableWidth: CGFloat)
public struct PollAttachmentView: View
- public init(factory: Factory,message: ChatMessage,poll: Poll,isFirst: Bool)
+ public init(factory: Factory,message: ChatMessage,poll: Poll,isFirst: Bool,width: CGFloat)
public enum ChannelAlertType: Equatable
- case deleteChannel(ChatChannel)
+ case muteChannel(ChatChannel)
- case error
+ case deleteChannel(ChatChannel)
+ case error
public struct ChatChannelListItem: View
- public init(factory: Factory = DefaultViewFactory.shared,channel: ChatChannel,channelName: String,injectedChannelInfo: InjectedChannelInfo? = nil,disabled: Bool = false,onItemTap: @escaping (ChatChannel) -> Void)
+ public init(factory: Factory = DefaultViewFactory.shared,channel: ChatChannel,channelName: String,isSelected: Bool = false,disabled: Bool = false,onItemTap: @escaping (ChatChannel) -> Void)
public struct ChannelsLazyVStack: View
- public init(factory: Factory,channels: [ChatChannel],selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,onItemTap: @escaping @MainActor (ChatChannel) -> Void,onItemAppear: @escaping @MainActor (Int) -> Void,channelDestination: @escaping @MainActor (ChannelSelectionInfo) -> Factory.ChannelDestination,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
+ public init(factory: Factory,channels: [ChatChannel],selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,onItemTap: @escaping @MainActor (ChatChannel) -> Void,onItemAppear: @escaping @MainActor (Int) -> Void,channelDestination: (@MainActor (ChannelSelectionInfo) -> Factory.ChannelDestination)? = nil,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
extension ViewFactory
- public func makeNoChannelsView(options: NoChannelsViewOptions)-> some View
+ public func makeEmptyChannelsView(options: EmptyChannelsViewOptions)-> some View
- public func makeNewMessagesIndicatorView(options: NewMessagesIndicatorViewOptions)-> some View
+ public func makeNewMessagesDividerView(options: NewMessagesDividerViewOptions)-> some View
- public func makeJumpToUnreadButton(options: JumpToUnreadButtonOptions)-> some View
+ public func makeThreadRepliesDividerView(options: ThreadRepliesDividerViewOptions)-> some View
- public func makePollView(options: PollViewOptions)-> some View
+ public func makeJumpToUnreadButtonOverlay(options: JumpToUnreadButtonOptions)-> some ViewModifier
- 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 makeEmptyThreadsView(options: EmptyThreadsViewOptions)-> 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 makeStreamTextView(options: StreamTextViewOptions)-> some View
+ public func makeAttachmentTextView(options: AttachmentTextViewOptions)-> some View
+ public func makeStreamTextView(options: StreamTextViewOptions)-> some View
public struct ChatChannelNavigatableListItem: View
- public init(factory: Factory = DefaultViewFactory.shared,channel: ChatChannel,channelName: String,disabled: Bool = false,handleTabBarVisibility: Bool = true,selectedChannel: Binding<ChannelSelectionInfo?>,channelDestination: @escaping (ChannelSelectionInfo) -> ChannelDestination,onItemTap: @escaping (ChatChannel) -> Void)
+ public init(factory: Factory = DefaultViewFactory.shared,channel: ChatChannel,channelName: String,disabled: Bool = false,handleTabBarVisibility: Bool = true,selectedChannel: Binding<ChannelSelectionInfo?>,channelDestination: ((ChannelSelectionInfo) -> ChannelDestination)? = nil,onItemTap: @escaping (ChatChannel) -> Void)
@MainActor open class MessageComposerViewModel: ObservableObject
- public var waveformTargetSamples: Int
+ public var editedMessage: Binding<ChatMessage?>?
- public internal var pendingAudioRecording: AddedVoiceRecording?
+ public var willSendMessage: (() -> Void)?
- public var canSendPoll: Bool
+ public var waveformTargetSamples: Int
- public lazy var commandsHandler
+ public internal var pendingAudioRecording: AddedVoiceRecording?
- public var instantCommands: [CommandHandler]
+ public var canSendPoll: Bool
- public var mentionedUsers
+ public lazy var commandsHandler
- public var canSendMessage: Bool
+ public var instantCommands: [CommandHandler]
- public var hasContent: Bool
+ public var mentionedUsers
- public var shouldShowRecordingGestureOverlay: 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,editedMessage: Binding<ChatMessage?>? = nil,willSendMessage: (() -> Void)? = 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(isSilent: Bool = false,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: (@MainActor () -> Void)? = nil)
- 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])
@MainActor public final class MessageListConfig
- public let bouncedMessagesAlertActionsEnabled: Bool
+ public let attachmentPreviewWidth: CGFloat
- public let skipEditedMessageLabel: (ChatMessage) -> Bool
+ public let bouncedMessagesAlertActionsEnabled: Bool
- public let draftMessagesEnabled: Bool
+ public let skipEditedMessageLabel: (ChatMessage) -> Bool
- public let downloadFileAttachmentsEnabled: Bool
+ public let draftMessagesEnabled: Bool
- public init(messageListType: MessageListType = .messaging,typingIndicatorPlacement: TypingIndicatorPlacement = .automatic,groupMessages: Bool = true,messageDisplayOptions: MessageDisplayOptions = MessageDisplayOptions(),messagePaddings: MessagePaddings = MessagePaddings(),dateIndicatorPlacement: DateIndicatorPlacement = .overlay,pageSize: Int = 25,messagePopoverEnabled: Bool = true,doubleTapOverlayEnabled: Bool = false,becomesFirstResponderOnOpen: Bool = false,resignsFirstResponderOnScrollDown: Bool = true,updateChannelsFromMessageList: Bool = false,maxTimeIntervalBetweenMessagesInGroup: TimeInterval = 60,cacheSizeOnChatDismiss: Int = 1024 * 1024 * 100,iPadSplitViewEnabled: Bool = true,scrollingAnchor: UnitPoint = .center,showNewMessagesSeparator: Bool = true,highlightMessageWhenJumping: Bool = true,handleTabBarVisibility: Bool = true,messageListAlignment: MessageListAlignment = .standard,uniqueReactionsEnabled: Bool = false,localLinkDetectionEnabled: Bool = true,isMessageEditedLabelEnabled: Bool = true,markdownSupportEnabled: Bool = true,userBlockingEnabled: Bool = false,bouncedMessagesAlertActionsEnabled: Bool = true,skipEditedMessageLabel: @escaping (ChatMessage) -> Bool = { _ in false },draftMessagesEnabled: Bool = false,downloadFileAttachmentsEnabled: Bool = false,hidesCommandsOverlayOnMessageListTap: Bool = true,hidesAttachmentsPickersOnMessageListTap: Bool = true,navigationBarDisplayMode: NavigationBarItem.TitleDisplayMode = .inline,supportedMessageActions: @escaping @MainActor (SupportedMessageActionsOptions) -> [MessageAction] = MessageAction.defaultActions(for:))
+ public init(messageListType: MessageListType = .messaging,typingIndicatorPlacement: TypingIndicatorPlacement = .automatic,groupMessages: Bool = true,messageDisplayOptions: MessageDisplayOptions = MessageDisplayOptions(),messagePaddings: MessagePaddings = MessagePaddings(),dateIndicatorPlacement: DateIndicatorPlacement = .messageList,pageSize: Int = 25,messagePopoverEnabled: Bool = true,doubleTapOverlayEnabled: Bool = false,becomesFirstResponderOnOpen: Bool = false,resignsFirstResponderOnScrollDown: Bool = true,updateChannelsFromMessageList: Bool = false,maxTimeIntervalBetweenMessagesInGroup: TimeInterval = 60,cacheSizeOnChatDismiss: Int = 1024 * 1024 * 100,iPadSplitViewEnabled: Bool = true,scrollingAnchor: UnitPoint = .center,showNewMessagesSeparator: Bool = true,highlightMessageWhenJumping: Bool = true,handleTabBarVisibility: Bool = true,messageListAlignment: MessageListAlignment = .standard,uniqueReactionsEnabled: Bool = false,localLinkDetectionEnabled: Bool = true,isMessageEditedLabelEnabled: Bool = true,markdownSupportEnabled: Bool = false,userBlockingEnabled: Bool = true,bouncedMessagesAlertActionsEnabled: Bool = true,skipEditedMessageLabel: @escaping (ChatMessage) -> Bool = { _ in false },draftMessagesEnabled: Bool = true,hidesCommandsOverlayOnMessageListTap: Bool = true,hidesAttachmentsPickersOnMessageListTap: Bool = true,attachmentPreviewWidth: CGFloat = 256,navigationBarDisplayMode: NavigationBarItem.TitleDisplayMode = .inline,supportedMessageActions: @escaping @MainActor (SupportedMessageActionsOptions) -> [MessageAction] = MessageAction.defaultActions(for:))
public final class ChannelListItemOptions
- public let channelDestination: @MainActor (ChannelSelectionInfo) -> ChannelDestination
+ public let channelDestination: (@MainActor (ChannelSelectionInfo) -> ChannelDestination)?
- public init(channel: ChatChannel,channelName: String,disabled: Bool,selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,channelDestination: @escaping @MainActor (ChannelSelectionInfo) -> ChannelDestination,onItemTap: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
+ public init(channel: ChatChannel,channelName: String,disabled: Bool,selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,channelDestination: (@MainActor (ChannelSelectionInfo) -> ChannelDestination)? = nil,onItemTap: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void,trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void,leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void)
public final class ComposerConfig
- public var inputViewMinHeight: CGFloat
+ public var isVoiceRecordingAutoSendEnabled: Bool
- public var inputViewMaxHeight: CGFloat
+ public var inputViewMinHeight: CGFloat
- public var inputViewCornerRadius: CGFloat
+ public var inputViewMaxHeight: CGFloat
- public var inputFont: UIFont
+ public var inputViewCornerRadius: CGFloat
- public var gallerySupportedTypes: GallerySupportedTypes
+ public var inputFont: UIFont
- public var maxGalleryAssetsCount: Int?
+ public var gallerySupportedTypes: GallerySupportedTypes
- public var adjustMessageOnSend: (String) -> (String)
+ public var maxGalleryAssetsCount: Int?
- public var adjustMessageOnRead: (String) -> (String)
+ public var adjustMessageOnSend: (String) -> (String)
-
+ public var adjustMessageOnRead: (String) -> (String)
-
+
- public init(isVoiceRecordingEnabled: Bool = false,inputViewMinHeight: CGFloat = 40,inputViewMaxHeight: CGFloat = 120,inputViewCornerRadius: CGFloat = 20,inputFont: UIFont = UIFont.preferredFont(forTextStyle: .body),gallerySupportedTypes: GallerySupportedTypes = .imagesAndVideo,maxGalleryAssetsCount: Int? = nil,adjustMessageOnSend: @escaping (String) -> (String) = { $0 },adjustMessageOnRead: @escaping (String) -> (String) = { $0 })
+
+ public init(isVoiceRecordingEnabled: Bool = true,isVoiceRecordingAutoSendEnabled: Bool = false,inputViewMinHeight: CGFloat = 40,inputViewMaxHeight: CGFloat = 120,inputViewCornerRadius: CGFloat = 20,inputFont: UIFont = UIFont.preferredFont(forTextStyle: .body),gallerySupportedTypes: GallerySupportedTypes = .imagesAndVideo,maxGalleryAssetsCount: Int? = nil,adjustMessageOnSend: @escaping (String) -> (String) = { $0 },adjustMessageOnRead: @escaping (String) -> (String) = { $0 })
public struct UserAvatar: View
- public init(user: ChatUser,size: CGFloat,showsIndicator: Bool = false,showsBorder: Bool = true)
+ public init(user: ChatUser,size: CGFloat,indicator: AvatarIndicator = .none,showsBorder: Bool = true)
public final class UserAvatarViewOptions: Sendable
-
+ public let showsBorder: Bool
-
+
- public init(user: ChatUser,size: CGFloat,showsIndicator: Bool)
+
+ public init(user: ChatUser,size: CGFloat,showsIndicator: Bool,showsBorder: Bool = true)
public struct LinkAttachmentView: View
- public init(linkAttachment: ChatMessageLinkAttachment,width: CGFloat,isFirst: Bool,onImageTap: ((ChatMessageLinkAttachment) -> Void)? = nil)
+ public init(linkAttachment: ChatMessageLinkAttachment,width: CGFloat,isFirst: Bool,isRightAligned: Bool,onImageTap: ((ChatMessageLinkAttachment) -> Void)? = nil)
public final class ChannelAvatarViewOptions: Sendable
-
+ public let showsIndicator: Bool
-
+ public let showsBorder: Bool
- public init(channel: ChatChannel,size: CGFloat)
+
+
+ public init(channel: ChatChannel,size: CGFloat,showsIndicator: Bool = true,showsBorder: Bool = true)
- open class StreamImageCDN: ImageCDN
+ open class StreamImageCDN: ImageCDN, @unchecked Sendable
public struct MessageComposerView: View, KeyboardReadable
- public init(viewFactory: Factory,viewModel: MessageComposerViewModel? = nil,channelController: ChatChannelController,messageController: ChatMessageController? = nil,quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,onMessageSent: @escaping () -> Void)
+ public init(viewFactory: Factory,viewModel: MessageComposerViewModel? = nil,channelController: ChatChannelController,messageController: ChatMessageController? = nil,quotedMessage: Binding<ChatMessage?>,editedMessage: Binding<ChatMessage?>,willSendMessage: @escaping () -> Void)
-
-
- public func sendMessage()
@MainActor open class ChatChannelListViewModel: ObservableObject, ChatChannelListControllerDelegate, ChatMessageSearchControllerDelegate
- public func onMoreTapped(channel: ChatChannel)
+ public func onMuteTapped(channel: ChatChannel)
- public func delete(channel: ChatChannel)
+ public func onMoreTapped(channel: ChatChannel)
- open func showErrorPopup(_ error: Error?)
+ public func delete(channel: ChatChannel)
- open func setChannelAlertType(_ channelAlertType: ChannelAlertType)
+ public func mute(channel: ChatChannel)
- public func controller(_ controller: ChatChannelListController,didChangeChannels changes: [ListChange<ChatChannel>])
+ open func showErrorPopup(_ error: Error?)
- open func controller(_ controller: ChatChannelListController,shouldAddNewChannelToList channel: ChatChannel)-> Bool
+ open func setChannelAlertType(_ channelAlertType: ChannelAlertType)
- open func controller(_ controller: ChatChannelListController,shouldListUpdatedChannel channel: ChatChannel)-> Bool
+ public func controller(_ controller: ChatChannelListController,didChangeChannels changes: [ListChange<ChatChannel>])
- public func preselectChannelIfNeeded()
+ open func controller(_ controller: ChatChannelListController,shouldAddNewChannelToList channel: ChatChannel)-> Bool
- public func controller(_ controller: ChatMessageSearchController,didChangeMessages changes: [ListChange<ChatMessage>])
+ open func controller(_ controller: ChatChannelListController,shouldListUpdatedChannel channel: ChatChannel)-> Bool
- open func performMessageSearch()
+ public func preselectChannelIfNeeded()
- open func performChannelSearch()
+ public func controller(_ controller: ChatMessageSearchController,didChangeMessages changes: [ListChange<ChatMessage>])
+ open func performMessageSearch()
+ open func performChannelSearch()
- open class NukeImageLoader: ImageLoading
+ open class NukeImageLoader: ImageLoading, @unchecked Sendable
public struct ChatInfoGroupHeaderView: View
- public init(viewModel: ChatChannelInfoViewModel)
+ public init(factory: Factory = DefaultViewFactory.shared,viewModel: ChatChannelInfoViewModel)
public final class ComposerInputTrailingViewOptions: @unchecked Sendable
- public let composerInputState: MessageComposerInputState
+ @Binding public var composerCommand: ComposerCommand?
- public let startRecording: @MainActor () -> Void
+ public let composerInputState: MessageComposerInputState
- public let stopRecording: @MainActor () -> Void
+ public let startRecording: @MainActor () -> Void
- public let showRecordingTip: @MainActor () -> Void
+ public let stopRecording: @MainActor () -> Void
- public let sendMessage: @MainActor () -> Void
+ public let showRecordingTip: @MainActor () -> Void
-
+ public let sendMessage: @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 init(text: Binding<String>,recordingState: Binding<VoiceRecordingState>,composerCommand: Binding<ComposerCommand?>,composerInputState: MessageComposerInputState,startRecording: @escaping @MainActor () -> Void,stopRecording: @escaping @MainActor () -> Void,showRecordingTip: @escaping @MainActor () -> Void,sendMessage: @escaping @MainActor () -> Void)
public enum MessageComposerInputState
- case creating(hasContent: Bool)
+ case creating(hasContent: Bool, hasCommand: Bool)
public final class MessageDisplayOptions
- public init(showIncomingMessageAvatar: Bool = true,showOutgoingMessageAvatar: Bool = false,showAvatarsInGroups: Bool = true,showMessageDate: Bool = true,showAuthorName: Bool = true,animateChanges: Bool = true,overlayDateLabelSize: CGFloat = 40,lastInGroupHeaderSize: CGFloat = 0,newMessagesSeparatorSize: CGFloat = 50,minimumSwipeGestureDistance: CGFloat = 20,currentUserMessageTransition: AnyTransition = .identity,otherUserMessageTransition: AnyTransition = .identity,shouldAnimateReactions: Bool = true,reactionsPlacement: ReactionsPlacement = .top,reactionsStyle: ReactionsStyle = .segmented,showOriginalTranslatedButton: Bool = false,messageLinkDisplayResolver: @escaping @MainActor (ChatMessage) -> [NSAttributedString.Key: Any] = MessageDisplayOptions
+ public init(showIncomingMessageAvatar: Bool = true,showOutgoingMessageAvatar: Bool = false,showAvatarsInGroups: Bool = true,showMessageDate: Bool = true,showAuthorName: Bool = true,animateChanges: Bool = true,overlayDateLabelSize: CGFloat = 40,lastInGroupHeaderSize: CGFloat = 0,newMessagesSeparatorSize: CGFloat = 50,minimumSwipeGestureDistance: CGFloat = 20,currentUserMessageTransition: AnyTransition = .identity,otherUserMessageTransition: AnyTransition = .identity,shouldAnimateReactions: Bool = true,reactionsPlacement: ReactionsPlacement = .top,reactionsStyle: ReactionsStyle = .segmented,showOriginalTranslatedButton: Bool = true,messageLinkDisplayResolver: @escaping @MainActor (ChatMessage) -> [NSAttributedString.Key: Any] = MessageDisplayOptions
public struct ChatChannelListContentView: View
- public init(viewFactory: Factory,viewModel: ChatChannelListViewModel,onItemTap: (@MainActor (ChatChannel) -> Void)? = nil)
+ public init(viewFactory: Factory,viewModel: ChatChannelListViewModel,channelDestination: (@MainActor (ChannelSelectionInfo) -> Factory.ChannelDestination)? = nil,onItemTap: (@MainActor (ChatChannel) -> Void)? = nil)
public struct ChannelList: View
- public init(factory: Factory,channels: [ChatChannel],selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,scrolledChannelId: Binding<String?> = .constant(nil),scrollable: Bool = true,onItemTap: @escaping @MainActor (ChatChannel) -> Void,onItemAppear: @escaping @MainActor (Int) -> Void,channelDestination: @escaping @MainActor (ChannelSelectionInfo) -> Factory.ChannelDestination,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in },trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in },leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in })
+ public init(factory: Factory,channels: [ChatChannel],selectedChannel: Binding<ChannelSelectionInfo?>,swipedChannelId: Binding<String?>,scrolledChannelId: Binding<String?> = .constant(nil),scrollable: Bool = true,onItemTap: @escaping @MainActor (ChatChannel) -> Void,onItemAppear: @escaping @MainActor (Int) -> Void,channelDestination: (@MainActor (ChannelSelectionInfo) -> Factory.ChannelDestination)? = nil,trailingSwipeRightButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in },trailingSwipeLeftButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in },leadingSwipeButtonTapped: @escaping @MainActor (ChatChannel) -> Void = { _ in })
- public protocol ImageCDN
+ public protocol ImageCDN: Sendable
public struct MessageMediaAttachmentContentView: View
- public init(factory: Factory,source: MediaAttachment,width: CGFloat,height: CGFloat,cornerRadius: CGFloat? = nil,isOutgoing: Bool = false)
+ public init(factory: Factory,source: MediaAttachment,width: CGFloat,height: CGFloat,cornerRadius: CGFloat? = nil,corners: UIRectCorner? = nil,isOutgoing: Bool = false)
public final class ChannelSelectionInfo: Identifiable, @unchecked Sendable
- public var injectedChannelInfo: InjectedChannelInfo?
+ public let searchType: ChannelListSearchType
- public let searchType: ChannelListSearchType
+
-
+
-
+ public init(channel: ChatChannel,message: ChatMessage?,searchType: ChannelListSearchType = .messages)
- public init(channel: ChatChannel,message: ChatMessage?,searchType: ChannelListSearchType = .messages)
public struct ChannelAvatar: View
- public init(channel: ChatChannel,size: CGFloat,showsIndicator: Bool = false,showsBorder: Bool = true)
+ public init(channel: ChatChannel,size: CGFloat,indicator: AvatarIndicator = .none,showsBorder: Bool = true)
public final class JumpToUnreadButtonOptions: Sendable
- public let channel: ChatChannel
+ public let isShown: Bool
- public let onJumpToMessage: @MainActor () -> Void
+ public let channel: ChatChannel
- public let onClose: @MainActor () -> Void
+ public let onJumpToMessage: @MainActor () -> Void
-
+ public let onClose: @MainActor () -> Void
-
+
- public init(channel: ChatChannel,onJumpToMessage: @escaping @MainActor () -> Void,onClose: @escaping @MainActor () -> Void)
+
+ public init(isShown: Bool = true,channel: ChatChannel,onJumpToMessage: @escaping @MainActor () -> Void,onClose: @escaping @MainActor () -> Void)
@MainActor open class MessageViewModel: ObservableObject
- public var isRightAligned: Bool
+ public var pinnedByText: String
- public var messageAuthor: ChatUser?
+ public var isRightAligned: Bool
- public var isDoubleTapOverlayEnabled: Bool
+ public var messageAuthor: ChatUser?
- open var isSwipeToQuoteReplyPossible: Bool
+ public var isDoubleTapOverlayEnabled: Bool
- open var textContent: String
+ open var isSwipeToQuoteReplyPossible: Bool
- public var translatedText: String?
+ open var textContent: String
- public var translatedLanguageText: String?
+ public var translatedText: String?
- public var annotationsShown: Bool
+ public var translatedLanguageText: String?
- public var threadRepliesShown: Bool
+ public var annotationsShown: Bool
- public var sentInChannelShown: Bool
+ public var threadRepliesShown: Bool
- public var repliedToThreadShown: Bool
+ public var sentInChannelShown: Bool
- public var hasReminder: Bool
+ public var repliedToThreadShown: Bool
- public var reminderTimeText: String?
+ public var hasReminder: Bool
-
+ public var reminderTimeText: String?
-
+
- public init(message: ChatMessage,channel: ChatChannel,isInThread: Bool = false)
+
-
+ public init(message: ChatMessage,channel: ChatChannel,isInThread: Bool = false)
-
+
- public func showOriginalText()
+
- public func hideOriginalText()
+ public func showOriginalText()
- public func isHighlighted(messageId: String?)-> Bool
+ public func hideOriginalText()
+ public func isHighlighted(messageId: String?)-> Bool
@MainActor public class ViewModelsFactory
- public static func makeMessageComposerViewModel(with channelController: ChatChannelController,messageController: ChatMessageController?,quotedMessage: Binding<ChatMessage?>? = nil)-> MessageComposerViewModel
+ public static func makeMessageComposerViewModel(with channelController: ChatChannelController,messageController: ChatMessageController?,quotedMessage: Binding<ChatMessage?>?,editedMessage: Binding<ChatMessage?>?,willSendMessage: (() -> Void)?)-> MessageComposerViewModel
- public protocol ImageLoading: AnyObject
+ public protocol ImageLoading: AnyObject, Sendable |
Collaborator
SDK Size
|
Collaborator
StreamChatSwiftUI XCSize
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


π Issue Links
π― Goal
Fix the scroll to bottom button not responding when tapped while the message list is actively decelerating (scrolling via inertia).
π Summary
DispatchQueue.main.asyncwrapper from theonChange(of: scrolledId)handler inMessageListView, so thatscrollView.scrollTo()fires synchronously.π Implementation
The
onChange(of: scrolledId)handler was wrapped inDispatchQueue.main.async, which delayed thescrollView.scrollTo()call to the next run loop iteration. During that delay, the ongoing scroll deceleration would continue and override the programmatic scroll, making the button appear unresponsive.In the v4 branch, the
scrollTocall was synchronous insideonChange, giving it priority over any active deceleration. This change restores that behavior by removing theDispatchQueue.main.asyncwrapper while keeping thewithAnimationblock intact.π§ͺ Manual Testing Notes
βοΈ Contributor Checklist
docs-contentrepo