273 lines
11 KiB
Swift
273 lines
11 KiB
Swift
//
|
||
// CryptoChatServiceMsgsHistory.swift
|
||
// Wallet
|
||
//
|
||
// Created by Saveliy Stavitsky on 8/17/20.
|
||
// Copyright © 2020 List. All rights reserved.
|
||
//
|
||
|
||
import Foundation
|
||
import RealmSwift
|
||
import eosswift
|
||
|
||
extension String {
|
||
fileprivate static var didFirstSync = "didFirstSync"
|
||
}
|
||
|
||
extension Account.Model {
|
||
var didFirstSync: Bool {
|
||
get { Settings()[.key("\(username)@\(keyType.rawValue)", suffix: .didFirstSync)] }
|
||
set { Settings()[.key("\(username)@\(keyType.rawValue)", suffix: .didFirstSync)] = newValue }
|
||
}
|
||
}
|
||
|
||
extension CryptoChat.Service {
|
||
class MsgsHistory {
|
||
|
||
private let realm: Realm
|
||
private let username: String
|
||
private var historyService: Account.Service.AccountHistory
|
||
|
||
init(username: String, encryptionKey: String) throws {
|
||
guard let key = try? Data(eosioPrivateKey: encryptionKey), key.count == 32,
|
||
let publicKey = try? EOSPrivateKey(base58: encryptionKey).publicKey.base58
|
||
else { throw "EncryptionKey wrong or to small" }
|
||
guard username.count > 0 else { throw "username wrong or to small" }
|
||
|
||
guard let historyService = Account.Service.History.shared.current else { throw "No history service was found for username" }
|
||
self.historyService = historyService
|
||
|
||
self.username = username
|
||
var config = Realm.Configuration(// Set the new schema version. This must be greater than the previously used
|
||
// version (if you've never set a schema version before, the version is 0).
|
||
schemaVersion: 4,
|
||
|
||
// Set the block which will be called automatically when opening a Realm with
|
||
// a schema version lower than the one set above
|
||
migrationBlock: { migration, oldSchemaVersion in
|
||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||
if oldSchemaVersion < 1 {
|
||
// Nothing to do!
|
||
// Realm will automatically detect new properties and removed properties
|
||
// And will update the schema on disk automatically
|
||
}
|
||
}
|
||
)
|
||
// Use the default directory, but replace the filename with the username
|
||
config.fileURL = config.fileURL!.deletingLastPathComponent()
|
||
.appendingPathComponent("\(username)_\(publicKey)_msgs.realm")
|
||
config.encryptionKey = (key.bytes + Array(repeating: UInt8(0), count: 64 - key.bytes.count)).toData()
|
||
|
||
config.objectTypes = [CryptoChatModelRealmChat.self, CryptoChatModelRealmMessage.self]
|
||
// config.schemaVersion = 2
|
||
// config.deleteRealmIfMigrationNeeded = true
|
||
|
||
realm = try Realm(configuration: config)
|
||
}
|
||
|
||
private var isLoading = false
|
||
|
||
var didUpdate: (() -> Void)?
|
||
|
||
lazy var chats: Results<CryptoChatModelRealmChat> = {
|
||
realm.objects(CryptoChatModelRealmChat.self)
|
||
.sorted(by: \.timestamp, ascending: false)
|
||
}()
|
||
|
||
func filterChats(query: String) -> Results<CryptoChatModelRealmChat> {
|
||
realm.objects(CryptoChatModelRealmChat.self)
|
||
.where({ $0.chatName.contains(query) })
|
||
.sorted(by: \.timestamp, ascending: false)
|
||
|
||
}
|
||
|
||
func filterMsgs(query: String) -> Results<CryptoChatModelRealmMessage> {
|
||
if query.isEmpty { // || query.count < 3 {
|
||
return realm.objects(CryptoChatModelRealmMessage.self)
|
||
.filter(NSPredicate(value: false))
|
||
}
|
||
return realm.objects(CryptoChatModelRealmMessage.self)
|
||
.where({ $0.deleted == false })
|
||
.where({ $0.searchText.contains(query.lowercased()) })
|
||
.sorted(by: \.timestamp, ascending: false)
|
||
}
|
||
|
||
func deleteMsg(ephemPublicKey: String) {
|
||
try? realm.write {
|
||
if let msg = realm.object(ofType: CryptoChatModelRealmMessage.self,
|
||
forPrimaryKey: ephemPublicKey) {
|
||
realm.delete(msg)
|
||
}
|
||
}
|
||
}
|
||
|
||
func hideMsg(ephemPublicKey: String) {
|
||
try? realm.write {
|
||
if let msg = realm.object(ofType: CryptoChatModelRealmMessage.self,
|
||
forPrimaryKey: ephemPublicKey) {
|
||
msg.deleted = true
|
||
}
|
||
}
|
||
}
|
||
|
||
func hideChatMsgs(chatName: String) {
|
||
try? realm.write {
|
||
for msg in realm.objects(CryptoChatModelRealmMessage.self)
|
||
.filter("chatName = '\(chatName)'") {
|
||
msg.deleted = true
|
||
}
|
||
if let chat = realm.object(ofType: CryptoChatModelRealmChat.self,
|
||
forPrimaryKey: chatName) {
|
||
realm.delete(chat)
|
||
}
|
||
saveReadCount()
|
||
didUpdate?()
|
||
}
|
||
}
|
||
|
||
func markChatAsRead(username: String) {
|
||
try? realm.write {
|
||
if let chat = realm.object(ofType: CryptoChatModelRealmChat.self, forPrimaryKey: username) {
|
||
chat.unreadCount = 0
|
||
}
|
||
}
|
||
saveReadCount()
|
||
}
|
||
|
||
func saveReadCount() {
|
||
var readDict = (UserDefaults.standard.dictionary(forKey: "chatsReadCount") as? [String: Int]) ?? [:]
|
||
readDict[username] = readCount
|
||
UserDefaults.standard.setValue(readDict, forKey: "chatsReadCount")
|
||
}
|
||
|
||
private func markAllChatsAsRead() {
|
||
try? realm.write {
|
||
for chat in realm.objects(CryptoChatModelRealmChat.self) {
|
||
print("\(chat.chatName) - \(chat.unreadCount)")
|
||
chat.unreadCount = 0
|
||
}
|
||
}
|
||
saveReadCount()
|
||
}
|
||
|
||
private var unreadCount: Int {
|
||
realm.objects(CryptoChatModelRealmChat.self)
|
||
.reduce(0, { $0 + $1.unreadCount })
|
||
}
|
||
var readCount: Int {
|
||
historyService.actions
|
||
.filter("account = '\(Network.servers.current.contract(.chat))'")
|
||
.filter("name = '\(Network.Model.Blockchain.Action.chatSendDm.rawValue)'")
|
||
.filter("receiver = '\(self.username)'").count - unreadCount
|
||
}
|
||
|
||
func openChatWith(_ username: String) -> Results<CryptoChatModelRealmMessage> {
|
||
realm.objects(CryptoChatModelRealmMessage.self)
|
||
.filter("chatName = '\(username)'")
|
||
.filter("deleted = \(false)")
|
||
.sorted(by: \.timestamp, ascending: true)
|
||
}
|
||
|
||
func saveMsg(_ msg: CryptoChatModelRealmMessage, ignoreTimestamp: Bool = false) {
|
||
try? realm.write {
|
||
realm.add(msg)
|
||
|
||
if let chat = realm.object(ofType: CryptoChatModelRealmChat.self, forPrimaryKey: msg.chatName) {
|
||
if ignoreTimestamp {
|
||
chat.timestamp = msg.timestamp
|
||
chat.data = msg.data
|
||
} else {
|
||
if chat.timestamp < msg.timestamp {
|
||
chat.timestamp = msg.timestamp
|
||
chat.data = msg.data
|
||
}
|
||
if msg.chatName == msg.from {
|
||
chat.unreadCount += 1
|
||
}
|
||
}
|
||
} else {
|
||
realm.add(CryptoChatModelRealmChat(chatName: msg.chatName, timestamp: msg.timestamp, data: msg.data, unreadCount: msg.chatName == msg.from ? 1 : 0))
|
||
}
|
||
}
|
||
}
|
||
|
||
var lastSyncTimestamp: Date {
|
||
realm.objects(CryptoChatModelRealmMessage.self)
|
||
.filter("isInSyncWithBlockchain = \(true)")
|
||
.sorted(by: \.timestamp, ascending: false)
|
||
.first?.timestamp ?? Date(timeIntervalSince1970: 0)
|
||
}
|
||
|
||
private var chatActions: Results<AccountAction> {
|
||
historyService.actions
|
||
.filter("account = '\(Network.servers.current.contract(.chat))'")
|
||
.filter("name = '\(Network.Model.Blockchain.Action.chatSendDm.rawValue)'")
|
||
.filter("receiver = '\(self.username)'")
|
||
.filter("blockTime >= %@", lastSyncTimestamp)
|
||
.sorted(by: \.accountActionId, ascending: true)
|
||
}
|
||
|
||
func setMsgSynced(ephemPublicKey: String) {
|
||
try? realm.write {
|
||
if let msg = realm.object(ofType: CryptoChatModelRealmMessage.self,
|
||
forPrimaryKey: ephemPublicKey) {
|
||
msg.isInSyncWithBlockchain = true
|
||
msg.isUnsent = false
|
||
}
|
||
}
|
||
}
|
||
|
||
func markMsgUnsent(ephemPublicKey: String) {
|
||
try? realm.write {
|
||
if let msg = realm.object(ofType: CryptoChatModelRealmMessage.self,
|
||
forPrimaryKey: ephemPublicKey) {
|
||
msg.isUnsent = true
|
||
}
|
||
}
|
||
}
|
||
|
||
func fetchFromLocalHistory() {
|
||
guard !isLoading else { return }
|
||
guard let privateKey = realm.configuration.encryptionKey?.dropLast(32).toEosioK1PrivateKey.split(separator: "_").last.map(String.init) else {
|
||
return
|
||
}
|
||
isLoading = true
|
||
for action in self.chatActions {
|
||
if let serverMsgData = action.data(as: CryptoChat.Model.Msg.self),
|
||
let ephemPubK1Key = (try? Data(eosioPublicKey: serverMsgData.ephemPublicKey).toEosioK1PublicKey) {
|
||
if let msg = realm.object(ofType: CryptoChatModelRealmMessage.self,
|
||
forPrimaryKey: ephemPubK1Key) {
|
||
setMsgSynced(ephemPublicKey: msg.id)
|
||
} else {
|
||
saveMsg(CryptoChatModelRealmMessage(msg: serverMsgData, username: username, privateKey: privateKey, timestamp: action.timestamp))
|
||
}
|
||
}
|
||
}
|
||
|
||
if !UserDefaults.standard.bool(forKey: "\(username)_firstMsgsSyncDone") {
|
||
self.markAllChatsAsRead()
|
||
UserDefaults.standard.setValue(true, forKey: "\(username)_firstMsgsSyncDone")
|
||
}
|
||
|
||
self.isLoading = false
|
||
self.saveReadCount()
|
||
self.didUpdate?()
|
||
}
|
||
|
||
|
||
func copy(username: String, privateKey: String, publicKey: String) {
|
||
guard let key = try? Data(eosioPrivateKey: privateKey), key.count == 32 else { return }
|
||
let fileURL = realm.configuration.fileURL!.deletingLastPathComponent()
|
||
.appendingPathComponent("\(username)_\(publicKey)_msgs.realm")
|
||
let encryptionKey = (key.bytes + Array(repeating: UInt8(0), count: 64 - key.bytes.count)).toData()
|
||
try? realm.writeCopy(toFile: fileURL, encryptionKey: encryptionKey)
|
||
}
|
||
|
||
|
||
func removeDatabase() {
|
||
try? FileManager.default.removeItem(at: realm.configuration.fileURL!)
|
||
}
|
||
|
||
}
|
||
}
|