ios: profile names (remove full name) (#3168)

* ios: profile names (remove full name)

* create/update groups

* focus display name
This commit is contained in:
Evgeny Poberezkin 2023-10-04 17:45:39 +01:00 committed by GitHub
parent 91fc238ddc
commit 0d8558a6d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 315 additions and 192 deletions

View File

@ -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)")
} }
} }

View File

@ -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))
} }
} }

View File

@ -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 {

View File

@ -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()

View File

@ -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)
}
} }
} }
} }

View File

@ -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)

View File

@ -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);