-
Notifications
You must be signed in to change notification settings - Fork 234
Expand file tree
/
Copy pathCurrentUserDTO.swift
More file actions
341 lines (288 loc) · 13.4 KB
/
Copy pathCurrentUserDTO.swift
File metadata and controls
341 lines (288 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//
// Copyright © 2026 Stream.io Inc. All rights reserved.
//
import CoreData
import Foundation
@objc(CurrentUserDTO)
class CurrentUserDTO: NSManagedObject {
/// JSON-encoded `[groupKey: unreadCount]`.
@NSManaged var unreadGroupedChannelsCounts: Data?
@NSManaged var unreadChannelsCount: Int64
@NSManaged var unreadMessagesCount: Int64
@NSManaged var unreadThreadsCount: Int64
/// Contains the timestamp when last sync process was finished.
/// The date later serves as reference date for the last event synced using `/sync` endpoint
@NSManaged var lastSynchedEventDate: DBDate?
@NSManaged var blockedUserIds: Set<String>
@NSManaged var flaggedUsers: Set<UserDTO>
@NSManaged var flaggedMessages: Set<MessageDTO>
@NSManaged var mutedUsers: Set<UserDTO>
@NSManaged var user: UserDTO
@NSManaged var devices: Set<DeviceDTO>
@NSManaged var currentDevice: DeviceDTO?
@NSManaged var channelMutes: Set<ChannelMuteDTO>
@NSManaged var isInvisible: Bool
// UserPrivacySettings. For now, these booleans are enough for the DTO to make it simpler.
// But if new properties are added, we might need to create new DTOs specific to each setting.
@NSManaged var isTypingIndicatorsEnabled: Bool
@NSManaged var isReadReceiptsEnabled: Bool
@NSManaged var isDeliveryReceiptsEnabled: Bool
@NSManaged var pushPreference: PushPreferenceDTO?
/// Returns a default fetch request for the current user.
static var defaultFetchRequest: NSFetchRequest<CurrentUserDTO> {
let request = NSFetchRequest<CurrentUserDTO>(entityName: CurrentUserDTO.entityName)
CurrentUserDTO.applyPrefetchingState(to: request)
// Sorting doesn't matter here as soon as we have a single current-user in a database.
// It's here to make the request safe for FRC
request.sortDescriptors = [.init(keyPath: \CurrentUserDTO.unreadMessagesCount, ascending: true)]
return request
}
}
extension CurrentUserDTO {
/// Returns an existing `CurrentUserDTO`. Returns `nil` of no `CurrentUserDTO` exists in the DB.
///
/// - Parameter context: The context used to fetch `CurrentUserDTO`
fileprivate static func load(context: NSManagedObjectContext) -> CurrentUserDTO? {
let request = NSFetchRequest<CurrentUserDTO>(entityName: CurrentUserDTO.entityName)
CurrentUserDTO.applyPrefetchingState(to: request)
let result = load(by: request, context: context)
log.assert(
result.count <= 1,
"The database is corrupted. There is more than 1 entity of the type `CurrentUserDTO` in the DB."
)
return result.first
}
/// If the `CurrentUserDTO` entity exists in the context, fetches and returns it. Otherwise create a new `CurrentUserDTO`.
///
/// - Parameter context: The context used to fetch/create `CurrentUserDTO`
fileprivate static func loadOrCreate(context: NSManagedObjectContext) -> CurrentUserDTO {
let request = NSFetchRequest<CurrentUserDTO>(entityName: CurrentUserDTO.entityName)
CurrentUserDTO.applyPrefetchingState(to: request)
let result = load(by: request, context: context)
log.assert(
result.count <= 1,
"The database is corrupted. There is more than 1 entity of the type `CurrentUserDTO` in the DB."
)
if let existing = result.first {
return existing
}
let new = NSEntityDescription.insertNewObject(into: context, for: request)
return new
}
static func load(context: NSManagedObjectContext) throws -> CurrentChatUser {
guard let dto = load(context: context) else { throw ClientError.CurrentUserDoesNotExist() }
return try dto.asModel()
}
}
extension NSManagedObjectContext: CurrentUserDatabaseSession {
func saveCurrentUser(payload: CurrentUserPayload) throws -> CurrentUserDTO {
invalidateCurrentUserCache()
let dto = CurrentUserDTO.loadOrCreate(context: self)
dto.user = try saveUser(payload: payload)
dto.isInvisible = payload.isInvisible
// If not privacy setting is provided by the backend then we treat as enabled by default.
// This is a bit different than the rest of the backend responses, but it was done like this
// for backwards compatibility reasons on the server side.
dto.isReadReceiptsEnabled = payload.privacySettings?.readReceipts?.enabled ?? true
dto.isTypingIndicatorsEnabled = payload.privacySettings?.typingIndicators?.enabled ?? true
dto.isDeliveryReceiptsEnabled = payload.privacySettings?.deliveryReceipts?.enabled ?? true
// Save push preference
if let pushPreference = payload.pushPreference {
dto.pushPreference = try savePushPreference(id: payload.id, payload: pushPreference)
}
let mutedUsers = try payload.mutedUsers.map { try saveUser(payload: $0.mutedUser) }
dto.mutedUsers = Set(mutedUsers)
dto.blockedUserIds = payload.blockedUserIds
let channelMutes = Set(
try payload.mutedChannels.map { try saveChannelMute(payload: $0) }
)
dto.channelMutes.subtracting(channelMutes).forEach { delete($0) }
dto.channelMutes = channelMutes
if let unreadCount = payload.unreadCount {
try saveCurrentUserUnreadCount(count: unreadCount)
}
_ = try saveCurrentUserDevices(payload.devices, clearExisting: true)
return dto
}
func saveCurrentUserUnreadCount(count: UnreadCountPayload) throws {
invalidateCurrentUserCache()
guard let dto = currentUser else {
throw ClientError.CurrentUserDoesNotExist()
}
if let unreadChannels = count.channels {
dto.unreadChannelsCount = Int64(clamping: unreadChannels)
}
if let unreadMessages = count.messages {
dto.unreadMessagesCount = Int64(clamping: unreadMessages)
}
if let threadsCount = count.threads {
dto.unreadThreadsCount = Int64(clamping: threadsCount)
}
}
/// Merges per-group unread channel counts into `CurrentUserDTO.unreadChannelCountsByGroup`.
/// Called from `queryGroupedChannels` responses and from WS events carrying
/// `grouped_unread_channels`; both paths use merge semantics, so keys absent from the input
/// are left untouched and a group that disappears from a server snapshot will keep its
/// locally-cached count until something explicitly clears it.
func mergeCurrentUserUnreadChannelCountsByGroup(_ unreadChannelCountsByGroup: [String: Int]) throws {
invalidateCurrentUserCache()
guard let dto = currentUser else {
throw ClientError.CurrentUserDoesNotExist()
}
dto.unreadChannelCountsByGroup = (dto.unreadChannelCountsByGroup ?? [:]).merging(unreadChannelCountsByGroup) { _, new in new }
}
func adjustUnreadChannelCount(forGroup groupKey: String, by delta: Int) {
invalidateCurrentUserCache()
guard let dto = currentUser, var counts = dto.unreadChannelCountsByGroup, let existing = counts[groupKey] else {
return
}
counts[groupKey] = max(0, existing + delta)
dto.unreadChannelCountsByGroup = counts
}
func saveCurrentUserDevices(_ devices: [Device], clearExisting: Bool) throws -> [DeviceDTO] {
invalidateCurrentUserCache()
guard let currentUser = currentUser else {
throw ClientError.CurrentUserDoesNotExist()
}
if clearExisting {
currentUser.devices.removeAll()
if !devices.contains(where: { $0.id == currentUser.currentDevice?.id }) {
currentUser.currentDevice = nil
}
}
let deviceDTOs = devices.map { device -> DeviceDTO in
let dto = DeviceDTO.loadOrCreate(id: device.id, context: self)
dto.createdAt = device.createdAt?.bridgeDate
dto.disabled = device.disabled.map { NSNumber(value: $0) }
dto.disabledReason = device.disabledReason
dto.hardwareId = device.hardwareId
dto.pushProvider = device.pushProvider
dto.pushProviderName = device.pushProviderName
dto.userId = device.userId
dto.voip = device.voip.map { NSNumber(value: $0) }
dto.user = currentUser
return dto
}
return deviceDTOs
}
func saveCurrentDevice(_ deviceId: String) throws {
invalidateCurrentUserCache()
guard let currentUser = currentUser else {
throw ClientError.CurrentUserDoesNotExist()
}
let dto = DeviceDTO.loadOrCreate(id: deviceId, context: self)
dto.user = currentUser
currentUser.currentDevice = dto
}
func deleteDevice(id: String) {
if let dto = DeviceDTO.load(id: id, context: self) {
delete(dto)
}
}
static let currentUserKey = "io.getStream.chat.core.context.current_user_key"
var currentUser: CurrentUserDTO? {
if let objectId = userInfo[Self.currentUserKey] as? NSManagedObjectID {
if let dto = try? existingObject(with: objectId) as? CurrentUserDTO {
return dto.isDeleted ? nil : dto
}
}
if let dto = CurrentUserDTO.load(context: self) {
if !dto.objectID.isTemporaryID {
userInfo[Self.currentUserKey] = dto.objectID
}
return dto
}
return nil
}
func invalidateCurrentUserCache() {
userInfo[Self.currentUserKey] = nil
}
func deleteCurrentUser() {
guard let currentUser else { return }
delete(currentUser)
}
}
extension CurrentUserDTO {
var unreadChannelCountsByGroup: [String: Int]? {
get {
guard let unreadGroupedChannelsCounts else { return nil }
return try? JSONDecoder.default.decode([String: Int].self, from: unreadGroupedChannelsCounts)
}
set {
unreadGroupedChannelsCounts = newValue.flatMap { try? JSONEncoder.default.encode($0) }
}
}
}
extension CurrentUserDTO {
override class func prefetchedRelationshipKeyPaths() -> [String] {
[
KeyPath.string(\CurrentUserDTO.channelMutes),
KeyPath.string(\CurrentUserDTO.currentDevice),
KeyPath.string(\CurrentUserDTO.devices),
KeyPath.string(\CurrentUserDTO.flaggedMessages),
KeyPath.string(\CurrentUserDTO.flaggedUsers),
KeyPath.string(\CurrentUserDTO.mutedUsers),
KeyPath.string(\CurrentUserDTO.user)
]
}
}
extension CurrentUserDTO {
/// Snapshots the current state of `CurrentUserDTO` and returns an immutable model object from it.
func asModel() throws -> CurrentChatUser { try .create(fromDTO: self) }
}
extension CurrentChatUser {
fileprivate static func create(fromDTO dto: CurrentUserDTO) throws -> CurrentChatUser {
try dto.isNotDeleted()
let user = dto.user
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeRawJSON(from: dto.user.extraData)
} catch {
log.error("Failed to decode extra data for user with id: <\(dto.user.id)>, using default value instead. Error: \(error)")
extraData = [:]
}
let mutedUsers: [ChatUser] = try dto.mutedUsers.map { try $0.asModel() }
let flaggedUsers: [ChatUser] = try dto.flaggedUsers.map { try $0.asModel() }
let flaggedMessagesIDs: [MessageId] = dto.flaggedMessages.map(\.id)
let mutedChannels = Set(dto.channelMutes.compactMap { try? $0.channel.asModel() })
let language: TranslationLanguage? = dto.user.language.map(TranslationLanguage.init)
let pushPreference = try dto.pushPreference?.asModel()
return try CurrentChatUser(
id: user.id,
name: user.name,
imageURL: user.imageURL,
isOnline: user.isOnline,
isInvisible: dto.isInvisible,
isBanned: user.isBanned,
userRole: UserRole(rawValue: user.userRoleRaw),
teamsRole: user.teamsRole?.mapValues { UserRole(rawValue: $0) },
createdAt: user.userCreatedAt.bridgeDate,
updatedAt: user.userUpdatedAt.bridgeDate,
deactivatedAt: user.userDeactivatedAt?.bridgeDate,
lastActiveAt: user.lastActivityAt?.bridgeDate,
teams: Set(user.teams),
language: language,
extraData: extraData,
devices: dto.devices.map { try $0.asModel() },
currentDevice: dto.currentDevice?.asModel(),
blockedUserIds: dto.blockedUserIds,
mutedUsers: Set(mutedUsers),
flaggedUsers: Set(flaggedUsers),
flaggedMessageIDs: Set(flaggedMessagesIDs),
unreadCount: UnreadCount(
channels: Int(dto.unreadChannelsCount),
messages: Int(dto.unreadMessagesCount),
threads: Int(dto.unreadThreadsCount)
),
unreadChannelCountsByGroup: dto.unreadChannelCountsByGroup,
mutedChannels: mutedChannels,
privacySettings: .init(
typingIndicators: .init(enabled: dto.isTypingIndicatorsEnabled),
readReceipts: .init(enabled: dto.isReadReceiptsEnabled),
deliveryReceipts: .init(enabled: dto.isDeliveryReceiptsEnabled)
),
avgResponseTime: dto.user.avgResponseTime?.intValue,
pushPreference: pushPreference
)
}
}