Skip to content
Closed
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
### 🐞 Fixed
- Fix scrolling in the message list when presented with a sheet on iOS 26 [#1065](https://github.com/GetStream/stream-chat-swiftui/pull/1065)

# [4.94.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.94.0)
_December 02, 2025_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
@StateObject var messageViewModel: MessageViewModel
@Environment(\.channelTranslationLanguage) var translationLanguage
@Environment(\.highlightedMessageId) var highlightedMessageId
@Environment(\.messageListSwipe) var messageListSwipe

@Injected(\.fonts) private var fonts
@Injected(\.colors) private var colors
Expand All @@ -32,7 +33,6 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
@State private var computeFrame = false
@State private var offsetX: CGFloat = 0
@State private var offsetYAvatar: CGFloat = 0
@GestureState private var offset: CGSize = .zero

private let replyThreshold: CGFloat = 60
private var paddingValue: CGFloat {
Expand Down Expand Up @@ -129,6 +129,9 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
.onChange(of: computeFrame, perform: { _ in
frame = proxy.frame(in: .global)
})
.onChange(of: messageListSwipe, perform: { messageListSwipe in
handleMessageListSwipe(messageListSwipe, geometry: proxy)
})
}
)
.onTapGesture(count: 2) {
Expand All @@ -140,40 +143,6 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
handleGestureForMessage(showsMessageActions: true)
})
.offset(x: min(self.offsetX, maximumHorizontalSwipeDisplacement))
.simultaneousGesture(
DragGesture(
minimumDistance: minimumSwipeDistance,
coordinateSpace: .local
)
.updating($offset) { (value, gestureState, _) in
guard messageViewModel.isSwipeToQuoteReplyPossible else {
return
}
// Using updating since onEnded is not called if the gesture is canceled.
let diff = CGSize(
width: value.location.x - value.startLocation.x,
height: value.location.y - value.startLocation.y
)

if diff == .zero {
gestureState = .zero
} else {
gestureState = value.translation
}
}
)
.onChange(of: offset, perform: { _ in
if !channel.config.quotesEnabled {
return
}

if offset == .zero {
// gesture ended or cancelled
setOffsetX(value: 0)
} else {
dragChanged(to: offset.width)
}
})
.accessibilityElement(children: .contain)
.accessibilityIdentifier("MessageView")

Expand Down Expand Up @@ -351,6 +320,18 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
private var messageListConfig: MessageListConfig {
utils.messageListConfig
}

private func handleMessageListSwipe(_ messageListSwipe: MessageListSwipe?, geometry: GeometryProxy) {
guard messageViewModel.isSwipeToQuoteReplyPossible else { return }
guard let messageListSwipe else { return }
// The view is moving during the swipe handling, therefore we skip the contains check if it is in progress
guard offsetX > 0 || geometry.frame(in: .global).contains(messageListSwipe.startLocation) else { return }
if messageListSwipe.horizontalOffset == 0 {
setOffsetX(value: 0)
} else {
dragChanged(to: messageListSwipe.horizontalOffset)
}
}

private func dragChanged(to value: CGFloat) {
let horizontalTranslation = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@State private var scrollDirection = ScrollDirection.up
@State private var unreadMessagesBannerShown = false
@State private var unreadButtonDismissed = false
@State private var messageListSwipe: MessageListSwipe?

private var messageRenderingUtil = MessageRenderingUtil.shared
private var skipRenderingMessageIds = [String]()
Expand Down Expand Up @@ -191,6 +192,7 @@
isLast: !showsLastInGroupInfo && message == messages.last
)
.environment(\.channelTranslationLanguage, channel.membership?.language)
.environment(\.messageListSwipe, messageListSwipe)
.onAppear {
if index == nil {
index = messageListDateUtils.index(for: message, in: messages)
Expand Down Expand Up @@ -310,6 +312,20 @@
}
}
}
.if(channel.config.quotesEnabled, transform: { view in

Check warning on line 315 in Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 2 closure expressions.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZruc1EGUQBAy1nasV8c&open=AZruc1EGUQBAy1nasV8c&pullRequest=1065
view.simultaneousGesture(
DragGesture(
minimumDistance: utils.messageListConfig.messageDisplayOptions.minimumSwipeGestureDistance,
coordinateSpace: .global
)
.onChanged { value in
messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: value.translation.width)
}
.onEnded { value in
messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: 0)
}
)
})
.accessibilityIdentifier("MessageListScrollView")
}

Expand Down Expand Up @@ -651,6 +667,15 @@
static let defaultValue: MessageViewModel? = nil
}

private struct MessageListSwipeKey: EnvironmentKey {
static let defaultValue: MessageListSwipe? = nil
}

struct MessageListSwipe: Equatable {
let startLocation: CGPoint
let horizontalOffset: CGFloat
}

extension EnvironmentValues {
var channelTranslationLanguage: TranslationLanguage? {
get {
Expand All @@ -669,4 +694,18 @@
self[MessageViewModelKey.self] = newValue
}
}

/// Propagates the drag state to message items.
///
/// - Important: Since iOS 26 simultaneous gestures do not update ancestors.
/// The gesture handler should be attached to the ScrollView and then propagating
/// the state to items which decide if the drag should be handled.
var messageListSwipe: MessageListSwipe? {
get {
self[MessageListSwipeKey.self]
}
set {
self[MessageListSwipeKey.self] = newValue
}
}
}
Loading