347 lines
10 KiB
Swift
347 lines
10 KiB
Swift
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
|
|
}
|
|
}
|