Skip to content

Commit 0d96d1e

Browse files
authored
Improve accuracy of in-memory updates of LivestreamChannelController (#3787)
* Change livestream controller to handle membership updates in-memory * Handle channel updated event manually * Make sure deletedAt is always updated for soft deleted message * Use channel updater sync * Update channel.pinnedMessages when a message is pinned * Make Channel.membership internal set * Fix crash when removing a pinned message * Add test coverage to membership updates * Add test coverage to manual channel updates * Add test coverage to pinned messages update * Add test coverage to change of channel sync * Fix channel mock not compiling * Update CHANGELOG.md * Fix tests
1 parent fd029e7 commit 0d96d1e

File tree

10 files changed

+1214
-179
lines changed

10 files changed

+1214
-179
lines changed

CHANGELOG.md

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

66
## StreamChat
77
### 🐞 Fixed
8-
- Fix LivestreamChannelController not reconnecting when connection is dropped [#3782](https://github.com/GetStream/stream-chat-swift/pull/3782)
8+
- Fix `LivestreamChannelController` not reconnecting when connection is dropped [#3782](https://github.com/GetStream/stream-chat-swift/pull/3782)
99
- Fix `StreamAudioRecorder` not overridable because of init method [#3783](https://github.com/GetStream/stream-chat-swift/pull/3783)
1010
- Fix channel list query filtering by both blocked and non-blocked channels [#3785](https://github.com/GetStream/stream-chat-swift/pull/3785)
11+
- Fix `LivestreamChannelController.synchronize()` not working if client not connected [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
12+
- Fix membership updates in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
13+
- Fix deleted messages updates in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
14+
- Fix `channel.pinnedMessages` not updated when pinning a message in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
15+
- Fix `LivestreamChannelController` not watching the channel automatically when the current user joins the channel [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
1116

1217
# [4.85.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.85.0)
1318
_August 13, 2025_

Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -832,13 +832,6 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
832832

833833
switch result {
834834
case .success(let payload):
835-
// If it is the first page, save channel to the DB to make sure manual event handling
836-
// can fetch the channel from the DB.
837-
if channelQuery.pagination == nil {
838-
client.databaseContainer.write { session in
839-
try session.saveChannel(payload: payload)
840-
}
841-
}
842835
self.handleChannelPayload(payload, channelQuery: channelQuery)
843836
completion?(nil)
844837

@@ -851,7 +844,11 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
851844
}
852845
}
853846

854-
apiClient.request(endpoint: endpoint, completion: requestCompletion)
847+
updater.update(
848+
channelQuery: channelQuery,
849+
isInRecoveryMode: false,
850+
completion: requestCompletion
851+
)
855852
}
856853

857854
private func handleChannelPayload(_ payload: ChannelPayload, channelQuery: ChannelQuery) {
@@ -936,7 +933,10 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
936933
handleDeletedMessage(messageDeletedEvent.message)
937934
return
938935
}
939-
handleUpdatedMessage(messageDeletedEvent.message)
936+
let deletedMessage = messageDeletedEvent.message.changing(
937+
deletedAt: messageDeletedEvent.createdAt
938+
)
939+
handleUpdatedMessage(deletedMessage)
940940

941941
case let newMessageErrorEvent as NewMessageErrorEvent:
942942
guard let message = messages.first(where: { $0.id == newMessageErrorEvent.messageId }) else {
@@ -957,15 +957,73 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
957957
case let channelUpdatedEvent as ChannelUpdatedEvent:
958958
handleChannelUpdated(channelUpdatedEvent)
959959

960-
case is MemberAddedEvent,
961-
is MemberRemovedEvent,
962-
is MemberUpdatedEvent,
963-
is NotificationAddedToChannelEvent,
964-
is NotificationRemovedFromChannelEvent,
965-
is NotificationInvitedEvent,
966-
is NotificationInviteAcceptedEvent,
967-
is NotificationInviteRejectedEvent:
968-
updateChannelFromDataStore()
960+
case let notificationAddedToChannelEvent as NotificationAddedToChannelEvent:
961+
var members = Set(channel?.lastActiveMembers ?? [])
962+
members.insert(notificationAddedToChannelEvent.member)
963+
let memberCount = channel?.memberCount ?? 0
964+
channel = channel?.changing(
965+
members: Array(members),
966+
membership: notificationAddedToChannelEvent.member,
967+
memberCount: memberCount + 1
968+
)
969+
startWatching(isInRecoveryMode: false)
970+
971+
case let notificationRemovedFromChannelEvent as NotificationRemovedFromChannelEvent:
972+
var members = channel?.lastActiveMembers ?? []
973+
members.removeAll(where: { $0.id == notificationRemovedFromChannelEvent.user.id })
974+
let memberCount = channel?.memberCount ?? 0
975+
976+
channel = channel?.changing(members: members, memberCount: memberCount - 1)
977+
channel?.membership = nil
978+
979+
case let memberAddedEvent as MemberAddedEvent:
980+
var members = Set(channel?.lastActiveMembers ?? [])
981+
members.insert(memberAddedEvent.member)
982+
let memberCount = channel?.memberCount ?? 0
983+
984+
var membership: ChatChannelMember?
985+
if memberAddedEvent.member.id == currentUserId {
986+
membership = memberAddedEvent.member
987+
}
988+
channel = channel?.changing(
989+
members: Array(members),
990+
membership: membership,
991+
memberCount: memberCount + 1
992+
)
993+
994+
case let memberRemovedEvent as MemberRemovedEvent:
995+
var members = channel?.lastActiveMembers ?? []
996+
members.removeAll(where: { $0.id == memberRemovedEvent.user.id })
997+
let memberCount = channel?.memberCount ?? 0
998+
999+
var membership: ChatChannelMember? = channel?.membership
1000+
if memberRemovedEvent.user.id == currentUserId {
1001+
membership = nil
1002+
}
1003+
channel = channel?.changing(members: members, memberCount: memberCount - 1)
1004+
channel?.membership = membership
1005+
1006+
case let userWatchingEvent as UserWatchingEvent:
1007+
var watchers = channel?.lastActiveWatchers ?? []
1008+
if userWatchingEvent.isStarted {
1009+
watchers.append(userWatchingEvent.user)
1010+
} else {
1011+
watchers.removeAll(where: { $0.id == userWatchingEvent.user.id })
1012+
}
1013+
channel = channel?.changing(watchers: watchers, watcherCount: userWatchingEvent.watcherCount)
1014+
1015+
case let memberUpdatedEvent as MemberUpdatedEvent:
1016+
var members = channel?.lastActiveMembers ?? []
1017+
if let index = members.firstIndex(where: { $0.id == memberUpdatedEvent.member.id }) {
1018+
members[index] = memberUpdatedEvent.member
1019+
}
1020+
1021+
var membership: ChatChannelMember? = channel?.membership
1022+
if memberUpdatedEvent.member.id == currentUserId {
1023+
membership = memberUpdatedEvent.member
1024+
}
1025+
1026+
channel = channel?.changing(members: members, membership: membership)
9691027

9701028
case is UserBannedEvent,
9711029
is UserUnbannedEvent:
@@ -1008,7 +1066,18 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
10081066

10091067
private func handleUpdatedMessage(_ updatedMessage: ChatMessage) {
10101068
if let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) {
1069+
let existingMessage = messages[index]
10111070
messages[index] = updatedMessage
1071+
1072+
if existingMessage.isPinned != updatedMessage.isPinned {
1073+
var pinnedMessages = channel?.pinnedMessages ?? []
1074+
if updatedMessage.isPinned {
1075+
pinnedMessages.append(updatedMessage)
1076+
} else {
1077+
pinnedMessages.removeAll(where: { $0.id == existingMessage.id })
1078+
}
1079+
channel = channel?.changing(pinnedMessages: pinnedMessages)
1080+
}
10121081
}
10131082
}
10141083

@@ -1037,7 +1106,31 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
10371106
}
10381107

10391108
private func handleChannelUpdated(_ event: ChannelUpdatedEvent) {
1040-
channel = event.channel
1109+
channel = channel?.changing(
1110+
name: event.channel.name,
1111+
imageURL: event.channel.imageURL,
1112+
lastMessageAt: event.channel.lastMessageAt,
1113+
createdAt: event.channel.createdAt,
1114+
deletedAt: event.channel.deletedAt,
1115+
updatedAt: event.channel.updatedAt,
1116+
truncatedAt: event.channel.truncatedAt,
1117+
isHidden: event.channel.isHidden,
1118+
createdBy: event.channel.createdBy,
1119+
config: event.channel.config,
1120+
ownCapabilities: event.channel.ownCapabilities,
1121+
isFrozen: event.channel.isFrozen,
1122+
isDisabled: event.channel.isDisabled,
1123+
isBlocked: event.channel.isBlocked,
1124+
reads: event.channel.reads,
1125+
members: event.channel.lastActiveMembers,
1126+
membership: event.channel.membership,
1127+
memberCount: event.channel.memberCount,
1128+
watchers: event.channel.lastActiveWatchers,
1129+
watcherCount: event.channel.watcherCount,
1130+
team: event.channel.team,
1131+
cooldownDuration: event.channel.cooldownDuration,
1132+
extraData: event.channel.extraData
1133+
)
10411134
}
10421135

10431136
// For events that do not have the channel data, and still

Sources/StreamChat/Models/Channel.swift

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ public struct ChatChannel {
7777
public let currentlyTypingUsers: Set<ChatUser>
7878

7979
/// If the current user is a member of the channel, this variable contains the details about the membership.
80-
public let membership: ChatChannelMember?
81-
80+
public internal(set) var membership: ChatChannelMember?
81+
8282
/// Returns `true`, if the channel is archived.
8383
public var isArchived: Bool {
8484
membership?.archivedAt != nil
@@ -293,39 +293,59 @@ public struct ChatChannel {
293293
public func changing(
294294
name: String? = nil,
295295
imageURL: URL? = nil,
296+
lastMessageAt: Date? = nil,
297+
createdAt: Date? = nil,
298+
deletedAt: Date? = nil,
299+
updatedAt: Date? = nil,
300+
truncatedAt: Date? = nil,
301+
isHidden: Bool? = nil,
302+
createdBy: ChatUser? = nil,
303+
config: ChannelConfig? = nil,
304+
ownCapabilities: Set<ChannelCapability>? = nil,
305+
isFrozen: Bool? = nil,
306+
isDisabled: Bool? = nil,
307+
isBlocked: Bool? = nil,
296308
reads: [ChatChannelRead]? = nil,
309+
members: [ChatChannelMember]? = nil,
310+
membership: ChatChannelMember? = nil,
311+
memberCount: Int? = nil,
312+
watchers: [ChatUser]? = nil,
313+
watcherCount: Int? = nil,
314+
team: TeamId? = nil,
315+
cooldownDuration: Int? = nil,
316+
pinnedMessages: [ChatMessage]? = nil,
297317
extraData: [String: RawJSON]? = nil
298318
) -> ChatChannel {
299319
.init(
300320
cid: cid,
301321
name: name ?? self.name,
302322
imageURL: imageURL ?? self.imageURL,
303-
lastMessageAt: lastMessageAt,
304-
createdAt: createdAt,
305-
updatedAt: updatedAt,
306-
deletedAt: deletedAt,
307-
truncatedAt: truncatedAt,
308-
isHidden: isHidden,
309-
createdBy: createdBy,
310-
config: config,
311-
ownCapabilities: ownCapabilities,
312-
isFrozen: isFrozen,
313-
isDisabled: isDisabled,
314-
isBlocked: isBlocked,
315-
lastActiveMembers: lastActiveMembers,
316-
membership: membership,
323+
lastMessageAt: lastMessageAt ?? self.lastMessageAt,
324+
createdAt: createdAt ?? self.createdAt,
325+
updatedAt: updatedAt ?? self.updatedAt,
326+
deletedAt: deletedAt ?? self.deletedAt,
327+
truncatedAt: truncatedAt ?? self.truncatedAt,
328+
isHidden: isHidden ?? self.isHidden,
329+
createdBy: createdBy ?? self.createdBy,
330+
config: config ?? self.config,
331+
ownCapabilities: ownCapabilities ?? self.ownCapabilities,
332+
isFrozen: isFrozen ?? self.isFrozen,
333+
isDisabled: isDisabled ?? self.isDisabled,
334+
isBlocked: isBlocked ?? self.isBlocked,
335+
lastActiveMembers: members ?? lastActiveMembers,
336+
membership: membership ?? self.membership,
317337
currentlyTypingUsers: currentlyTypingUsers,
318-
lastActiveWatchers: lastActiveWatchers,
319-
team: team,
338+
lastActiveWatchers: watchers ?? lastActiveWatchers,
339+
team: team ?? self.team,
320340
unreadCount: unreadCount,
321-
watcherCount: watcherCount,
322-
memberCount: memberCount,
341+
watcherCount: watcherCount ?? self.watcherCount,
342+
memberCount: memberCount ?? self.memberCount,
323343
reads: reads ?? self.reads,
324-
cooldownDuration: cooldownDuration,
325-
extraData: extraData ?? [:],
344+
cooldownDuration: cooldownDuration ?? self.cooldownDuration,
345+
extraData: extraData ?? self.extraData,
326346
latestMessages: latestMessages,
327347
lastMessageFromCurrentUser: lastMessageFromCurrentUser,
328-
pinnedMessages: pinnedMessages,
348+
pinnedMessages: pinnedMessages ?? self.pinnedMessages,
329349
pendingMessages: pendingMessages,
330350
muteDetails: muteDetails,
331351
previewMessage: previewMessage,

Sources/StreamChat/Models/ChatMessage.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ public struct ChatMessage {
285285
originalLanguage: TranslationLanguage? = nil,
286286
moderationDetails: MessageModerationDetails? = nil,
287287
readBy: Set<ChatUser>? = nil,
288+
deletedAt: Date? = nil,
288289
extraData: [String: RawJSON]? = nil
289290
) -> ChatMessage {
290291
.init(
@@ -296,7 +297,7 @@ public struct ChatMessage {
296297
createdAt: createdAt,
297298
locallyCreatedAt: locallyCreatedAt,
298299
updatedAt: updatedAt,
299-
deletedAt: deletedAt,
300+
deletedAt: deletedAt ?? self.deletedAt,
300301
arguments: arguments ?? self.arguments,
301302
parentMessageId: parentMessageId,
302303
showReplyInChannel: showReplyInChannel,

0 commit comments

Comments
 (0)