From 161cc343da9b6a7094b41f8d35417cad74388559 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Thu, 21 May 2026 00:15:50 +0300 Subject: [PATCH] Add Swift UniFFI session bridge adapter --- .../Sources/TeleTuiIOSCore/Models.swift | 9 + .../TeleTuiIOSCore/UniFfiSessionBridge.swift | 227 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift index 854b4ff..25ae7d2 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Models.swift @@ -156,12 +156,21 @@ public struct Profile: Equatable, Sendable { public enum SessionEvent: Equatable, Sendable { case authChanged(AuthState) case chatListChanged([ChatSummary]) + case folderListChanged([Folder]) case messageAdded(Int64, Message) + case messageUpdated(Int64, Message) + case messageDeleted(Int64, [Int64]) + case reactionChanged(Int64, Int64, [Reaction]) + case incomingNotificationCandidate(ChatSummary, Message, String) + case networkChanged(NetworkState) + case draftChanged(Draft) case profileLoaded(Profile) + case mediaDownloadProgress(fileId: Int32, downloadedSize: Int64, totalSize: Int64) } public enum NetworkState: Equatable, Sendable { case waitingForNetwork + case connectingToProxy case connecting case updating case ready diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift new file mode 100644 index 0000000..3fe7793 --- /dev/null +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/UniFfiSessionBridge.swift @@ -0,0 +1,227 @@ +import Foundation + +#if canImport(tele_ios_ffiFFI) +import tele_ios_ffiFFI + +public actor UniFfiSessionBridge: SessionBridge { + private let handle: SessionHandle + private let defaultLimit: Int32 + + public init(account: Account, useFakeTdlib: Bool = true, defaultLimit: Int32 = 100) throws { + self.handle = try createSession(config: IosSessionConfig( + accountId: account.id, + displayName: account.displayName, + databasePath: account.databasePath.path, + useFakeTdlib: useFakeTdlib + )) + self.defaultLimit = defaultLimit + } + + public init(handle: SessionHandle, defaultLimit: Int32 = 100) { + self.handle = handle + self.defaultLimit = defaultLimit + } + + public func authState() async throws -> AuthState { + Self.mapAuthState(handle.authState()) + } + + public func pollEvents() async throws -> [SessionEvent] { + handle.pollEvents().map(Self.mapEvent) + } + + public func sendPhoneNumber(_ phone: String) async throws { + try handle.sendPhoneNumber(phone: phone) + } + + public func sendCode(_ code: String) async throws { + try handle.sendCode(code: code) + } + + public func sendPassword(_ password: String) async throws { + try handle.sendPassword(password: password) + } + + public func loadFolders() async throws -> [Folder] { + handle.loadFolders().map(Self.mapFolder) + } + + public func loadChats(folderId: Int32?) async throws -> [ChatSummary] { + let chats = if let folderId { + try handle.loadFolderChats(folderId: folderId, limit: defaultLimit) + } else { + try handle.loadChats(limit: defaultLimit) + } + return chats.map(Self.mapChatSummary) + } + + public func loadHistory(chatId: Int64) async throws -> [Message] { + try handle.loadHistory(chatId: chatId, limit: defaultLimit) + .map { Self.mapMessage($0, chatId: chatId) } + } + + public func searchMessages(chatId: Int64, query: String) async throws -> [Message] { + try handle.searchMessages(chatId: chatId, query: query) + .map { Self.mapMessage($0.message, chatId: $0.chatId) } + } + + public func openProfile(chatId: Int64) async throws -> Profile { + try Self.mapProfile(handle.openProfile(chatId: chatId)) + } + + public func sendMessage(chatId: Int64, text: String, replyToMessageId: Int64?) async throws -> Message { + try Self.mapMessage( + handle.sendMessage(chatId: chatId, text: text, replyToMessageId: replyToMessageId), + chatId: chatId + ) + } + + public func editMessage(chatId: Int64, messageId: Int64, text: String) async throws -> Message { + try Self.mapMessage( + handle.editMessage(chatId: chatId, messageId: messageId, text: text), + chatId: chatId + ) + } + + public func deleteMessages(chatId: Int64, messageIds: [Int64]) async throws { + try handle.deleteMessages(chatId: chatId, messageIds: messageIds, revoke: true) + } + + public func forwardMessages(toChatId: Int64, fromChatId: Int64, messageIds: [Int64]) async throws { + try handle.forwardMessages(toChatId: toChatId, fromChatId: fromChatId, messageIds: messageIds) + } + + public func react(chatId: Int64, messageId: Int64, reaction: String) async throws -> [Reaction] { + try handle.react(chatId: chatId, messageId: messageId, reaction: reaction) + .map(Self.mapReaction) + } + + public func pinnedMessages(chatId: Int64) async throws -> [Message] { + [] + } + + public func copyPayload(chatId: Int64, messageId: Int64) async throws -> String { + try handle.copyPayload(chatId: chatId, messageId: messageId) + } + + private static func mapAuthState(_ state: IosAuthState) -> AuthState { + switch state { + case .waitTdlibParameters: + .waitTdlibParameters + case .waitPhoneNumber: + .waitPhoneNumber + case .waitCode: + .waitCode + case .waitPassword: + .waitPassword + case .ready: + .ready + case .closed: + .closed + case let .error(message): + .error(message) + } + } + + private static func mapNetworkState(_ state: IosNetworkState) -> NetworkState { + switch state { + case .waitingForNetwork: + .waitingForNetwork + case .connectingToProxy: + .connectingToProxy + case .connecting: + .connecting + case .updating: + .updating + case .ready: + .ready + } + } + + private static func mapFolder(_ folder: IosFolder) -> Folder { + Folder(id: folder.id, name: folder.name) + } + + private static func mapDraft(_ draft: IosDraft) -> Draft { + Draft(chatId: draft.chatId, text: draft.text) + } + + private static func mapChatSummary(_ chat: IosChatSummary) -> ChatSummary { + ChatSummary( + id: chat.id, + title: chat.title, + username: chat.username, + lastMessage: chat.lastMessage, + unreadCount: chat.unreadCount, + unreadMentionCount: chat.unreadMentionCount, + isPinned: chat.isPinned, + folderIds: chat.folderIds, + isMuted: chat.isMuted, + draft: chat.draft.map(mapDraft) + ) + } + + private static func mapReaction(_ reaction: IosReaction) -> Reaction { + Reaction(emoji: reaction.emoji, count: reaction.count, isChosen: reaction.isChosen) + } + + private static func mapMessage(_ message: IosMessage, chatId: Int64) -> Message { + Message( + id: message.id, + chatId: chatId, + senderName: message.senderName, + text: message.text, + isOutgoing: message.isOutgoing, + isRead: message.isRead, + editDate: message.editDate, + replyText: message.reply?.text, + forwardSenderName: message.forward?.senderName, + reactions: message.reactions.map(mapReaction) + ) + } + + private static func mapProfile(_ profile: IosProfile) -> Profile { + Profile( + chatId: profile.chatId, + title: profile.title, + username: profile.username, + bio: profile.bio ?? profile.description, + isGroup: profile.isGroup, + memberCount: profile.memberCount + ) + } + + private static func mapEvent(_ event: IosEvent) -> SessionEvent { + switch event { + case let .authChanged(state): + .authChanged(mapAuthState(state)) + case let .chatListChanged(chats): + .chatListChanged(chats.map(mapChatSummary)) + case let .folderListChanged(folders): + .folderListChanged(folders.map(mapFolder)) + case let .messageAdded(chatId, message): + .messageAdded(chatId, mapMessage(message, chatId: chatId)) + case let .messageUpdated(chatId, message): + .messageUpdated(chatId, mapMessage(message, chatId: chatId)) + case let .messageDeleted(chatId, messageIds): + .messageDeleted(chatId, messageIds) + case let .reactionChanged(chatId, messageId, reactions): + .reactionChanged(chatId, messageId, reactions.map(mapReaction)) + case let .incomingNotificationCandidate(chat, message, senderName): + .incomingNotificationCandidate( + mapChatSummary(chat), + mapMessage(message, chatId: chat.id), + senderName + ) + case let .networkChanged(state): + .networkChanged(mapNetworkState(state)) + case let .draftChanged(draft): + .draftChanged(mapDraft(draft)) + case let .profileLoaded(profile): + .profileLoaded(mapProfile(profile)) + case let .mediaDownloadProgress(fileId, downloadedSize, totalSize): + .mediaDownloadProgress(fileId: fileId, downloadedSize: downloadedSize, totalSize: totalSize) + } + } +} +#endif