diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 50b189c..4b2546f 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -133,6 +133,9 @@ 54F0B81C282316F5003664BD /* RegisterAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54F0B81B282316F5003664BD /* RegisterAccount.swift */; }; 6E9488F6997650B805476F25 /* Pods_another_im.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F29121F912380F72CCE51747 /* Pods_another_im.framework */; }; 7D40218FEAB3BA882811A682 /* Pods_Monal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C40963CED187B2F1B4B88F7 /* Pods_Monal.framework */; }; + 7E1C0AC72CEF68C000B8FEC0 /* MainTabScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1C0AC62CEF68C000B8FEC0 /* MainTabScreen.swift */; }; + 7E1C0ACB2CEF6C7800B8FEC0 /* ContactsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1C0AC92CEF6C7800B8FEC0 /* ContactsScreen.swift */; }; + 7E1C0ACC2CEF6C7800B8FEC0 /* AddContactOrChannelScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1C0AC82CEF6C7800B8FEC0 /* AddContactOrChannelScreen.swift */; }; 7E6AF38F2CEB9110004328B5 /* MonalXmppWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E6AF38E2CEB9110004328B5 /* MonalXmppWrapper.swift */; }; 7E6AF3902CEB982F004328B5 /* sworim.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 2601D9CA0FBF25EF004DB939 /* sworim.sqlite */; }; 7E71758D2CECC5C70059F30B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7E71758B2CECC5C70059F30B /* Localizable.strings */; }; @@ -635,6 +638,9 @@ 79A6AA4819B69B5FFFA28236 /* Pods-NotificationService.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.appstore.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.appstore.xcconfig"; sourceTree = ""; }; 7D281334DB441077E42E3E89 /* Pods-another.im.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-another.im.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-another.im/Pods-another.im.appstore-quicksy.xcconfig"; sourceTree = ""; }; 7D6715099247A9CCC180EE30 /* Pods-MonalUITests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.beta.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.beta.xcconfig"; sourceTree = ""; }; + 7E1C0AC62CEF68C000B8FEC0 /* MainTabScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabScreen.swift; sourceTree = ""; }; + 7E1C0AC82CEF6C7800B8FEC0 /* AddContactOrChannelScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactOrChannelScreen.swift; sourceTree = ""; }; + 7E1C0AC92CEF6C7800B8FEC0 /* ContactsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsScreen.swift; sourceTree = ""; }; 7E6AF38E2CEB9110004328B5 /* MonalXmppWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonalXmppWrapper.swift; sourceTree = ""; }; 7E6E446D2CECB76500505D5C /* another.im.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = another.im.entitlements; sourceTree = ""; }; 7E7175892CECC5C70059F30B /* launchscreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = launchscreen.storyboard; sourceTree = ""; }; @@ -1460,6 +1466,15 @@ path = Enter; sourceTree = ""; }; + 7E1C0ACA2CEF6C7800B8FEC0 /* Contacts */ = { + isa = PBXGroup; + children = ( + 7E1C0AC82CEF6C7800B8FEC0 /* AddContactOrChannelScreen.swift */, + 7E1C0AC92CEF6C7800B8FEC0 /* ContactsScreen.swift */, + ); + path = Contacts; + sourceTree = ""; + }; 7E71758C2CECC5C70059F30B /* Strings */ = { isa = PBXGroup; children = ( @@ -1474,6 +1489,7 @@ 7E995F222CEAC5D2005B30EE /* RootView.swift */, 7E8D7AF92CECEDB3009AD3DF /* SharedComponents */, 54D8CBD8978DA29C88226FBB /* Enter */, + D7FD95FF8F72ECE4DBEE1095 /* Main */, ); path = Views; sourceTree = ""; @@ -1707,6 +1723,15 @@ path = localization; sourceTree = ""; }; + D7FD95FF8F72ECE4DBEE1095 /* Main */ = { + isa = PBXGroup; + children = ( + 7E1C0AC62CEF68C000B8FEC0 /* MainTabScreen.swift */, + 7E1C0ACA2CEF6C7800B8FEC0 /* Contacts */, + ); + path = Main; + sourceTree = ""; + }; D91581D0612C23AB5B3F867C /* Frameworks */ = { isa = PBXGroup; children = ( @@ -2607,6 +2632,7 @@ 7E8D7B1D2CECEE79009AD3DF /* EdgeInsets+Extensions.swift in Sources */, 7E8D7B1E2CECEE79009AD3DF /* Binding+Extensions.swift in Sources */, 7E8D7B1F2CECEE79009AD3DF /* UIApplication+Extensions.swift in Sources */, + 7E1C0AC72CEF68C000B8FEC0 /* MainTabScreen.swift in Sources */, 7E8D7B202CECEE79009AD3DF /* View+Debug.swift in Sources */, 7E8D7B212CECEE79009AD3DF /* Map+Extensions.swift in Sources */, 7E8D7B222CECEE79009AD3DF /* View+Flip.swift in Sources */, @@ -2621,6 +2647,8 @@ 7E8D7AFB2CECEDB3009AD3DF /* SharedSectionTitle.swift in Sources */, 7E8D7AFC2CECEDB3009AD3DF /* SharedNavigationBar.swift in Sources */, 7E8D7AFD2CECEDB3009AD3DF /* UniversalInputCollection.swift in Sources */, + 7E1C0ACB2CEF6C7800B8FEC0 /* ContactsScreen.swift in Sources */, + 7E1C0ACC2CEF6C7800B8FEC0 /* AddContactOrChannelScreen.swift in Sources */, 7E8D7AFE2CECEDB3009AD3DF /* LoadingScreen.swift in Sources */, 7E995F252CEAC5D2005B30EE /* RootView.swift in Sources */, 7E6AF38F2CEB9110004328B5 /* MonalXmppWrapper.swift in Sources */, diff --git a/Monal/another.im/AnotherIMApp.swift b/Monal/another.im/AnotherIMApp.swift index cf5e028..2fc2aee 100644 --- a/Monal/another.im/AnotherIMApp.swift +++ b/Monal/another.im/AnotherIMApp.swift @@ -13,9 +13,4 @@ struct AnotherIMApp: App { } } -class AppDelegate: NSObject, UIApplicationDelegate { - // func application(_: UIApplication, willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - // // [IPC initializeForProcess:@"MainApp"]; - // // [MLProcessLock initializeForProcess:@"MainApp"]; - // } -} +class AppDelegate: NSObject, UIApplicationDelegate {} diff --git a/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift b/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift new file mode 100644 index 0000000..310126b --- /dev/null +++ b/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift @@ -0,0 +1,145 @@ +import SwiftUI + +struct AddContactOrChannelScreen: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + @Environment(\.router) var router + + enum Field { + case account + case contact + } + + @FocusState private var focus: Field? + + // @State private var ownerCredentials: Credentials? + @State private var contactJID: String = "" + + 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: { + router.dismissScreen() + } + ), + centerText: .init(text: L10n.Contacts.Add.title), + rightButton: .init( + image: Image(systemName: "plus.viewfinder"), + action: { + print("Scan QR-code") + } + ) + ) + + // Content + 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: activeClientsCredentials, + // selected: $ownerCredentials, + // 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 { + Task { + await save() + } + } label: { + Text(L10n.Global.save) + } + .buttonStyle(PrimaryButtonStyle()) + // .disabled(!inputValid) + .padding(.top) + Spacer() + } + .padding(.horizontal, 32) + } + } + .onAppear { + // if let exists = activeClientsCredentials.first { + // ownerCredentials = exists + // } + } + } + + // private var activeClientsCredentials: [Credentials] { + // clientsStore.clients + // .filter { $0.state != .disabled } + // .map { $0.credentials } + // } + + // private var inputValid: Bool { + // ownerCredentials != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID) + // } + // + private func save() async { + // guard let ownerCredentials = ownerCredentials else { return } + + router.showModal { + LoadingScreen() + } + + defer { + router.dismissModal() + } + + do { + // try await clientsStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: []) + router.dismissScreen() + } catch { + router.showAlert( + .alert, + title: L10n.Global.Error.title, + subtitle: L10n.Contacts.Add.serverError + ) { + Button(L10n.Global.ok, role: .cancel) {} + } + } + } +} diff --git a/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift b/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift new file mode 100644 index 0000000..8ad28e2 --- /dev/null +++ b/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift @@ -0,0 +1,202 @@ +import SwiftUI + +struct ContactsScreen: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + @Environment(\.router) var router + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + // Header + SharedNavigationBar( + centerText: .init(text: L10n.Contacts.title), + rightButton: .init( + image: Image(systemName: "plus"), + action: { + router.showScreen(.fullScreenCover) { _ in + AddContactOrChannelScreen() + } + } + ) + ) + + // Contacts list +// if !clientsStore.actualRosters.isEmpty { +// List { +// ForEach(elements.indices, id: \.self) { index in +// let element = elements[index] +// if let roster = element as? Roster { +// ContactsScreenRow( +// roster: roster +// ) +// } else if let bareJid = element as? String { +// SharedSectionTitle(text: bareJid) +// } +// } +// } +// .listStyle(.plain) +// .background(Color.Material.Background.light) +// } else { +// Spacer() +// } + } + } + } + + private var elements: [Any] { + [] + // if clientsStore.clients.filter({ $0.credentials.isActive }).count == 1 { + // return clientsStore.actualRosters + // } else { + // var result: [Any] = [] + // for roster in clientsStore.actualRosters { + // if result.isEmpty { + // result.append(roster.bareJid) + // } else if let last = result.last as? Roster, last.bareJid != roster.bareJid { + // result.append(roster.bareJid) + // } + // result.append(roster) + // } + // return result + // } + } +} + +private struct ContactsScreenRow: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + @Environment(\.router) var router + + // var roster: Roster + + var body: some View { + Text("nothing for now") + // SharedListRow( + // iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter), + // text: roster.contactBareJid, + // controlType: .none + // ) + // .onTapGesture { + // startChat() + // } + // .swipeActions(edge: .trailing, allowsFullSwipe: false) { + // Button { + // router.showAlert(.confirmationDialog, title: L10n.Contacts.deleteContact, subtitle: L10n.Contacts.Delete.message) { + // deleteConfirmation + // } + // } label: { + // Label("", systemImage: "trash") + // } + // .tint(Color.red) + // } + // .contextMenu { + // Button(L10n.Contacts.sendMessage, systemImage: "message") { + // startChat() + // } + // Divider() + // + // Button(L10n.Contacts.editContact) { + // print("Edit contact") + // } + // + // Button(L10n.Contacts.selectContact) { + // print("Select contact") + // } + // + // Divider() + // Button(L10n.Contacts.deleteContact, systemImage: "trash", role: .destructive) { + // router.showAlert(.confirmationDialog, title: L10n.Contacts.deleteContact, subtitle: L10n.Contacts.Delete.message) { + // deleteConfirmation + // } + // } + // } + } + + @ViewBuilder private var deleteConfirmation: some View { + Button(role: .destructive) { + Task { + await deleteFromDevice() + } + } label: { + Text(L10n.Contacts.Delete.deleteFromDevice) + } + + Button(role: .destructive) { + Task { + await deleteCompletely() + } + } label: { + Text(L10n.Contacts.Delete.deleteCompletely) + } + + Button(role: .cancel) {} label: { + Text(L10n.Global.cancel) + } + } + + private func deleteFromDevice() async { + router.showModal { + LoadingScreen() + } + + defer { + router.dismissModal() + } + + //var roster = roster + //try? await roster.setLocallyDeleted(true) + } + + private func deleteCompletely() async { + router.showModal { + LoadingScreen() + } + + defer { + router.dismissModal() + } + + do { + // try await clientsStore.deleteRoster(roster) + } catch { + router.showAlert( + .alert, + title: L10n.Global.Error.title, + subtitle: L10n.Contacts.Delete.error + ) { + Button(L10n.Global.ok, role: .cancel) {} + } + } + } + + private func startChat() { + Task { + router.showModal { + LoadingScreen() + } + defer { + router.dismissModal() + } + + do { + // let (messages, attachments, settings) = try await clientsStore.conversationStores(for: roster) + // router.showScreen(.push) { _ in + // ConversationScreen(messagesStore: messages, attachments: attachments, settings: settings) + // .navigationBarHidden(true) + // } + } catch { + router.showAlert( + .alert, + title: L10n.Global.Error.title, + subtitle: L10n.Conversation.startError + ) { + Button(L10n.Global.ok, role: .cancel) {} + } + } + } + } +} diff --git a/Monal/another.im/Views/Main/MainTabScreen.swift b/Monal/another.im/Views/Main/MainTabScreen.swift new file mode 100644 index 0000000..0a21cd5 --- /dev/null +++ b/Monal/another.im/Views/Main/MainTabScreen.swift @@ -0,0 +1,118 @@ +import Foundation +import SwiftfulRouting +import SwiftUI + +private enum Tab { + case chats + case contacts + case settings +} + +struct MainTabScreen: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + + @State private var selectedTab: Tab = .chats + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + switch selectedTab { + case .chats: + Text("chats") + // ChatsListScreen() + + case .contacts: + Text("contacts") + // ContactsScreen() + + case .settings: + Text("settings") + // SettingsScreen() + // .environment(\.settingsParent, .main) + } + + // Tab bar + TabBar(selectedTab: $selectedTab) + } + } + .onLoad { + wrapper.reconnectAll() + } + } +} + +private struct TabBar: View { + @Binding var selectedTab: Tab + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 0.2) + .foregroundColor(.Material.Shape.separator) + HStack(spacing: 0) { + TabBarButton(buttonType: .contacts, selectedTab: $selectedTab) + TabBarButton(buttonType: .chats, selectedTab: $selectedTab) + TabBarButton(buttonType: .settings, selectedTab: $selectedTab) + } + .background(Color.Material.Background.dark) + } + .frame(height: 50) + } +} + +private struct TabBarButton: View { + let buttonType: Tab + + @Binding var selectedTab: Tab + + var body: some View { + ZStack { + VStack(spacing: 2) { + buttonImg + .foregroundColor(buttonType == selectedTab ? .Material.Elements.active : .Material.Elements.inactive) + .font(.system(size: 24, weight: .light)) + .symbolRenderingMode(.hierarchical) + Text(buttonTitle) + .font(.sub1) + .foregroundColor(buttonType == selectedTab ? .Material.Text.main : .Material.Elements.inactive) + } + Rectangle() + .foregroundColor(.white.opacity(0.01)) + .onTapGesture { + selectedTab = buttonType + } + } + } + + var buttonImg: Image { + switch buttonType { + case .contacts: + return Image(systemName: "person.2.fill") + + case .chats: + return Image(systemName: "bubble.left.fill") + + case .settings: + return Image(systemName: "gearshape.fill") + } + } + + var buttonTitle: String { + switch buttonType { + case .contacts: + return L10n.Tabs.Name.contacts + + case .chats: + return L10n.Tabs.Name.conversations + + case .settings: + return L10n.Tabs.Name.settings + } + } +} diff --git a/Monal/another.im/Views/RootView.swift b/Monal/another.im/Views/RootView.swift index da73dea..636d7e8 100644 --- a/Monal/another.im/Views/RootView.swift +++ b/Monal/another.im/Views/RootView.swift @@ -6,8 +6,9 @@ struct RootView: View { var body: some View { if wrapper.accountsAvailability == .someEnabled { - // main flow here - Text("Test ME!") + RouterView { _ in + MainTabScreen() + } } else { RouterView { _ in WelcomeScreen() diff --git a/Monal/another.im/XMPP/MonalXmppWrapper.swift b/Monal/another.im/XMPP/MonalXmppWrapper.swift index d776dd9..27ee148 100644 --- a/Monal/another.im/XMPP/MonalXmppWrapper.swift +++ b/Monal/another.im/XMPP/MonalXmppWrapper.swift @@ -14,12 +14,13 @@ final class MonalXmppWrapper: ObservableObject { private let db: DataLayer init() { - xmpp = MLXMPPManager.sharedInstance() - db = DataLayer.sharedInstance() - // here is some inits (just for now) MLProcessLock.initialize(forProcess: "MainApp") + // init monalxmpp components + xmpp = MLXMPPManager.sharedInstance() + db = DataLayer.sharedInstance() + checkAccountsOnLoad() } @@ -31,6 +32,10 @@ final class MonalXmppWrapper: ObservableObject { throw AimErrors.loginError } } + + func reconnectAll() { + xmpp.reconnectAll() + } } // MARK: - Accounts @@ -50,7 +55,7 @@ private extension MonalXmppWrapper { } } -// MARK: - Login from Login screen +// MARK: - Try login from Login screen private final class LoginTry { weak var xmpp: MLXMPPManager? @@ -61,7 +66,7 @@ private final class LoginTry { self.xmpp = xmpp } - // TODO: Добавить автовключение отключенных аккаунтов при попытке ввести тот же JID\ + // TODO: Добавить автовключение отключенных аккаунтов при попытке ввести тот же JID // Обработать кейс когда бесячий monalxmpp возвращает nil при попытке добавить тот же JID func tryLogin(_ login: String, _ password: String) async -> Bool { async let notify = await withCheckedContinuation { [weak self] continuation in