Skip to content
Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `MentionUsersView`
- `ParticipantInfoView`
- `ChatThreadListItem`
- Fix scrolling in the message list when presented with a sheet on iOS 26 [#1065](https://github.com/GetStream/stream-chat-swiftui/pull/1065)
- Fix reading messages from muted users [#1063](https://github.com/GetStream/stream-chat-swiftui/pull/1063)

# [4.94.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.94.0)
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,23 @@ 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 {
setOffsetX(value: 0)
return
}
if utils.messageCachingUtils.swipeToReplyId == nil, geometry.frame(in: .global).contains(messageListSwipe.startLocation) {
utils.messageCachingUtils.swipeToReplyId = message.id
}
guard utils.messageCachingUtils.swipeToReplyId == message.id 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 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
@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 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
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,25 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
}
}
}
.if(channel.config.quotesEnabled, transform: { view in
view.simultaneousGesture(
DragGesture(
minimumDistance: utils.messageListConfig.messageDisplayOptions.minimumSwipeGestureDistance,
coordinateSpace: .global
)
.onChanged { value in
guard value.velocity == .zero || abs(value.velocity.width) > abs(value.velocity.height) else { return }
guard abs(value.translation.width) > abs(value.translation.height) else { return }
guard value.translation.width != messageListSwipe?.horizontalOffset else { return }
messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: value.translation.width)
}
.onEnded { _ in
guard let offset = messageListSwipe?.horizontalOffset, offset != 0 else { return }
utils.messageCachingUtils.swipeToReplyId = nil
messageListSwipe = nil
}
)
})
.accessibilityIdentifier("MessageListScrollView")
}

Expand Down Expand Up @@ -651,6 +672,15 @@ private struct MessageViewModelKey: EnvironmentKey {
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 +699,18 @@ extension EnvironmentValues {
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
}
}
}
3 changes: 3 additions & 0 deletions Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ class MessageCachingUtils {
}

var jumpToReplyId: String?

var swipeToReplyId: MessageId?

func clearCache() {
log.debug("Clearing cached message data")
scrollOffset = 0
messageThreadShown = false
swipeToReplyId = nil
}
}

Expand Down
Loading