Files
ios-mail/Modules/InboxContacts/Sources/Screens/Contacts/ContactsScreen.swift
T
2026-04-15 13:58:23 +02:00

148 lines
5.0 KiB
Swift

// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import InboxCore
import InboxCoreUI
import InboxDesignSystem
import SwiftUI
import proton_app_uniffi
public struct ContactsScreen: View {
@Environment(\.dismissTestable) var dismiss: Dismissable
@StateObject private var store: ContactsStateStore
private let contactViewFactory: ContactViewFactory
/// `state` parameter is exposed only for testing purposes to be able to rely on data source in synchronous manner.
public init(
apiConfig: ApiConfig,
state: ContactsScreenState = .initial,
mailUserSession: MailUserSession,
contactsProvider: GroupedContactsProvider,
contactsWatcher: ContactsWatcher,
draftPresenter: ContactsDraftPresenter
) {
UISearchBar.appearance().tintColor = UIColor(DS.Color.Text.accent)
_store = .init(
wrappedValue: .init(
apiConfig: apiConfig,
state: state,
mailUserSession: mailUserSession,
contactsWrappers: .productionInstance(
contactsProvider: contactsProvider,
contactsWatcher: contactsWatcher
)
)
)
self.contactViewFactory = .init(
apiConfig: apiConfig,
mailUserSession: mailUserSession,
draftPresenter: draftPresenter
)
}
public var body: some View {
NavigationStack(path: navigationPath(store: store)) {
ContactsControllerRepresentable(
contacts: store.state.displayItems,
onDeleteItem: { item in handle(action: .onDeleteItem(item)) },
onTapItem: { item in handle(action: .onTapItem(item)) }
)
.ignoresSafeArea()
.navigationTitle(L10n.Contacts.title.string)
.navigationDestination(for: ContactsRoute.self) { route in
contactViewFactory
.makeView(for: route)
.environmentObject(store.router)
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItemFactory.back { handle(action: .goBack) }
}
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
ButtonFactory.close {
dismiss()
}
}
ToolbarItemFactory.trailing(Image(symbol: .plus)) {
handle(action: .createTapped)
}
}
}
.sheet(
isPresented: $store.state.displayCreateContactSheet,
content: {
PromptSheet(
model: .createInWeb(
onAction: { handle(action: .createSheetAction(.openSafari)) },
onDismiss: { handle(action: .createSheetAction(.dismiss)) }
)
)
}
)
.sheet(
item: $store.state.createContactURL,
onDismiss: { handle(action: .dismissCreateSheet) },
content: SafariView.init
)
.alert(model: deletionAlert)
.searchable(
text: $store.state.search.query,
isPresented: $store.state.search.isActive,
placement: .navigationBarDrawer(displayMode: .always)
)
.onLoad { handle(action: .onLoad) }
}
// MARK: - Private
private func navigationPath(store: ContactsStateStore) -> Binding<[ContactsRoute]> {
.init(
get: { store.router.stack },
set: { newStack in store.router.stack = newStack }
)
}
private var deletionAlert: Binding<AlertModel?> {
.readonly {
store.state.itemToDelete.map { itemType in
DeleteConfirmationAlertFactory.make(
for: itemType,
action: { action in handle(action: .onDeleteItemAlertAction(action)) }
)
}
}
}
private func handle(action: ContactsStateStore.Action) {
Task {
await store.handle(action: action)
}
}
}
#Preview {
ContactsScreen(
apiConfig: .debugPreview,
mailUserSession: .init(noPointer: .init()),
contactsProvider: .previewInstance(),
contactsWatcher: .previewInstance(),
draftPresenter: ContactsDraftPresenterDummy()
)
}