diff --git a/yobble/Services/IncomingMessageCenter.swift b/yobble/Services/IncomingMessageCenter.swift index 9b27e48..170109d 100644 --- a/yobble/Services/IncomingMessageCenter.swift +++ b/yobble/Services/IncomingMessageCenter.swift @@ -4,6 +4,7 @@ import Combine final class IncomingMessageCenter: ObservableObject { @Published private(set) var banner: IncomingMessageBanner? @Published var presentedChat: PrivateChatListItem? + @Published var pendingNavigation: ChatNavigationTarget? var currentUserId: String? var activeChatId: String? @@ -31,7 +32,14 @@ final class IncomingMessageCenter: ObservableObject { func openCurrentChat() { guard let banner else { return } activeChatId = banner.message.chatId - presentedChat = makeChatItem(from: banner.message) + let chatItem = makeChatItem(from: banner.message) + if AppConfig.PRESENT_CHAT_AS_SHEET { + presentedChat = chatItem + pendingNavigation = nil + } else { + pendingNavigation = ChatNavigationTarget(chat: chatItem) + presentedChat = nil + } dismissBanner() } @@ -44,7 +52,9 @@ final class IncomingMessageCenter: ObservableObject { return } - if let presentedChat, presentedChat.chatId == message.chatId { + if AppConfig.PRESENT_CHAT_AS_SHEET, + let presentedChat, + presentedChat.chatId == message.chatId { return } @@ -112,4 +122,9 @@ final class IncomingMessageCenter: ObservableObject { dismissWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } + + struct ChatNavigationTarget: Identifiable { + let id = UUID() + let chat: PrivateChatListItem + } } diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index b71710d..b188026 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -12,6 +12,7 @@ import UIKit struct ChatsTab: View { @ObservedObject private var loginViewModel: LoginViewModel + @Binding private var pendingNavigation: IncomingMessageCenter.ChatNavigationTarget? @Binding var searchRevealProgress: CGFloat @Binding var searchText: String private let searchService = SearchService() @@ -40,10 +41,12 @@ struct ChatsTab: View { init( loginViewModel: LoginViewModel, + pendingNavigation: Binding, searchRevealProgress: Binding, searchText: Binding ) { self._loginViewModel = ObservedObject(wrappedValue: loginViewModel) + self._pendingNavigation = pendingNavigation self._searchRevealProgress = searchRevealProgress self._searchText = searchText } @@ -97,6 +100,13 @@ struct ChatsTab: View { globalSearchTask?.cancel() globalSearchTask = nil } + .onChange(of: pendingNavigation?.id) { _ in + guard let target = pendingNavigation else { return } + handleNavigationTarget(target.chat) + DispatchQueue.main.async { + pendingNavigation = nil + } + } } @ViewBuilder @@ -542,6 +552,30 @@ private extension ChatsTab { #endif } + func handleNavigationTarget(_ chatItem: PrivateChatListItem) { + dismissKeyboard() + if !searchText.isEmpty { + searchText = "" + } + if searchRevealProgress > 0 { + withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { + searchRevealProgress = 0 + } + } + + let existingChat = viewModel.chats.first(where: { $0.chatId == chatItem.chatId }) + pendingChatItem = existingChat ?? chatItem + selectedChatId = chatItem.chatId + isPendingChatActive = true + + if existingChat == nil { + if loginViewModel.chatLoadingState != .loading { + loginViewModel.chatLoadingState = .loading + } + viewModel.refresh() + } + } + func handleSearchQueryChange(_ query: String) { let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) @@ -1161,10 +1195,12 @@ struct ChatsTab_Previews: PreviewProvider { @State private var progress: CGFloat = 1 @State private var searchText: String = "" @StateObject private var loginViewModel = LoginViewModel() + @State private var pendingNavigation: IncomingMessageCenter.ChatNavigationTarget? var body: some View { ChatsTab( loginViewModel: loginViewModel, + pendingNavigation: $pendingNavigation, searchRevealProgress: $progress, searchText: $searchText ) diff --git a/yobble/Views/Tab/MainView.swift b/yobble/Views/Tab/MainView.swift index e35a3cd..2295401 100644 --- a/yobble/Views/Tab/MainView.swift +++ b/yobble/Views/Tab/MainView.swift @@ -2,6 +2,7 @@ import SwiftUI struct MainView: View { @ObservedObject var viewModel: LoginViewModel + @EnvironmentObject private var messageCenter: IncomingMessageCenter @State private var selectedTab: Int = 0 // @StateObject private var newHomeTabViewModel = NewHomeTabViewModel() @@ -33,6 +34,12 @@ struct MainView: View { var body: some View { NavigationView { + let pendingNavigationBinding: Binding = AppConfig.PRESENT_CHAT_AS_SHEET + ? .constant(nil) + : Binding( + get: { messageCenter.pendingNavigation }, + set: { messageCenter.pendingNavigation = $0 } + ) ZStack(alignment: .top) { ZStack(alignment: .leading) { // Выравниваем ZStack по левому краю // Основной контент @@ -57,6 +64,7 @@ struct MainView: View { ChatsTab( loginViewModel: viewModel, + pendingNavigation: pendingNavigationBinding, searchRevealProgress: $chatSearchRevealProgress, searchText: $chatSearchText ) @@ -137,6 +145,20 @@ struct MainView: View { menuOffset = presented ? menuWidth : 0 } } + .onChange(of: messageCenter.pendingNavigation?.id) { _ in + guard !AppConfig.PRESENT_CHAT_AS_SHEET, + messageCenter.pendingNavigation != nil else { return } + withAnimation(.easeInOut) { + selectedTab = 2 + isSideMenuPresented = false + menuOffset = 0 + } + } + .onChange(of: selectedTab) { newValue in + if newValue != 3 { + isSettingsPresented = false + } + } } } @@ -144,6 +166,7 @@ struct MainView_Previews: PreviewProvider { static var previews: some View { let mockViewModel = LoginViewModel() MainView(viewModel: mockViewModel) + .environmentObject(IncomingMessageCenter()) .environmentObject(ThemeManager()) } } diff --git a/yobble/config.swift b/yobble/config.swift index 801b633..dc7d87c 100644 --- a/yobble/config.swift +++ b/yobble/config.swift @@ -13,6 +13,8 @@ struct AppConfig { static let APP_VERSION = "0.1" static let DISABLE_DB = false + /// Controls whether incoming chat opens as a modal sheet (`true`) or navigates to Chats tab (`false`). + static let PRESENT_CHAT_AS_SHEET = false /// Fallback SQLCipher key used until the user sets an application password. static let DEFAULT_DATABASE_ENCRYPTION_KEY = "yobble_dev_change_me" } diff --git a/yobble/yobbleApp.swift b/yobble/yobbleApp.swift index 6b253b5..f464a4b 100644 --- a/yobble/yobbleApp.swift +++ b/yobble/yobbleApp.swift @@ -41,7 +41,7 @@ struct yobbleApp: App { } } .animation(.spring(response: 0.35, dampingFraction: 0.8), value: messageCenter.banner != nil) - .sheet(item: $messageCenter.presentedChat) { chatItem in + .sheet(item: AppConfig.PRESENT_CHAT_AS_SHEET ? $messageCenter.presentedChat : .constant(nil)) { chatItem in NavigationView { PrivateChatView( chat: chatItem,