diff --git a/ConversationsClassic/AppData/Stores/NavigationStore.swift b/ConversationsClassic/AppData/Stores/NavigationStore.swift index f5bfd2f..9f022ae 100644 --- a/ConversationsClassic/AppData/Stores/NavigationStore.swift +++ b/ConversationsClassic/AppData/Stores/NavigationStore.swift @@ -4,14 +4,19 @@ import Foundation @MainActor final class NavigationStore: ObservableObject { enum Flow: Equatable { - enum Entering { + enum Entering: Equatable { case welcome case login case registration } - enum Main { - case contacts + enum Main: Equatable { + enum Contacts: Equatable { + case list + case add + } + + case contacts(Contacts) case conversations case settings } diff --git a/ConversationsClassic/ConversationsClassicApp.swift b/ConversationsClassic/ConversationsClassicApp.swift index 75e2977..63993d7 100644 --- a/ConversationsClassic/ConversationsClassicApp.swift +++ b/ConversationsClassic/ConversationsClassicApp.swift @@ -7,10 +7,15 @@ struct ConversationsClassic: App { private var clientsStore = ClientsStore() private var navigationStore = NavigationStore() + init() { + // There's a bug on iOS 17 where sheet may not load with large title, even if modifiers are set, which causes some tests to fail + // https://stackoverflow.com/questions/77253122/swiftui-navigationstack-title-loads-inline-instead-of-large-when-sheet-is-pres + UINavigationBar.appearance().prefersLargeTitles = true + } + var body: some Scene { WindowGroup { AppRootView() - .environmentObject(navigationStore) .environmentObject(clientsStore) } } diff --git a/ConversationsClassic/View/AppRootView.swift b/ConversationsClassic/View/AppRootView.swift index 808d238..f74fdc4 100644 --- a/ConversationsClassic/View/AppRootView.swift +++ b/ConversationsClassic/View/AppRootView.swift @@ -9,8 +9,8 @@ struct AppRootView: View { case .start: StartScreen() - case .entering(let entering): - switch entering { + case .entering(let kind): + switch kind { case .welcome: WelcomeScreen() @@ -23,8 +23,17 @@ struct AppRootView: View { case .main(let main): switch main { - case .contacts: - ContactsScreen() + case .contacts(let kind): + switch kind { + case .list: + ContactsScreen() + + case .add: + ContactsScreen() + .fullScreenCover(isPresented: .constant(true)) { + AddContactOrChannelScreen() + } + } case .conversations: EmptyView() diff --git a/ConversationsClassic/View/Contacts/AddContactOrChannelScreen.swift b/ConversationsClassic/View/Contacts/AddContactOrChannelScreen.swift new file mode 100644 index 0000000..0ba62ef --- /dev/null +++ b/ConversationsClassic/View/Contacts/AddContactOrChannelScreen.swift @@ -0,0 +1,153 @@ +import SwiftUI + +struct AddContactOrChannelScreen: View { + @EnvironmentObject var navigation: NavigationStore + // @EnvironmentObject var store: AppStore + + // enum Field { + // case account + // case contact + // } + // + // @FocusState private var focus: Field? + // + // @Binding var isPresented: Bool + // @State private var contactJID: String = "" + // // @State private var ownerAccount: Account? + // + // @State private var isShowingLoader = false + // @State private var isShowingAlert = false + // @State private var errorMsg = "" + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + // Header + SharedNavigationBar( + leftButton: .init( + image: Image(systemName: "xmark"), + action: { + withAnimation { + navigation.flow = .main(.contacts(.list)) + } + // isPresented = false + } + ), + centerText: .init(text: L10n.Contacts.Add.title), + rightButton: .init( + image: Image(systemName: "plus.viewfinder"), + action: { + print("Scan QR-code") + } + ) + ) + + // VStack(spacing: 16) { + // // Explanation text + // + // Text(L10n.Contacts.Add.explanation) + // .font(.body3) + // .foregroundColor(.Material.Shape.separator) + // .multilineTextAlignment(.center) + // .padding(.top, 16) + // + // // Account selector + // HStack(spacing: 0) { + // Text("Use account:") + // .font(.body2) + // .foregroundColor(.Material.Text.main) + // .frame(alignment: .leading) + // Spacer() + // } + // // UniversalInputCollection.DropDownMenu( + // // prompt: "Use account", + // // elements: store.state.accountsState.accounts, + // // selected: $ownerAccount, + // // focus: $focus, + // // fieldType: .account + // // ) + // + // // Contact text input + // HStack(spacing: 0) { + // Text("Contact JID:") + // .font(.body2) + // .foregroundColor(.Material.Text.main) + // .frame(alignment: .leading) + // Spacer() + // } + // UniversalInputCollection.TextField( + // prompt: "Contact or channel JID", + // text: $contactJID, + // focus: $focus, + // fieldType: .contact, + // contentType: .emailAddress, + // keyboardType: .emailAddress, + // submitLabel: .done, + // action: { + // focus = .account + // } + // ) + // + // // Save button + // Button { + // navigation.flow = .main(.contacts(.list)) + // } label: { + // Text(L10n.Global.save) + // } + // .buttonStyle(PrimaryButtonStyle()) + // .disabled(!inputValid) + // .padding(.top) + // Spacer() + // } + .padding(.horizontal, 32) + } + } + // .onAppear { + // if let exists = store.state.accountsState.accounts.first, exists.isActive { + // ownerAccount = exists + // } + // } + // .loadingIndicator(isShowingLoader) + // .alert(isPresented: $isShowingAlert) { + // Alert( + // title: Text(L10n.Global.Error.title), + // message: Text(errorMsg), + // dismissButton: .default(Text(L10n.Global.ok)) + // ) + // } + // .onChange(of: store.state.rostersState.newAddedRosterJid) { jid in + // if jid != nil, isShowingLoader { + // isShowingLoader = false + // isPresented = false + // } + // } + // .onChange(of: store.state.rostersState.newAddedRosterError) { error in + // if let error = error, isShowingLoader { + // isShowingLoader = false + // errorMsg = error + // isShowingAlert = true + // } + // } + } + + private var inputValid: Bool { + true + // ownerAccount != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID) + } + + // private func save() { + // guard let ownerAccount else { return } + // if let exists = store.state.rostersState.rosters.first(where: { $0.bareJid == ownerAccount.bareJid && $0.contactBareJid == contactJID }), exists.locallyDeleted { + // store.dispatch(.rostersAction(.unmarkRosterAsLocallyDeleted(ownerJID: ownerAccount.bareJid, contactJID: contactJID))) + // isPresented = false + // } else { + // isShowingLoader = true + // store.dispatch(.rostersAction(.addRoster(ownerJID: ownerAccount.bareJid, contactJID: contactJID, name: nil, groups: []))) + // } + // } +} diff --git a/ConversationsClassic/View/Contacts/ContactsScreen.swift b/ConversationsClassic/View/Contacts/ContactsScreen.swift index 64ade2e..d259725 100644 --- a/ConversationsClassic/View/Contacts/ContactsScreen.swift +++ b/ConversationsClassic/View/Contacts/ContactsScreen.swift @@ -1,6 +1,7 @@ import SwiftUI struct ContactsScreen: View { + @EnvironmentObject var navigation: NavigationStore @EnvironmentObject var clientsStore: ClientsStore @StateObject var rostersStore = RostersStore(clientsPublisher: ClientsStore.shared.$clients) // @State private var addPanelPresented = false @@ -8,8 +9,6 @@ struct ContactsScreen: View { // @State private var errorAlertMessage = "" // @State private var isShowingLoader = false - @State private var rosters: [Roster] = [] - var body: some View { ZStack { // Background color @@ -24,15 +23,18 @@ struct ContactsScreen: View { rightButton: .init( image: Image(systemName: "plus"), action: { + withAnimation { + navigation.flow = .main(.contacts(.add)) + } // addPanelPresented = true } ) ) // Contacts list - if !rosters.isEmpty { + if !rostersStore.rosters.isEmpty { List { - ForEach(rosters) { roster in + ForEach(rostersStore.rosters) { roster in ContactsScreenRow( roster: roster // isErrorAlertPresented: $isErrorAlertPresented, @@ -51,9 +53,9 @@ struct ContactsScreen: View { SharedTabBar() } } - .task { - await fetchRosters() - } + // .task { + // await fetchRosters() + // } // .loadingIndicator(isShowingLoader) // .fullScreenCover(isPresented: $addPanelPresented) { // AddContactOrChannelScreen(isPresented: $addPanelPresented) @@ -67,27 +69,27 @@ struct ContactsScreen: View { // } } - private func fetchRosters() async { - let jids = clientsStore.clients - .filter { $0.state != .disabled } - .map { $0.credentials.bareJid } - - do { - try await withThrowingTaskGroup(of: [Roster].self) { group in - for jid in jids { - group.addTask { - try await Roster.fetchAll(for: jid) - } - } - - var allRosters: [Roster] = [] - for try await rosters in group { - allRosters.append(contentsOf: rosters) - } - self.rosters = allRosters.sorted { $0.contactBareJid < $1.contactBareJid } - } - } catch {} - } + // private func fetchRosters() async { + // let jids = clientsStore.clients + // .filter { $0.state != .disabled } + // .map { $0.credentials.bareJid } + // + // do { + // try await withThrowingTaskGroup(of: [Roster].self) { group in + // for jid in jids { + // group.addTask { + // try await Roster.fetchAll(for: jid) + // } + // } + // + // var allRosters: [Roster] = [] + // for try await rosters in group { + // allRosters.append(contentsOf: rosters) + // } + // self.rosters = allRosters.sorted { $0.contactBareJid < $1.contactBareJid } + // } + // } catch {} + // } } private struct ContactsScreenRow: View { diff --git a/ConversationsClassic/View/Entering/LoginScreen.swift b/ConversationsClassic/View/Entering/LoginScreen.swift index b7aa509..b47a77a 100644 --- a/ConversationsClassic/View/Entering/LoginScreen.swift +++ b/ConversationsClassic/View/Entering/LoginScreen.swift @@ -130,7 +130,7 @@ struct LoginScreen: View { isLoading = false isError = false if navigation.flow == .entering(.login) { - navigation.flow = .main(.contacts) + navigation.flow = .main(.contacts(.list)) } case .failure: diff --git a/ConversationsClassic/View/SharedComponents/SharedTabBar.swift b/ConversationsClassic/View/SharedComponents/SharedTabBar.swift index f8620ec..ff1949b 100644 --- a/ConversationsClassic/View/SharedComponents/SharedTabBar.swift +++ b/ConversationsClassic/View/SharedComponents/SharedTabBar.swift @@ -8,7 +8,7 @@ struct SharedTabBar: View { .frame(height: 0.2) .foregroundColor(.Material.Shape.separator) HStack(spacing: 0) { - SharedTabBarButton(buttonFlow: .main(.contacts)) + SharedTabBarButton(buttonFlow: .main(.contacts(.list))) SharedTabBarButton(buttonFlow: .main(.conversations)) SharedTabBarButton(buttonFlow: .main(.settings)) } @@ -62,7 +62,7 @@ private struct SharedTabBarButton: View { var buttonTitle: String { switch buttonFlow { - case .main(.contacts): + case .main(.contacts(.list)): return "Contacts" case .main(.conversations): diff --git a/project.yml b/project.yml index 4b35192..a07200b 100644 --- a/project.yml +++ b/project.yml @@ -5,6 +5,9 @@ options: postGenCommand: swiftgen packages: + SwiftfulRouting: + url: https://github.com/SwiftfulThinking/SwiftfulRouting + majorVersion: 5.3.5 MartinOMEMO: url: https://github.com/tigase/MartinOMEMO majorVersion: 2.2.3 @@ -74,6 +77,8 @@ targets: - sdk: Security.framework # - framework: Lib/WebRTC.xcframework # - target: Engine + - package: SwiftfulRouting + link: true - package: MartinOMEMO link: true - package: KeychainAccess