Skip to content

Commit da6f2f6

Browse files
committed
Merge branch 'develop' into v5
# Conflicts: # StreamChatSwiftUI.xcodeproj/project.pbxproj
2 parents 1e36bde + d8b5cf3 commit da6f2f6

29 files changed

+408
-102
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
### ✅ Added
7+
- Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1032)
8+
- Display double grey checkmark when delivery events are enabled [#1038](https://github.com/GetStream/stream-chat-swiftui/pull/1038)
9+
10+
### 🐞 Fixed
11+
- Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030)
712

813
# [4.91.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.91.0)
914
_October 22, 2025_

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ The SwiftUI SDK offers three types of components:
3939
- Stateful components - Offer more customization options and possibility to inject custom views. Also fairly simple to integrate, if the extension points are suitable for your chat use-case. These components come with view models.
4040
- Stateless components - These are the building blocks for the other two types of components. In order to use them, you would have to provide the state and data. Using these components only make sense if you want to implement completely custom chat experience.
4141

42+
## Documentation Generation
43+
44+
To generate the documentation for SwiftUI StreamChat SDK, run the following command:
45+
46+
```bash
47+
xcodebuild docbuild -skipMacroValidation -skipPackagePluginValidation -derivedDataPath .derivedData -scheme StreamChatSwiftUI -destination generic/platform=iOS | xcpretty
48+
open .derivedData/Build/Products/Debug-iphoneos/StreamChatSwiftUI.doccarchive
49+
```
50+
51+
This will build the documentation archive and automatically open it in Xcode.
52+
4253
## Free for Makers
4354

4455
Stream is free for most side and hobby projects. You can use Stream Chat for free if you have less than five team members and no more than $10,000 in monthly revenue.

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable {
7373
},
7474
onJumpToMessage: viewModel.jumpToMessage(messageId:)
7575
)
76+
.environment(\.highlightedMessageId, viewModel.highlightedMessageId)
7677
.dismissKeyboardOnTap(enabled: true) {
7778
hideComposerCommandsAndAttachmentsPicker()
7879
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import SwiftUI
5656
public var messageController: ChatMessageController?
5757

5858
@Published public var scrolledId: String?
59+
@Published public var highlightedMessageId: String?
5960
@Published public var listId = UUID().uuidString
6061

6162
@Published public var showScrollToLatestButton = false
@@ -172,6 +173,20 @@ import SwiftUI
172173
self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId
173174
} else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId {
174175
self?.scrolledId = jumpToReplyId
176+
// Trigger highlight when jumping to reply in thread
177+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
178+
self?.highlightedMessageId = jumpToReplyId
179+
}
180+
// Clear scroll ID after 2 seconds
181+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
182+
self?.scrolledId = nil
183+
}
184+
// Clear highlight after animation completes
185+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
186+
withAnimation {
187+
self?.highlightedMessageId = nil
188+
}
189+
}
175190
self?.messageCachingUtils.jumpToReplyId = nil
176191
} else if messageController == nil {
177192
self?.scrolledId = scrollToMessage?.messageId
@@ -232,6 +247,12 @@ import SwiftUI
232247
if let message = notification.userInfo?[MessageRepliesConstants.selectedMessage] as? ChatMessage {
233248
threadMessage = message
234249
threadMessageShown = true
250+
251+
// Only set jumpToReplyId if there's a specific reply message to highlight
252+
// (for showReplyInChannel messages). The parent message should never be highlighted.
253+
if let replyMessage = notification.userInfo?[MessageRepliesConstants.threadReplyMessage] as? ChatMessage {
254+
messageCachingUtils.jumpToReplyId = replyMessage.messageId
255+
}
235256
}
236257
}
237258

@@ -297,9 +318,20 @@ import SwiftUI
297318
if scrolledId == nil {
298319
scrolledId = messageId
299320
}
321+
// Trigger highlight after a short delay to allow scroll animation to start
322+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
323+
self?.highlightedMessageId = messageId
324+
}
325+
// Clear scroll ID after 2 seconds
300326
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
301327
self?.scrolledId = nil
302328
}
329+
// Clear highlight after animation completes
330+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
331+
withAnimation {
332+
self?.highlightedMessageId = nil
333+
}
334+
}
303335
return true
304336
} else {
305337
let message = channelController.dataStore.message(id: baseId)
@@ -325,9 +357,19 @@ import SwiftUI
325357
if toJumpId == baseId, let message = channelController.dataStore.message(id: toJumpId) {
326358
toJumpId = message.messageId
327359
}
328-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
360+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
329361
self?.scrolledId = toJumpId
330362
self?.loadingMessagesAround = false
363+
// Trigger highlight after scroll starts
364+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
365+
self?.highlightedMessageId = toJumpId
366+
}
367+
// Clear highlight after animation completes
368+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
369+
withAnimation {
370+
self?.highlightedMessageId = nil
371+
}
372+
}
331373
}
332374
}
333375
return false

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -912,14 +912,6 @@ extension MessageComposerViewModel: EventsControllerDelegate {
912912
fillDraftMessage()
913913
}
914914
}
915-
916-
if let event = event as? DraftDeletedEvent {
917-
let isFromSameThread = messageController?.messageId == event.threadId
918-
let isFromSameChannel = channelController.cid == event.cid && messageController == nil
919-
if isFromSameThread || isFromSameChannel {
920-
clearInputData()
921-
}
922-
}
923915
}
924916
}
925917

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import SwiftUI
99
public struct MessageContainerView<Factory: ViewFactory>: View {
1010
@StateObject var messageViewModel: MessageViewModel
1111
@Environment(\.channelTranslationLanguage) var translationLanguage
12-
12+
@Environment(\.highlightedMessageId) var highlightedMessageId
13+
1314
@Injected(\.fonts) private var fonts
1415
@Injected(\.colors) private var colors
1516
@Injected(\.images) private var images
@@ -300,7 +301,17 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
300301
.padding(.horizontal, messageListConfig.messagePaddings.horizontal)
301302
.padding(.bottom, showsAllInfo || messageViewModel.isPinned ? paddingValue : groupMessageInterItemSpacing)
302303
.padding(.top, isLast ? paddingValue : 0)
303-
.background(messageViewModel.isPinned ? Color(colors.pinnedBackground) : nil)
304+
.background(
305+
Group {
306+
if utils.messageListConfig.highlightMessageWhenJumping,
307+
let highlightedMessageId = highlightedMessageId,
308+
highlightedMessageId == message.messageId {
309+
Color(colors.messageCellHighlightBackground)
310+
} else if messageViewModel.isPinned {
311+
Color(colors.pinnedBackground)
312+
}
313+
}
314+
)
304315
.padding(.bottom, messageViewModel.isPinned ? paddingValue / 2 : 0)
305316
.transition(
306317
message.isSentByCurrentUser ?
@@ -414,6 +425,18 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
414425
}
415426
}
416427

428+
// Environment plumbing colocated to avoid adding new files to the package list.
429+
private struct HighlightedMessageIdKey: EnvironmentKey {
430+
static let defaultValue: String? = nil
431+
}
432+
433+
extension EnvironmentValues {
434+
var highlightedMessageId: String? {
435+
get { self[HighlightedMessageIdKey.self] }
436+
set { self[HighlightedMessageIdKey.self] = newValue }
437+
}
438+
}
439+
417440
struct SendFailureIndicator: View {
418441
@Injected(\.colors) private var colors
419442
@Injected(\.images) private var images

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public struct MessageListConfig {
2626
iPadSplitViewEnabled: Bool = true,
2727
scrollingAnchor: UnitPoint = .center,
2828
showNewMessagesSeparator: Bool = true,
29+
highlightMessageWhenJumping: Bool = true,
2930
handleTabBarVisibility: Bool = true,
3031
messageListAlignment: MessageListAlignment = .standard,
3132
uniqueReactionsEnabled: Bool = false,
@@ -57,6 +58,7 @@ public struct MessageListConfig {
5758
self.iPadSplitViewEnabled = iPadSplitViewEnabled
5859
self.scrollingAnchor = scrollingAnchor
5960
self.showNewMessagesSeparator = showNewMessagesSeparator
61+
self.highlightMessageWhenJumping = highlightMessageWhenJumping
6062
self.handleTabBarVisibility = handleTabBarVisibility
6163
self.messageListAlignment = messageListAlignment
6264
self.uniqueReactionsEnabled = uniqueReactionsEnabled
@@ -121,6 +123,11 @@ public struct MessageListConfig {
121123

122124
/// A boolean value that determines if download action is shown for file attachments.
123125
public let downloadFileAttachmentsEnabled: Bool
126+
127+
/// Highlights the message background when jumping to a message.
128+
///
129+
/// By default it is enabled and it uses the color from `ColorPalette.messageCellHighlightBackground`.
130+
public let highlightMessageWhenJumping: Bool
124131
}
125132

126133
/// Contains information about the message paddings.

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,18 @@ public struct MessageReadIndicatorView: View {
100100

101101
var readUsers: [ChatUser]
102102
var showReadCount: Bool
103+
var showDelivered: Bool
103104
var localState: LocalMessageState?
104105

105-
public init(readUsers: [ChatUser], showReadCount: Bool, localState: LocalMessageState? = nil) {
106+
public init(
107+
readUsers: [ChatUser],
108+
showReadCount: Bool,
109+
showDelivered: Bool = false,
110+
localState: LocalMessageState? = nil
111+
) {
106112
self.readUsers = readUsers
107113
self.showReadCount = showReadCount
114+
self.showDelivered = showDelivered
108115
self.localState = localState
109116
}
110117

@@ -135,7 +142,7 @@ public struct MessageReadIndicatorView: View {
135142
}
136143

137144
private var image: UIImage {
138-
shouldShowReads ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent)
145+
shouldShowReads || showDelivered ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent)
139146
}
140147

141148
private var isMessageSending: Bool {

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SwiftUI
88
enum MessageRepliesConstants {
99
static let selectedMessageThread = "selectedMessageThread"
1010
static let selectedMessage = "selectedMessage"
11+
static let threadReplyMessage = "threadReplyMessage"
1112
}
1213

1314
/// View shown below a message, when there are replies to it.
@@ -21,21 +22,24 @@ public struct MessageRepliesView<Factory: ViewFactory>: View {
2122
var replyCount: Int
2223
var isRightAligned: Bool
2324
var showReplyCount: Bool
25+
var threadReplyMessage: ChatMessage? // The actual reply message (for showReplyInChannel messages)
2426

2527
public init(
2628
factory: Factory,
2729
channel: ChatChannel,
2830
message: ChatMessage,
2931
replyCount: Int,
3032
showReplyCount: Bool = true,
31-
isRightAligned: Bool? = nil
33+
isRightAligned: Bool? = nil,
34+
threadReplyMessage: ChatMessage? = nil
3235
) {
3336
self.factory = factory
3437
self.channel = channel
3538
self.message = message
3639
self.replyCount = replyCount
3740
self.isRightAligned = isRightAligned ?? message.isRightAligned
3841
self.showReplyCount = showReplyCount
42+
self.threadReplyMessage = threadReplyMessage
3943
}
4044

4145
public var body: some View {
@@ -44,10 +48,14 @@ public struct MessageRepliesView<Factory: ViewFactory>: View {
4448
resignFirstResponder()
4549
// NOTE: this is used to avoid breaking changes.
4650
// Will be updated in a major release.
51+
var userInfo: [String: Any] = [MessageRepliesConstants.selectedMessage: message]
52+
if let threadReplyMessage = threadReplyMessage {
53+
userInfo[MessageRepliesConstants.threadReplyMessage] = threadReplyMessage
54+
}
4755
NotificationCenter.default.post(
4856
name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread),
4957
object: nil,
50-
userInfo: [MessageRepliesConstants.selectedMessage: message]
58+
userInfo: userInfo
5159
)
5260
} label: {
5361
HStack {

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
8787
MessageReadIndicatorView(
8888
readUsers: channel.readUsers(
8989
currentUserId: chatClient.currentUserId,
90-
message: channel.latestMessages.first
90+
message: channel.previewMessage
9191
),
92-
showReadCount: false
92+
showReadCount: false,
93+
showDelivered: channel.previewMessage?.deliveryStatus(for: channel) == .delivered
9394
)
9495
}
9596
SubtitleText(text: injectedChannelInfo?.timestamp ?? channel.timestampText)
@@ -161,9 +162,8 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
161162
}
162163

163164
private var shouldShowReadEvents: Bool {
164-
if let message = channel.latestMessages.first,
165-
message.isSentByCurrentUser,
166-
!message.isDeleted {
165+
if let message = channel.previewMessage,
166+
message.isSentByCurrentUser {
167167
return channel.config.readEventsEnabled
168168
}
169169

0 commit comments

Comments
 (0)