import Foundation import Combine @MainActor public final class SessionStore: ObservableObject { @Published public private(set) var account: Account @Published public private(set) var authState: AuthState = .waitTdlibParameters @Published public private(set) var networkState: NetworkState = .ready @Published public private(set) var typingState: TypingState = .idle @Published public private(set) var errorMessage: String? public let bridge: SessionBridge public init(account: Account, bridge: SessionBridge) { self.account = account self.bridge = bridge } public func refreshAuthState() async { do { authState = try await bridge.authState() errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func refreshNetworkState() async { do { networkState = try await bridge.networkState() errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func apply(events: [SessionEvent]) { for event in events { switch event { case let .authChanged(state): authState = state case let .networkChanged(state): networkState = state case let .typingChanged(state): typingState = state default: break } } } } @MainActor public final class AuthViewModel: ObservableObject { @Published public var phone = "" @Published public var code = "" @Published public var password = "" @Published public private(set) var isLoading = false @Published public private(set) var errorMessage: String? private let store: SessionStore public init(store: SessionStore) { self.store = store } public func submitCurrentStep() async { isLoading = true defer { isLoading = false } do { switch store.authState { case .waitPhoneNumber: try await store.bridge.sendPhoneNumber(phone) case .waitCode: try await store.bridge.sendCode(code) case .waitPassword: try await store.bridge.sendPassword(password) default: break } let events = try await store.bridge.pollEvents() store.apply(events: events) errorMessage = nil } catch { errorMessage = error.localizedDescription } } } @MainActor public final class ChatListViewModel: ObservableObject { @Published public private(set) var folders: [Folder] = [] @Published public private(set) var chats: [ChatSummary] = [] @Published public var selectedFolderId: Int32? @Published public var searchText = "" @Published public private(set) var isLoading = false @Published public private(set) var errorMessage: String? private let bridge: SessionBridge public init(bridge: SessionBridge) { self.bridge = bridge } public var filteredChats: [ChatSummary] { guard !searchText.isEmpty else { return chats } return chats.filter { chat in chat.title.localizedCaseInsensitiveContains(searchText) || (chat.username?.localizedCaseInsensitiveContains(searchText) ?? false) } } public func load() async { isLoading = true defer { isLoading = false } do { folders = try await bridge.loadFolders() chats = try await bridge.loadChats(folderId: selectedFolderId) errorMessage = nil } catch { errorMessage = error.localizedDescription } } } @MainActor public final class ChatViewModel: ObservableObject { @Published public private(set) var chat: ChatSummary @Published public private(set) var messages: [Message] = [] @Published public var composeText: String @Published public var replyTo: Message? @Published public var searchText = "" @Published public private(set) var searchResults: [Message] = [] @Published public private(set) var pinnedMessages: [Message] = [] @Published public private(set) var copiedPayload: String? @Published public private(set) var isLoading = false @Published public private(set) var errorMessage: String? private let bridge: SessionBridge public init(chat: ChatSummary, bridge: SessionBridge) { self.chat = chat self.bridge = bridge self.composeText = chat.draft?.text ?? "" } public func load() async { isLoading = true defer { isLoading = false } do { messages = try await bridge.loadHistory(chatId: chat.id) pinnedMessages = try await bridge.pinnedMessages(chatId: chat.id) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func send() async { let text = composeText.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return } do { let sent = try await bridge.sendMessage( chatId: chat.id, text: text, replyToMessageId: replyTo?.id ) messages.append(sent) composeText = "" replyTo = nil errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func search() async { do { searchResults = try await bridge.searchMessages(chatId: chat.id, query: searchText) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func beginReply(to message: Message) { replyTo = message } public func cancelReply() { replyTo = nil } public func edit(message: Message, text: String) async { do { let edited = try await bridge.editMessage(chatId: chat.id, messageId: message.id, text: text) replaceMessage(edited) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func delete(message: Message) async { do { try await bridge.deleteMessages(chatId: chat.id, messageIds: [message.id]) messages.removeAll { $0.id == message.id } errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func forward(message: Message, to chatId: Int64) async { do { try await bridge.forwardMessages(toChatId: chatId, fromChatId: chat.id, messageIds: [message.id]) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func react(message: Message, reaction: String) async { do { let reactions = try await bridge.react(chatId: chat.id, messageId: message.id, reaction: reaction) var updated = message updated.reactions = reactions replaceMessage(updated) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func copyPayload(for message: Message) async { do { copiedPayload = try await bridge.copyPayload(chatId: chat.id, messageId: message.id) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func saveDraft() async { do { try await bridge.setDraft(chatId: chat.id, text: composeText) errorMessage = nil } catch { errorMessage = error.localizedDescription } } private func replaceMessage(_ message: Message) { if let index = messages.firstIndex(where: { $0.id == message.id }) { messages[index] = message } } } @MainActor public final class ProfileViewModel: ObservableObject { @Published public private(set) var profile: Profile? @Published public private(set) var isLoading = false @Published public private(set) var errorMessage: String? private let bridge: SessionBridge public init(bridge: SessionBridge) { self.bridge = bridge } public func load(chatId: Int64) async { isLoading = true defer { isLoading = false } do { profile = try await bridge.openProfile(chatId: chatId) errorMessage = nil } catch { errorMessage = error.localizedDescription } } public func leave(chatId: Int64) async { do { try await bridge.leaveChat(chatId: chatId) errorMessage = nil } catch { errorMessage = error.localizedDescription } } } @MainActor public final class MediaViewModel: ObservableObject { @Published public private(set) var activePhotoPath: String? @Published public private(set) var activeVoicePath: String? @Published public private(set) var isVoicePlaying = false private let cache: MediaCache? private let voicePlayer: VoicePlayback? public init(cache: MediaCache? = nil, voicePlayer: VoicePlayback? = nil) { self.cache = cache self.voicePlayer = voicePlayer } public func showPhoto(path: String) { activePhotoPath = path } public func showVoice(path: String) { activeVoicePath = path } public func cachedPhotoPath(fileId: Int32) -> URL? { cache?.photoPath(fileId: fileId) } public func cachedVoicePath(fileId: Int32) -> URL? { cache?.voicePath(fileId: fileId) } public func playVoice(url: URL) async { do { try await voicePlayer?.load(url: url) await voicePlayer?.play() isVoicePlaying = true } catch { isVoicePlaying = false } } public func pauseVoice() async { await voicePlayer?.pause() isVoicePlaying = false } }