Skip to content

Commit 5417035

Browse files
authored
Make resume() and pause() of LivestreamChannelController more flexible (#3774)
* Fix copy action not doing anything in livestream UI * Fix livestream square button actions background not visible in dark mode * Improve resume and pause logic of the livestream controller * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Fix copy action not showing alert * Fix forgotten completion call when resume finishes * Update CHANGELOG.md * Remove unnecessary test * Remove redudant comments
1 parent 15146fc commit 5417035

File tree

9 files changed

+254
-135
lines changed

9 files changed

+254
-135
lines changed

CHANGELOG.md

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

44
# Upcoming
55

6+
## StreamChat
7+
### ✅ Added
8+
- Add a completion block to `LivestreamChannelController.resume()` to observe possible errors [#3774](https://github.com/GetStream/stream-chat-swift/pull/3774)
9+
### 🐞 Fixed
10+
- Fix pending message being added to `LivestreamChannelController.messages` when in paused state [#3774](https://github.com/GetStream/stream-chat-swift/pull/3774)
611
### 🔄 Changed
12+
- The `LivestreamChannelController.resume()` should be manually called, previously, it was automatically called on a new message [#3774](https://github.com/GetStream/stream-chat-swift/pull/3774)
713

814
# [4.84.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.84.0)
915
_August 06, 2025_

DemoApp/Screens/Livestream/DemoLivestreamChatChannelVC.swift

Lines changed: 26 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class DemoLivestreamChatChannelVC: _ViewController,
1616
/// Controller for observing data changes within the channel.
1717
var livestreamChannelController: LivestreamChannelController!
1818

19+
/// Controller to observe web socket events.
20+
lazy var eventsController = livestreamChannelController.client.eventsController()
21+
1922
/// User search controller for suggestion users when typing in the composer.
2023
lazy var userSuggestionSearchController: ChatUserSearchController =
2124
livestreamChannelController.client.userSearchController()
2225

23-
/// A controller for observing web socket events.
24-
lazy var eventsController: EventsController = client.eventsController()
25-
2626
/// The size of the channel avatar.
2727
var channelAvatarSize: CGSize {
2828
CGSize(width: 32, height: 32)
@@ -58,38 +58,8 @@ class DemoLivestreamChatChannelVC: _ViewController,
5858
messageListVC.listView.isLastCellFullyVisible
5959
}
6060

61-
private var isLastMessageVisibleOrSeen: Bool {
62-
isLastMessageFullyVisible
63-
}
64-
6561
/// Banner view to show when chat is paused due to scrolling
66-
private lazy var pauseBannerView: UIView = {
67-
let banner = UIView()
68-
banner.backgroundColor = appearance.colorPalette.background2
69-
banner.layer.cornerRadius = 12
70-
banner.layer.shadowColor = UIColor.black.cgColor
71-
banner.layer.shadowOffset = CGSize(width: 0, height: 2)
72-
banner.layer.shadowOpacity = 0.1
73-
banner.layer.shadowRadius = 4
74-
banner.translatesAutoresizingMaskIntoConstraints = false
75-
76-
let label = UILabel()
77-
label.text = "Chat paused due to scroll"
78-
label.font = appearance.fonts.footnote
79-
label.textColor = appearance.colorPalette.text
80-
label.textAlignment = .center
81-
label.translatesAutoresizingMaskIntoConstraints = false
82-
83-
banner.addSubview(label)
84-
NSLayoutConstraint.activate([
85-
label.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 16),
86-
label.trailingAnchor.constraint(equalTo: banner.trailingAnchor, constant: -16),
87-
label.topAnchor.constraint(equalTo: banner.topAnchor, constant: 8),
88-
label.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -8)
89-
])
90-
91-
return banner
92-
}()
62+
private lazy var pauseBannerView = LivestreamPauseBannerView()
9363

9464
override func setUp() {
9565
super.setUp()
@@ -113,12 +83,6 @@ class DemoLivestreamChatChannelVC: _ViewController,
11383
messageListVC.swipeToReplyGestureHandler.onReply = { [weak self] message in
11484
self?.messageComposerVC.content.quoteMessage(message)
11585
}
116-
117-
// Initialize messages from controller
118-
messages = livestreamChannelController.messages
119-
120-
// Initialize pause banner state
121-
pauseBannerView.alpha = 0.0
12286
}
12387

12488
private func setChannelControllerToComposerIfNeeded() {
@@ -172,9 +136,6 @@ class DemoLivestreamChatChannelVC: _ViewController,
172136
constant: -16
173137
)
174138
])
175-
176-
// Initially hide the banner
177-
pauseBannerView.isHidden = true
178139
}
179140

180141
override func viewDidAppear(_ animated: Bool) {
@@ -183,14 +144,6 @@ class DemoLivestreamChatChannelVC: _ViewController,
183144
keyboardHandler.start()
184145
}
185146

186-
override func viewWillAppear(_ animated: Bool) {
187-
super.viewWillAppear(animated)
188-
189-
if let draftMessage = livestreamChannelController.channel?.draftMessage {
190-
messageComposerVC.content.draftMessage(draftMessage)
191-
}
192-
}
193-
194147
override func viewWillDisappear(_ animated: Bool) {
195148
super.viewWillDisappear(animated)
196149

@@ -211,29 +164,6 @@ class DemoLivestreamChatChannelVC: _ViewController,
211164
messageComposerVC.updateContent()
212165
}
213166

214-
// MARK: - Actions
215-
216-
/// Jump to a given message.
217-
/// In case the message is already loaded, it directly goes to it.
218-
/// If not, it will load the messages around it and go to that page.
219-
///
220-
/// This function is an high-level abstraction of `messageListVC.jumpToMessage(id:onHighlight:)`.
221-
///
222-
/// - Parameters:
223-
/// - id: The id of message which the message list should go to.
224-
/// - animated: `true` if you want to animate the change in position; `false` if it should be immediate.
225-
/// - shouldHighlight: Whether the message should be highlighted when jumping to it. By default it is highlighted.
226-
func jumpToMessage(id: MessageId, animated: Bool = true, shouldHighlight: Bool = true) {
227-
if shouldHighlight {
228-
messageListVC.jumpToMessage(id: id, animated: animated) { [weak self] indexPath in
229-
self?.messageListVC.highlightCell(at: indexPath)
230-
}
231-
return
232-
}
233-
234-
messageListVC.jumpToMessage(id: id, animated: animated)
235-
}
236-
237167
// MARK: - ChatMessageListVCDataSource
238168

239169
var messages: [ChatMessage] = []
@@ -298,8 +228,8 @@ class DemoLivestreamChatChannelVC: _ViewController,
298228
livestreamChannelController.resume()
299229
}
300230

301-
if isLastMessageFullyVisible {
302-
messageListVC.scrollToBottomButton.isHidden = true
231+
if !isLastMessageFullyVisible && !livestreamChannelController.isPaused && scrollView.isDragging {
232+
livestreamChannelController.pause()
303233
}
304234
}
305235

@@ -317,9 +247,6 @@ class DemoLivestreamChatChannelVC: _ViewController,
317247

318248
// Load older messages when displaying messages near the end of the array
319249
if indexPath.item >= messageCount - 10 {
320-
if messageListVC.listView.isDragging && !messageListVC.listView.isLastCellFullyVisible {
321-
livestreamChannelController.pause()
322-
}
323250
livestreamChannelController.loadPreviousMessages()
324251
}
325252
}
@@ -345,6 +272,11 @@ class DemoLivestreamChatChannelVC: _ViewController,
345272
}
346273
case is MarkUnreadActionItem:
347274
dismiss(animated: true)
275+
case is CopyActionItem:
276+
UIPasteboard.general.string = message.text
277+
dismiss(animated: true) { [weak self] in
278+
self?.presentAlert(title: "Message copied to clipboard")
279+
}
348280
default:
349281
return
350282
}
@@ -420,30 +352,33 @@ class DemoLivestreamChatChannelVC: _ViewController,
420352
messageListVC.scrollToBottomButton.content = .init(messages: skippedMessagesAmount, mentions: 0)
421353
}
422354

423-
// MARK: - EventsControllerDelegate
355+
func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
356+
if event is NewMessagePendingEvent {
357+
if livestreamChannelController.isPaused {
358+
pauseBannerView.setState(.resuming)
359+
}
360+
}
424361

425-
func eventsController(_ controller: EventsController, didReceiveEvent event: Event) {
426-
if let newMessagePendingEvent = event as? NewMessagePendingEvent {
427-
let newMessage = newMessagePendingEvent.message
428-
if !isFirstPageLoaded && newMessage.isSentByCurrentUser && !newMessage.isPartOfThread {
429-
livestreamChannelController.loadFirstPage()
362+
if let newMessageEvent = event as? MessageNewEvent, newMessageEvent.message.isSentByCurrentUser {
363+
if livestreamChannelController.isPaused {
364+
pauseBannerView.setState(.resuming)
365+
livestreamChannelController.resume()
430366
}
431367
}
432368
}
433369

434-
/// Shows or hides the pause banner with animation
370+
/// Shows or hides the pause banner.
435371
private func showPauseBanner(_ show: Bool) {
436-
UIView.animate(withDuration: 0.3, animations: {
437-
self.pauseBannerView.isHidden = !show
438-
self.pauseBannerView.alpha = show ? 1.0 : 0.0
439-
})
372+
if show {
373+
pauseBannerView.setState(.paused)
374+
}
375+
pauseBannerView.setVisible(show, animated: true)
440376
}
441377
}
442378

443379
/// A custom composer view controller for livestream channels that uses LivestreamChannelController
444380
/// and disables voice recording functionality.
445381
class DemoLivestreamComposerVC: ComposerVC {
446-
/// Reference to the livestream channel controller
447382
var livestreamChannelController: LivestreamChannelController?
448383

449384
override func addAttachmentToContent(

DemoApp/Screens/Livestream/DemoLivestreamChatMessageListVC.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import UIKit
1010
/// A custom message list view controller for livestream channels that uses LivestreamChannelController
1111
/// instead of MessageController and shows a custom bottom sheet for message actions.
1212
class DemoLivestreamChatMessageListVC: ChatMessageListVC {
13-
/// The livestream channel controller for managing channel operations
1413
public weak var livestreamChannelController: LivestreamChannelController?
1514

1615
override func didSelectMessageCell(at indexPath: IndexPath) {
@@ -22,13 +21,11 @@ class DemoLivestreamChatMessageListVC: ChatMessageListVC {
2221
let livestreamChannelController = livestreamChannelController
2322
else { return }
2423

25-
// Create the custom livestream actions view controller
2624
let actionsController = DemoLivestreamMessageActionsVC()
2725
actionsController.message = message
2826
actionsController.livestreamChannelController = livestreamChannelController
2927
actionsController.delegate = self
30-
31-
// Present as bottom sheet
28+
3229
actionsController.modalPresentationStyle = .pageSheet
3330

3431
if #available(iOS 16.0, *) {
@@ -54,14 +51,12 @@ class DemoLivestreamChatMessageListVC: ChatMessageListVC {
5451
let message = messageContentView.content,
5552
let livestreamChannelController = livestreamChannelController
5653
else { return }
57-
58-
// Create SwiftUI reactions list view
54+
5955
let reactionsView = DemoLivestreamReactionsListView(
6056
message: message,
6157
controller: livestreamChannelController
6258
)
63-
64-
// Present as a SwiftUI sheet
59+
6560
let hostingController = UIHostingController(rootView: reactionsView)
6661
hostingController.modalPresentationStyle = .pageSheet
6762

@@ -89,7 +84,6 @@ extension DemoLivestreamChatMessageListVC: LivestreamMessageActionsVCDelegate {
8984
message: ChatMessage,
9085
didTapOnActionItem actionItem: ChatMessageActionItem
9186
) {
92-
// Handle action items that need to be delegated to the parent
9387
delegate?.chatMessageListVC(self, didTapOnAction: actionItem, for: message)
9488
}
9589
}

DemoApp/Screens/Livestream/DemoLivestreamMessageActionsVC.swift

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ protocol LivestreamMessageActionsVCDelegate: AnyObject {
1616
func livestreamMessageActionsVCDidFinish(_ vc: DemoLivestreamMessageActionsVC)
1717
}
1818

19-
/// Custom bottom sheet view controller for livestream message actions
19+
/// Custom bottom sheet view controller for livestream message actions.
2020
class DemoLivestreamMessageActionsVC: UIViewController {
2121
// MARK: - Properties
2222

@@ -51,7 +51,7 @@ class DemoLivestreamMessageActionsVC: UIViewController {
5151
// MARK: - Setup
5252

5353
private func setupUI() {
54-
view.backgroundColor = UIColor.systemBackground
54+
view.backgroundColor = Appearance.default.colorPalette.background
5555

5656
view.addSubview(mainStackView)
5757
NSLayoutConstraint.activate([
@@ -70,8 +70,7 @@ class DemoLivestreamMessageActionsVC: UIViewController {
7070
private func setupReactions() {
7171
guard let channel = livestreamChannelController?.channel,
7272
channel.canSendReaction else { return }
73-
74-
// Use available reactions from the appearance system, ordered by raw value
73+
7574
let availableReactions = Appearance.default.images.availableReactions
7675

7776
let reactionButtons = availableReactions
@@ -142,29 +141,20 @@ class DemoLivestreamMessageActionsVC: UIViewController {
142141
button.setImage(image, for: .normal)
143142
button.contentMode = .scaleAspectFit
144143
button.imageView?.contentMode = .scaleAspectFit
145-
146-
// Add some padding around the image for better visual balance
147144
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
148-
149-
// Check if current user has reacted with this type
145+
150146
let isSelected = message?.currentUserReactions.contains { $0.type == reactionType } ?? false
151-
152-
// Configure appearance based on selection state
153147
let colorPalette = Appearance.default.colorPalette
154148
button.backgroundColor = isSelected ? colorPalette.accentPrimary.withAlphaComponent(0.5) : .systemGray6
155149
button.layer.borderWidth = isSelected ? 2 : 0.5
156150
button.layer.borderColor = isSelected ? colorPalette.accentPrimary.cgColor : UIColor.separator.cgColor
157-
158-
// Apply styling and constraints
159151
button.layer.cornerRadius = 20
160152
button.layer.shadowColor = UIColor.black.cgColor
161153
button.layer.shadowOffset = CGSize(width: 0, height: 1)
162154
button.layer.shadowOpacity = 0.1
163155
button.layer.shadowRadius = 2
164-
165156
button.width(40).height(40)
166-
167-
// Add press animation
157+
168158
button.addTarget(self, action: #selector(reactionButtonPressed(_:)), for: .touchDown)
169159
button.addTarget(self, action: #selector(reactionButtonReleased(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
170160

DemoApp/Screens/Livestream/DemoLivestreamReactionsListView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ private struct ReactionRowView: View {
112112

113113
var body: some View {
114114
HStack(spacing: 12) {
115-
// User avatar
116115
if #available(iOS 15.0, *) {
117116
AsyncImage(url: reaction.author.imageURL) { image in
118117
image
@@ -143,7 +142,6 @@ private struct ReactionRowView: View {
143142

144143
Spacer()
145144

146-
// Reaction emoji/image
147145
if let reactionAppearance = Appearance.default.images.availableReactions[reaction.type] {
148146
if #available(iOS 15.0, *) {
149147
Image(uiImage: reactionAppearance.largeIcon)

0 commit comments

Comments
 (0)