mv-experiment #1
|
@ -4,14 +4,19 @@ import Foundation
|
||||||
@MainActor
|
@MainActor
|
||||||
final class NavigationStore: ObservableObject {
|
final class NavigationStore: ObservableObject {
|
||||||
enum Flow: Equatable {
|
enum Flow: Equatable {
|
||||||
enum Entering {
|
enum Entering: Equatable {
|
||||||
case welcome
|
case welcome
|
||||||
case login
|
case login
|
||||||
case registration
|
case registration
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Main {
|
enum Main: Equatable {
|
||||||
case contacts
|
enum Contacts: Equatable {
|
||||||
|
case list
|
||||||
|
case add
|
||||||
|
}
|
||||||
|
|
||||||
|
case contacts(Contacts)
|
||||||
case conversations
|
case conversations
|
||||||
case settings
|
case settings
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,15 @@ struct ConversationsClassic: App {
|
||||||
private var clientsStore = ClientsStore()
|
private var clientsStore = ClientsStore()
|
||||||
private var navigationStore = NavigationStore()
|
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 {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
AppRootView()
|
AppRootView()
|
||||||
.environmentObject(navigationStore)
|
|
||||||
.environmentObject(clientsStore)
|
.environmentObject(clientsStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ struct AppRootView: View {
|
||||||
case .start:
|
case .start:
|
||||||
StartScreen()
|
StartScreen()
|
||||||
|
|
||||||
case .entering(let entering):
|
case .entering(let kind):
|
||||||
switch entering {
|
switch kind {
|
||||||
case .welcome:
|
case .welcome:
|
||||||
WelcomeScreen()
|
WelcomeScreen()
|
||||||
|
|
||||||
|
@ -23,9 +23,18 @@ struct AppRootView: View {
|
||||||
|
|
||||||
case .main(let main):
|
case .main(let main):
|
||||||
switch main {
|
switch main {
|
||||||
case .contacts:
|
case .contacts(let kind):
|
||||||
|
switch kind {
|
||||||
|
case .list:
|
||||||
ContactsScreen()
|
ContactsScreen()
|
||||||
|
|
||||||
|
case .add:
|
||||||
|
ContactsScreen()
|
||||||
|
.fullScreenCover(isPresented: .constant(true)) {
|
||||||
|
AddContactOrChannelScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case .conversations:
|
case .conversations:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|
||||||
|
|
|
@ -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: [])))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContactsScreen: View {
|
struct ContactsScreen: View {
|
||||||
|
@EnvironmentObject var navigation: NavigationStore
|
||||||
@EnvironmentObject var clientsStore: ClientsStore
|
@EnvironmentObject var clientsStore: ClientsStore
|
||||||
@StateObject var rostersStore = RostersStore(clientsPublisher: ClientsStore.shared.$clients)
|
@StateObject var rostersStore = RostersStore(clientsPublisher: ClientsStore.shared.$clients)
|
||||||
// @State private var addPanelPresented = false
|
// @State private var addPanelPresented = false
|
||||||
|
@ -8,8 +9,6 @@ struct ContactsScreen: View {
|
||||||
// @State private var errorAlertMessage = ""
|
// @State private var errorAlertMessage = ""
|
||||||
// @State private var isShowingLoader = false
|
// @State private var isShowingLoader = false
|
||||||
|
|
||||||
@State private var rosters: [Roster] = []
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Background color
|
// Background color
|
||||||
|
@ -24,15 +23,18 @@ struct ContactsScreen: View {
|
||||||
rightButton: .init(
|
rightButton: .init(
|
||||||
image: Image(systemName: "plus"),
|
image: Image(systemName: "plus"),
|
||||||
action: {
|
action: {
|
||||||
|
withAnimation {
|
||||||
|
navigation.flow = .main(.contacts(.add))
|
||||||
|
}
|
||||||
// addPanelPresented = true
|
// addPanelPresented = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Contacts list
|
// Contacts list
|
||||||
if !rosters.isEmpty {
|
if !rostersStore.rosters.isEmpty {
|
||||||
List {
|
List {
|
||||||
ForEach(rosters) { roster in
|
ForEach(rostersStore.rosters) { roster in
|
||||||
ContactsScreenRow(
|
ContactsScreenRow(
|
||||||
roster: roster
|
roster: roster
|
||||||
// isErrorAlertPresented: $isErrorAlertPresented,
|
// isErrorAlertPresented: $isErrorAlertPresented,
|
||||||
|
@ -51,9 +53,9 @@ struct ContactsScreen: View {
|
||||||
SharedTabBar()
|
SharedTabBar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
// .task {
|
||||||
await fetchRosters()
|
// await fetchRosters()
|
||||||
}
|
// }
|
||||||
// .loadingIndicator(isShowingLoader)
|
// .loadingIndicator(isShowingLoader)
|
||||||
// .fullScreenCover(isPresented: $addPanelPresented) {
|
// .fullScreenCover(isPresented: $addPanelPresented) {
|
||||||
// AddContactOrChannelScreen(isPresented: $addPanelPresented)
|
// AddContactOrChannelScreen(isPresented: $addPanelPresented)
|
||||||
|
@ -67,27 +69,27 @@ struct ContactsScreen: View {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchRosters() async {
|
// private func fetchRosters() async {
|
||||||
let jids = clientsStore.clients
|
// let jids = clientsStore.clients
|
||||||
.filter { $0.state != .disabled }
|
// .filter { $0.state != .disabled }
|
||||||
.map { $0.credentials.bareJid }
|
// .map { $0.credentials.bareJid }
|
||||||
|
//
|
||||||
do {
|
// do {
|
||||||
try await withThrowingTaskGroup(of: [Roster].self) { group in
|
// try await withThrowingTaskGroup(of: [Roster].self) { group in
|
||||||
for jid in jids {
|
// for jid in jids {
|
||||||
group.addTask {
|
// group.addTask {
|
||||||
try await Roster.fetchAll(for: jid)
|
// try await Roster.fetchAll(for: jid)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
var allRosters: [Roster] = []
|
// var allRosters: [Roster] = []
|
||||||
for try await rosters in group {
|
// for try await rosters in group {
|
||||||
allRosters.append(contentsOf: rosters)
|
// allRosters.append(contentsOf: rosters)
|
||||||
}
|
// }
|
||||||
self.rosters = allRosters.sorted { $0.contactBareJid < $1.contactBareJid }
|
// self.rosters = allRosters.sorted { $0.contactBareJid < $1.contactBareJid }
|
||||||
}
|
// }
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ContactsScreenRow: View {
|
private struct ContactsScreenRow: View {
|
||||||
|
|
|
@ -130,7 +130,7 @@ struct LoginScreen: View {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
isError = false
|
isError = false
|
||||||
if navigation.flow == .entering(.login) {
|
if navigation.flow == .entering(.login) {
|
||||||
navigation.flow = .main(.contacts)
|
navigation.flow = .main(.contacts(.list))
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure:
|
case .failure:
|
||||||
|
|
|
@ -8,7 +8,7 @@ struct SharedTabBar: View {
|
||||||
.frame(height: 0.2)
|
.frame(height: 0.2)
|
||||||
.foregroundColor(.Material.Shape.separator)
|
.foregroundColor(.Material.Shape.separator)
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
SharedTabBarButton(buttonFlow: .main(.contacts))
|
SharedTabBarButton(buttonFlow: .main(.contacts(.list)))
|
||||||
SharedTabBarButton(buttonFlow: .main(.conversations))
|
SharedTabBarButton(buttonFlow: .main(.conversations))
|
||||||
SharedTabBarButton(buttonFlow: .main(.settings))
|
SharedTabBarButton(buttonFlow: .main(.settings))
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ private struct SharedTabBarButton: View {
|
||||||
|
|
||||||
var buttonTitle: String {
|
var buttonTitle: String {
|
||||||
switch buttonFlow {
|
switch buttonFlow {
|
||||||
case .main(.contacts):
|
case .main(.contacts(.list)):
|
||||||
return "Contacts"
|
return "Contacts"
|
||||||
|
|
||||||
case .main(.conversations):
|
case .main(.conversations):
|
||||||
|
|
|
@ -5,6 +5,9 @@ options:
|
||||||
postGenCommand: swiftgen
|
postGenCommand: swiftgen
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
SwiftfulRouting:
|
||||||
|
url: https://github.com/SwiftfulThinking/SwiftfulRouting
|
||||||
|
majorVersion: 5.3.5
|
||||||
MartinOMEMO:
|
MartinOMEMO:
|
||||||
url: https://github.com/tigase/MartinOMEMO
|
url: https://github.com/tigase/MartinOMEMO
|
||||||
majorVersion: 2.2.3
|
majorVersion: 2.2.3
|
||||||
|
@ -74,6 +77,8 @@ targets:
|
||||||
- sdk: Security.framework
|
- sdk: Security.framework
|
||||||
# - framework: Lib/WebRTC.xcframework
|
# - framework: Lib/WebRTC.xcframework
|
||||||
# - target: Engine
|
# - target: Engine
|
||||||
|
- package: SwiftfulRouting
|
||||||
|
link: true
|
||||||
- package: MartinOMEMO
|
- package: MartinOMEMO
|
||||||
link: true
|
link: true
|
||||||
- package: KeychainAccess
|
- package: KeychainAccess
|
||||||
|
|
Loading…
Reference in a new issue