2022-01-24 16:07:17 +00:00
|
|
|
//
|
|
|
|
// ChatModel.swift
|
|
|
|
// SimpleX
|
|
|
|
//
|
|
|
|
// Created by Evgeny Poberezkin on 22/01/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import Combine
|
2022-01-31 21:28:07 +00:00
|
|
|
import SwiftUI
|
2022-05-24 19:34:27 +01:00
|
|
|
import WebKit
|
2022-05-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2022-01-24 16:07:17 +00:00
|
|
|
|
|
|
|
final class ChatModel: ObservableObject {
|
2022-05-09 09:52:09 +01:00
|
|
|
@Published var onboardingStage: OnboardingStage?
|
2022-07-01 22:45:58 +01:00
|
|
|
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
2022-01-24 16:07:17 +00:00
|
|
|
@Published var currentUser: User?
|
2023-01-20 12:38:38 +00:00
|
|
|
@Published var users: [UserInfo] = []
|
2022-09-23 12:51:40 +01:00
|
|
|
@Published var chatInitialized = false
|
2022-06-24 13:52:20 +01:00
|
|
|
@Published var chatRunning: Bool?
|
|
|
|
@Published var chatDbChanged = false
|
2022-09-07 12:49:41 +01:00
|
|
|
@Published var chatDbEncrypted: Bool?
|
|
|
|
@Published var chatDbStatus: DBMigrationResult?
|
2022-02-02 12:51:39 +00:00
|
|
|
// list of chat "previews"
|
|
|
|
@Published var chats: [Chat] = []
|
2023-01-20 17:35:39 +04:00
|
|
|
// map of connections network statuses, key is agent connection id
|
|
|
|
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
|
2022-02-02 12:51:39 +00:00
|
|
|
// current chat
|
|
|
|
@Published var chatId: String?
|
2022-08-15 21:07:11 +01:00
|
|
|
@Published var reversedChatItems: [ChatItem] = []
|
2022-02-12 15:59:43 +00:00
|
|
|
@Published var chatToTop: String?
|
2022-08-09 13:43:19 +04:00
|
|
|
@Published var groupMembers: [GroupMember] = []
|
2022-02-02 12:51:39 +00:00
|
|
|
// items in the terminal view
|
2022-01-29 23:37:02 +00:00
|
|
|
@Published var terminalItems: [TerminalItem] = []
|
2022-10-23 11:16:56 +01:00
|
|
|
@Published var userAddress: UserContactLink?
|
2022-11-21 08:37:13 +00:00
|
|
|
@Published var userSMPServers: [ServerCfg]?
|
|
|
|
@Published var presetSMPServers: [String]?
|
2022-10-03 16:42:43 +04:00
|
|
|
@Published var chatItemTTL: ChatItemTTL = .none
|
2022-02-01 20:30:33 +00:00
|
|
|
@Published var appOpenUrl: URL?
|
2022-06-27 23:03:27 +01:00
|
|
|
@Published var deviceToken: DeviceToken?
|
2022-07-01 09:49:30 +01:00
|
|
|
@Published var savedToken: DeviceToken?
|
2022-07-09 09:29:56 +01:00
|
|
|
@Published var tokenRegistered = false
|
2022-07-01 09:49:30 +01:00
|
|
|
@Published var tokenStatus: NtfTknStatus?
|
|
|
|
@Published var notificationMode = NotificationsMode.off
|
2022-07-06 11:52:10 +01:00
|
|
|
@Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get()
|
2022-08-23 18:18:12 +04:00
|
|
|
@Published var incognito: Bool = incognitoGroupDefault.get()
|
2022-07-22 08:10:37 +01:00
|
|
|
// pending notification actions
|
|
|
|
@Published var ntfContactRequest: ChatId?
|
|
|
|
@Published var ntfCallInvitationAction: (ChatId, NtfCallAction)?
|
2022-05-07 06:40:46 +01:00
|
|
|
// current WebRTC call
|
2022-07-05 15:15:15 +04:00
|
|
|
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
2022-05-07 06:40:46 +01:00
|
|
|
@Published var activeCall: Call?
|
|
|
|
@Published var callCommand: WCallCommand?
|
2022-05-21 12:13:37 +01:00
|
|
|
@Published var showCallView = false
|
2022-10-01 10:57:18 +01:00
|
|
|
// currently showing QR code
|
|
|
|
@Published var connReqInv: String?
|
2022-11-24 21:18:28 +04:00
|
|
|
// audio recording and playback
|
|
|
|
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
|
2022-05-24 19:34:27 +01:00
|
|
|
var callWebView: WKWebView?
|
2022-02-28 10:44:48 +00:00
|
|
|
|
|
|
|
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
|
|
|
|
2022-02-09 22:53:06 +00:00
|
|
|
static let shared = ChatModel()
|
2022-02-02 12:51:39 +00:00
|
|
|
|
2022-09-07 20:06:16 +01:00
|
|
|
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
func hasChat(_ id: String) -> Bool {
|
|
|
|
chats.first(where: { $0.id == id }) != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getChat(_ id: String) -> Chat? {
|
|
|
|
chats.first(where: { $0.id == id })
|
|
|
|
}
|
|
|
|
|
2022-08-29 14:08:46 +01:00
|
|
|
func getContactChat(_ contactId: Int64) -> Chat? {
|
|
|
|
chats.first { chat in
|
|
|
|
if case let .direct(contact) = chat.chatInfo {
|
|
|
|
return contact.contactId == contactId
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-05 20:10:47 +00:00
|
|
|
private func getChatIndex(_ id: String) -> Int? {
|
|
|
|
chats.firstIndex(where: { $0.id == id })
|
|
|
|
}
|
|
|
|
|
2022-07-10 14:28:00 +01:00
|
|
|
func addChat(_ chat: Chat, at position: Int = 0) {
|
2022-02-02 16:46:05 +00:00
|
|
|
withAnimation {
|
2022-07-10 14:28:00 +01:00
|
|
|
chats.insert(chat, at: position)
|
2022-02-02 16:46:05 +00:00
|
|
|
}
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func updateChatInfo(_ cInfo: ChatInfo) {
|
2022-02-12 15:59:43 +00:00
|
|
|
if let i = getChatIndex(cInfo.id) {
|
|
|
|
chats[i].chatInfo = cInfo
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-25 10:39:28 +01:00
|
|
|
func updateContactConnection(_ contactConnection: PendingContactConnection) {
|
|
|
|
updateChat(.contactConnection(contactConnection: contactConnection))
|
|
|
|
}
|
|
|
|
|
2022-02-05 20:10:47 +00:00
|
|
|
func updateContact(_ contact: Contact) {
|
2022-12-12 15:27:52 +04:00
|
|
|
updateChat(.direct(contact: contact), addMissing: contact.directOrUsed)
|
2022-04-25 10:39:28 +01:00
|
|
|
}
|
|
|
|
|
2022-07-18 21:58:32 +04:00
|
|
|
func updateGroup(_ groupInfo: GroupInfo) {
|
|
|
|
updateChat(.group(groupInfo: groupInfo))
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
|
2022-04-25 10:39:28 +01:00
|
|
|
if hasChat(cInfo.id) {
|
2022-02-05 20:10:47 +00:00
|
|
|
updateChatInfo(cInfo)
|
2022-07-18 21:58:32 +04:00
|
|
|
} else if addMissing {
|
2022-02-05 20:10:47 +00:00
|
|
|
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-21 12:32:11 +01:00
|
|
|
private func _updateChat(_ id: ChatId, _ update: @escaping (Chat) -> Void) {
|
|
|
|
if let i = getChatIndex(id) {
|
|
|
|
// we need to separately update the chat object, as it is ObservedObject,
|
|
|
|
// and chat in the list so the list view is updated...
|
|
|
|
// simply updating chats[i] replaces the object without updating the current object in the list
|
|
|
|
let chat = chats[i]
|
|
|
|
update(chat)
|
|
|
|
chats[i] = chat
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
func replaceChat(_ id: String, _ chat: Chat) {
|
2022-02-12 15:59:43 +00:00
|
|
|
if let i = getChatIndex(id) {
|
|
|
|
chats[i] = chat
|
2022-02-02 12:51:39 +00:00
|
|
|
} else {
|
|
|
|
// invalid state, correcting
|
|
|
|
chats.insert(chat, at: 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-02 17:18:45 +01:00
|
|
|
func updateChats(with newChats: [ChatData]) {
|
2022-07-10 14:28:00 +01:00
|
|
|
for i in 0..<newChats.count {
|
|
|
|
let c = newChats[i]
|
|
|
|
if let j = getChatIndex(c.id) {
|
|
|
|
let chat = chats[j]
|
2022-07-02 17:18:45 +01:00
|
|
|
chat.chatInfo = c.chatInfo
|
|
|
|
chat.chatItems = c.chatItems
|
|
|
|
chat.chatStats = c.chatStats
|
2022-07-10 14:28:00 +01:00
|
|
|
if i != j {
|
|
|
|
if chatId != c.chatInfo.id {
|
|
|
|
popChat_(j, to: i)
|
|
|
|
} else if i == 0 {
|
|
|
|
chatToTop = c.chatInfo.id
|
|
|
|
}
|
|
|
|
}
|
2022-07-02 17:18:45 +01:00
|
|
|
} else {
|
2022-07-10 14:28:00 +01:00
|
|
|
addChat(Chat(c), at: i)
|
2022-07-02 17:18:45 +01:00
|
|
|
}
|
2022-07-01 22:45:58 +01:00
|
|
|
}
|
2023-01-23 13:20:58 +00:00
|
|
|
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
2022-07-01 22:45:58 +01:00
|
|
|
}
|
|
|
|
|
2022-07-14 16:40:32 +04:00
|
|
|
// func addGroup(_ group: SimpleXChat.Group) {
|
|
|
|
// groups[group.groupInfo.id] = group
|
|
|
|
// }
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
2022-02-12 15:59:43 +00:00
|
|
|
// update previews
|
|
|
|
if let i = getChatIndex(cInfo.id) {
|
|
|
|
chats[i].chatItems = [cItem]
|
|
|
|
if case .rcvNew = cItem.meta.itemStatus {
|
|
|
|
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
|
2023-01-19 16:22:56 +00:00
|
|
|
increaseUnreadCounter(user: currentUser!)
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
if i > 0 {
|
2022-02-05 14:24:23 +00:00
|
|
|
if chatId == nil {
|
2022-02-12 15:59:43 +00:00
|
|
|
withAnimation { popChat_(i) }
|
|
|
|
} else if chatId == cInfo.id {
|
|
|
|
chatToTop = cInfo.id
|
2022-02-05 14:24:23 +00:00
|
|
|
} else {
|
2022-02-12 15:59:43 +00:00
|
|
|
popChat_(i)
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
} else {
|
|
|
|
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
// add to current chat
|
2022-02-02 12:51:39 +00:00
|
|
|
if chatId == cInfo.id {
|
2022-10-10 10:40:30 +01:00
|
|
|
_ = _upsertChatItem(cInfo, cItem)
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
|
|
|
// update previews
|
|
|
|
var res: Bool
|
|
|
|
if let chat = getChat(cInfo.id) {
|
2022-10-10 10:40:30 +01:00
|
|
|
if let pItem = chat.chatItems.last {
|
|
|
|
if pItem.id == cItem.id || (chatId == cInfo.id && reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
|
|
|
|
chat.chatItems = [cItem]
|
|
|
|
}
|
|
|
|
} else {
|
2022-02-12 15:59:43 +00:00
|
|
|
chat.chatItems = [cItem]
|
|
|
|
}
|
|
|
|
res = false
|
|
|
|
} else {
|
|
|
|
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
|
|
|
res = true
|
|
|
|
}
|
|
|
|
// update current chat
|
2022-10-10 10:40:30 +01:00
|
|
|
return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res
|
|
|
|
}
|
|
|
|
|
|
|
|
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
|
|
|
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
|
|
|
let ci = reversedChatItems[i]
|
2023-01-10 19:12:48 +00:00
|
|
|
withAnimation {
|
2022-10-10 10:40:30 +01:00
|
|
|
self.reversedChatItems[i] = cItem
|
|
|
|
self.reversedChatItems[i].viewTimestamp = .now
|
2022-12-03 15:21:14 +00:00
|
|
|
// on some occasions the confirmation of message being accepted by the server (tick)
|
|
|
|
// arrives earlier than the response from API, and item remains without tick
|
2022-10-10 10:40:30 +01:00
|
|
|
if case .sndNew = cItem.meta.itemStatus {
|
2022-12-03 15:21:14 +00:00
|
|
|
self.reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
return false
|
2022-02-12 15:59:43 +00:00
|
|
|
} else {
|
2023-01-10 19:12:48 +00:00
|
|
|
withAnimation(itemAnimation()) {
|
|
|
|
reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
return true
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
2023-01-10 19:12:48 +00:00
|
|
|
|
|
|
|
func itemAnimation() -> Animation? {
|
|
|
|
switch cItem.chatDir {
|
|
|
|
case .directSnd, .groupSnd: return cItem.meta.isLive ? nil : .default
|
|
|
|
default: return .default
|
|
|
|
}
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
2022-03-30 20:37:47 +04:00
|
|
|
|
|
|
|
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
2022-12-07 20:46:38 +04:00
|
|
|
if cItem.isRcvNew {
|
|
|
|
decreaseUnreadCounter(cInfo)
|
|
|
|
}
|
2022-03-30 20:37:47 +04:00
|
|
|
// update previews
|
|
|
|
if let chat = getChat(cInfo.id) {
|
|
|
|
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
|
2022-12-07 20:46:38 +04:00
|
|
|
chat.chatItems = [ChatItem.deletedItemDummy()]
|
2022-03-30 20:37:47 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// remove from current chat
|
|
|
|
if chatId == cInfo.id {
|
2022-08-15 21:07:11 +01:00
|
|
|
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
2022-03-30 20:37:47 +04:00
|
|
|
_ = withAnimation {
|
2022-08-15 21:07:11 +01:00
|
|
|
self.reversedChatItems.remove(at: i)
|
2022-03-30 20:37:47 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
|
2022-10-10 10:40:30 +01:00
|
|
|
func nextChatItemData<T>(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? {
|
|
|
|
guard var i = reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil }
|
|
|
|
if previous {
|
|
|
|
while i < reversedChatItems.count - 1 {
|
|
|
|
i += 1
|
|
|
|
if let res = map(reversedChatItems[i]) { return res }
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
while i > 0 {
|
|
|
|
i -= 1
|
|
|
|
if let res = map(reversedChatItems[i]) { return res }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-11 12:01:02 +00:00
|
|
|
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
|
|
|
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
2023-01-10 19:12:48 +00:00
|
|
|
withAnimation {
|
|
|
|
reversedChatItems.insert(cItem, at: 0)
|
|
|
|
}
|
|
|
|
return cItem
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeLiveDummy(animated: Bool = true) {
|
|
|
|
if hasLiveDummy {
|
|
|
|
if animated {
|
|
|
|
withAnimation { _ = reversedChatItems.removeFirst() }
|
|
|
|
} else {
|
|
|
|
_ = reversedChatItems.removeFirst()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var hasLiveDummy: Bool {
|
|
|
|
reversedChatItems.first?.isLiveDummy == true
|
|
|
|
}
|
|
|
|
|
2022-02-12 15:59:43 +00:00
|
|
|
func markChatItemsRead(_ cInfo: ChatInfo) {
|
|
|
|
// update preview
|
2022-10-21 12:32:11 +01:00
|
|
|
_updateChat(cInfo.id) { chat in
|
2023-01-23 13:20:58 +00:00
|
|
|
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
2022-02-12 15:59:43 +00:00
|
|
|
chat.chatStats = ChatStats()
|
|
|
|
}
|
|
|
|
// update current chat
|
|
|
|
if chatId == cInfo.id {
|
2022-08-16 13:13:29 +01:00
|
|
|
markCurrentChatRead()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func markCurrentChatRead(fromIndex i: Int = 0) {
|
|
|
|
var j = i
|
|
|
|
while j < reversedChatItems.count {
|
2022-12-21 12:59:45 +00:00
|
|
|
markChatItemRead_(j)
|
2022-08-16 13:13:29 +01:00
|
|
|
j += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
|
|
|
|
if let cItem = aboveItem {
|
|
|
|
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
|
|
|
markCurrentChatRead(fromIndex: i)
|
2022-10-21 12:32:11 +01:00
|
|
|
_updateChat(cInfo.id) { chat in
|
2022-08-16 13:13:29 +01:00
|
|
|
var unreadBelow = 0
|
|
|
|
var j = i - 1
|
|
|
|
while j >= 0 {
|
2022-10-21 12:32:11 +01:00
|
|
|
if case .rcvNew = self.reversedChatItems[j].meta.itemStatus {
|
2022-08-16 13:13:29 +01:00
|
|
|
unreadBelow += 1
|
|
|
|
}
|
|
|
|
j -= 1
|
|
|
|
}
|
|
|
|
// update preview
|
|
|
|
let markedCount = chat.chatStats.unreadCount - unreadBelow
|
|
|
|
if markedCount > 0 {
|
|
|
|
chat.chatStats.unreadCount -= markedCount
|
2023-01-19 16:22:56 +00:00
|
|
|
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
|
2022-08-16 13:13:29 +01:00
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-16 13:13:29 +01:00
|
|
|
} else {
|
|
|
|
markChatItemsRead(cInfo)
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-21 12:32:11 +01:00
|
|
|
|
|
|
|
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
|
|
|
_updateChat(cInfo.id) { chat in
|
|
|
|
chat.chatStats.unreadChat = unreadChat
|
|
|
|
}
|
|
|
|
}
|
2022-02-02 12:51:39 +00:00
|
|
|
|
2022-05-17 22:48:54 +04:00
|
|
|
func clearChat(_ cInfo: ChatInfo) {
|
|
|
|
// clear preview
|
|
|
|
if let chat = getChat(cInfo.id) {
|
2023-01-23 13:20:58 +00:00
|
|
|
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
2022-05-17 22:48:54 +04:00
|
|
|
chat.chatItems = []
|
|
|
|
chat.chatStats = ChatStats()
|
2022-05-20 12:00:58 +04:00
|
|
|
chat.chatInfo = cInfo
|
2022-05-17 22:48:54 +04:00
|
|
|
}
|
|
|
|
// clear current chat
|
|
|
|
if chatId == cInfo.id {
|
2022-08-15 21:07:11 +01:00
|
|
|
reversedChatItems = []
|
2022-05-17 22:48:54 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-12 15:59:43 +00:00
|
|
|
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
|
|
|
// update preview
|
2022-12-07 20:46:38 +04:00
|
|
|
decreaseUnreadCounter(cInfo)
|
2022-02-12 15:59:43 +00:00
|
|
|
// update current chat
|
2022-12-21 12:59:45 +00:00
|
|
|
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
|
|
|
markChatItemRead_(i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func markChatItemRead_(_ i: Int) {
|
|
|
|
let meta = reversedChatItems[i].meta
|
|
|
|
if case .rcvNew = meta.itemStatus {
|
|
|
|
reversedChatItems[i].meta.itemStatus = .rcvRead
|
|
|
|
reversedChatItems[i].viewTimestamp = .now
|
|
|
|
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
|
|
|
|
reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-07 20:46:38 +04:00
|
|
|
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
|
|
|
|
if let i = getChatIndex(cInfo.id) {
|
|
|
|
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
|
2023-01-19 16:22:56 +00:00
|
|
|
decreaseUnreadCounter(user: currentUser!)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func increaseUnreadCounter(user: User) {
|
|
|
|
changeUnreadCounter(user: user, by: 1)
|
2023-01-23 13:20:58 +00:00
|
|
|
NtfManager.shared.incNtfBadgeCount()
|
2023-01-19 16:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func decreaseUnreadCounter(user: User, by: Int = 1) {
|
|
|
|
changeUnreadCounter(user: user, by: -by)
|
2023-01-23 13:20:58 +00:00
|
|
|
NtfManager.shared.decNtfBadgeCount(by: by)
|
2023-01-19 16:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func changeUnreadCounter(user: User, by: Int) {
|
|
|
|
if let i = users.firstIndex(where: { $0.user.id == user.id }) {
|
2023-01-23 13:20:58 +00:00
|
|
|
users[i].unreadCount += by
|
2022-12-07 20:46:38 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-23 13:20:58 +00:00
|
|
|
func totalUnreadCountForAllUsers() -> Int {
|
|
|
|
chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) +
|
|
|
|
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
|
2022-07-20 08:58:53 +01:00
|
|
|
}
|
|
|
|
|
2022-03-30 08:57:42 +01:00
|
|
|
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
2022-08-15 21:07:11 +01:00
|
|
|
if let i = reversedChatItems.firstIndex(where: { $0.id == ci.id }), i < reversedChatItems.count - 1 {
|
|
|
|
return reversedChatItems[i + 1]
|
2022-03-30 08:57:42 +01:00
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-12 15:59:43 +00:00
|
|
|
func popChat(_ id: String) {
|
|
|
|
if let i = getChatIndex(id) {
|
|
|
|
popChat_(i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-10 14:28:00 +01:00
|
|
|
private func popChat_(_ i: Int, to position: Int = 0) {
|
2022-02-12 15:59:43 +00:00
|
|
|
let chat = chats.remove(at: i)
|
2022-07-10 14:28:00 +01:00
|
|
|
chats.insert(chat, at: position)
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
|
|
|
|
2022-10-01 10:57:18 +01:00
|
|
|
func dismissConnReqView(_ id: String) {
|
|
|
|
if let connReqInv = connReqInv,
|
|
|
|
let c = getChat(id),
|
|
|
|
case let .contactConnection(contactConnection) = c.chatInfo,
|
|
|
|
connReqInv == contactConnection.connReqInv {
|
|
|
|
dismissAllSheets()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
func removeChat(_ id: String) {
|
2022-02-02 16:46:05 +00:00
|
|
|
withAnimation {
|
|
|
|
chats.removeAll(where: { $0.id == id })
|
|
|
|
}
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
2022-08-09 13:43:19 +04:00
|
|
|
|
|
|
|
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
|
2022-11-07 21:05:59 +04:00
|
|
|
// user member was updated
|
|
|
|
if groupInfo.membership.groupMemberId == member.groupMemberId {
|
|
|
|
updateGroup(groupInfo)
|
|
|
|
return false
|
|
|
|
}
|
2022-08-09 13:43:19 +04:00
|
|
|
// update current chat
|
|
|
|
if chatId == groupInfo.id {
|
|
|
|
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
|
|
|
|
withAnimation(.default) {
|
|
|
|
self.groupMembers[i] = member
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
withAnimation { groupMembers.append(member) }
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 13:13:29 +01:00
|
|
|
|
|
|
|
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
|
|
|
|
var i = 0
|
|
|
|
var totalBelow = 0
|
|
|
|
var unreadBelow = 0
|
|
|
|
while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) {
|
|
|
|
totalBelow += 1
|
2022-12-07 20:46:38 +04:00
|
|
|
if reversedChatItems[i].isRcvNew {
|
2022-08-16 13:13:29 +01:00
|
|
|
unreadBelow += 1
|
|
|
|
}
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow)
|
|
|
|
}
|
|
|
|
|
|
|
|
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
|
|
|
let maxIx = reversedChatItems.count - 1
|
|
|
|
var i = 0
|
|
|
|
let inView = { itemsInView.contains(self.reversedChatItems[$0].viewId) }
|
|
|
|
while i < maxIx && !inView(i) { i += 1 }
|
|
|
|
while i < maxIx && inView(i) { i += 1 }
|
|
|
|
return reversedChatItems[min(i - 1, maxIx)]
|
|
|
|
}
|
2023-01-19 16:22:56 +00:00
|
|
|
|
2023-01-20 17:35:39 +04:00
|
|
|
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
|
|
|
networkStatuses[contact.activeConn.agentConnId] = status
|
2023-01-20 14:56:05 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
2023-01-20 17:35:39 +04:00
|
|
|
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
2023-01-20 14:56:05 +04:00
|
|
|
}
|
2022-08-16 13:13:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
struct UnreadChatItemCounts {
|
|
|
|
var totalBelow: Int
|
|
|
|
var unreadBelow: Int
|
2022-01-24 16:07:17 +00:00
|
|
|
}
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
final class Chat: ObservableObject, Identifiable {
|
|
|
|
@Published var chatInfo: ChatInfo
|
|
|
|
@Published var chatItems: [ChatItem]
|
2022-02-12 15:59:43 +00:00
|
|
|
@Published var chatStats: ChatStats
|
2022-07-01 22:45:58 +01:00
|
|
|
var created = Date.now
|
2022-02-05 20:10:47 +00:00
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
init(_ cData: ChatData) {
|
|
|
|
self.chatInfo = cData.chatInfo
|
|
|
|
self.chatItems = cData.chatItems
|
2022-02-12 15:59:43 +00:00
|
|
|
self.chatStats = cData.chatStats
|
2022-02-02 12:51:39 +00:00
|
|
|
}
|
2022-01-29 23:37:02 +00:00
|
|
|
|
2023-01-20 14:56:05 +04:00
|
|
|
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) {
|
2022-01-29 23:37:02 +00:00
|
|
|
self.chatInfo = chatInfo
|
|
|
|
self.chatItems = chatItems
|
2022-02-12 15:59:43 +00:00
|
|
|
self.chatStats = chatStats
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
2022-01-31 21:28:07 +00:00
|
|
|
|
2022-02-09 22:53:06 +00:00
|
|
|
var id: ChatId { get { chatInfo.id } }
|
2022-07-01 22:45:58 +01:00
|
|
|
|
|
|
|
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
2022-12-21 12:59:45 +00:00
|
|
|
|
|
|
|
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
2023-01-20 14:56:05 +04:00
|
|
|
|
|
|
|
enum NetworkStatus: Decodable, Equatable {
|
|
|
|
case unknown
|
|
|
|
case connected
|
|
|
|
case disconnected
|
|
|
|
case error(String)
|
|
|
|
|
|
|
|
var statusString: LocalizedStringKey {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .connected: return "connected"
|
|
|
|
case .error: return "error"
|
|
|
|
default: return "connecting"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var statusExplanation: LocalizedStringKey {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .connected: return "You are connected to the server used to receive messages from this contact."
|
|
|
|
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
|
|
|
default: return "Trying to connect to the server used to receive messages from this contact."
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var imageName: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .unknown: return "circle.dotted"
|
|
|
|
case .connected: return "circle.fill"
|
|
|
|
case .disconnected: return "ellipsis.circle.fill"
|
|
|
|
case .error: return "exclamationmark.circle.fill"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|