import SwiftUI public struct RootView: View { @StateObject private var store: SessionStore @StateObject private var authViewModel: AuthViewModel @StateObject private var chatListViewModel: ChatListViewModel public init(store: SessionStore) { let authViewModel = AuthViewModel(store: store) let chatListViewModel = ChatListViewModel(bridge: store.bridge) _store = StateObject(wrappedValue: store) _authViewModel = StateObject(wrappedValue: authViewModel) _chatListViewModel = StateObject(wrappedValue: chatListViewModel) } public var body: some View { Group { switch store.authState { case .ready: ChatListView(viewModel: chatListViewModel, bridge: store.bridge) default: AuthView(state: store.authState, viewModel: authViewModel) } } .task { await store.refreshAuthState() } } } public struct AuthView: View { public var state: AuthState @ObservedObject public var viewModel: AuthViewModel public init(state: AuthState, viewModel: AuthViewModel) { self.state = state self.viewModel = viewModel } public var body: some View { VStack(spacing: 16) { Text("Telegram") .font(.largeTitle) .fontWeight(.semibold) authField Button(action: { Task { await viewModel.submitCurrentStep() } }) { Text(buttonTitle) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(viewModel.isLoading || !canSubmit) if let errorMessage = viewModel.errorMessage { Text(errorMessage) .font(.footnote) .foregroundStyle(.red) } } .padding() } @ViewBuilder private var authField: some View { switch state { case .waitPhoneNumber, .waitTdlibParameters: TextField("Phone number", text: $viewModel.phone) .textContentType(.telephoneNumber) .textFieldStyle(.roundedBorder) case .waitCode: TextField("Code", text: $viewModel.code) .textContentType(.oneTimeCode) .textFieldStyle(.roundedBorder) case .waitPassword: SecureField("Password", text: $viewModel.password) .textContentType(.password) .textFieldStyle(.roundedBorder) case .ready: Text("Ready") case .closed: Text("Session closed") case let .error(message): Text(message) .foregroundStyle(.red) } } private var buttonTitle: String { switch state { case .waitPhoneNumber, .waitTdlibParameters: "Continue" case .waitCode: "Verify" case .waitPassword: "Unlock" default: "Continue" } } private var canSubmit: Bool { switch state { case .waitPhoneNumber, .waitTdlibParameters: !viewModel.phone.isEmpty case .waitCode: !viewModel.code.isEmpty case .waitPassword: !viewModel.password.isEmpty default: false } } } public struct ChatListView: View { @ObservedObject public var viewModel: ChatListViewModel public let bridge: SessionBridge @State private var selectedChat: ChatSummary? @State private var showsAccountSwitcher = false public init(viewModel: ChatListViewModel, bridge: SessionBridge) { self.viewModel = viewModel self.bridge = bridge } public var body: some View { NavigationSplitView { List(selection: $selectedChat) { ForEach(viewModel.filteredChats) { chat in NavigationLink(value: chat) { ChatRow(chat: chat) } } } .navigationTitle("Chats") .searchable(text: $viewModel.searchText) .toolbar { ToolbarItem { Button("Accounts") { showsAccountSwitcher = true } } ToolbarItem { folderMenu } } .sheet(isPresented: $showsAccountSwitcher) { AccountSwitcherView() } .task { await viewModel.load() } } detail: { if let selectedChat { ChatDetailView(viewModel: ChatViewModel(chat: selectedChat, bridge: bridge), bridge: bridge) } else { Text("Select a chat") } } } private var folderMenu: some View { Menu("Folders") { Button("All") { viewModel.selectedFolderId = nil Task { await viewModel.load() } } ForEach(viewModel.folders) { folder in Button(folder.name) { viewModel.selectedFolderId = folder.id Task { await viewModel.load() } } } } } } public struct ChatRow: View { public var chat: ChatSummary public init(chat: ChatSummary) { self.chat = chat } public var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { Text(chat.title) .font(.headline) .lineLimit(1) if chat.isPinned { Image(systemName: "pin.fill") .font(.caption) } Spacer() if chat.unreadCount > 0 { Text("\(chat.unreadCount)") .font(.caption) .padding(.horizontal, 7) .padding(.vertical, 3) .background(.blue, in: Capsule()) .foregroundStyle(.white) } } Text(chat.draft?.text ?? chat.lastMessage) .font(.subheadline) .foregroundStyle(chat.draft == nil ? Color.secondary : Color.red) .lineLimit(2) } .padding(.vertical, 4) } } public struct ChatDetailView: View { @StateObject public var viewModel: ChatViewModel public let bridge: SessionBridge @StateObject private var profileViewModel: ProfileViewModel @State private var showsProfile = false public init(viewModel: ChatViewModel, bridge: SessionBridge) { _viewModel = StateObject(wrappedValue: viewModel) self.bridge = bridge _profileViewModel = StateObject(wrappedValue: ProfileViewModel(bridge: bridge)) } public var body: some View { VStack(spacing: 0) { List(viewModel.messages) { message in MessageRow(message: message) .listRowSeparator(.hidden) } ComposeBar(text: $viewModel.composeText) { Task { await viewModel.send() } } } .navigationTitle(viewModel.chat.title) .toolbar { Button("Profile") { showsProfile = true Task { await profileViewModel.load(chatId: viewModel.chat.id) } } } .sheet(isPresented: $showsProfile) { ProfileView(viewModel: profileViewModel) } .task { await viewModel.load() } } } public struct MessageRow: View { public var message: Message public init(message: Message) { self.message = message } public var body: some View { HStack { if message.isOutgoing { Spacer(minLength: 48) } VStack(alignment: .leading, spacing: 5) { if !message.isOutgoing { Text(message.senderName) .font(.caption) .foregroundStyle(.secondary) } if let replyText = message.replyText { Text(replyText) .font(.caption) .foregroundStyle(.secondary) .padding(.leading, 6) } Text(message.text) .textSelection(.enabled) if !message.reactions.isEmpty { Text(message.reactions.map(\.emoji).joined(separator: " ")) .font(.caption) } } .padding(10) .background(message.isOutgoing ? Color.blue.opacity(0.16) : Color.gray.opacity(0.12), in: RoundedRectangle(cornerRadius: 8)) if !message.isOutgoing { Spacer(minLength: 48) } } } } public struct ComposeBar: View { @Binding public var text: String public var send: () -> Void public init(text: Binding, send: @escaping () -> Void) { _text = text self.send = send } public var body: some View { HStack(spacing: 10) { TextField("Message", text: $text, axis: .vertical) .textFieldStyle(.roundedBorder) .lineLimit(1...5) Button(action: send) { Image(systemName: "paperplane.fill") } .buttonStyle(.borderedProminent) .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } .padding() .background(.bar) } } public struct ProfileView: View { @ObservedObject public var viewModel: ProfileViewModel public init(viewModel: ProfileViewModel) { self.viewModel = viewModel } public var body: some View { NavigationStack { List { if let profile = viewModel.profile { Section { Text(profile.title) .font(.title2) if let username = profile.username { Text("@\(username)") .foregroundStyle(.secondary) } if let bio = profile.bio { Text(bio) } } if let memberCount = profile.memberCount { Section { Text("\(memberCount) members") } } } else { ProgressView() } } .navigationTitle("Profile") } } } public struct AccountSwitcherView: View { public init() {} public var body: some View { NavigationStack { List { Section { Label("Default", systemImage: "person.crop.circle") Label("Add account", systemImage: "plus.circle") } } .navigationTitle("Accounts") } } }