Files
element-x-ios/ElementX/Sources/Screens/HealthChatProfileEditScreen/View/HealthChatProfileEditScreen.swift
T

221 lines
8.0 KiB
Swift

//
// Copyright 2026 Novaloop AG
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct HealthChatProfileEditScreen: View {
@Bindable var context: HealthChatProfileEditScreenViewModel.Context
var body: some View {
content
.navigationTitle(L10n.screenEditProfileTitle)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(context.viewState.canSave)
.toolbar { toolbar }
.alert(item: $context.alertInfo)
}
// MARK: - Private
@ViewBuilder
private var content: some View {
if context.viewState.loadFailed {
failureView
} else {
form
}
}
private var form: some View {
Form {
Section {
avatar
}
Section {
textFieldRow(HealthChatL10n.commonAcademicTitle, text: $context.title)
textFieldRow(requiredLabel(HealthChatL10n.commonFirstName), text: $context.firstName)
textFieldRow(requiredLabel(HealthChatL10n.commonLastName), text: $context.lastName)
} header: {
Text(HealthChatL10n.screenProfileEditSectionPersonal)
.compoundListSectionHeader()
}
Section {
textFieldRow(requiredLabel(HealthChatL10n.commonSpecialty), text: $context.specialty)
textFieldRow(requiredLabel(HealthChatL10n.commonPracticeName), text: $context.practiceName)
textFieldRow(requiredLabel(HealthChatL10n.commonCity), text: $context.businessCity)
textFieldRow(HealthChatL10n.commonFurtherInfo, text: $context.furtherInfo, axis: .vertical)
} header: {
Text(HealthChatL10n.screenProfileEditSectionProfessional)
.compoundListSectionHeader()
}
Section {
textFieldRow(HealthChatL10n.commonBusinessEmail, text: $context.businessEmail)
textFieldRow(HealthChatL10n.commonBusinessTel, text: $context.businessTel)
} header: {
Text(HealthChatL10n.screenProfileEditSectionContact)
.compoundListSectionHeader()
}
Section {
textFieldRow(HealthChatL10n.commonStreet, text: $context.street)
textFieldRow(HealthChatL10n.commonHouseNumber, text: $context.houseNumber)
textFieldRow(HealthChatL10n.commonPostalCode, text: $context.postalCode)
textFieldRow(HealthChatL10n.commonWebsite, text: $context.website)
} header: {
Text(HealthChatL10n.screenProfileEditSectionAddress)
.compoundListSectionHeader()
}
}
.compoundList()
.scrollDismissesKeyboard(.immediately)
.disabled(context.viewState.initialProfile == nil)
}
private var failureView: some View {
VStack(spacing: 16) {
BigIcon(icon: \.error)
Text(L10n.commonSomethingWentWrong)
.font(.compound.bodyLG)
.foregroundColor(.compound.textSecondary)
.multilineTextAlignment(.center)
Button(L10n.actionRetry) {
context.send(viewAction: .retry)
}
.buttonStyle(.compound(.secondary, size: .medium))
.fixedSize()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(16)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
if context.viewState.canSave {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionSave) {
context.send(viewAction: .save)
}
.disabled(!context.viewState.canSave)
}
}
private var avatar: some View {
Button {
context.send(viewAction: .presentMediaSource)
} label: {
OverridableAvatarImage(overrideURL: context.viewState.localMedia?.thumbnailURL,
url: context.viewState.selectedAvatarURL,
name: context.viewState.initialProfile?.displayName,
contentID: context.viewState.initialProfile?.userID ?? "",
shape: .circle,
avatarSize: .user(on: .editUserDetails),
mediaProvider: context.mediaProvider)
.confirmationDialog("", isPresented: $context.showMediaSheet) {
mediaActionSheet
}
}
.buttonStyle(EditAvatarButtonStyle())
.disabled(!context.viewState.canEditAvatar)
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
}
private func textFieldRow(_ label: String, text: Binding<String>, axis: Axis = .horizontal) -> some View {
ListRow(label: .plain(title: label),
kind: .textField(text: text, axis: axis))
}
private func requiredLabel(_ label: String) -> String {
"\(label) *"
}
@ViewBuilder
private var mediaActionSheet: some View {
Button {
context.send(viewAction: .displayCameraPicker)
} label: {
Text(L10n.actionTakePhoto)
}
Button {
context.send(viewAction: .displayMediaPicker)
} label: {
Text(L10n.actionChoosePhoto)
}
Button {
context.send(viewAction: .displayFilePicker)
} label: {
Text(L10n.actionChooseFile)
}
if context.viewState.showDeleteImageAction {
Button(role: .destructive) {
context.send(viewAction: .removeImage)
} label: {
Text(L10n.actionRemove)
}
}
}
}
// MARK: - Previews
struct HealthChatProfileEditScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel()
static var previews: some View {
ElementNavigationStack {
HealthChatProfileEditScreen(context: viewModel.context)
}
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.initialProfile).map { $0 != nil })
.previewDisplayName("Profile")
}
static func makeViewModel() -> HealthChatProfileEditScreenViewModel {
let apiService = HealthChatAPIServiceMock()
let member = try? JSONDecoder().decode(HealthChatMember.self, from: Data("""
{
"userId": "@alice:healthchat.ch",
"displayName": "Dr. Alice Alpha",
"title": "Dr. med.",
"firstName": "Alice",
"lastName": "Alpha",
"specialty": "Physiotherapie",
"practiceName": "Praxis Alpha",
"businessCity": "Zürich",
"businessEmail": "praxis@example.ch",
"businessTel": "+41 44 123 45 67",
"street": "Friedaustrasse",
"houseNumber": "17",
"postalCode": "8003",
"website": "https://example.ch"
}
""".utf8))
if let member {
apiService.ownProfileReturnValue = .success(member)
}
return HealthChatProfileEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))),
apiService: apiService,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .mock()),
userIndicatorController: UserIndicatorControllerMock())
}
}