Skip to content

Commit 4e4279c

Browse files
Merged develop
2 parents 18df4b6 + 7a7130c commit 4e4279c

File tree

15 files changed

+344
-56
lines changed

15 files changed

+344
-56
lines changed

.github/workflows/cron-checks.yml

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,24 @@ jobs:
2121
strategy:
2222
matrix:
2323
include:
24-
- ios: 18.5
25-
xcode: 16.4
26-
os: macos-15
24+
# - ios: "26.0" TODO: IOS-1181
25+
# device: "iPhone 17 Pro"
26+
# xcode: "26.0.1"
27+
# setup_runtime: false
28+
- ios: "18.5"
2729
device: "iPhone 16 Pro"
30+
xcode: "26.0.1"
2831
setup_runtime: false
29-
- ios: 17.5
30-
xcode: 15.4
31-
os: macos-14
32+
- ios: "17.5"
3233
device: "iPhone 15 Pro"
33-
setup_runtime: false
34-
- ios: 16.4
35-
xcode: 15.3 # fails on 15.4
36-
os: macos-14
34+
xcode: "26.0.1"
35+
setup_runtime: true
36+
- ios: "16.4"
3737
device: "iPhone 14 Pro"
38+
xcode: "16.4"
3839
setup_runtime: true
3940
fail-fast: false
40-
runs-on: ${{ matrix.os }}
41+
runs-on: macos-15
4142
env:
4243
GITHUB_EVENT: ${{ toJson(github.event) }}
4344
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
@@ -91,9 +92,11 @@ jobs:
9192
strategy:
9293
matrix:
9394
include:
94-
- xcode: 16.4
95+
- xcode: 26.0.1 # swift 6.2
96+
os: macos-15
97+
- xcode: 16.4 # swift 6.1
9598
os: macos-15
96-
- xcode: 16.1
99+
- xcode: 16.1 # swift 6.0
97100
os: macos-14
98101
fail-fast: false
99102
runs-on: ${{ matrix.os }}
@@ -107,20 +110,20 @@ jobs:
107110
XCODE_VERSION: ${{ matrix.xcode }}
108111

109112
build-old-xcode:
110-
name: Build SDKs (Xcode 15)
113+
name: Build SDKs (Old Xcode)
111114
runs-on: macos-14
112115
env:
113-
XCODE_VERSION: "15.4"
116+
XCODE_VERSION: "16.1"
114117
steps:
115118
- name: Connect Bot
116119
uses: webfactory/ssh-agent@v0.7.0
117120
with:
118121
ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }}
119122
- uses: actions/checkout@v3.1.0
123+
- uses: ./.github/actions/xcode-cache
120124
- uses: ./.github/actions/ruby-cache
121-
timeout-minutes: 25
122125
- name: Build SwiftUI
123-
run: bundle exec fastlane test_ui device:"iPhone 15" build_for_testing:true
126+
run: bundle exec fastlane test_ui device:"iPhone 16" build_for_testing:true
124127
timeout-minutes: 25
125128
- name: Build XCFrameworks
126129
run: bundle exec fastlane build_xcframeworks
@@ -133,7 +136,7 @@ jobs:
133136
name: Automated Code Review
134137
runs-on: macos-14
135138
env:
136-
XCODE_VERSION: "15.4"
139+
XCODE_VERSION: "16.1"
137140
steps:
138141
- uses: actions/checkout@v4.1.1
139142
- uses: ./.github/actions/bootstrap

.github/workflows/smoke-checks.yml

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ concurrency:
2020

2121
env:
2222
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
23-
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.5)"
23+
IOS_SIMULATOR_DEVICE: "iPhone 17 Pro (26.0)"
2424
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2525
GITHUB_PR_NUM: ${{ github.event.pull_request.number }}
2626

@@ -50,7 +50,7 @@ jobs:
5050
runs-on: macos-14
5151
if: ${{ github.event.inputs.record_snapshots != 'true' }}
5252
env:
53-
XCODE_VERSION: "15.4"
53+
XCODE_VERSION: "16.1"
5454
steps:
5555
- uses: actions/checkout@v4.1.1
5656
- uses: ./.github/actions/bootstrap
@@ -60,24 +60,25 @@ jobs:
6060
- run: bundle exec fastlane rubocop
6161
- run: bundle exec fastlane run_swift_format strict:true
6262
- run: bundle exec fastlane validate_public_interface
63+
- run: bundle exec fastlane pod_lint
64+
if: startsWith(github.event.pull_request.head.ref, 'release/')
6365

64-
build-xcode15:
65-
name: Build SDKs (Xcode 15)
66-
runs-on: macos-15
67-
#if: ${{ github.event.inputs.record_snapshots != 'true' }}
68-
if: false
66+
build-old-xcode:
67+
name: Build SDKs (Old Xcode)
68+
runs-on: macos-14
69+
if: ${{ github.event.inputs.record_snapshots != 'true' }}
6970
env:
70-
XCODE_VERSION: "15.4"
71+
XCODE_VERSION: "16.1"
7172
steps:
7273
- name: Connect Bot
7374
uses: webfactory/ssh-agent@v0.7.0
7475
with:
7576
ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }}
7677
- uses: actions/checkout@v3.1.0
78+
- uses: ./.github/actions/xcode-cache
7779
- uses: ./.github/actions/ruby-cache
78-
timeout-minutes: 25
7980
- name: Build SwiftUI
80-
run: bundle exec fastlane test_ui device:"iPhone 15" build_for_testing:true
81+
run: bundle exec fastlane test_ui device:"iPhone 16" build_for_testing:true
8182
timeout-minutes: 25
8283
- name: Build XCFrameworks
8384
run: bundle exec fastlane build_xcframeworks
@@ -89,6 +90,10 @@ jobs:
8990
test-ui-debug:
9091
name: Test SwiftUI (Debug)
9192
runs-on: macos-15
93+
env:
94+
GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} # to open a PR
95+
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.5)" # TODO: IOS-1181
96+
XCODE_VERSION: "16.4" # TODO: IOS-1181
9297
steps:
9398
- uses: actions/checkout@v4.1.1
9499
- uses: ./.github/actions/bootstrap
@@ -98,8 +103,6 @@ jobs:
98103
- name: Run UI Tests (Debug)
99104
run: bundle exec fastlane test_ui device:"${{ env.IOS_SIMULATOR_DEVICE }}" record:"${{ github.event.inputs.record_snapshots }}"
100105
timeout-minutes: 120
101-
env:
102-
GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} # to open a PR
103106
- name: Run Sonar analysis
104107
if: ${{ github.event.inputs.record_snapshots != 'true' }}
105108
run: bundle exec fastlane sonar_upload
@@ -156,6 +159,8 @@ jobs:
156159
- build-test-app-and-frameworks
157160
env:
158161
LAUNCH_ID: ${{ needs.allure_testops_launch.outputs.launch_id }}
162+
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.5)" # TODO: IOS-1181
163+
XCODE_VERSION: "16.4" # TODO: IOS-1181
159164
strategy:
160165
matrix:
161166
batch: [0, 1]

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88
- Opens `MarkdownFormatter` so that it can be customised [#978](https://github.com/GetStream/stream-chat-swiftui/pull/978)
99
- Add participant actions in channel info view [#982](https://github.com/GetStream/stream-chat-swiftui/pull/982)
1010
- Add support for overriding `onImageTap` in `LinkAttachmentView` [#986](https://github.com/GetStream/stream-chat-swiftui/pull/986)
11+
- Add support for customizing text colors in `LinkAttachmentView` [#992](https://github.com/GetStream/stream-chat-swiftui/pull/992)
1112

1213
### 🐞 Fixed
1314
- Fix openChannel not working when searching or another chat shown [#975](https://github.com/GetStream/stream-chat-swiftui/pull/975)
1415
- Fix crash when using a font that does not support bold or italic trait [#976](https://github.com/GetStream/stream-chat-swiftui/pull/976)
16+
- Fix unread messages banner not shown for one-page channels [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
17+
- Fix unread messages banner not shown if the whole channel is unread [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
18+
- Fix channel not marking read when passing by the unread message [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
19+
- Fix random scroll after marking a message unread [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
20+
- Fix marking channel read when the user scrolls to the bottom after marking a message as unread [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
21+
- Fix replying to unread messages marking them instantly as read [#989](https://github.com/GetStream/stream-chat-swiftui/pull/989)
1522

1623
# [4.89.1](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.89.1)
1724
_September 23, 2025_

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,17 @@ class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate {
9191
}
9292

9393
var firstUnreadMessageId: String? {
94-
controller.firstUnreadMessageId
94+
if controller.firstUnreadMessageId == nil && controller.lastReadMessageId == nil {
95+
let currentUserReadHasRead = controller.channel?.reads.first(where: {
96+
$0.user.id == controller.client.currentUserId
97+
}) != nil
98+
// If the current user has unread state but no unread message is available
99+
// it means the whole channel is unread, so the first message is the unread message.
100+
if currentUserReadHasRead {
101+
return controller.messages.last?.id
102+
}
103+
}
104+
return controller.firstUnreadMessageId
95105
}
96106

97107
init(controller: ChatChannelController) {

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,12 @@ import SwiftUI
128128
}
129129
}
130130
}
131-
131+
132+
// A boolean value indicating if the user marked a message as unread
133+
// in the current session of the channel. If it is true,
134+
// it should not call markRead() in any scenario.
135+
public var currentUserMarkedMessageUnread: Bool = false
136+
132137
@Published public private(set) var channel: ChatChannel?
133138

134139
public var isMessageThread: Bool {
@@ -347,7 +352,7 @@ import SwiftUI
347352
if utils.messageListConfig.dateIndicatorPlacement == .overlay {
348353
save(lastDate: message.createdAt)
349354
}
350-
if index == 0, channelDataSource.hasLoadedAllNextMessages {
355+
if channelDataSource.hasLoadedAllNextMessages {
351356
let isActive = UIApplication.shared.applicationState == .active
352357
if isActive && canMarkRead {
353358
sendReadEventIfNeeded(for: message)
@@ -573,7 +578,12 @@ import SwiftUI
573578
}
574579

575580
private func sendReadEventIfNeeded(for message: ChatMessage) {
576-
guard let channel, channel.unreadCount.messages > 0 else { return }
581+
guard let channel, channel.unreadCount.messages > 0 else {
582+
return
583+
}
584+
if currentUserMarkedMessageUnread {
585+
return
586+
}
577587
throttler.execute { [weak self] in
578588
self?.channelController.markRead()
579589
// We keep `firstUnreadMessageId` value set which keeps showing the new messages header in the channel view
@@ -681,7 +691,7 @@ import SwiftUI
681691
canMarkRead = true
682692

683693
if channel.unreadCount.messages > 0 {
684-
if channelController.firstUnreadMessageId != nil {
694+
if channelDataSource.firstUnreadMessageId != nil {
685695
firstUnreadMessageId = channelController.firstUnreadMessageId
686696
canMarkRead = false
687697
} else if channelController.lastReadMessageId != nil {

Sources/StreamChatSwiftUI/ChatChannel/MessageList/LinkAttachmentView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public struct LinkAttachmentView: View {
135135
if !authorHidden {
136136
BottomLeftView {
137137
Text(linkAttachment.author ?? "")
138-
.foregroundColor(colors.tintColor)
138+
.foregroundColor(colors.messageLinkAttachmentAuthorColor)
139139
.font(fonts.bodyBold)
140140
.standardPadding()
141141
.bubble(
@@ -152,12 +152,14 @@ public struct LinkAttachmentView: View {
152152
if let title = linkAttachment.title {
153153
Text(title)
154154
.font(fonts.footnoteBold)
155+
.foregroundColor(colors.messageLinkAttachmentTitleColor)
155156
.lineLimit(1)
156157
}
157158

158159
if let description = linkAttachment.text {
159160
Text(description)
160161
.font(fonts.footnote)
162+
.foregroundColor(colors.messageLinkAttachmentTextColor)
161163
.lineLimit(3)
162164
}
163165
}

Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public class MessageActionsResolver: MessageActionsResolving {
3939
}
4040
} else if info.identifier == MessageActionId.markUnread {
4141
viewModel.firstUnreadMessageId = info.message.messageId
42+
viewModel.currentUserMarkedMessageUnread = true
43+
viewModel.scrolledId = info.message.messageId
4244
}
4345

4446
viewModel.reactionsShown = false

Sources/StreamChatSwiftUI/ColorPalette.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ public struct ColorPalette {
1111
navigationBarTitle = text
1212
navigationBarSubtitle = textLowEmphasis
1313
navigationBarTintColor = tintColor
14+
15+
messageLinkAttachmentAuthorColor = tintColor
16+
messageLinkAttachmentTitleColor = Color(text)
17+
messageLinkAttachmentTextColor = Color(text)
1418
}
1519

1620
/// Tint color used in UI components.
@@ -86,6 +90,12 @@ public struct ColorPalette {
8690
public lazy var selectedReactionBackgroundColor: UIColor? = nil
8791
public var voiceMessageControlBackground: UIColor = .streamWhiteStatic
8892

93+
// MARK: - Link Attachment View
94+
95+
public var messageLinkAttachmentAuthorColor: Color
96+
public var messageLinkAttachmentTitleColor: Color
97+
public var messageLinkAttachmentTextColor: Color
98+
8999
// MARK: - Composer
90100

91101
public lazy var composerPlaceholderColor: UIColor = subtitleText

StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelDataSource_Tests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,66 @@ import XCTest
165165
XCTAssert(noMessagesCall == false)
166166
XCTAssert(messagesCall == true)
167167
}
168+
169+
// MARK: - firstUnreadMessageId Tests
170+
171+
func test_channelDataSource_firstUnreadMessageId_whenControllerHasFirstUnreadMessageId() {
172+
// Given
173+
let firstUnreadMessageId = "first-unread-message-id"
174+
let controller = makeChannelController(messages: [message])
175+
controller.mockFirstUnreadMessageId = firstUnreadMessageId
176+
let channelDataSource = ChatChannelDataSource(controller: controller)
177+
178+
// When
179+
let result = channelDataSource.firstUnreadMessageId
180+
181+
// Then
182+
XCTAssertEqual(result, firstUnreadMessageId)
183+
}
184+
185+
func test_channelDataSource_firstUnreadMessageId_whenNilAndCurrentUserHasRead() {
186+
// Given
187+
let currentUserId = chatClient.currentUserId!
188+
let read = ChatChannelRead.mock(
189+
lastReadAt: Date(),
190+
lastReadMessageId: nil,
191+
unreadMessagesCount: 0,
192+
user: .mock(id: currentUserId)
193+
)
194+
let channel = ChatChannel.mockDMChannel(reads: [read])
195+
let controller = makeChannelController(messages: [.mock(), .mock(), message])
196+
controller.channel_mock = channel
197+
controller.mockFirstUnreadMessageId = nil
198+
let channelDataSource = ChatChannelDataSource(controller: controller)
199+
200+
// When
201+
let result = channelDataSource.firstUnreadMessageId
202+
203+
// Then
204+
XCTAssertEqual(result, message.id)
205+
}
206+
207+
func test_channelDataSource_firstUnreadMessageId_whenNilAndCurrentUserHasNotRead() {
208+
// Given
209+
let otherUserId = UserId.unique
210+
let read = ChatChannelRead.mock(
211+
lastReadAt: Date(),
212+
lastReadMessageId: nil,
213+
unreadMessagesCount: 0,
214+
user: .mock(id: otherUserId)
215+
)
216+
let channel = ChatChannel.mockDMChannel(reads: [read])
217+
let controller = makeChannelController(messages: [message])
218+
controller.channel_mock = channel
219+
controller.mockFirstUnreadMessageId = .unique
220+
let channelDataSource = ChatChannelDataSource(controller: controller)
221+
222+
// When
223+
let result = channelDataSource.firstUnreadMessageId
224+
225+
// Then
226+
XCTAssertNotEqual(result, message.id)
227+
}
168228

169229
// MARK: - private
170230

0 commit comments

Comments
 (0)