ios: profile names (remove full name) (#3168)
* ios: profile names (remove full name) * create/update groups * focus display name
This commit is contained in:
parent
91fc238ddc
commit
0d8558a6d0
@ -9,6 +9,18 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
enum GroupProfileAlert: Identifiable {
|
||||||
|
case saveError(err: String)
|
||||||
|
case invalidName(validName: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .saveError(err): return "saveError \(err)"
|
||||||
|
case let .invalidName(validName): return "invalidName \(validName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct GroupProfileView: View {
|
struct GroupProfileView: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@ -18,8 +30,7 @@ struct GroupProfileView: View {
|
|||||||
@State private var showImagePicker = false
|
@State private var showImagePicker = false
|
||||||
@State private var showTakePhoto = false
|
@State private var showTakePhoto = false
|
||||||
@State private var chosenImage: UIImage? = nil
|
@State private var chosenImage: UIImage? = nil
|
||||||
@State private var showSaveErrorAlert = false
|
@State private var alert: GroupProfileAlert?
|
||||||
@State private var saveGroupError: String? = nil
|
|
||||||
@FocusState private var focusDisplayName
|
@FocusState private var focusDisplayName
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -47,20 +58,29 @@ struct GroupProfileView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if !validDisplayName(groupProfile.displayName) {
|
if !validNewProfileName() {
|
||||||
Image(systemName: "exclamationmark.circle")
|
Button {
|
||||||
.foregroundColor(.red)
|
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
|
||||||
.padding(.bottom, 10)
|
} label: {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||||
}
|
}
|
||||||
profileNameTextEdit("Group display name", $groupProfile.displayName)
|
profileNameTextEdit("Group display name", $groupProfile.displayName)
|
||||||
.focused($focusDisplayName)
|
.focused($focusDisplayName)
|
||||||
}
|
}
|
||||||
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
|
.padding(.bottom)
|
||||||
|
let fullName = groupInfo.groupProfile.fullName
|
||||||
|
if fullName != "" && fullName != groupProfile.displayName {
|
||||||
|
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
Button("Cancel") { dismiss() }
|
Button("Cancel") { dismiss() }
|
||||||
Button("Save group profile") { saveProfile() }
|
Button("Save group profile") { saveProfile() }
|
||||||
.disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName))
|
.disabled(groupProfile.displayName == "" || !validNewProfileName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||||
@ -99,27 +119,35 @@ struct GroupProfileView: View {
|
|||||||
focusDisplayName = true
|
focusDisplayName = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(isPresented: $showSaveErrorAlert) {
|
.alert(item: $alert) { a in
|
||||||
Alert(
|
switch a {
|
||||||
title: Text("Error saving group profile"),
|
case let .saveError(err):
|
||||||
message: Text("\(saveGroupError ?? "Unexpected error")")
|
return Alert(
|
||||||
)
|
title: Text("Error saving group profile"),
|
||||||
|
message: Text(err)
|
||||||
|
)
|
||||||
|
case let .invalidName(name):
|
||||||
|
return createInvalidNameAlert(name, $groupProfile.displayName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { hideKeyboard() }
|
.onTapGesture { hideKeyboard() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func validNewProfileName() -> Bool {
|
||||||
|
groupProfile.displayName == groupInfo.groupProfile.displayName
|
||||||
|
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
|
||||||
|
}
|
||||||
|
|
||||||
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
||||||
TextField(label, text: name)
|
TextField(label, text: name)
|
||||||
.textInputAutocapitalization(.never)
|
.padding(.leading, 32)
|
||||||
.disableAutocorrection(true)
|
|
||||||
.padding(.bottom)
|
|
||||||
.padding(.leading, 28)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveProfile() {
|
func saveProfile() {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
|
||||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
groupInfo = gInfo
|
groupInfo = gInfo
|
||||||
@ -128,8 +156,7 @@ struct GroupProfileView: View {
|
|||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
let err = responseError(error)
|
let err = responseError(error)
|
||||||
saveGroupError = err
|
alert = .saveError(err: err)
|
||||||
showSaveErrorAlert = true
|
|
||||||
logger.error("GroupProfile apiUpdateGroup error: \(err)")
|
logger.error("GroupProfile apiUpdateGroup error: \(err)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,11 @@ struct AddGroupView: View {
|
|||||||
@State private var groupInfo: GroupInfo?
|
@State private var groupInfo: GroupInfo?
|
||||||
@State private var profile = GroupProfile(displayName: "", fullName: "")
|
@State private var profile = GroupProfile(displayName: "", fullName: "")
|
||||||
@FocusState private var focusDisplayName
|
@FocusState private var focusDisplayName
|
||||||
@FocusState private var focusFullName
|
|
||||||
@State private var showChooseSource = false
|
@State private var showChooseSource = false
|
||||||
@State private var showImagePicker = false
|
@State private var showImagePicker = false
|
||||||
@State private var showTakePhoto = false
|
@State private var showTakePhoto = false
|
||||||
@State private var chosenImage: UIImage? = nil
|
@State private var chosenImage: UIImage? = nil
|
||||||
|
@State private var showInvalidNameAlert = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let chat = chat, let groupInfo = groupInfo {
|
if let chat = chat, let groupInfo = groupInfo {
|
||||||
@ -76,26 +76,24 @@ struct AddGroupView: View {
|
|||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if !validDisplayName(profile.displayName) {
|
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||||
Image(systemName: "exclamationmark.circle")
|
if name != mkValidName(name) {
|
||||||
.foregroundColor(.red)
|
Button {
|
||||||
.padding(.top, 4)
|
showInvalidNameAlert = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||||
}
|
}
|
||||||
textField("Group display name", text: $profile.displayName)
|
textField("Enter group name…", text: $profile.displayName)
|
||||||
.focused($focusDisplayName)
|
.focused($focusDisplayName)
|
||||||
.submitLabel(.next)
|
.submitLabel(.go)
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
if canCreateProfile() { focusFullName = true }
|
if canCreateProfile() { createGroup() }
|
||||||
else { focusDisplayName = true }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textField("Group full name (optional)", text: $profile.fullName)
|
.padding(.bottom)
|
||||||
.focused($focusFullName)
|
|
||||||
.submitLabel(.go)
|
|
||||||
.onSubmit {
|
|
||||||
if canCreateProfile() { createGroup() }
|
|
||||||
else { focusFullName = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -133,6 +131,9 @@ struct AddGroupView: View {
|
|||||||
didSelectItem in showImagePicker = false
|
didSelectItem in showImagePicker = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(isPresented: $showInvalidNameAlert) {
|
||||||
|
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
|
||||||
|
}
|
||||||
.onChange(of: chosenImage) { image in
|
.onChange(of: chosenImage) { image in
|
||||||
if let image = image {
|
if let image = image {
|
||||||
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
|
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
|
||||||
@ -146,15 +147,13 @@ struct AddGroupView: View {
|
|||||||
|
|
||||||
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
|
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
|
||||||
TextField(placeholder, text: text)
|
TextField(placeholder, text: text)
|
||||||
.textInputAutocapitalization(.never)
|
.padding(.leading, 32)
|
||||||
.disableAutocorrection(true)
|
|
||||||
.padding(.leading, 28)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGroup() {
|
func createGroup() {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
do {
|
do {
|
||||||
|
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||||
let gInfo = try apiNewGroup(profile)
|
let gInfo = try apiNewGroup(profile)
|
||||||
Task {
|
Task {
|
||||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||||
@ -180,7 +179,7 @@ struct AddGroupView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func canCreateProfile() -> Bool {
|
func canCreateProfile() -> Bool {
|
||||||
profile.displayName != "" && validDisplayName(profile.displayName)
|
profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,175 +9,244 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
enum UserProfileAlert: Identifiable {
|
||||||
|
case duplicateUserError
|
||||||
|
case createUserError(error: LocalizedStringKey)
|
||||||
|
case invalidNameError(validName: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .duplicateUserError: return "duplicateUserError"
|
||||||
|
case .createUserError: return "createUserError"
|
||||||
|
case let .invalidNameError(validName): return "invalidNameError \(validName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CreateProfile: View {
|
struct CreateProfile: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State private var displayName: String = ""
|
||||||
|
@FocusState private var focusDisplayName
|
||||||
|
@State private var alert: UserProfileAlert?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
TextField("Enter your name…", text: $displayName)
|
||||||
|
.focused($focusDisplayName)
|
||||||
|
Button {
|
||||||
|
createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss)
|
||||||
|
} label: {
|
||||||
|
Label("Create profile", systemImage: "checkmark")
|
||||||
|
}
|
||||||
|
.disabled(!canCreateProfile(displayName))
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Your profile")
|
||||||
|
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||||
|
let validName = mkValidName(name)
|
||||||
|
if name != validName {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "exclamationmark.circle")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.onTapGesture {
|
||||||
|
alert = .invalidNameError(validName: validName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 20)
|
||||||
|
} footer: {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||||
|
Text("The profile is only shared with your contacts.")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Create your profile")
|
||||||
|
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
|
||||||
|
.onAppear() {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
focusDisplayName = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.keyboardPadding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreateFirstProfile: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State private var displayName: String = ""
|
@State private var displayName: String = ""
|
||||||
@State private var fullName: String = ""
|
|
||||||
@FocusState private var focusDisplayName
|
@FocusState private var focusDisplayName
|
||||||
@FocusState private var focusFullName
|
|
||||||
@State private var alert: CreateProfileAlert?
|
|
||||||
|
|
||||||
private enum CreateProfileAlert: Identifiable {
|
|
||||||
case duplicateUserError
|
|
||||||
case createUserError(error: LocalizedStringKey)
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case .duplicateUserError: return "duplicateUserError"
|
|
||||||
case .createUserError: return "createUserError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Create your profile")
|
Group {
|
||||||
.font(.largeTitle)
|
Text("Create your profile")
|
||||||
.bold()
|
.font(.largeTitle)
|
||||||
.padding(.bottom, 4)
|
.bold()
|
||||||
.frame(maxWidth: .infinity)
|
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
.foregroundColor(.secondary)
|
||||||
.padding(.bottom, 4)
|
Text("The profile is only shared with your contacts.")
|
||||||
Text("The profile is only shared with your contacts.")
|
.foregroundColor(.secondary)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if !validDisplayName(displayName) {
|
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||||
Image(systemName: "exclamationmark.circle")
|
let validName = mkValidName(name)
|
||||||
.foregroundColor(.red)
|
if name != validName {
|
||||||
.padding(.top, 4)
|
Button {
|
||||||
|
showAlert(.invalidNameError(validName: validName))
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||||
}
|
}
|
||||||
textField("Display name", text: $displayName)
|
TextField("Enter your name…", text: $displayName)
|
||||||
.focused($focusDisplayName)
|
.focused($focusDisplayName)
|
||||||
.submitLabel(.next)
|
.padding(.leading, 32)
|
||||||
.onSubmit {
|
|
||||||
if canCreateProfile() { focusFullName = true }
|
|
||||||
else { focusDisplayName = true }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
textField("Full name (optional)", text: $fullName)
|
.padding(.bottom)
|
||||||
.focused($focusFullName)
|
|
||||||
.submitLabel(.go)
|
|
||||||
.onSubmit {
|
|
||||||
if canCreateProfile() { createProfile() }
|
|
||||||
else { focusFullName = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
onboardingButtons()
|
||||||
HStack {
|
|
||||||
if m.users.isEmpty {
|
|
||||||
Button {
|
|
||||||
hideKeyboard()
|
|
||||||
withAnimation {
|
|
||||||
m.onboardingStage = .step1_SimpleXInfo
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "lessthan")
|
|
||||||
Text("About SimpleX")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button {
|
|
||||||
createProfile()
|
|
||||||
} label: {
|
|
||||||
Text("Create")
|
|
||||||
Image(systemName: "greaterthan")
|
|
||||||
}
|
|
||||||
.disabled(!canCreateProfile())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
focusDisplayName = true
|
focusDisplayName = true
|
||||||
setLastVersionDefault()
|
setLastVersionDefault()
|
||||||
}
|
}
|
||||||
.alert(item: $alert) { a in
|
|
||||||
switch a {
|
|
||||||
case .duplicateUserError: return duplicateUserAlert
|
|
||||||
case let .createUserError(err): return creatUserErrorAlert(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.keyboardPadding()
|
.keyboardPadding()
|
||||||
}
|
}
|
||||||
|
|
||||||
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
|
func onboardingButtons() -> some View {
|
||||||
TextField(placeholder, text: text)
|
HStack {
|
||||||
.textInputAutocapitalization(.never)
|
Button {
|
||||||
.disableAutocorrection(true)
|
hideKeyboard()
|
||||||
.padding(.leading, 28)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createProfile() {
|
|
||||||
hideKeyboard()
|
|
||||||
let profile = Profile(
|
|
||||||
displayName: displayName,
|
|
||||||
fullName: fullName
|
|
||||||
)
|
|
||||||
do {
|
|
||||||
m.currentUser = try apiCreateActiveUser(profile)
|
|
||||||
if m.users.isEmpty {
|
|
||||||
try startChat()
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
|
m.onboardingStage = .step1_SimpleXInfo
|
||||||
m.onboardingStage = .step3_CreateSimpleXAddress
|
|
||||||
}
|
}
|
||||||
} else {
|
} label: {
|
||||||
onboardingStageDefault.set(.onboardingComplete)
|
HStack {
|
||||||
m.onboardingStage = .onboardingComplete
|
Image(systemName: "lessthan")
|
||||||
dismiss()
|
Text("About SimpleX")
|
||||||
m.users = try listUsers()
|
|
||||||
try getUserChatData()
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
switch error as? ChatResponse {
|
|
||||||
case .chatCmdError(_, .errorStore(.duplicateName)),
|
|
||||||
.chatCmdError(_, .error(.userExists)):
|
|
||||||
if m.currentUser == nil {
|
|
||||||
AlertManager.shared.showAlert(duplicateUserAlert)
|
|
||||||
} else {
|
|
||||||
alert = .duplicateUserError
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
|
||||||
if m.currentUser == nil {
|
|
||||||
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
|
||||||
} else {
|
|
||||||
alert = .createUserError(error: err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.error("Failed to create user or start chat: \(responseError(error))")
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Create")
|
||||||
|
Image(systemName: "greaterthan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(!canCreateProfile(displayName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func canCreateProfile() -> Bool {
|
private func showAlert(_ alert: UserProfileAlert) {
|
||||||
displayName != "" && validDisplayName(displayName)
|
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
|
||||||
}
|
|
||||||
|
|
||||||
private var duplicateUserAlert: Alert {
|
|
||||||
Alert(
|
|
||||||
title: Text("Duplicate display name!"),
|
|
||||||
message: Text("You already have a chat profile with the same display name. Please choose another name.")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
|
||||||
Alert(
|
|
||||||
title: Text("Error creating profile!"),
|
|
||||||
message: Text(err)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) {
|
||||||
|
hideKeyboard()
|
||||||
|
let profile = Profile(
|
||||||
|
displayName: displayName.trimmingCharacters(in: .whitespaces),
|
||||||
|
fullName: ""
|
||||||
|
)
|
||||||
|
let m = ChatModel.shared
|
||||||
|
do {
|
||||||
|
m.currentUser = try apiCreateActiveUser(profile)
|
||||||
|
if m.users.isEmpty {
|
||||||
|
try startChat()
|
||||||
|
withAnimation {
|
||||||
|
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
|
||||||
|
m.onboardingStage = .step3_CreateSimpleXAddress
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onboardingStageDefault.set(.onboardingComplete)
|
||||||
|
m.onboardingStage = .onboardingComplete
|
||||||
|
dismiss()
|
||||||
|
m.users = try listUsers()
|
||||||
|
try getUserChatData()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
switch error as? ChatResponse {
|
||||||
|
case .chatCmdError(_, .errorStore(.duplicateName)),
|
||||||
|
.chatCmdError(_, .error(.userExists)):
|
||||||
|
if m.currentUser == nil {
|
||||||
|
AlertManager.shared.showAlert(duplicateUserAlert)
|
||||||
|
} else {
|
||||||
|
showAlert(.duplicateUserError)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||||
|
if m.currentUser == nil {
|
||||||
|
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
||||||
|
} else {
|
||||||
|
showAlert(.createUserError(error: err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.error("Failed to create user or start chat: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func canCreateProfile(_ displayName: String) -> Bool {
|
||||||
|
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||||
|
return name != "" && mkValidName(name) == name
|
||||||
|
}
|
||||||
|
|
||||||
|
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
|
||||||
|
switch alert {
|
||||||
|
case .duplicateUserError: return duplicateUserAlert
|
||||||
|
case let .createUserError(err): return creatUserErrorAlert(err)
|
||||||
|
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var duplicateUserAlert: Alert {
|
||||||
|
Alert(
|
||||||
|
title: Text("Duplicate display name!"),
|
||||||
|
message: Text("You already have a chat profile with the same display name. Please choose another name.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
||||||
|
Alert(
|
||||||
|
title: Text("Error creating profile!"),
|
||||||
|
message: Text(err)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInvalidNameAlert(_ name: String, _ displayName: Binding<String>) -> Alert {
|
||||||
|
name == ""
|
||||||
|
? Alert(title: Text("Invalid name!"))
|
||||||
|
: Alert(
|
||||||
|
title: Text("Invalid name!"),
|
||||||
|
message: Text("Correct name to \(name)?"),
|
||||||
|
primaryButton: .default(
|
||||||
|
Text("Ok"),
|
||||||
|
action: { displayName.wrappedValue = name }
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func validDisplayName(_ name: String) -> Bool {
|
func validDisplayName(_ name: String) -> Bool {
|
||||||
name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#"
|
mkValidName(name.trimmingCharacters(in: .whitespaces)) == name
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkValidName(_ s: String) -> String {
|
||||||
|
var c = s.cString(using: .utf8)!
|
||||||
|
return fromCString(chat_valid_name(&c)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateProfile_Previews: PreviewProvider {
|
struct CreateProfile_Previews: PreviewProvider {
|
||||||
|
@ -14,7 +14,7 @@ struct OnboardingView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
switch onboarding {
|
switch onboarding {
|
||||||
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
|
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
|
||||||
case .step2_CreateProfile: CreateProfile()
|
case .step2_CreateProfile: CreateFirstProfile()
|
||||||
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
|
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
|
||||||
case .step4_SetNotificationsMode: SetNotificationsMode()
|
case .step4_SetNotificationsMode: SetNotificationsMode()
|
||||||
case .onboardingComplete: EmptyView()
|
case .onboardingComplete: EmptyView()
|
||||||
|
@ -381,7 +381,9 @@ struct ProfilePreview: View {
|
|||||||
Text(profileOf.displayName)
|
Text(profileOf.displayName)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
Text(profileOf.fullName)
|
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
|
||||||
|
Text(profileOf.fullName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ struct UserProfile: View {
|
|||||||
@State private var showImagePicker = false
|
@State private var showImagePicker = false
|
||||||
@State private var showTakePhoto = false
|
@State private var showTakePhoto = false
|
||||||
@State private var chosenImage: UIImage? = nil
|
@State private var chosenImage: UIImage? = nil
|
||||||
|
@State private var alert: UserProfileAlert?
|
||||||
|
@FocusState private var focusDisplayName
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let user: User = chatModel.currentUser!
|
let user: User = chatModel.currentUser!
|
||||||
@ -47,18 +49,27 @@ struct UserProfile: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
if !validDisplayName(profile.displayName) {
|
if !validNewProfileName(user) {
|
||||||
Image(systemName: "exclamationmark.circle")
|
Button {
|
||||||
.foregroundColor(.red)
|
alert = .invalidNameError(validName: mkValidName(profile.displayName))
|
||||||
.padding(.bottom, 10)
|
} label: {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||||
}
|
}
|
||||||
profileNameTextEdit("Display name", $profile.displayName)
|
profileNameTextEdit("Profile name", $profile.displayName)
|
||||||
|
.focused($focusDisplayName)
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
if showFullName(user) {
|
||||||
|
profileNameTextEdit("Full name (optional)", $profile.fullName)
|
||||||
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
profileNameTextEdit("Full name (optional)", $profile.fullName)
|
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
Button("Cancel") { editProfile = false }
|
Button("Cancel") { editProfile = false }
|
||||||
Button("Save (and notify contacts)") { saveProfile() }
|
Button("Save (and notify contacts)") { saveProfile() }
|
||||||
.disabled(profile.displayName == "" || !validDisplayName(profile.displayName))
|
.disabled(!canSaveProfile(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||||
@ -74,11 +85,14 @@ struct UserProfile: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
profileNameView("Display name:", user.profile.displayName)
|
profileNameView("Profile name:", user.profile.displayName)
|
||||||
profileNameView("Full name:", user.profile.fullName)
|
if showFullName(user) {
|
||||||
|
profileNameView("Full name:", user.profile.fullName)
|
||||||
|
}
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
profile = fromLocalProfile(user.profile)
|
profile = fromLocalProfile(user.profile)
|
||||||
editProfile = true
|
editProfile = true
|
||||||
|
focusDisplayName = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||||
@ -117,14 +131,12 @@ struct UserProfile: View {
|
|||||||
profile.image = nil
|
profile.image = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
||||||
TextField(label, text: name)
|
TextField(label, text: name)
|
||||||
.textInputAutocapitalization(.never)
|
.padding(.leading, 32)
|
||||||
.disableAutocorrection(true)
|
|
||||||
.padding(.bottom)
|
|
||||||
.padding(.leading, 28)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
|
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
|
||||||
@ -141,9 +153,22 @@ struct UserProfile: View {
|
|||||||
showChooseSource = true
|
showChooseSource = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func validNewProfileName(_ user: User) -> Bool {
|
||||||
|
profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showFullName(_ user: User) -> Bool {
|
||||||
|
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
private func canSaveProfile(_ user: User) -> Bool {
|
||||||
|
profile.displayName != "" && validNewProfileName(user)
|
||||||
|
}
|
||||||
|
|
||||||
func saveProfile() {
|
func saveProfile() {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||||
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
|
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
chatModel.updateCurrentUser(newProfile)
|
chatModel.updateCurrentUser(newProfile)
|
||||||
|
@ -23,6 +23,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
|
|||||||
extern char *chat_parse_markdown(char *str);
|
extern char *chat_parse_markdown(char *str);
|
||||||
extern char *chat_parse_server(char *str);
|
extern char *chat_parse_server(char *str);
|
||||||
extern char *chat_password_hash(char *pwd, char *salt);
|
extern char *chat_password_hash(char *pwd, char *salt);
|
||||||
|
extern char *chat_valid_name(char *name);
|
||||||
extern char *chat_encrypt_media(char *key, char *frame, int len);
|
extern char *chat_encrypt_media(char *key, char *frame, int len);
|
||||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user