add cache
This commit is contained in:
		
							parent
							
								
									6fd82e25c1
								
							
						
					
					
						commit
						7c8940da5b
					
				
							
								
								
									
										233
									
								
								yobble/CoreData/ChatCacheWriter.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								yobble/CoreData/ChatCacheWriter.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,233 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import CoreData
 | 
			
		||||
 | 
			
		||||
final class ChatCacheWriter {
 | 
			
		||||
    static let shared = ChatCacheWriter()
 | 
			
		||||
 | 
			
		||||
    private let persistenceController: PersistenceController
 | 
			
		||||
    private let userDefaults: UserDefaults
 | 
			
		||||
    private let keychainService: KeychainService
 | 
			
		||||
 | 
			
		||||
    init(
 | 
			
		||||
        persistenceController: PersistenceController = .shared,
 | 
			
		||||
        userDefaults: UserDefaults = .standard,
 | 
			
		||||
        keychainService: KeychainService = .shared
 | 
			
		||||
    ) {
 | 
			
		||||
        self.persistenceController = persistenceController
 | 
			
		||||
        self.userDefaults = userDefaults
 | 
			
		||||
        self.keychainService = keychainService
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func storePrivateChatList(_ data: PrivateChatListData) {
 | 
			
		||||
        guard !AppConfig.DISABLE_DB else { return }
 | 
			
		||||
        guard let accountUserId = resolveCurrentAccountUserId() else { return }
 | 
			
		||||
 | 
			
		||||
        let context = persistenceController.newBackgroundContext()
 | 
			
		||||
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
 | 
			
		||||
 | 
			
		||||
        context.perform { [weak self] in
 | 
			
		||||
            guard let self else { return }
 | 
			
		||||
            do {
 | 
			
		||||
                try self.sync(chatList: data, accountUserId: accountUserId, context: context)
 | 
			
		||||
                if context.hasChanges {
 | 
			
		||||
                    try context.save()
 | 
			
		||||
                }
 | 
			
		||||
            } catch {
 | 
			
		||||
                if AppConfig.DEBUG {
 | 
			
		||||
                    print("[ChatCacheWriter] Failed to store private chat list: \(error)")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func resolveCurrentAccountUserId() -> String? {
 | 
			
		||||
        guard let login = userDefaults.string(forKey: "currentUser") else {
 | 
			
		||||
            return nil
 | 
			
		||||
        }
 | 
			
		||||
        return keychainService.get(forKey: "userId", service: login)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func sync(chatList: PrivateChatListData, accountUserId: String, context: NSManagedObjectContext) throws {
 | 
			
		||||
        var profileCache: [String: CDProfile] = [:]
 | 
			
		||||
        var chatCache: [String: CDPrivateChat] = [:]
 | 
			
		||||
        var messageCache: [String: CDMessage] = [:]
 | 
			
		||||
 | 
			
		||||
        for item in chatList.items {
 | 
			
		||||
            let chat = try upsertChat(item, accountUserId: accountUserId, context: context, cache: &chatCache)
 | 
			
		||||
            let previousLastMessage = chat.lastMessage
 | 
			
		||||
 | 
			
		||||
            if let profileData = item.chatData {
 | 
			
		||||
                let profile = try upsertProfile(profileData, accountUserId: accountUserId, context: context, cache: &profileCache)
 | 
			
		||||
                chat.chatData = profile
 | 
			
		||||
            } else {
 | 
			
		||||
                chat.chatData = nil
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if let lastMessageData = item.lastMessage {
 | 
			
		||||
                let message = try upsertMessage(
 | 
			
		||||
                    lastMessageData,
 | 
			
		||||
                    accountUserId: accountUserId,
 | 
			
		||||
                    chat: chat,
 | 
			
		||||
                    context: context,
 | 
			
		||||
                    profileCache: &profileCache,
 | 
			
		||||
                    messageCache: &messageCache
 | 
			
		||||
                )
 | 
			
		||||
                if let previous = previousLastMessage, previous.objectID != message.objectID {
 | 
			
		||||
                    previous.chatAsLastMessage = nil
 | 
			
		||||
                }
 | 
			
		||||
                chat.lastMessage = message
 | 
			
		||||
            } else {
 | 
			
		||||
                previousLastMessage?.chatAsLastMessage = nil
 | 
			
		||||
                chat.lastMessage = nil
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func upsertChat(
 | 
			
		||||
        _ chatItem: PrivateChatListItem,
 | 
			
		||||
        accountUserId: String,
 | 
			
		||||
        context: NSManagedObjectContext,
 | 
			
		||||
        cache: inout [String: CDPrivateChat]
 | 
			
		||||
    ) throws -> CDPrivateChat {
 | 
			
		||||
        if let cached = cache[chatItem.chatId] {
 | 
			
		||||
            apply(chatItem: chatItem, to: cached, accountUserId: accountUserId)
 | 
			
		||||
            return cached
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let chat = try fetchChat(chatId: chatItem.chatId, accountUserId: accountUserId, context: context) ?? CDPrivateChat(context: context)
 | 
			
		||||
        apply(chatItem: chatItem, to: chat, accountUserId: accountUserId)
 | 
			
		||||
        cache[chatItem.chatId] = chat
 | 
			
		||||
        return chat
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func apply(chatItem: PrivateChatListItem, to chat: CDPrivateChat, accountUserId: String) {
 | 
			
		||||
        chat.accountUserId = accountUserId
 | 
			
		||||
        chat.chatId = chatItem.chatId
 | 
			
		||||
        chat.chatType = chatItem.chatType.rawValue
 | 
			
		||||
        chat.createdAt = chatItem.createdAt
 | 
			
		||||
        chat.unreadCount = Int32(chatItem.unreadCount)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func upsertProfile(
 | 
			
		||||
        _ profileData: ChatProfile,
 | 
			
		||||
        accountUserId: String,
 | 
			
		||||
        context: NSManagedObjectContext,
 | 
			
		||||
        cache: inout [String: CDProfile]
 | 
			
		||||
    ) throws -> CDProfile {
 | 
			
		||||
        if let cached = cache[profileData.userId] {
 | 
			
		||||
            apply(profile: profileData, to: cached, accountUserId: accountUserId)
 | 
			
		||||
            return cached
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let profile = try fetchProfile(userId: profileData.userId, accountUserId: accountUserId, context: context) ?? CDProfile(context: context)
 | 
			
		||||
        apply(profile: profileData, to: profile, accountUserId: accountUserId)
 | 
			
		||||
        cache[profileData.userId] = profile
 | 
			
		||||
        return profile
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func apply(profile profileData: ChatProfile, to profile: CDProfile, accountUserId: String) {
 | 
			
		||||
        profile.accountUserId = accountUserId
 | 
			
		||||
        profile.userId = profileData.userId
 | 
			
		||||
        profile.login = profileData.login
 | 
			
		||||
        profile.fullName = profileData.fullName
 | 
			
		||||
        profile.customName = profileData.customName
 | 
			
		||||
        profile.bio = profileData.bio
 | 
			
		||||
        if let lastSeen = profileData.lastSeen {
 | 
			
		||||
            profile.setValue(NSNumber(value: lastSeen), forKey: "lastSeen")
 | 
			
		||||
        } else {
 | 
			
		||||
            profile.setValue(nil, forKey: "lastSeen")
 | 
			
		||||
        }
 | 
			
		||||
        profile.createdAt = profileData.createdAt
 | 
			
		||||
        profile.isOfficial = profileData.isOfficial
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func upsertMessage(
 | 
			
		||||
        _ messageData: MessageItem,
 | 
			
		||||
        accountUserId: String,
 | 
			
		||||
        chat: CDPrivateChat,
 | 
			
		||||
        context: NSManagedObjectContext,
 | 
			
		||||
        profileCache: inout [String: CDProfile],
 | 
			
		||||
        messageCache: inout [String: CDMessage]
 | 
			
		||||
    ) throws -> CDMessage {
 | 
			
		||||
        if let cached = messageCache[messageData.messageId] {
 | 
			
		||||
            apply(message: messageData, to: cached, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache)
 | 
			
		||||
            return cached
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let message = try fetchMessage(messageId: messageData.messageId, accountUserId: accountUserId, context: context) ?? CDMessage(context: context)
 | 
			
		||||
        apply(message: messageData, to: message, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache)
 | 
			
		||||
        messageCache[messageData.messageId] = message
 | 
			
		||||
        return message
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func apply(
 | 
			
		||||
        message messageData: MessageItem,
 | 
			
		||||
        to message: CDMessage,
 | 
			
		||||
        accountUserId: String,
 | 
			
		||||
        chat: CDPrivateChat,
 | 
			
		||||
        context: NSManagedObjectContext,
 | 
			
		||||
        profileCache: inout [String: CDProfile]
 | 
			
		||||
    ) {
 | 
			
		||||
        message.accountUserId = accountUserId
 | 
			
		||||
        message.messageId = messageData.messageId
 | 
			
		||||
        message.chatId = messageData.chatId
 | 
			
		||||
        message.messageType = messageData.messageType
 | 
			
		||||
        message.content = messageData.content
 | 
			
		||||
        message.mediaLink = messageData.mediaLink
 | 
			
		||||
        message.createdAt = messageData.createdAt
 | 
			
		||||
        message.updatedAt = messageData.updatedAt
 | 
			
		||||
        message.senderId = messageData.senderId
 | 
			
		||||
        message.chat = chat
 | 
			
		||||
        message.chatAsLastMessage = chat
 | 
			
		||||
 | 
			
		||||
        if let isViewed = messageData.isViewed {
 | 
			
		||||
            message.setValue(NSNumber(value: isViewed), forKey: "isViewed")
 | 
			
		||||
        } else {
 | 
			
		||||
            message.setValue(nil, forKey: "isViewed")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let forward = messageData.forwardMetadata {
 | 
			
		||||
            message.forwardType = forward.forwardType
 | 
			
		||||
            message.forwardSenderId = forward.forwardSenderId
 | 
			
		||||
            message.forwardMessageId = forward.forwardMessageId
 | 
			
		||||
        } else {
 | 
			
		||||
            message.forwardType = nil
 | 
			
		||||
            message.forwardSenderId = nil
 | 
			
		||||
            message.forwardMessageId = nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let senderProfileData = messageData.senderData {
 | 
			
		||||
            if let profile = try? upsertProfile(
 | 
			
		||||
                senderProfileData,
 | 
			
		||||
                accountUserId: accountUserId,
 | 
			
		||||
                context: context,
 | 
			
		||||
                cache: &profileCache
 | 
			
		||||
            ) {
 | 
			
		||||
                message.sender = profile
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            message.sender = nil
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func fetchChat(chatId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDPrivateChat? {
 | 
			
		||||
        let request = NSFetchRequest<CDPrivateChat>(entityName: "CDPrivateChat")
 | 
			
		||||
        request.fetchLimit = 1
 | 
			
		||||
        request.predicate = NSPredicate(format: "accountUserId == %@ AND chatId == %@", accountUserId, chatId)
 | 
			
		||||
        return try context.fetch(request).first
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func fetchProfile(userId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDProfile? {
 | 
			
		||||
        let request = NSFetchRequest<CDProfile>(entityName: "CDProfile")
 | 
			
		||||
        request.fetchLimit = 1
 | 
			
		||||
        request.predicate = NSPredicate(format: "accountUserId == %@ AND userId == %@", accountUserId, userId)
 | 
			
		||||
        return try context.fetch(request).first
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func fetchMessage(messageId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDMessage? {
 | 
			
		||||
        let request = NSFetchRequest<CDMessage>(entityName: "CDMessage")
 | 
			
		||||
        request.fetchLimit = 1
 | 
			
		||||
        request.predicate = NSPredicate(format: "accountUserId == %@ AND messageId == %@", accountUserId, messageId)
 | 
			
		||||
        return try context.fetch(request).first
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -19,12 +19,14 @@ enum ChatServiceError: LocalizedError {
 | 
			
		||||
final class ChatService {
 | 
			
		||||
    private let client: NetworkClient
 | 
			
		||||
    private let decoder: JSONDecoder
 | 
			
		||||
    private let cacheWriter: ChatCacheWriter
 | 
			
		||||
 | 
			
		||||
    init(client: NetworkClient = .shared) {
 | 
			
		||||
    init(client: NetworkClient = .shared, cacheWriter: ChatCacheWriter = .shared) {
 | 
			
		||||
        self.client = client
 | 
			
		||||
        self.decoder = JSONDecoder()
 | 
			
		||||
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
 | 
			
		||||
        self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
 | 
			
		||||
        self.cacheWriter = cacheWriter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func fetchPrivateChats(
 | 
			
		||||
@ -42,7 +44,7 @@ final class ChatService {
 | 
			
		||||
            method: .get,
 | 
			
		||||
            query: query,
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
        ) { [decoder] result in
 | 
			
		||||
        ) { [decoder, weak self] result in
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let response):
 | 
			
		||||
                do {
 | 
			
		||||
@ -52,7 +54,9 @@ final class ChatService {
 | 
			
		||||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
			
		||||
                        return
 | 
			
		||||
                    }
 | 
			
		||||
                    completion(.success(apiResponse.data))
 | 
			
		||||
                    let chatData = apiResponse.data
 | 
			
		||||
                    self?.cacheWriter.storePrivateChatList(chatData)
 | 
			
		||||
                    completion(.success(chatData))
 | 
			
		||||
                } catch {
 | 
			
		||||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
			
		||||
                    if AppConfig.DEBUG {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user