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-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2022-01-24 16:07:17 +00:00
|
|
|
|
2023-08-17 18:21:05 +01:00
|
|
|
actor TerminalItems {
|
|
|
|
private var terminalItems: [TerminalItem] = []
|
|
|
|
|
|
|
|
static let shared = TerminalItems()
|
|
|
|
|
|
|
|
func items() -> [TerminalItem] {
|
|
|
|
terminalItems
|
|
|
|
}
|
|
|
|
|
|
|
|
func add(_ item: TerminalItem) async {
|
|
|
|
addTermItem(&terminalItems, item)
|
|
|
|
let m = ChatModel.shared
|
|
|
|
if m.showingTerminal {
|
|
|
|
await MainActor.run {
|
|
|
|
addTermItem(&m.terminalItems, item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-05 11:51:22 +01:00
|
|
|
func addCommand<R: ChatAPIResult>(_ start: Date, _ cmd: ChatCommand, _ res: APIResult<R>) async {
|
2023-08-21 13:52:09 +01:00
|
|
|
await add(.cmd(start, cmd))
|
2025-05-05 11:51:22 +01:00
|
|
|
await addResult(res)
|
|
|
|
}
|
|
|
|
|
|
|
|
func addResult<R: ChatAPIResult>(_ res: APIResult<R>) async {
|
|
|
|
let item: TerminalItem = switch res {
|
|
|
|
case let .result(r): .res(.now, r)
|
|
|
|
case let .error(e): .err(.now, e)
|
|
|
|
case let .invalid(type, json): .bad(.now, type, json)
|
|
|
|
}
|
|
|
|
await add(item)
|
2023-08-17 18:21:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
|
|
|
if items.count >= 200 {
|
|
|
|
items.removeFirst()
|
|
|
|
}
|
|
|
|
items.append(item)
|
|
|
|
}
|
|
|
|
|
2024-07-29 23:17:14 +03:00
|
|
|
class ItemsModel: ObservableObject {
|
|
|
|
static let shared = ItemsModel()
|
|
|
|
private let publisher = ObservableObjectPublisher()
|
|
|
|
private var bag = Set<AnyCancellable>()
|
|
|
|
var reversedChatItems: [ChatItem] = [] {
|
|
|
|
willSet { publisher.send() }
|
|
|
|
}
|
2024-08-11 12:07:45 +03:00
|
|
|
var itemAdded = false {
|
|
|
|
willSet { publisher.send() }
|
|
|
|
}
|
2025-02-18 01:21:40 +07:00
|
|
|
|
|
|
|
let chatState = ActiveChatState()
|
|
|
|
|
2024-08-13 21:37:48 +03:00
|
|
|
// Publishes directly to `objectWillChange` publisher,
|
|
|
|
// this will cause reversedChatItems to be rendered without throttling
|
|
|
|
@Published var isLoading = false
|
2025-02-19 23:13:20 +07:00
|
|
|
@Published var showLoadingProgress: ChatId? = nil
|
|
|
|
|
|
|
|
private var navigationTimeoutTask: Task<Void, Never>? = nil
|
|
|
|
private var loadChatTask: Task<Void, Never>? = nil
|
2024-08-13 21:37:48 +03:00
|
|
|
|
2025-03-14 05:36:45 +07:00
|
|
|
var lastItemsLoaded: Bool {
|
|
|
|
chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id
|
|
|
|
}
|
|
|
|
|
2024-07-29 23:17:14 +03:00
|
|
|
init() {
|
|
|
|
publisher
|
2024-09-09 16:58:22 +03:00
|
|
|
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
|
2024-07-29 23:17:14 +03:00
|
|
|
.sink { self.objectWillChange.send() }
|
|
|
|
.store(in: &bag)
|
|
|
|
}
|
2024-08-13 21:37:48 +03:00
|
|
|
|
|
|
|
func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) {
|
2025-02-19 23:13:20 +07:00
|
|
|
navigationTimeoutTask?.cancel()
|
|
|
|
loadChatTask?.cancel()
|
|
|
|
navigationTimeoutTask = Task {
|
2024-08-13 21:37:48 +03:00
|
|
|
do {
|
2025-02-20 17:39:04 +07:00
|
|
|
try await Task.sleep(nanoseconds: 250_000000)
|
2024-08-13 21:37:48 +03:00
|
|
|
await MainActor.run {
|
|
|
|
ChatModel.shared.chatId = chatId
|
2025-02-19 23:13:20 +07:00
|
|
|
willNavigate()
|
2024-08-13 21:37:48 +03:00
|
|
|
}
|
|
|
|
} catch {}
|
|
|
|
}
|
2025-02-19 23:13:20 +07:00
|
|
|
loadChatTask = Task {
|
2025-02-18 01:21:40 +07:00
|
|
|
await MainActor.run { self.isLoading = true }
|
2025-02-19 23:13:20 +07:00
|
|
|
// try? await Task.sleep(nanoseconds: 1000_000000)
|
2025-02-18 01:21:40 +07:00
|
|
|
await loadChat(chatId: chatId)
|
2025-02-19 23:13:20 +07:00
|
|
|
if !Task.isCancelled {
|
|
|
|
await MainActor.run {
|
|
|
|
self.isLoading = false
|
|
|
|
self.showLoadingProgress = nil
|
|
|
|
}
|
2024-08-13 21:37:48 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-02-28 21:45:24 +07:00
|
|
|
|
|
|
|
func loadOpenChatNoWait(_ chatId: ChatId, _ openAroundItemId: ChatItem.ID? = nil) {
|
|
|
|
navigationTimeoutTask?.cancel()
|
|
|
|
loadChatTask?.cancel()
|
|
|
|
loadChatTask = Task {
|
|
|
|
// try? await Task.sleep(nanoseconds: 1000_000000)
|
|
|
|
await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
|
|
|
if !Task.isCancelled {
|
|
|
|
await MainActor.run {
|
|
|
|
if openAroundItemId == nil {
|
|
|
|
ChatModel.shared.chatId = chatId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-29 23:17:14 +03:00
|
|
|
}
|
|
|
|
|
2024-12-19 10:48:26 +00:00
|
|
|
class ChatTagsModel: ObservableObject {
|
|
|
|
static let shared = ChatTagsModel()
|
|
|
|
|
|
|
|
@Published var userTags: [ChatTag] = []
|
|
|
|
@Published var activeFilter: ActiveFilter? = nil
|
2024-12-20 11:43:11 +00:00
|
|
|
@Published var presetTags: [PresetTag:Int] = [:]
|
|
|
|
@Published var unreadTags: [Int64:Int] = [:]
|
|
|
|
|
|
|
|
func updateChatTags(_ chats: [Chat]) {
|
|
|
|
let tm = ChatTagsModel.shared
|
|
|
|
var newPresetTags: [PresetTag:Int] = [:]
|
|
|
|
var newUnreadTags: [Int64:Int] = [:]
|
|
|
|
for chat in chats {
|
|
|
|
for tag in PresetTag.allCases {
|
2025-01-11 17:39:39 +07:00
|
|
|
if presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats) {
|
2024-12-20 11:43:11 +00:00
|
|
|
newPresetTags[tag] = (newPresetTags[tag] ?? 0) + 1
|
|
|
|
}
|
|
|
|
}
|
2024-12-23 16:31:47 +00:00
|
|
|
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
2024-12-20 11:43:11 +00:00
|
|
|
for tag in tags {
|
|
|
|
newUnreadTags[tag] = (newUnreadTags[tag] ?? 0) + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
presetTags = newPresetTags
|
|
|
|
unreadTags = newUnreadTags
|
2025-02-25 01:31:41 +07:00
|
|
|
clearActiveChatFilterIfNeeded()
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func updateChatFavorite(favorite: Bool, wasFavorite: Bool) {
|
|
|
|
let count = presetTags[.favorites]
|
|
|
|
if favorite && !wasFavorite {
|
|
|
|
presetTags[.favorites] = (count ?? 0) + 1
|
|
|
|
} else if !favorite && wasFavorite, let count {
|
|
|
|
presetTags[.favorites] = max(0, count - 1)
|
2025-02-25 01:31:41 +07:00
|
|
|
clearActiveChatFilterIfNeeded()
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-11 17:39:39 +07:00
|
|
|
func addPresetChatTags(_ chatInfo: ChatInfo, _ chatStats: ChatStats) {
|
2024-12-20 11:43:11 +00:00
|
|
|
for tag in PresetTag.allCases {
|
2025-01-11 17:39:39 +07:00
|
|
|
if presetTagMatchesChat(tag, chatInfo, chatStats) {
|
2024-12-20 11:43:11 +00:00
|
|
|
presetTags[tag] = (presetTags[tag] ?? 0) + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-19 10:48:26 +00:00
|
|
|
|
2025-01-11 17:39:39 +07:00
|
|
|
func removePresetChatTags(_ chatInfo: ChatInfo, _ chatStats: ChatStats) {
|
2024-12-19 10:48:26 +00:00
|
|
|
for tag in PresetTag.allCases {
|
2025-01-11 17:39:39 +07:00
|
|
|
if presetTagMatchesChat(tag, chatInfo, chatStats) {
|
2024-12-20 11:43:11 +00:00
|
|
|
if let count = presetTags[tag] {
|
2025-01-11 17:39:39 +07:00
|
|
|
if count > 1 {
|
|
|
|
presetTags[tag] = count - 1
|
|
|
|
} else {
|
|
|
|
presetTags.removeValue(forKey: tag)
|
|
|
|
}
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
2024-12-19 10:48:26 +00:00
|
|
|
}
|
|
|
|
}
|
2025-02-25 01:31:41 +07:00
|
|
|
clearActiveChatFilterIfNeeded()
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func markChatTagRead(_ chat: Chat) -> Void {
|
2024-12-23 16:31:47 +00:00
|
|
|
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
2024-12-25 22:09:18 +00:00
|
|
|
decTagsReadCount(tags)
|
2024-12-19 10:48:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-20 11:43:11 +00:00
|
|
|
func updateChatTagRead(_ chat: Chat, wasUnread: Bool) -> Void {
|
|
|
|
guard let tags = chat.chatInfo.chatTags else { return }
|
2024-12-23 16:31:47 +00:00
|
|
|
let nowUnread = chat.unreadTag
|
2024-12-20 11:43:11 +00:00
|
|
|
if nowUnread && !wasUnread {
|
|
|
|
for tag in tags {
|
|
|
|
unreadTags[tag] = (unreadTags[tag] ?? 0) + 1
|
|
|
|
}
|
|
|
|
} else if !nowUnread && wasUnread {
|
2024-12-25 22:09:18 +00:00
|
|
|
decTagsReadCount(tags)
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-25 22:09:18 +00:00
|
|
|
func decTagsReadCount(_ tags: [Int64]) -> Void {
|
2024-12-20 11:43:11 +00:00
|
|
|
for tag in tags {
|
|
|
|
if let count = unreadTags[tag] {
|
|
|
|
unreadTags[tag] = max(0, count - 1)
|
|
|
|
}
|
|
|
|
}
|
2024-12-19 10:48:26 +00:00
|
|
|
}
|
2025-01-11 17:39:39 +07:00
|
|
|
|
|
|
|
func changeGroupReportsTag(_ by: Int = 0) {
|
|
|
|
if by == 0 { return }
|
2025-02-25 01:31:41 +07:00
|
|
|
presetTags[.groupReports] = max(0, (presetTags[.groupReports] ?? 0) + by)
|
|
|
|
clearActiveChatFilterIfNeeded()
|
|
|
|
}
|
|
|
|
|
|
|
|
func clearActiveChatFilterIfNeeded() {
|
|
|
|
let clear = switch activeFilter {
|
|
|
|
case let .presetTag(tag): (presetTags[tag] ?? 0) == 0
|
|
|
|
case let .userTag(tag): !userTags.contains(tag)
|
|
|
|
case .unread, nil: false
|
|
|
|
}
|
|
|
|
if clear { activeFilter = nil }
|
2025-01-11 17:39:39 +07:00
|
|
|
}
|
2024-12-19 10:48:26 +00:00
|
|
|
}
|
|
|
|
|
2024-08-13 23:08:04 +03:00
|
|
|
class NetworkModel: ObservableObject {
|
|
|
|
// map of connections network statuses, key is agent connection id
|
|
|
|
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
|
|
|
|
|
|
|
|
static let shared = NetworkModel()
|
|
|
|
|
|
|
|
private init() { }
|
|
|
|
|
|
|
|
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
|
|
|
if let conn = contact.activeConn {
|
|
|
|
networkStatuses[conn.agentConnId] = status
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
|
|
|
if let conn = contact.activeConn {
|
|
|
|
networkStatuses[conn.agentConnId] ?? .unknown
|
|
|
|
} else {
|
|
|
|
.unknown
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-25 21:21:24 +03:00
|
|
|
/// ChatItemWithMenu can depend on previous or next item for it's appearance
|
|
|
|
/// This dummy model is used to force an update of all chat items,
|
|
|
|
/// when they might have changed appearance.
|
|
|
|
class ChatItemDummyModel: ObservableObject {
|
|
|
|
static let shared = ChatItemDummyModel()
|
|
|
|
func sendUpdate() { objectWillChange.send() }
|
|
|
|
}
|
|
|
|
|
2022-01-24 16:07:17 +00:00
|
|
|
final class ChatModel: ObservableObject {
|
2022-05-09 09:52:09 +01:00
|
|
|
@Published var onboardingStage: OnboardingStage?
|
2023-07-13 23:48:25 +01:00
|
|
|
@Published var setDeliveryReceipts = false
|
2022-07-01 22:45:58 +01:00
|
|
|
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
2024-07-03 22:42:13 +01:00
|
|
|
@Published var currentUser: User? {
|
|
|
|
didSet {
|
|
|
|
ThemeManager.applyTheme(currentThemeDefault.get())
|
|
|
|
}
|
|
|
|
}
|
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?
|
2024-01-10 04:01:41 +07:00
|
|
|
@Published var ctrlInitInProgress: Bool = false
|
2024-10-18 18:07:38 +04:00
|
|
|
@Published var notificationResponse: UNNotificationResponse?
|
2023-12-18 22:04:49 +04:00
|
|
|
// local authentication
|
|
|
|
@Published var contentViewAccessAuthenticated: Bool = false
|
2023-04-12 12:22:55 +02:00
|
|
|
@Published var laRequest: LocalAuthRequest?
|
2022-02-02 12:51:39 +00:00
|
|
|
// list of chat "previews"
|
2024-08-20 09:29:52 +01:00
|
|
|
@Published private(set) var chats: [Chat] = []
|
2024-01-10 23:57:34 +07:00
|
|
|
@Published var deletedChats: Set<String> = []
|
2022-02-02 12:51:39 +00:00
|
|
|
// current chat
|
|
|
|
@Published var chatId: String?
|
2025-02-28 21:45:24 +07:00
|
|
|
@Published var openAroundItemId: ChatItem.ID? = nil
|
2023-10-18 11:23:35 +01:00
|
|
|
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
2022-02-12 15:59:43 +00:00
|
|
|
@Published var chatToTop: String?
|
2023-10-31 09:44:57 +00:00
|
|
|
@Published var groupMembers: [GMember] = []
|
2024-07-10 16:15:14 +04:00
|
|
|
@Published var groupMembersIndexes: Dictionary<Int64, Int> = [:] // groupMemberId to index in groupMembers list
|
2024-07-11 10:57:56 +03:00
|
|
|
@Published var membersLoaded = false
|
2022-02-02 12:51:39 +00:00
|
|
|
// items in the terminal view
|
2023-08-17 18:21:05 +01:00
|
|
|
@Published var showingTerminal = false
|
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-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
|
2025-02-07 17:41:15 +04:00
|
|
|
@Published var reRegisterTknStatus: NtfTknStatus? = nil
|
2022-07-01 09:49:30 +01:00
|
|
|
@Published var tokenStatus: NtfTknStatus?
|
|
|
|
@Published var notificationMode = NotificationsMode.off
|
2024-02-13 17:58:54 +04:00
|
|
|
@Published var notificationServer: String?
|
2023-07-19 16:37:46 +01:00
|
|
|
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
2022-07-22 08:10:37 +01:00
|
|
|
// pending notification actions
|
2023-08-08 17:26:56 +04:00
|
|
|
@Published var ntfContactRequest: NTFContactRequest?
|
2022-07-22 08:10:37 +01:00
|
|
|
@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?
|
2023-11-29 01:36:05 +08:00
|
|
|
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
2022-05-21 12:13:37 +01:00
|
|
|
@Published var showCallView = false
|
2024-02-13 22:04:42 +07:00
|
|
|
@Published var activeCallViewIsCollapsed = false
|
2023-11-16 16:53:44 +00:00
|
|
|
// remote desktop
|
|
|
|
@Published var remoteCtrlSession: RemoteCtrlSession?
|
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
|
|
|
// currently showing invitation
|
|
|
|
@Published var showingInvitation: ShowingInvitation?
|
2024-03-12 23:25:06 +07:00
|
|
|
@Published var migrationState: MigrationToState? = MigrationToDeviceState.makeMigrationState()
|
2022-11-24 21:18:28 +04:00
|
|
|
// audio recording and playback
|
2023-04-06 20:26:48 +03:00
|
|
|
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
2023-01-25 08:35:25 +00:00
|
|
|
@Published var draft: ComposeState?
|
|
|
|
@Published var draftChatId: String?
|
2024-04-11 17:31:30 +07:00
|
|
|
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
|
2024-11-19 15:37:00 +04:00
|
|
|
// usage conditions
|
|
|
|
@Published var conditions: ServerOperatorConditions = .empty
|
2023-03-15 18:32:27 +03:00
|
|
|
|
2022-02-28 10:44:48 +00:00
|
|
|
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
|
|
|
|
2023-04-06 20:26:48 +03:00
|
|
|
var filesToDelete: Set<URL> = []
|
2023-01-27 22:09:39 +00:00
|
|
|
|
2022-02-09 22:53:06 +00:00
|
|
|
static let shared = ChatModel()
|
2022-02-02 12:51:39 +00:00
|
|
|
|
2024-07-29 23:17:14 +03:00
|
|
|
let im = ItemsModel.shared
|
|
|
|
|
2022-09-07 20:06:16 +01:00
|
|
|
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
|
|
|
|
2023-12-11 12:59:49 +00:00
|
|
|
let ntfEnableLocal = true
|
2023-07-19 16:37:46 +01:00
|
|
|
|
|
|
|
var ntfEnablePeriodic: Bool {
|
2023-12-09 21:59:40 +00:00
|
|
|
notificationMode != .off
|
2023-07-19 16:37:46 +01:00
|
|
|
}
|
|
|
|
|
2023-11-16 16:53:44 +00:00
|
|
|
var activeRemoteCtrl: Bool {
|
|
|
|
remoteCtrlSession?.active ?? false
|
|
|
|
}
|
|
|
|
|
2023-03-22 15:58:01 +00:00
|
|
|
func getUser(_ userId: Int64) -> User? {
|
|
|
|
currentUser?.userId == userId
|
|
|
|
? currentUser
|
|
|
|
: users.first { $0.user.userId == userId }?.user
|
|
|
|
}
|
|
|
|
|
|
|
|
func getUserIndex(_ user: User) -> Int? {
|
|
|
|
users.firstIndex { $0.user.userId == user.userId }
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateUser(_ user: User) {
|
|
|
|
if let i = getUserIndex(user) {
|
|
|
|
users[i].user = user
|
|
|
|
}
|
|
|
|
if currentUser?.userId == user.userId {
|
|
|
|
currentUser = user
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeUser(_ user: User) {
|
2024-01-19 22:52:13 +07:00
|
|
|
if let i = getUserIndex(user) {
|
2023-03-22 15:58:01 +00:00
|
|
|
users.remove(at: i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-13 19:19:00 +04:00
|
|
|
func getGroupChat(_ groupId: Int64) -> Chat? {
|
|
|
|
chats.first { chat in
|
|
|
|
if case let .group(groupInfo) = chat.chatInfo {
|
|
|
|
return groupInfo.groupId == groupId
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-10 16:15:14 +04:00
|
|
|
func populateGroupMembersIndexes() {
|
|
|
|
groupMembersIndexes.removeAll()
|
|
|
|
for (i, member) in groupMembers.enumerated() {
|
|
|
|
groupMembersIndexes[member.groupMemberId] = i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-31 09:44:57 +00:00
|
|
|
func getGroupMember(_ groupMemberId: Int64) -> GMember? {
|
2024-07-10 16:15:14 +04:00
|
|
|
if let i = groupMembersIndexes[groupMemberId] {
|
|
|
|
return groupMembers[i]
|
|
|
|
}
|
|
|
|
return nil
|
2023-10-31 09:44:57 +00:00
|
|
|
}
|
|
|
|
|
2024-07-11 10:57:56 +03:00
|
|
|
func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async {
|
|
|
|
let groupMembers = await apiListMembers(groupInfo.groupId)
|
|
|
|
await MainActor.run {
|
|
|
|
if chatId == groupInfo.id {
|
|
|
|
self.groupMembers = groupMembers.map { GMember.init($0) }
|
|
|
|
self.populateGroupMembersIndexes()
|
|
|
|
self.membersLoaded = true
|
|
|
|
updateView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-05 20:10:47 +00:00
|
|
|
private func getChatIndex(_ id: String) -> Int? {
|
|
|
|
chats.firstIndex(where: { $0.id == id })
|
|
|
|
}
|
|
|
|
|
2024-08-10 14:04:37 +01:00
|
|
|
func addChat(_ chat: Chat) {
|
|
|
|
if chatId == nil {
|
|
|
|
withAnimation { addChat_(chat, at: 0) }
|
|
|
|
} else {
|
|
|
|
addChat_(chat, at: 0)
|
2022-02-02 16:46:05 +00:00
|
|
|
}
|
2024-08-10 14:04:37 +01:00
|
|
|
popChatCollector.throttlePopChat(chat.chatInfo.id, currentPosition: 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func addChat_(_ chat: Chat, at position: Int = 0) {
|
|
|
|
chats.insert(chat, at: position)
|
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
|
2023-10-31 09:44:57 +00:00
|
|
|
chats[i].created = Date.now
|
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
|
|
|
}
|
|
|
|
|
2023-07-10 19:01:22 +04:00
|
|
|
func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) {
|
|
|
|
var updatedConn = contact.activeConn
|
2023-11-10 10:16:06 +04:00
|
|
|
updatedConn?.connectionStats = connectionStats
|
2023-07-10 19:01:22 +04:00
|
|
|
var updatedContact = contact
|
|
|
|
updatedContact.activeConn = updatedConn
|
|
|
|
updateContact(updatedContact)
|
|
|
|
}
|
|
|
|
|
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: []))
|
2025-01-11 17:39:39 +07:00
|
|
|
ChatTagsModel.shared.addPresetChatTags(cInfo, ChatStats())
|
2022-02-05 20:10:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-20 09:29:52 +01:00
|
|
|
func updateChats(_ newChats: [ChatData]) {
|
|
|
|
chats = newChats.map { Chat($0) }
|
2023-01-23 13:20:58 +00:00
|
|
|
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
2024-08-10 14:04:37 +01:00
|
|
|
popChatCollector.clear()
|
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) {
|
2024-08-12 15:49:34 +01:00
|
|
|
// mark chat non deleted
|
|
|
|
if case let .direct(contact) = cInfo, contact.chatDeleted {
|
|
|
|
var updatedContact = contact
|
|
|
|
updatedContact.chatDeleted = false
|
|
|
|
updateContact(updatedContact)
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
// update previews
|
|
|
|
if let i = getChatIndex(cInfo.id) {
|
2023-11-27 17:14:12 +04:00
|
|
|
chats[i].chatItems = switch cInfo {
|
|
|
|
case .group:
|
|
|
|
if let currentPreviewItem = chats[i].chatItems.first {
|
|
|
|
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
|
|
|
|
[cItem]
|
|
|
|
} else {
|
|
|
|
[currentPreviewItem]
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
[cItem]
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
[cItem]
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
if case .rcvNew = cItem.meta.itemStatus {
|
2025-02-03 20:47:32 +00:00
|
|
|
unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
2024-08-10 14:04:37 +01:00
|
|
|
popChatCollector.throttlePopChat(cInfo.id, currentPosition: i)
|
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 {
|
2024-07-29 23:17:14 +03:00
|
|
|
if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
|
2022-10-10 10:40:30 +01:00
|
|
|
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
|
|
|
|
}
|
2024-07-24 00:11:42 +07:00
|
|
|
if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
|
|
|
|
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
// 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 {
|
2023-05-16 10:34:25 +02:00
|
|
|
if let i = getChatItemIndex(cItem) {
|
2024-08-25 21:21:24 +03:00
|
|
|
_updateChatItem(at: i, with: cItem)
|
|
|
|
ChatItemDummyModel.shared.sendUpdate()
|
2022-10-10 10:40:30 +01:00
|
|
|
return false
|
2022-02-12 15:59:43 +00:00
|
|
|
} else {
|
2024-08-25 21:21:24 +03:00
|
|
|
var ci = cItem
|
|
|
|
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
|
|
|
ci.meta.itemStatus = status
|
2023-01-10 19:12:48 +00:00
|
|
|
}
|
2024-08-25 21:21:24 +03:00
|
|
|
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
2024-08-25 21:21:24 +03:00
|
|
|
im.itemAdded = true
|
|
|
|
ChatItemDummyModel.shared.sendUpdate()
|
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
|
|
|
}
|
2023-05-16 10:34:25 +02:00
|
|
|
|
2023-10-18 11:23:35 +01:00
|
|
|
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
2023-05-16 10:34:25 +02:00
|
|
|
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
2024-07-03 10:24:26 +01:00
|
|
|
withConditionalAnimation {
|
2023-05-16 10:34:25 +02:00
|
|
|
_updateChatItem(at: i, with: cItem)
|
|
|
|
}
|
2023-10-18 11:23:35 +01:00
|
|
|
} else if let status = status {
|
|
|
|
chatItemStatuses.updateValue(status, forKey: cItem.id)
|
2023-05-16 10:34:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems[i] = cItem
|
|
|
|
im.reversedChatItems[i].viewTimestamp = .now
|
2023-05-16 10:34:25 +02:00
|
|
|
}
|
|
|
|
|
2023-10-31 09:44:57 +00:00
|
|
|
func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
2023-05-16 10:34:25 +02:00
|
|
|
}
|
|
|
|
|
2022-03-30 20:37:47 +04:00
|
|
|
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
2024-08-06 01:36:33 +03:00
|
|
|
if cItem.isRcvNew {
|
2025-02-03 20:47:32 +00:00
|
|
|
unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
|
2022-12-07 20:46:38 +04:00
|
|
|
}
|
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 {
|
2023-05-16 10:34:25 +02:00
|
|
|
if let i = getChatItemIndex(cItem) {
|
2025-02-18 01:21:40 +07:00
|
|
|
withAnimation {
|
|
|
|
let item = im.reversedChatItems.remove(at: i)
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
2022-03-30 20:37:47 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-24 00:11:42 +07:00
|
|
|
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
2022-03-30 20:37:47 +04:00
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
|
2025-03-07 22:08:00 +07:00
|
|
|
func removeMemberItems(_ removedMember: GroupMember, byMember: GroupMember, _ groupInfo: GroupInfo) {
|
|
|
|
// this should not happen, only another member can "remove" user, user can only "leave" (another event).
|
|
|
|
if byMember.groupMemberId == groupInfo.membership.groupMemberId {
|
|
|
|
logger.debug("exiting removeMemberItems")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if chatId == groupInfo.id {
|
|
|
|
for i in 0..<im.reversedChatItems.count {
|
|
|
|
if let updatedItem = removedUpdatedItem(im.reversedChatItems[i]) {
|
|
|
|
_updateChatItem(at: i, with: updatedItem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if let chat = getChat(groupInfo.id),
|
|
|
|
chat.chatItems.count > 0,
|
|
|
|
let updatedItem = removedUpdatedItem(chat.chatItems[0]) {
|
|
|
|
chat.chatItems = [updatedItem]
|
|
|
|
}
|
|
|
|
|
|
|
|
func removedUpdatedItem(_ item: ChatItem) -> ChatItem? {
|
|
|
|
let newContent: CIContent
|
|
|
|
if case .groupSnd = item.chatDir, removedMember.groupMemberId == groupInfo.membership.groupMemberId {
|
|
|
|
newContent = .sndModerated
|
|
|
|
} else if case let .groupRcv(groupMember) = item.chatDir, groupMember.groupMemberId == removedMember.groupMemberId {
|
|
|
|
newContent = .rcvModerated
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var updatedItem = item
|
|
|
|
updatedItem.meta.itemDeleted = .moderated(deletedTs: Date.now, byGroupMember: byMember)
|
|
|
|
if groupInfo.fullGroupPreferences.fullDelete.on {
|
|
|
|
updatedItem.content = newContent
|
|
|
|
}
|
|
|
|
if item.isActiveReport {
|
|
|
|
decreaseGroupReportsCounter(groupInfo.id)
|
|
|
|
}
|
|
|
|
return updatedItem
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-10 10:40:30 +01:00
|
|
|
func nextChatItemData<T>(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? {
|
2024-07-29 23:17:14 +03:00
|
|
|
guard var i = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil }
|
2022-10-10 10:40:30 +01:00
|
|
|
if previous {
|
2024-07-29 23:17:14 +03:00
|
|
|
while i < im.reversedChatItems.count - 1 {
|
2022-10-10 10:40:30 +01:00
|
|
|
i += 1
|
2024-07-29 23:17:14 +03:00
|
|
|
if let res = map(im.reversedChatItems[i]) { return res }
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
while i > 0 {
|
|
|
|
i -= 1
|
2024-07-29 23:17:14 +03:00
|
|
|
if let res = map(im.reversedChatItems[i]) { return res }
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-23 15:48:29 +00:00
|
|
|
func updateCurrentUser(_ newProfile: Profile, _ preferences: FullPreferences? = nil) {
|
|
|
|
if let current = currentUser {
|
|
|
|
currentUser?.profile = toLocalProfile(current.profile.profileId, newProfile, "")
|
|
|
|
if let preferences = preferences {
|
|
|
|
currentUser?.fullPreferences = preferences
|
|
|
|
}
|
|
|
|
if let current = currentUser, let i = users.firstIndex(where: { $0.user.userId == current.userId }) {
|
|
|
|
users[i].user = current
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-03 22:42:13 +01:00
|
|
|
|
|
|
|
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
|
2024-10-07 22:29:46 +03:00
|
|
|
guard var current = currentUser, current.uiThemes != uiThemes else { return }
|
2024-07-03 22:42:13 +01:00
|
|
|
current.uiThemes = uiThemes
|
|
|
|
let i = users.firstIndex(where: { $0.user.userId == current.userId })
|
|
|
|
if let i {
|
|
|
|
users[i].user = current
|
|
|
|
}
|
|
|
|
currentUser = current
|
|
|
|
}
|
2023-01-23 15:48:29 +00:00
|
|
|
|
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 {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems.insert(cItem, at: 0)
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0)
|
2024-08-11 12:07:45 +03:00
|
|
|
im.itemAdded = true
|
2023-01-10 19:12:48 +00:00
|
|
|
}
|
|
|
|
return cItem
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeLiveDummy(animated: Bool = true) {
|
|
|
|
if hasLiveDummy {
|
|
|
|
if animated {
|
2024-07-29 23:17:14 +03:00
|
|
|
withAnimation { _ = im.reversedChatItems.removeFirst() }
|
2023-01-10 19:12:48 +00:00
|
|
|
} else {
|
2024-07-29 23:17:14 +03:00
|
|
|
_ = im.reversedChatItems.removeFirst()
|
2023-01-10 19:12:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var hasLiveDummy: Bool {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems.first?.isLiveDummy == true
|
2023-01-10 19:12:48 +00:00
|
|
|
}
|
|
|
|
|
2025-02-19 03:44:24 +07:00
|
|
|
func markAllChatItemsRead(_ cInfo: ChatInfo) {
|
2022-02-12 15:59:43 +00:00
|
|
|
// update preview
|
2022-10-21 12:32:11 +01:00
|
|
|
_updateChat(cInfo.id) { chat in
|
2025-02-03 20:47:32 +00:00
|
|
|
self.decreaseUnreadCounter(user: self.currentUser!, chat: chat)
|
2024-12-20 11:43:11 +00:00
|
|
|
ChatTagsModel.shared.markChatTagRead(chat)
|
2022-02-12 15:59:43 +00:00
|
|
|
chat.chatStats = ChatStats()
|
|
|
|
}
|
|
|
|
// update current chat
|
|
|
|
if chatId == cInfo.id {
|
2025-02-19 03:44:24 +07:00
|
|
|
var i = 0
|
|
|
|
while i < im.reversedChatItems.count {
|
|
|
|
markChatItemRead_(i)
|
|
|
|
i += 1
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.itemsRead(nil, im.reversedChatItems.reversed())
|
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
|
2024-12-23 16:31:47 +00:00
|
|
|
let wasUnread = chat.unreadTag
|
2022-10-21 12:32:11 +01:00
|
|
|
chat.chatStats.unreadChat = unreadChat
|
2024-12-20 11:43:11 +00:00
|
|
|
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread)
|
2022-10-21 12:32:11 +01:00
|
|
|
}
|
|
|
|
}
|
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) {
|
2025-02-03 20:47:32 +00:00
|
|
|
self.decreaseUnreadCounter(user: self.currentUser!, chat: chat)
|
2022-05-17 22:48:54 +04:00
|
|
|
chat.chatItems = []
|
2024-12-20 11:43:11 +00:00
|
|
|
ChatTagsModel.shared.markChatTagRead(chat)
|
2022-05-17 22:48:54 +04:00
|
|
|
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 {
|
2023-10-18 11:23:35 +01:00
|
|
|
chatItemStatuses = [:]
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems = []
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.clear()
|
2022-05-17 22:48:54 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], _ mentionsRead: Int) {
|
2024-09-09 16:58:22 +03:00
|
|
|
if self.chatId == cInfo.id {
|
2025-02-18 01:21:40 +07:00
|
|
|
var unreadItemIds: Set<ChatItem.ID> = []
|
2025-02-19 03:44:24 +07:00
|
|
|
var i = 0
|
|
|
|
var ids = Set(itemIds)
|
|
|
|
while i < im.reversedChatItems.count && !ids.isEmpty {
|
|
|
|
let item = im.reversedChatItems[i]
|
|
|
|
if ids.contains(item.id) && item.isRcvNew {
|
2024-09-09 16:58:22 +03:00
|
|
|
markChatItemRead_(i)
|
2025-02-19 03:44:24 +07:00
|
|
|
unreadItemIds.insert(item.id)
|
|
|
|
ids.remove(item.id)
|
2024-07-11 10:57:56 +03:00
|
|
|
}
|
2025-02-19 03:44:24 +07:00
|
|
|
i += 1
|
2024-07-03 10:24:26 +01:00
|
|
|
}
|
2025-04-29 16:27:19 +00:00
|
|
|
im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed())
|
2022-12-21 12:59:45 +00:00
|
|
|
}
|
2025-02-03 20:47:32 +00:00
|
|
|
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead)
|
2022-12-21 12:59:45 +00:00
|
|
|
}
|
|
|
|
|
2024-07-29 23:17:14 +03:00
|
|
|
private let unreadCollector = UnreadCollector()
|
|
|
|
|
|
|
|
class UnreadCollector {
|
2024-08-02 16:08:10 +03:00
|
|
|
private let subject = PassthroughSubject<Void, Never>()
|
2024-07-29 23:17:14 +03:00
|
|
|
private var bag = Set<AnyCancellable>()
|
2025-02-03 20:47:32 +00:00
|
|
|
private var unreadCounts: [ChatId: (unread: Int, mentions: Int)] = [:]
|
2024-07-29 23:17:14 +03:00
|
|
|
|
|
|
|
init() {
|
|
|
|
subject
|
|
|
|
.debounce(for: 1, scheduler: DispatchQueue.main)
|
2024-08-02 16:08:10 +03:00
|
|
|
.sink {
|
|
|
|
let m = ChatModel.shared
|
2025-02-03 20:47:32 +00:00
|
|
|
for (chatId, (unread, mentions)) in self.unreadCounts {
|
|
|
|
if unread != 0 || mentions != 0, let i = m.getChatIndex(chatId) {
|
|
|
|
m.changeUnreadCounter(i, by: unread, unreadMentions: mentions)
|
2024-08-02 16:08:10 +03:00
|
|
|
}
|
2024-07-29 23:17:14 +03:00
|
|
|
}
|
2024-08-02 16:08:10 +03:00
|
|
|
self.unreadCounts = [:]
|
2024-07-29 23:17:14 +03:00
|
|
|
}
|
|
|
|
.store(in: &bag)
|
|
|
|
}
|
2024-08-06 01:36:33 +03:00
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
func changeUnreadCounter(_ chatId: ChatId, by count: Int, unreadMentions: Int) {
|
|
|
|
let (unread, mentions) = self.unreadCounts[chatId] ?? (0, 0)
|
|
|
|
self.unreadCounts[chatId] = (unread + count, mentions + unreadMentions)
|
2024-08-02 16:08:10 +03:00
|
|
|
subject.send()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let popChatCollector = PopChatCollector()
|
|
|
|
|
|
|
|
class PopChatCollector {
|
|
|
|
private let subject = PassthroughSubject<Void, Never>()
|
|
|
|
private var bag = Set<AnyCancellable>()
|
2024-08-10 14:04:37 +01:00
|
|
|
private var chatsToPop: [ChatId: Date] = [:]
|
|
|
|
private let popTsComparator = KeyPathComparator<Chat>(\.popTs, order: .reverse)
|
2024-08-02 16:08:10 +03:00
|
|
|
|
|
|
|
init() {
|
|
|
|
subject
|
|
|
|
.throttle(for: 2, scheduler: DispatchQueue.main, latest: true)
|
2024-08-10 14:04:37 +01:00
|
|
|
.sink { self.popCollectedChats() }
|
2024-08-02 16:08:10 +03:00
|
|
|
.store(in: &bag)
|
|
|
|
}
|
|
|
|
|
2024-08-10 14:04:37 +01:00
|
|
|
func throttlePopChat(_ chatId: ChatId, currentPosition: Int) {
|
|
|
|
let m = ChatModel.shared
|
|
|
|
if currentPosition > 0 && m.chatId == chatId {
|
|
|
|
m.chatToTop = chatId
|
|
|
|
}
|
|
|
|
if currentPosition > 0 || !chatsToPop.isEmpty {
|
|
|
|
chatsToPop[chatId] = Date.now
|
2024-08-02 16:08:10 +03:00
|
|
|
subject.send()
|
|
|
|
}
|
2024-07-29 23:17:14 +03:00
|
|
|
}
|
2024-08-10 14:04:37 +01:00
|
|
|
|
|
|
|
func clear() {
|
|
|
|
chatsToPop = [:]
|
|
|
|
}
|
|
|
|
|
|
|
|
func popCollectedChats() {
|
|
|
|
let m = ChatModel.shared
|
|
|
|
var ixs: IndexSet = []
|
|
|
|
var chs: [Chat] = []
|
|
|
|
// collect chats that received updates
|
|
|
|
for (chatId, popTs) in self.chatsToPop {
|
|
|
|
// Currently opened chat is excluded, removing it from the list would navigate out of it
|
|
|
|
// It will be popped to top later when user exits from the list.
|
|
|
|
if m.chatId != chatId, let i = m.getChatIndex(chatId) {
|
|
|
|
ixs.insert(i)
|
|
|
|
let ch = m.chats[i]
|
|
|
|
ch.popTs = popTs
|
|
|
|
chs.append(ch)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let removeInsert = {
|
|
|
|
m.chats.remove(atOffsets: ixs)
|
|
|
|
// sort chats by pop timestamp in descending order
|
|
|
|
m.chats.insert(contentsOf: chs.sorted(using: self.popTsComparator), at: 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.chatId == nil {
|
|
|
|
withAnimation { removeInsert() }
|
|
|
|
} else {
|
|
|
|
removeInsert()
|
|
|
|
}
|
|
|
|
|
|
|
|
self.chatsToPop = [:]
|
|
|
|
}
|
2024-07-29 23:17:14 +03:00
|
|
|
}
|
|
|
|
|
2022-12-21 12:59:45 +00:00
|
|
|
private func markChatItemRead_(_ i: Int) {
|
2024-07-29 23:17:14 +03:00
|
|
|
let meta = im.reversedChatItems[i].meta
|
2022-12-21 12:59:45 +00:00
|
|
|
if case .rcvNew = meta.itemStatus {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems[i].meta.itemStatus = .rcvRead
|
|
|
|
im.reversedChatItems[i].viewTimestamp = .now
|
2022-12-21 12:59:45 +00:00
|
|
|
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
|
2024-07-29 23:17:14 +03:00
|
|
|
im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
2022-12-21 12:59:45 +00:00
|
|
|
}
|
2022-02-12 15:59:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
func changeUnreadCounter(_ chatIndex: Int, by count: Int, unreadMentions: Int) {
|
2024-12-23 16:31:47 +00:00
|
|
|
let wasUnread = chats[chatIndex].unreadTag
|
2025-02-03 20:47:32 +00:00
|
|
|
let stats = chats[chatIndex].chatStats
|
|
|
|
chats[chatIndex].chatStats.unreadCount = stats.unreadCount + count
|
|
|
|
chats[chatIndex].chatStats.unreadMentions = stats.unreadMentions + unreadMentions
|
2024-12-20 11:43:11 +00:00
|
|
|
ChatTagsModel.shared.updateChatTagRead(chats[chatIndex], wasUnread: wasUnread)
|
2024-08-06 01:36:33 +03:00
|
|
|
changeUnreadCounter(user: currentUser!, by: count)
|
2023-01-19 16:22:56 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 12:20:21 +01:00
|
|
|
func increaseUnreadCounter(user: any UserLike) {
|
2023-01-19 16:22:56 +00:00
|
|
|
changeUnreadCounter(user: user, by: 1)
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
func decreaseUnreadCounter(user: any UserLike, chat: Chat) {
|
|
|
|
let by = chat.chatInfo.chatSettings?.enableNtfs == .mentions
|
|
|
|
? chat.chatStats.unreadMentions
|
|
|
|
: chat.chatStats.unreadCount
|
|
|
|
decreaseUnreadCounter(user: user, by: by)
|
|
|
|
}
|
|
|
|
|
2023-08-22 12:20:21 +01:00
|
|
|
func decreaseUnreadCounter(user: any UserLike, by: Int = 1) {
|
2023-01-19 16:22:56 +00:00
|
|
|
changeUnreadCounter(user: user, by: -by)
|
|
|
|
}
|
|
|
|
|
2023-08-22 12:20:21 +01:00
|
|
|
private func changeUnreadCounter(user: any UserLike, by: Int) {
|
|
|
|
if let i = users.firstIndex(where: { $0.user.userId == user.userId }) {
|
2023-01-23 13:20:58 +00:00
|
|
|
users[i].unreadCount += by
|
2022-12-07 20:46:38 +04:00
|
|
|
}
|
2024-08-06 01:36:33 +03:00
|
|
|
NtfManager.shared.changeNtfBadgeCount(by: by)
|
2022-12-07 20:46:38 +04:00
|
|
|
}
|
|
|
|
|
2023-01-23 13:20:58 +00:00
|
|
|
func totalUnreadCountForAllUsers() -> Int {
|
2025-02-03 20:47:32 +00:00
|
|
|
var unread: Int = 0
|
|
|
|
for chat in chats {
|
|
|
|
switch chat.chatInfo.chatSettings?.enableNtfs {
|
|
|
|
case .all: unread += chat.chatStats.unreadCount
|
|
|
|
case .mentions: unread += chat.chatStats.unreadMentions
|
|
|
|
default: ()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for u in users {
|
|
|
|
if !u.user.activeUser {
|
|
|
|
unread += u.unreadCount
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return unread
|
2022-07-20 08:58:53 +01:00
|
|
|
}
|
|
|
|
|
2025-01-11 17:39:39 +07:00
|
|
|
func increaseGroupReportsCounter(_ chatId: ChatId) {
|
|
|
|
changeGroupReportsCounter(chatId, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func decreaseGroupReportsCounter(_ chatId: ChatId, by: Int = 1) {
|
2025-02-10 22:07:14 +07:00
|
|
|
changeGroupReportsCounter(chatId, -by)
|
2025-01-11 17:39:39 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
private func changeGroupReportsCounter(_ chatId: ChatId, _ by: Int = 0) {
|
|
|
|
if by == 0 { return }
|
|
|
|
|
|
|
|
if let i = getChatIndex(chatId) {
|
|
|
|
let chat = chats[i]
|
|
|
|
let wasReportsCount = chat.chatStats.reportsCount
|
|
|
|
chat.chatStats.reportsCount = max(0, chat.chatStats.reportsCount + by)
|
|
|
|
let nowReportsCount = chat.chatStats.reportsCount
|
|
|
|
let by = wasReportsCount == 0 && nowReportsCount > 0 ? 1 : (wasReportsCount > 0 && nowReportsCount == 0) ? -1 : 0
|
|
|
|
ChatTagsModel.shared.changeGroupReportsTag(by)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-31 09:44:57 +00:00
|
|
|
// this function analyses "connected" events and assumes that each member will be there only once
|
|
|
|
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String]) {
|
|
|
|
var count = 0
|
2023-08-15 13:02:23 +01:00
|
|
|
var ns: [String] = []
|
2023-10-31 09:44:57 +00:00
|
|
|
if let ciCategory = chatItem.mergeCategory,
|
|
|
|
var i = getChatItemIndex(chatItem) {
|
2024-07-29 23:17:14 +03:00
|
|
|
while i < im.reversedChatItems.count {
|
|
|
|
let ci = im.reversedChatItems[i]
|
2023-10-31 09:44:57 +00:00
|
|
|
if ci.mergeCategory != ciCategory { break }
|
|
|
|
if let m = ci.memberConnected {
|
|
|
|
ns.append(m.displayName)
|
|
|
|
}
|
|
|
|
count += 1
|
|
|
|
i += 1
|
|
|
|
}
|
2023-08-15 13:02:23 +01:00
|
|
|
}
|
2023-10-31 09:44:57 +00:00
|
|
|
return (count, ns)
|
2023-08-15 13:02:23 +01:00
|
|
|
}
|
|
|
|
|
2023-10-31 09:44:57 +00:00
|
|
|
// returns the index of the passed item and the next item (it has smaller index)
|
|
|
|
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) {
|
|
|
|
if let i = getChatItemIndex(ci) {
|
2024-07-29 23:17:14 +03:00
|
|
|
(i, i > 0 ? im.reversedChatItems[i - 1] : nil)
|
2022-03-30 08:57:42 +01:00
|
|
|
} else {
|
2023-10-31 09:44:57 +00:00
|
|
|
(nil, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns the index of the first item in the same merged group (the first hidden item)
|
|
|
|
// and the previous visible item with another merge category
|
|
|
|
func getPrevShownChatItem(_ ciIndex: Int?, _ ciCategory: CIMergeCategory?) -> (Int?, ChatItem?) {
|
|
|
|
guard var i = ciIndex else { return (nil, nil) }
|
2024-07-29 23:17:14 +03:00
|
|
|
let fst = im.reversedChatItems.count - 1
|
2023-10-31 09:44:57 +00:00
|
|
|
while i < fst {
|
|
|
|
i = i + 1
|
2024-07-29 23:17:14 +03:00
|
|
|
let ci = im.reversedChatItems[i]
|
2023-10-31 09:44:57 +00:00
|
|
|
if ciCategory == nil || ciCategory != ci.mergeCategory {
|
|
|
|
return (i - 1, ci)
|
|
|
|
}
|
2023-08-14 17:34:22 +04:00
|
|
|
}
|
2023-10-31 09:44:57 +00:00
|
|
|
return (i, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns the previous member in the same merge group and the count of members in this group
|
|
|
|
func getPrevHiddenMember(_ member: GroupMember, _ range: ClosedRange<Int>) -> (GroupMember?, Int) {
|
2025-02-18 01:21:40 +07:00
|
|
|
let items = im.reversedChatItems
|
2023-10-31 09:44:57 +00:00
|
|
|
var prevMember: GroupMember? = nil
|
|
|
|
var memberIds: Set<Int64> = []
|
|
|
|
for i in range {
|
2025-02-18 01:21:40 +07:00
|
|
|
if i < items.count {
|
|
|
|
if case let .groupRcv(m) = items[i].chatDir {
|
|
|
|
if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m }
|
|
|
|
memberIds.insert(m.groupMemberId)
|
|
|
|
}
|
|
|
|
} else {
|
2025-02-20 17:39:04 +07:00
|
|
|
logger.error("getPrevHiddenMember: index >= count of reversed items: \(i) vs \(items.count), range: \(String(describing: range))")
|
2023-10-31 09:44:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return (prevMember, memberIds.count)
|
2023-08-14 17:34:22 +04:00
|
|
|
}
|
|
|
|
|
2022-02-12 15:59:43 +00:00
|
|
|
func popChat(_ id: String) {
|
|
|
|
if let i = getChatIndex(id) {
|
2024-08-10 14:04:37 +01:00
|
|
|
// no animation here, for it not to look like it just moved when leaving the chat
|
2022-02-12 15:59:43 +00:00
|
|
|
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) {
|
2024-11-26 14:43:39 +04:00
|
|
|
if id == showingInvitation?.pcc.id {
|
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
|
|
|
markShowingInvitationUsed()
|
2022-10-01 10:57:18 +01:00
|
|
|
dismissAllSheets()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
|
|
|
func markShowingInvitationUsed() {
|
|
|
|
showingInvitation?.connChatUsed = true
|
|
|
|
}
|
|
|
|
|
2022-02-02 12:51:39 +00:00
|
|
|
func removeChat(_ id: String) {
|
2022-02-02 16:46:05 +00:00
|
|
|
withAnimation {
|
2024-12-20 11:43:11 +00:00
|
|
|
if let i = getChatIndex(id) {
|
|
|
|
let removed = chats.remove(at: i)
|
2025-01-11 17:39:39 +07:00
|
|
|
ChatTagsModel.shared.removePresetChatTags(removed.chatInfo, removed.chatStats)
|
2025-01-13 17:42:14 +00:00
|
|
|
removeWallpaperFilesFromChat(removed)
|
2024-12-20 11:43:11 +00:00
|
|
|
}
|
2022-02-02 16:46:05 +00:00
|
|
|
}
|
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 {
|
2024-07-10 16:15:14 +04:00
|
|
|
if let i = groupMembersIndexes[member.groupMemberId] {
|
2022-08-09 13:43:19 +04:00
|
|
|
withAnimation(.default) {
|
2023-10-31 09:44:57 +00:00
|
|
|
self.groupMembers[i].wrapped = member
|
|
|
|
self.groupMembers[i].created = Date.now
|
2022-08-09 13:43:19 +04:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
} else {
|
2024-07-10 16:15:14 +04:00
|
|
|
withAnimation {
|
|
|
|
groupMembers.append(GMember(member))
|
|
|
|
groupMembersIndexes[member.groupMemberId] = groupMembers.count - 1
|
|
|
|
}
|
2022-08-09 13:43:19 +04:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 13:13:29 +01:00
|
|
|
|
2023-07-10 19:01:22 +04:00
|
|
|
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
|
2023-10-31 09:44:57 +00:00
|
|
|
if var conn = member.activeConn {
|
|
|
|
conn.connectionStats = connectionStats
|
2023-07-10 19:01:22 +04:00
|
|
|
var updatedMember = member
|
2023-10-31 09:44:57 +00:00
|
|
|
updatedMember.activeConn = conn
|
2023-07-10 19:01:22 +04:00
|
|
|
_ = upsertGroupMember(groupInfo, updatedMember)
|
|
|
|
}
|
|
|
|
}
|
2025-01-13 23:40:07 +07:00
|
|
|
|
|
|
|
func removeWallpaperFilesFromChat(_ chat: Chat) {
|
|
|
|
if case let .direct(contact) = chat.chatInfo {
|
|
|
|
removeWallpaperFilesFromTheme(contact.uiThemes)
|
|
|
|
} else if case let .group(groupInfo) = chat.chatInfo {
|
|
|
|
removeWallpaperFilesFromTheme(groupInfo.uiThemes)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeWallpaperFilesFromAllChats(_ user: User) {
|
|
|
|
// Currently, only removing everything from currently active user is supported. Inactive users are TODO
|
|
|
|
if user.userId == currentUser?.userId {
|
|
|
|
chats.forEach {
|
|
|
|
removeWallpaperFilesFromChat($0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 13:13:29 +01:00
|
|
|
}
|
|
|
|
|
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
|
|
|
struct ShowingInvitation {
|
2024-11-26 14:43:39 +04:00
|
|
|
var pcc: PendingContactConnection
|
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
|
|
|
var connChatUsed: Bool
|
|
|
|
}
|
|
|
|
|
2023-08-08 17:26:56 +04:00
|
|
|
struct NTFContactRequest {
|
|
|
|
var incognito: Bool
|
|
|
|
var chatId: String
|
|
|
|
}
|
|
|
|
|
2024-07-28 17:54:58 +01:00
|
|
|
final class Chat: ObservableObject, Identifiable, ChatLike {
|
2022-02-02 12:51:39 +00:00
|
|
|
@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
|
2024-08-10 14:04:37 +01:00
|
|
|
fileprivate var popTs: Date?
|
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
|
|
|
|
2023-06-19 11:13:30 +01:00
|
|
|
func copy(chatInfo: ChatInfo? = nil, chatItems: [ChatItem]? = nil, chatStats: ChatStats? = nil) -> Chat {
|
|
|
|
Chat(
|
|
|
|
chatInfo: chatInfo ?? self.chatInfo,
|
|
|
|
chatItems: chatItems ?? self.chatItems,
|
|
|
|
chatStats: chatStats ?? self.chatStats
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-06 13:54:43 +00:00
|
|
|
var userCanSend: Bool {
|
|
|
|
switch chatInfo {
|
|
|
|
case .direct: return true
|
|
|
|
case let .group(groupInfo):
|
|
|
|
let m = groupInfo.membership
|
|
|
|
return m.memberActive && m.memberRole >= .member
|
2024-01-18 22:57:14 +07:00
|
|
|
case .local:
|
|
|
|
return true
|
2023-03-06 13:54:43 +00:00
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-16 23:29:47 +00:00
|
|
|
var userIsObserver: Bool {
|
|
|
|
switch chatInfo {
|
|
|
|
case let .group(groupInfo):
|
|
|
|
let m = groupInfo.membership
|
|
|
|
return m.memberActive && m.memberRole == .observer
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-23 16:31:47 +00:00
|
|
|
var unreadTag: Bool {
|
2025-02-03 20:47:32 +00:00
|
|
|
switch chatInfo.chatSettings?.enableNtfs {
|
|
|
|
case .all: chatStats.unreadChat || chatStats.unreadCount > 0
|
|
|
|
case .mentions: chatStats.unreadChat || chatStats.unreadMentions > 0
|
|
|
|
default: chatStats.unreadChat
|
|
|
|
}
|
2024-12-20 11:43:11 +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-10-31 09:44:57 +00:00
|
|
|
|
|
|
|
final class GMember: ObservableObject, Identifiable {
|
|
|
|
@Published var wrapped: GroupMember
|
|
|
|
var created = Date.now
|
|
|
|
|
|
|
|
init(_ member: GroupMember) {
|
|
|
|
self.wrapped = member
|
|
|
|
}
|
|
|
|
|
|
|
|
var id: String { wrapped.id }
|
|
|
|
var groupId: Int64 { wrapped.groupId }
|
|
|
|
var groupMemberId: Int64 { wrapped.groupMemberId }
|
|
|
|
var displayName: String { wrapped.displayName }
|
|
|
|
var viewId: String { get { "\(wrapped.id) \(created.timeIntervalSince1970)" } }
|
|
|
|
static let sampleData = GMember(GroupMember.sampleData)
|
|
|
|
}
|
2023-11-16 16:53:44 +00:00
|
|
|
|
|
|
|
struct RemoteCtrlSession {
|
2023-11-23 21:22:29 +00:00
|
|
|
var ctrlAppInfo: CtrlAppInfo?
|
2023-11-16 16:53:44 +00:00
|
|
|
var appVersion: String
|
|
|
|
var sessionState: UIRemoteCtrlSessionState
|
|
|
|
|
|
|
|
func updateState(_ state: UIRemoteCtrlSessionState) -> RemoteCtrlSession {
|
|
|
|
RemoteCtrlSession(ctrlAppInfo: ctrlAppInfo, appVersion: appVersion, sessionState: state)
|
|
|
|
}
|
|
|
|
|
|
|
|
var active: Bool {
|
|
|
|
if case .connected = sessionState { true } else { false }
|
|
|
|
}
|
|
|
|
|
2023-11-23 21:22:29 +00:00
|
|
|
var discovery: Bool {
|
|
|
|
if case .searching = sessionState { true } else { false }
|
|
|
|
}
|
|
|
|
|
2023-11-16 16:53:44 +00:00
|
|
|
var sessionCode: String? {
|
|
|
|
switch sessionState {
|
|
|
|
case let .pendingConfirmation(_, sessionCode): sessionCode
|
|
|
|
case let .connected(_, sessionCode): sessionCode
|
|
|
|
default: nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum UIRemoteCtrlSessionState {
|
|
|
|
case starting
|
2023-11-23 21:22:29 +00:00
|
|
|
case searching
|
|
|
|
case found(remoteCtrl: RemoteCtrlInfo, compatible: Bool)
|
2023-11-16 16:53:44 +00:00
|
|
|
case connecting(remoteCtrl_: RemoteCtrlInfo?)
|
|
|
|
case pendingConfirmation(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
|
|
|
case connected(remoteCtrl: RemoteCtrlInfo, sessionCode: String)
|
|
|
|
}
|