Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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