mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-09 09:35:55 +00:00
ios: offer creating a profile for an invitation
Mirrors the Kotlin flow: both pickers get an "Add profile" row, and CreateProfile gains an optional onCreate so the same form is reused. Each picker's create function is five lines that end in the path a row tap already takes - changeProfile(newUser) for a prepared chat, selectedProfile plus .switchingUser for a one-time link. The sheets are attached to body rather than to a row, because rows live in a LazyVStack and profilePicker() is swapped for currentSelection() when listExpanded flips. Surface 1's picker height is computed from the row count, so it needs one more row. The list is refreshed after creating, for the same reason as Kotlin, and interactive dismissal is blocked while the create is in flight so backing out cannot leave the invitation moved. Not compiled: no Swift toolchain available.
This commit is contained in:
@@ -14,7 +14,7 @@ import SwiftUI
|
||||
// Spec: spec/api.md#ChatCommand
|
||||
enum ChatCommand: ChatCmdProtocol {
|
||||
case showActiveUser
|
||||
case createActiveUser(profile: Profile?, pastTimestamp: Bool)
|
||||
case createActiveUser(profile: Profile?, pastTimestamp: Bool, keepActiveUser: Bool)
|
||||
case listUsers
|
||||
case apiSetActiveUser(userId: Int64, viewPwd: String?)
|
||||
case setAllContactReceipts(enable: Bool)
|
||||
@@ -202,8 +202,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
get {
|
||||
switch self {
|
||||
case .showActiveUser: return "/u"
|
||||
case let .createActiveUser(profile, pastTimestamp):
|
||||
let user = NewUser(profile: profile, pastTimestamp: pastTimestamp)
|
||||
case let .createActiveUser(profile, pastTimestamp, keepActiveUser):
|
||||
let user = NewUser(profile: profile, pastTimestamp: pastTimestamp, keepActiveUser: keepActiveUser)
|
||||
return "/_create user \(encodeJSON(user))"
|
||||
case .listUsers: return "/users"
|
||||
case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
|
||||
@@ -1367,6 +1367,7 @@ struct NewUser: Encodable {
|
||||
var profile: Profile?
|
||||
var pastTimestamp: Bool
|
||||
var userChatRelay: Bool = false
|
||||
var keepActiveUser: Bool = false
|
||||
}
|
||||
|
||||
enum ChatPagination {
|
||||
|
||||
@@ -253,8 +253,8 @@ func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCreateActiveUser(_ p: Profile?, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User {
|
||||
let r: ChatResponse0 = try chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp), ctrl: ctrl)
|
||||
func apiCreateActiveUser(_ p: Profile?, pastTimestamp: Bool = false, keepActiveUser: Bool = false, ctrl: chat_ctrl? = nil) throws -> User {
|
||||
let r: ChatResponse0 = try chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp, keepActiveUser: keepActiveUser), ctrl: ctrl)
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ struct ContextProfilePickerView: View {
|
||||
@State private var listExpanded = false
|
||||
@State private var expandedListReady = false
|
||||
@State private var showIncognitoSheet = false
|
||||
@State private var showAddProfile = false
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
|
||||
@@ -34,6 +35,11 @@ struct ContextProfilePickerView: View {
|
||||
.sheet(isPresented: $showIncognitoSheet) {
|
||||
IncognitoHelp()
|
||||
}
|
||||
.sheet(isPresented: $showAddProfile) {
|
||||
NavigationView {
|
||||
CreateProfile(onCreate: createProfileForChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func viewBody() -> some View {
|
||||
@@ -79,6 +85,12 @@ struct ContextProfilePickerView: View {
|
||||
if expandedListReady {
|
||||
let scroll = ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
addProfileOption()
|
||||
.contentShape(Rectangle())
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
.padding(.leading, 48)
|
||||
|
||||
let otherUsers = users
|
||||
.filter { u in u.userId != selectedUser.userId }
|
||||
.sorted(using: KeyPathComparator<User>(\.activeOrder))
|
||||
@@ -113,7 +125,7 @@ struct ContextProfilePickerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 1))) // + 1 for incognito
|
||||
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 2)))
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(nil) {
|
||||
@@ -193,6 +205,42 @@ struct ContextProfilePickerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func addProfileOption() -> some View {
|
||||
Button {
|
||||
if chat.chatInfo.profileChangeProhibited {
|
||||
showCantChangeProfileAlert()
|
||||
} else {
|
||||
showAddProfile = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.resizable().scaledToFit().frame(width: 38, height: 38)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
Text("Add profile")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing)
|
||||
.frame(height: USER_ROW_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
private func createProfileForChat(_ profile: Profile) async throws {
|
||||
let newUser = try apiCreateActiveUser(profile, keepActiveUser: true)
|
||||
let updatedUsers = try? await listUsersAsync()
|
||||
await MainActor.run {
|
||||
showAddProfile = false
|
||||
if let updatedUsers {
|
||||
chatModel.users = updatedUsers
|
||||
users = updatedUsers.map { $0.user }.filter { u in u.activeUser || !u.hidden }
|
||||
}
|
||||
changeProfile(newUser)
|
||||
}
|
||||
}
|
||||
|
||||
private func changeProfile(_ newUser: User) {
|
||||
Task {
|
||||
do {
|
||||
|
||||
@@ -397,6 +397,7 @@ private struct ActiveProfilePicker: View {
|
||||
@State private var searchTextOrPassword = ""
|
||||
@State private var showIncognitoSheet = false
|
||||
@State private var incognitoFirst: Bool = false
|
||||
@State private var showAddProfile = false
|
||||
@State var selectedProfile: User
|
||||
var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)}
|
||||
|
||||
@@ -558,6 +559,36 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var addProfileOption: some View {
|
||||
Button {
|
||||
showAddProfile = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.resizable().scaledToFit().frame(width: 30, height: 30)
|
||||
.padding(.trailing, 2)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
Text("Add profile")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createProfileForConnection(_ profile: Profile) async throws {
|
||||
let newUser = try apiCreateActiveUser(profile, keepActiveUser: true)
|
||||
let updatedUsers = try? await listUsersAsync()
|
||||
await MainActor.run {
|
||||
showAddProfile = false
|
||||
if let updatedUsers {
|
||||
chatModel.users = updatedUsers
|
||||
profiles = updatedUsers.map { $0.user }
|
||||
}
|
||||
selectedProfile = newUser
|
||||
profileSwitchStatus = .switchingUser
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func profilePicker() -> some View {
|
||||
let incognitoOption = Button {
|
||||
if !incognitoEnabled {
|
||||
@@ -610,8 +641,17 @@ private struct ActiveProfilePicker: View {
|
||||
profilerPickerUserOption(p)
|
||||
}
|
||||
}
|
||||
|
||||
if contactConnection != nil {
|
||||
addProfileOption
|
||||
}
|
||||
}
|
||||
.opacity(switchingProfileByTimeout ? 0.4 : 1)
|
||||
.sheet(isPresented: $showAddProfile) {
|
||||
NavigationView {
|
||||
CreateProfile(onCreate: createProfileForConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ enum UserProfileAlert: Identifiable {
|
||||
let MAX_BIO_LENGTH_BYTES = 160
|
||||
|
||||
struct CreateProfile: View {
|
||||
var onCreate: ((Profile) async throws -> Void)? = nil
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var submitting = false
|
||||
@State private var displayName: String = ""
|
||||
@State private var profileBio: String = ""
|
||||
@FocusState private var focusDisplayName
|
||||
@@ -104,7 +106,7 @@ struct CreateProfile: View {
|
||||
Button(action: createProfile) {
|
||||
settingsRow("checkmark", color: theme.colors.primary) { Text("Create profile") }
|
||||
}
|
||||
.disabled(!canCreateProfile(displayName) || !bioFitsLimit())
|
||||
.disabled(submitting || !canCreateProfile(displayName) || !bioFitsLimit())
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Your profile is stored on your device and only shared with your contacts.")
|
||||
@@ -115,6 +117,7 @@ struct CreateProfile: View {
|
||||
.compactSectionSpacing()
|
||||
}
|
||||
.navigationTitle("Create your profile")
|
||||
.interactiveDismissDisabled(submitting)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
|
||||
.confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||
@@ -168,6 +171,18 @@ struct CreateProfile: View {
|
||||
shortDescr: shortDescr,
|
||||
image: profileImage
|
||||
)
|
||||
if let onCreate {
|
||||
submitting = true
|
||||
Task {
|
||||
do {
|
||||
try await onCreate(profile)
|
||||
} catch let error {
|
||||
await MainActor.run { showCreateProfileAlert(showAlert: { alert = $0 }, error) }
|
||||
}
|
||||
await MainActor.run { submitting = false }
|
||||
}
|
||||
return
|
||||
}
|
||||
let m = ChatModel.shared
|
||||
do {
|
||||
AppChatState.shared.set(.active)
|
||||
|
||||
@@ -50,7 +50,7 @@ The `ChatCommand` enum ([`AppAPITypes.swift` L15](../Shared/Model/AppAPITypes.sw
|
||||
| Command | Parameters | Description | Source |
|
||||
|---------|-----------|-------------|--------|
|
||||
| `showActiveUser` | -- | Get current active user | [L16](../Shared/Model/AppAPITypes.swift#L16) |
|
||||
| `createActiveUser` | `profile: Profile?, pastTimestamp: Bool` | Create new user profile | [L17](../Shared/Model/AppAPITypes.swift#L17) |
|
||||
| `createActiveUser` | `profile: Profile?, pastTimestamp: Bool, keepActiveUser: Bool` | Create new user profile | [L17](../Shared/Model/AppAPITypes.swift#L17) |
|
||||
| `listUsers` | -- | List all user profiles | [L18](../Shared/Model/AppAPITypes.swift#L18) |
|
||||
| `apiSetActiveUser` | `userId: Int64, viewPwd: String?` | Switch active user | [L19](../Shared/Model/AppAPITypes.swift#L19) |
|
||||
| `apiHideUser` | `userId: Int64, viewPwd: String` | Hide user behind password | [L24](../Shared/Model/AppAPITypes.swift#L24) |
|
||||
|
||||
Reference in New Issue
Block a user