Skip to content

Commit 73974a5

Browse files
authored
Handle member related events and channel truncated event in LivestreamChannelController (#3775)
* Handle Member Update Events in the Livestream channel controller * Handle channel truncation updated event in livestream channel controller * Update demo app livestream UI to show number of members and watchers * Add test coverage to member related events * Add test coverage to truncated channel even * Update CHANGELOG.md
1 parent 5417035 commit 73974a5

File tree

5 files changed

+300
-3
lines changed

5 files changed

+300
-3
lines changed

CHANGELOG.md

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

66
## StreamChat
77
### ✅ Added
8+
- Handle member-related events in `LivestreamChannelController` [#3775](https://github.com/GetStream/stream-chat-swift/pull/3775)
9+
- Handle channel truncation events in `LivestreamChannelController` [#3775](https://github.com/GetStream/stream-chat-swift/pull/3775)
810
- Add a completion block to `LivestreamChannelController.resume()` to observe possible errors [#3774](https://github.com/GetStream/stream-chat-swift/pull/3774)
911
### 🐞 Fixed
1012
- Fix pending message being added to `LivestreamChannelController.messages` when in paused state [#3774](https://github.com/GetStream/stream-chat-swift/pull/3774)

DemoApp/Screens/Livestream/DemoLivestreamChatChannelVC.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,35 @@ class DemoLivestreamChatChannelVC: _ViewController,
5050
.channelAvatarView.init()
5151
.withoutAutoresizingMaskConstraints
5252

53+
/// Title label shown in the navigation bar.
54+
private lazy var titleLabel: UILabel = {
55+
let label = UILabel()
56+
label.font = appearance.fonts.subheadlineBold
57+
label.textColor = appearance.colorPalette.text
58+
label.textAlignment = .center
59+
label.setContentHuggingPriority(.required, for: .vertical)
60+
return label
61+
}()
62+
63+
/// Subtitle label showing members and online watchers in the navigation bar.
64+
private lazy var subtitleLabel: UILabel = {
65+
let label = UILabel()
66+
label.font = appearance.fonts.footnote
67+
label.textColor = appearance.colorPalette.subtitleText
68+
label.textAlignment = .center
69+
label.setContentCompressionResistancePriority(.required, for: .vertical)
70+
return label
71+
}()
72+
73+
/// Stack view containing title and subtitle for the navigation bar.
74+
private lazy var titleStackView: UIStackView = {
75+
let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
76+
stack.axis = .vertical
77+
stack.alignment = .center
78+
stack.spacing = 0
79+
return stack
80+
}()
81+
5382
/// The message composer bottom constraint used for keyboard animation handling.
5483
var messageComposerBottomConstraint: NSLayoutConstraint?
5584

@@ -124,6 +153,10 @@ class DemoLivestreamChatChannelVC: _ViewController,
124153
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: channelAvatarView)
125154
channelAvatarView.content = (livestreamChannelController.channel, client.currentUserId)
126155

156+
// Set up custom title view with channel name and member/online counts
157+
navigationItem.titleView = titleStackView
158+
updateNavigationTitle()
159+
127160
// Add pause banner
128161
view.addSubview(pauseBannerView)
129162
NSLayoutConstraint.activate([
@@ -313,7 +346,7 @@ class DemoLivestreamChatChannelVC: _ViewController,
313346
didUpdateChannel channel: ChatChannel
314347
) {
315348
channelAvatarView.content = (livestreamChannelController.channel, client.currentUserId)
316-
navigationItem.title = channel.name
349+
updateNavigationTitle()
317350
}
318351

319352
func livestreamChannelController(
@@ -374,6 +407,19 @@ class DemoLivestreamChatChannelVC: _ViewController,
374407
}
375408
pauseBannerView.setVisible(show, animated: true)
376409
}
410+
411+
/// Updates the navigation title and subtitle with channel name, member count and online watcher count.
412+
private func updateNavigationTitle() {
413+
let channel = livestreamChannelController.channel
414+
titleLabel.text = channel?.name ?? ""
415+
416+
let memberCount = channel?.memberCount ?? 0
417+
let watcherCount = channel?.watcherCount ?? 0
418+
419+
let membersText = memberCount == 1 ? "1 member" : "\(memberCount) members"
420+
let onlineText = watcherCount == 1 ? "1 online" : "\(watcherCount) online"
421+
subtitleLabel.text = "\(membersText), \(onlineText)"
422+
}
377423
}
378424

379425
/// A custom composer view controller for livestream channels that uses LivestreamChannelController

Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,24 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
904904
case let channelUpdatedEvent as ChannelUpdatedEvent:
905905
handleChannelUpdated(channelUpdatedEvent)
906906

907+
case is MemberAddedEvent,
908+
is MemberRemovedEvent,
909+
is MemberUpdatedEvent,
910+
is NotificationAddedToChannelEvent,
911+
is NotificationRemovedFromChannelEvent,
912+
is NotificationInvitedEvent,
913+
is NotificationInviteAcceptedEvent,
914+
is NotificationInviteRejectedEvent:
915+
updateChannelFromDataStore()
916+
917+
case let channelTruncatedEvent as ChannelTruncatedEvent:
918+
channel = channelTruncatedEvent.channel
919+
if let message = channelTruncatedEvent.message {
920+
messages = [message]
921+
} else {
922+
messages = []
923+
}
924+
907925
default:
908926
break
909927
}
@@ -965,6 +983,15 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
965983
channel = event.channel
966984
}
967985

986+
// For events that do not have the channel data, and still
987+
// go through the middleware, lets fetch it from DB and update it.
988+
private func updateChannelFromDataStore() {
989+
guard let cid = cid, let updatedChannel = dataStore.channel(cid: cid) else {
990+
return
991+
}
992+
channel = updatedChannel
993+
}
994+
968995
private func createNewMessage(
969996
messageId: MessageId? = nil,
970997
text: String,

TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extension ChannelDetailPayload {
2626
isDisabled: Bool = false,
2727
isHidden: Bool? = nil,
2828
members: [MemberPayload] = [],
29+
memberCount: Int? = nil,
2930
team: String? = nil,
3031
cooldownDuration: Int = 0
3132
) -> Self {
@@ -48,7 +49,7 @@ extension ChannelDetailPayload {
4849
isBlocked: isBlocked,
4950
isHidden: isHidden,
5051
members: members,
51-
memberCount: members.count,
52+
memberCount: memberCount ?? members.count,
5253
team: team,
5354
cooldownDuration: cooldownDuration
5455
)

Tests/StreamChatTests/Controllers/ChannelController/LivestreamChannelController_Tests.swift

Lines changed: 222 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1226,10 +1226,231 @@ extension LivestreamChannelController_Tests {
12261226
),
12271227
didReceiveEvent: event
12281228
)
1229-
1229+
12301230
// Then
12311231
XCTAssertEqual(controller.channel?.name, "Updated Name")
12321232
}
1233+
1234+
// MARK: - Member Update Events Tests
1235+
1236+
func test_didReceiveEvent_memberRelatedEvents_updateChannelFromDataStore() {
1237+
let cid = controller.cid!
1238+
1239+
// Test data for all member-related events
1240+
let memberEvents: [(event: Event, description: String)] = [
1241+
(
1242+
MemberAddedEvent(
1243+
user: .mock(id: .unique),
1244+
cid: cid,
1245+
member: .mock(id: .unique),
1246+
createdAt: .unique
1247+
),
1248+
"MemberAddedEvent"
1249+
),
1250+
(
1251+
MemberRemovedEvent(
1252+
user: .mock(id: .unique),
1253+
cid: cid,
1254+
createdAt: .unique
1255+
),
1256+
"MemberRemovedEvent"
1257+
),
1258+
(
1259+
MemberUpdatedEvent(
1260+
user: .mock(id: .unique),
1261+
cid: cid,
1262+
member: .mock(id: .unique),
1263+
createdAt: .unique
1264+
),
1265+
"MemberUpdatedEvent"
1266+
),
1267+
(
1268+
NotificationAddedToChannelEvent(
1269+
channel: .mock(cid: cid),
1270+
unreadCount: nil,
1271+
member: .mock(id: .unique),
1272+
createdAt: .unique
1273+
),
1274+
"NotificationAddedToChannelEvent"
1275+
),
1276+
(
1277+
NotificationRemovedFromChannelEvent(
1278+
user: .mock(id: .unique),
1279+
cid: cid,
1280+
member: .mock(id: .unique),
1281+
createdAt: .unique
1282+
),
1283+
"NotificationRemovedFromChannelEvent"
1284+
),
1285+
(
1286+
NotificationInvitedEvent(
1287+
user: .mock(id: .unique),
1288+
cid: cid,
1289+
member: .mock(id: .unique),
1290+
createdAt: .unique
1291+
),
1292+
"NotificationInvitedEvent"
1293+
),
1294+
(
1295+
NotificationInviteAcceptedEvent(
1296+
user: .mock(id: .unique),
1297+
channel: .mock(cid: cid),
1298+
member: .mock(id: .unique),
1299+
createdAt: .unique
1300+
),
1301+
"NotificationInviteAcceptedEvent"
1302+
),
1303+
(
1304+
NotificationInviteRejectedEvent(
1305+
user: .mock(id: .unique),
1306+
channel: .mock(cid: cid),
1307+
member: .mock(id: .unique),
1308+
createdAt: .unique
1309+
),
1310+
"NotificationInviteRejectedEvent"
1311+
)
1312+
]
1313+
1314+
// Test each member-related event
1315+
for (index, (event, description)) in memberEvents.enumerated() {
1316+
let initialMemberCount = 10 + index
1317+
let updatedMemberCount = 20 + index
1318+
1319+
// Given - Set up initial channel in database
1320+
let initialChannelPayload = ChannelPayload.dummy(
1321+
channel: .dummy(cid: cid, memberCount: initialMemberCount)
1322+
)
1323+
try! client.databaseContainer.writeSynchronously { session in
1324+
try session.saveChannel(payload: initialChannelPayload)
1325+
}
1326+
1327+
// Load initial data
1328+
let exp = expectation(description: "sync completes")
1329+
controller.synchronize { _ in
1330+
exp.fulfill()
1331+
}
1332+
client.mockAPIClient.test_simulateResponse(.success(initialChannelPayload))
1333+
1334+
waitForExpectations(timeout: defaultTimeout)
1335+
XCTAssertEqual(controller.channel?.memberCount, initialMemberCount, "Initial setup failed for \(description)")
1336+
1337+
// Update channel in database with new member count
1338+
let updatedChannelPayload = ChannelPayload.dummy(
1339+
channel: .dummy(cid: cid, memberCount: updatedMemberCount)
1340+
)
1341+
try! client.databaseContainer.writeSynchronously { session in
1342+
try session.saveChannel(payload: updatedChannelPayload)
1343+
}
1344+
1345+
// When
1346+
controller.eventsController(
1347+
EventsController(notificationCenter: client.eventNotificationCenter),
1348+
didReceiveEvent: event
1349+
)
1350+
1351+
// Then
1352+
XCTAssertEqual(
1353+
controller.channel?.memberCount,
1354+
updatedMemberCount,
1355+
"\(description) should update channel from data store"
1356+
)
1357+
}
1358+
}
1359+
1360+
func test_didReceiveEvent_channelTruncatedEvent_updatesChannelAndMessages() {
1361+
// Given - Set up initial channel with messages
1362+
let cid = controller.cid!
1363+
let initialMessage1 = ChatMessage.mock(id: "message1", cid: cid, text: "Message 1")
1364+
let initialMessage2 = ChatMessage.mock(id: "message2", cid: cid, text: "Message 2")
1365+
1366+
// Load initial messages
1367+
controller.eventsController(
1368+
EventsController(notificationCenter: client.eventNotificationCenter),
1369+
didReceiveEvent: MessageNewEvent(
1370+
user: .mock(id: .unique),
1371+
message: initialMessage1,
1372+
channel: .mock(cid: cid),
1373+
createdAt: .unique,
1374+
watcherCount: nil,
1375+
unreadCount: nil
1376+
)
1377+
)
1378+
controller.eventsController(
1379+
EventsController(notificationCenter: client.eventNotificationCenter),
1380+
didReceiveEvent: MessageNewEvent(
1381+
user: .mock(id: .unique),
1382+
message: initialMessage2,
1383+
channel: .mock(cid: cid),
1384+
createdAt: .unique,
1385+
watcherCount: nil,
1386+
unreadCount: nil
1387+
)
1388+
)
1389+
XCTAssertEqual(controller.messages.count, 2)
1390+
1391+
// Create truncated channel and truncation message
1392+
let truncatedChannel = ChatChannel.mock(cid: cid, name: "Truncated Channel")
1393+
let truncationMessage = ChatMessage.mock(id: "truncation", cid: cid, text: "Channel was truncated")
1394+
1395+
let event = ChannelTruncatedEvent(
1396+
channel: truncatedChannel,
1397+
user: .mock(id: .unique),
1398+
message: truncationMessage,
1399+
createdAt: .unique
1400+
)
1401+
1402+
// When
1403+
controller.eventsController(
1404+
EventsController(notificationCenter: client.eventNotificationCenter),
1405+
didReceiveEvent: event
1406+
)
1407+
1408+
// Then
1409+
XCTAssertEqual(controller.channel?.name, "Truncated Channel")
1410+
XCTAssertEqual(controller.messages.count, 1)
1411+
XCTAssertEqual(controller.messages.first?.id, "truncation")
1412+
XCTAssertEqual(controller.messages.first?.text, "Channel was truncated")
1413+
}
1414+
1415+
func test_didReceiveEvent_channelTruncatedEventWithoutMessage_clearsMessages() {
1416+
// Given - Set up initial channel with messages
1417+
let cid = controller.cid!
1418+
let initialMessage = ChatMessage.mock(id: "message1", cid: cid, text: "Message 1")
1419+
1420+
// Load initial message
1421+
controller.eventsController(
1422+
EventsController(notificationCenter: client.eventNotificationCenter),
1423+
didReceiveEvent: MessageNewEvent(
1424+
user: .mock(id: .unique),
1425+
message: initialMessage,
1426+
channel: .mock(cid: cid),
1427+
createdAt: .unique,
1428+
watcherCount: nil,
1429+
unreadCount: nil
1430+
)
1431+
)
1432+
XCTAssertEqual(controller.messages.count, 1)
1433+
1434+
// Create truncated channel without truncation message
1435+
let truncatedChannel = ChatChannel.mock(cid: cid, name: "Truncated Channel")
1436+
1437+
let event = ChannelTruncatedEvent(
1438+
channel: truncatedChannel,
1439+
user: .mock(id: .unique),
1440+
message: nil, // No truncation message
1441+
createdAt: .unique
1442+
)
1443+
1444+
// When
1445+
controller.eventsController(
1446+
EventsController(notificationCenter: client.eventNotificationCenter),
1447+
didReceiveEvent: event
1448+
)
1449+
1450+
// Then
1451+
XCTAssertEqual(controller.channel?.name, "Truncated Channel")
1452+
XCTAssertTrue(controller.messages.isEmpty)
1453+
}
12331454

12341455
func test_didReceiveEvent_differentChannelEvent_isIgnored() {
12351456
let otherChannelId = ChannelId.unique

0 commit comments

Comments
 (0)