mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
ui: smaller QR code for verify code view, change iOS layout (#5948)
* ui: smaller QR code for verify code view, change iOS layout * ios: fix layout for editing group profile
This commit is contained in:
parent
cbaab06975
commit
686145ba36
9 changed files with 167 additions and 180 deletions
|
@ -633,9 +633,6 @@ struct GroupChatInfoView: View {
|
||||||
groupInfo: $groupInfo,
|
groupInfo: $groupInfo,
|
||||||
groupProfile: groupInfo.groupProfile
|
groupProfile: groupInfo.groupProfile
|
||||||
)
|
)
|
||||||
.navigationBarTitle("Group profile")
|
|
||||||
.modifier(ThemedBackground())
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
} label: {
|
} label: {
|
||||||
Label("Edit group profile", systemImage: "pencil")
|
Label("Edit group profile", systemImage: "pencil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ struct GroupProfileView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@Binding var groupInfo: GroupInfo
|
@Binding var groupInfo: GroupInfo
|
||||||
@State var groupProfile: GroupProfile
|
@State var groupProfile: GroupProfile
|
||||||
|
@State private var currentProfileHash: Int?
|
||||||
@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
|
||||||
|
@ -34,60 +35,40 @@ struct GroupProfileView: View {
|
||||||
@FocusState private var focusDisplayName
|
@FocusState private var focusDisplayName
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
return VStack(alignment: .leading) {
|
List {
|
||||||
Text("Group profile is stored on members' devices, not on the servers.")
|
EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource)
|
||||||
.padding(.vertical)
|
.if(!focusDisplayName) { $0.padding(.top) }
|
||||||
|
|
||||||
ZStack(alignment: .center) {
|
Section {
|
||||||
ZStack(alignment: .topTrailing) {
|
HStack {
|
||||||
profileImageView(groupProfile.image)
|
TextField("Group display name", text: $groupProfile.displayName)
|
||||||
if groupProfile.image != nil {
|
.focused($focusDisplayName)
|
||||||
Button {
|
if !validNewProfileName {
|
||||||
groupProfile.image = nil
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "multiply")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editImageButton { showChooseSource = true }
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
ZStack(alignment: .topLeading) {
|
|
||||||
if !validNewProfileName() {
|
|
||||||
Button {
|
Button {
|
||||||
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
|
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
|
||||||
}
|
}
|
||||||
profileNameTextEdit("Group display name", $groupProfile.displayName)
|
|
||||||
.focused($focusDisplayName)
|
|
||||||
}
|
}
|
||||||
.padding(.bottom)
|
|
||||||
let fullName = groupInfo.groupProfile.fullName
|
let fullName = groupInfo.groupProfile.fullName
|
||||||
if fullName != "" && fullName != groupProfile.displayName {
|
if fullName != "" && fullName != groupProfile.displayName {
|
||||||
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
|
TextField("Group full name (optional)", text: $groupProfile.fullName)
|
||||||
.padding(.bottom)
|
|
||||||
}
|
}
|
||||||
HStack(spacing: 20) {
|
} footer: {
|
||||||
Button("Cancel") { dismiss() }
|
Text("Group profile is stored on members' devices, not on the servers.")
|
||||||
Button("Save group profile") { saveProfile() }
|
|
||||||
.disabled(!canUpdateProfile())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button("Reset") {
|
||||||
|
groupProfile = groupInfo.groupProfile
|
||||||
|
currentProfileHash = groupProfile.hashValue
|
||||||
|
}
|
||||||
|
.disabled(currentProfileHash == groupProfile.hashValue)
|
||||||
|
Button("Save group profile", action: saveProfile)
|
||||||
|
.disabled(!canUpdateProfile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
|
||||||
.confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
.confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||||
Button("Take picture") {
|
Button("Take picture") {
|
||||||
showTakePhoto = true
|
showTakePhoto = true
|
||||||
|
@ -95,6 +76,11 @@ struct GroupProfileView: View {
|
||||||
Button("Choose from library") {
|
Button("Choose from library") {
|
||||||
showImagePicker = true
|
showImagePicker = true
|
||||||
}
|
}
|
||||||
|
if UIPasteboard.general.hasImages {
|
||||||
|
Button("Paste image") {
|
||||||
|
chosenImage = UIPasteboard.general.image
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $showTakePhoto) {
|
.fullScreenCover(isPresented: $showTakePhoto) {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
@ -120,8 +106,20 @@ struct GroupProfileView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
currentProfileHash = groupProfile.hashValue
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
focusDisplayName = true
|
withAnimation { focusDisplayName = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if canUpdateProfile {
|
||||||
|
showAlert(
|
||||||
|
title: NSLocalizedString("Save group profile?", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"),
|
||||||
|
buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"),
|
||||||
|
buttonAction: saveProfile,
|
||||||
|
cancelButton: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $alert) { a in
|
.alert(item: $alert) { a in
|
||||||
|
@ -135,30 +133,30 @@ struct GroupProfileView: View {
|
||||||
return createInvalidNameAlert(name, $groupProfile.displayName)
|
return createInvalidNameAlert(name, $groupProfile.displayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.navigationBarTitle("Group profile")
|
||||||
.onTapGesture { hideKeyboard() }
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
.navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canUpdateProfile() -> Bool {
|
private var canUpdateProfile: Bool {
|
||||||
groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName()
|
currentProfileHash != groupProfile.hashValue &&
|
||||||
|
groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
|
||||||
|
validNewProfileName
|
||||||
}
|
}
|
||||||
|
|
||||||
private func validNewProfileName() -> Bool {
|
private var validNewProfileName: Bool {
|
||||||
groupProfile.displayName == groupInfo.groupProfile.displayName
|
groupProfile.displayName == groupInfo.groupProfile.displayName
|
||||||
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
|
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
|
||||||
TextField(label, text: name)
|
|
||||||
.padding(.leading, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveProfile() {
|
func saveProfile() {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
|
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
|
||||||
|
groupProfile.fullName = groupProfile.fullName.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 {
|
||||||
|
currentProfileHash = groupProfile.hashValue
|
||||||
groupInfo = gInfo
|
groupInfo = gInfo
|
||||||
chatModel.updateGroup(gInfo)
|
chatModel.updateGroup(gInfo)
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|
|
@ -24,45 +24,37 @@ struct VerifyCodeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func verifyCodeView(_ code: String) -> some View {
|
private func verifyCodeView(_ code: String) -> some View {
|
||||||
ScrollView {
|
|
||||||
let splitCode = splitToParts(code, length: 24)
|
let splitCode = splitToParts(code, length: 24)
|
||||||
VStack(alignment: .leading) {
|
return List {
|
||||||
Group {
|
Section {
|
||||||
HStack {
|
QRCode(uri: code, small: true)
|
||||||
if connectionVerified {
|
|
||||||
Image(systemName: "checkmark.shield")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
Text("\(displayName) is verified")
|
|
||||||
} else {
|
|
||||||
Text("\(displayName) is not verified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 24)
|
|
||||||
|
|
||||||
QRCode(uri: code)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
Text(splitCode)
|
Text(splitCode)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.font(.body.monospaced())
|
.font(.body.monospaced())
|
||||||
.lineLimit(20)
|
.lineLimit(20)
|
||||||
.padding(.bottom, 8)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
} header: {
|
||||||
|
if connectionVerified {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.shield").foregroundColor(theme.colors.secondary)
|
||||||
|
Text("\(displayName) is verified").textCase(.none)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("\(displayName) is not verified").textCase(.none)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.")
|
Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.")
|
||||||
.padding(.bottom)
|
}
|
||||||
|
|
||||||
Group {
|
Section {
|
||||||
if connectionVerified {
|
if connectionVerified {
|
||||||
Button {
|
Button {
|
||||||
verifyCode(nil)
|
verifyCode(nil)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Clear verification", systemImage: "shield")
|
Label("Clear verification", systemImage: "shield")
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
} else {
|
} else {
|
||||||
HStack {
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
@ -71,7 +63,6 @@ struct VerifyCodeView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("Scan code", systemImage: "qrcode")
|
Label("Scan code", systemImage: "qrcode")
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
Button {
|
Button {
|
||||||
verifyCode(code) { verified in
|
verifyCode(code) { verified in
|
||||||
if !verified { showCodeError = true }
|
if !verified { showCodeError = true }
|
||||||
|
@ -79,17 +70,12 @@ struct VerifyCodeView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("Mark verified", systemImage: "checkmark.shield")
|
Label("Mark verified", systemImage: "checkmark.shield")
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.alert(isPresented: $showCodeError) {
|
.alert(isPresented: $showCodeError) {
|
||||||
Alert(title: Text("Incorrect security code!"))
|
Alert(title: Text("Incorrect security code!"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
@ -103,7 +89,6 @@ struct VerifyCodeView: View {
|
||||||
if connectionVerified { dismiss() }
|
if connectionVerified { dismiss() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) {
|
private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) {
|
||||||
if let (verified, existingCode) = verify(code) {
|
if let (verified, existingCode) = verify(code) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ enum DatabaseAlert: Identifiable {
|
||||||
case deleteLegacyDatabase
|
case deleteLegacyDatabase
|
||||||
case deleteFilesAndMedia
|
case deleteFilesAndMedia
|
||||||
case setChatItemTTL(ttl: ChatItemTTL)
|
case setChatItemTTL(ttl: ChatItemTTL)
|
||||||
case error(title: LocalizedStringKey, error: String = "")
|
case error(title: String, error: String = "")
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -456,7 +456,7 @@ struct DatabaseView: View {
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
alert = .error(title: NSLocalizedString("Error exporting chat database", comment: "alert title"), error: responseError(error))
|
||||||
progressIndicator = false
|
progressIndicator = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -492,10 +492,10 @@ struct DatabaseView: View {
|
||||||
return migration
|
return migration
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
|
await operationEnded(.error(title: NSLocalizedString("Error importing chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert)
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
|
await operationEnded(.error(title: NSLocalizedString("Error deleting chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showAlert("Error accessing database file")
|
showAlert("Error accessing database file")
|
||||||
|
@ -513,7 +513,7 @@ struct DatabaseView: View {
|
||||||
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
|
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
|
||||||
return true
|
return true
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert)
|
await DatabaseView.operationEnded(.error(title: NSLocalizedString("Error deleting database", comment: "alert title"), error: responseError(error)), $progressIndicator, $alert)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -522,7 +522,7 @@ struct DatabaseView: View {
|
||||||
if removeLegacyDatabaseAndFiles() {
|
if removeLegacyDatabaseAndFiles() {
|
||||||
legacyDatabase = false
|
legacyDatabase = false
|
||||||
} else {
|
} else {
|
||||||
alert = .error(title: "Error deleting old database")
|
alert = .error(title: NSLocalizedString("Error deleting old database", comment: "alert title"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,7 +546,7 @@ struct DatabaseView: View {
|
||||||
let (title, message) = chatDeletedAlertText()
|
let (title, message) = chatDeletedAlertText()
|
||||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||||
} else if case let .error(title, error) = dbAlert {
|
} else if case let .error(title, error) = dbAlert {
|
||||||
showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] })
|
showAlert(title, message: error, actions: { [okAlertActionWaiting] })
|
||||||
} else {
|
} else {
|
||||||
alert.wrappedValue = dbAlert
|
alert.wrappedValue = dbAlert
|
||||||
cont.resume()
|
cont.resume()
|
||||||
|
@ -567,7 +567,7 @@ struct DatabaseView: View {
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
alert = .error(title: "Error changing setting", error: responseError(error))
|
alert = .error(title: NSLocalizedString("Error changing setting", comment: "alert title"), error: responseError(error))
|
||||||
chatItemTTL = currentChatItemTTL
|
chatItemTTL = currentChatItemTTL
|
||||||
afterSetCiTTL()
|
afterSetCiTTL()
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ struct NewChatSheet: View {
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
AddGroupView()
|
AddGroupView()
|
||||||
.navigationTitle("Create secret group")
|
.navigationTitle("Create group")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -25,28 +25,8 @@ struct UserProfile: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Group {
|
EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource)
|
||||||
if profile.image != nil {
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
profileImageView(profile.image)
|
|
||||||
.onTapGesture { showChooseSource = true }
|
|
||||||
overlayButton("multiply", edge: .top) { profile.image = nil }
|
|
||||||
}
|
|
||||||
overlayButton("camera", edge: .bottom) { showChooseSource = true }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
profileImageView(profile.image)
|
|
||||||
editImageButton { showChooseSource = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -133,25 +113,6 @@ struct UserProfile: View {
|
||||||
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func overlayButton(
|
|
||||||
_ systemName: String,
|
|
||||||
edge: Edge.Set,
|
|
||||||
action: @escaping () -> Void
|
|
||||||
) -> some View {
|
|
||||||
Image(systemName: systemName)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(height: 12)
|
|
||||||
.foregroundColor(theme.colors.primary)
|
|
||||||
.padding(6)
|
|
||||||
.frame(width: 36, height: 36, alignment: .center)
|
|
||||||
.background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5))
|
|
||||||
.clipShape(Circle())
|
|
||||||
.contentShape(Circle())
|
|
||||||
.padding([.trailing, edge], -12)
|
|
||||||
.onTapGesture(perform: action)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showFullName(_ user: User) -> Bool {
|
private func showFullName(_ user: User) -> Bool {
|
||||||
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
|
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
|
||||||
}
|
}
|
||||||
|
@ -189,8 +150,54 @@ struct UserProfile: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileImageView(_ imageStr: String?) -> some View {
|
struct EditProfileImage: View {
|
||||||
ProfileImage(imageStr: imageStr, size: 192)
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||||
|
@Binding var profileImage: String?
|
||||||
|
@Binding var showChooseSource: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if profileImage != nil {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
ProfileImage(imageStr: profileImage, size: 160)
|
||||||
|
.onTapGesture { showChooseSource = true }
|
||||||
|
overlayButton("multiply", edge: .top) { profileImage = nil }
|
||||||
|
}
|
||||||
|
overlayButton("camera", edge: .bottom) { showChooseSource = true }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
ProfileImage(imageStr: profileImage, size: 160)
|
||||||
|
editImageButton { showChooseSource = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func overlayButton(
|
||||||
|
_ systemName: String,
|
||||||
|
edge: Edge.Set,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) -> some View {
|
||||||
|
Image(systemName: systemName)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(height: 12)
|
||||||
|
.foregroundColor(theme.colors.primary)
|
||||||
|
.padding(6)
|
||||||
|
.frame(width: 36, height: 36, alignment: .center)
|
||||||
|
.background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5))
|
||||||
|
.clipShape(Circle())
|
||||||
|
.contentShape(Circle())
|
||||||
|
.padding([.trailing, edge], -12)
|
||||||
|
.onTapGesture(perform: action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func editImageButton(action: @escaping () -> Void) -> some View {
|
func editImageButton(action: @escaping () -> Void) -> some View {
|
||||||
|
|
|
@ -1596,13 +1596,13 @@ set passcode view */
|
||||||
|
|
||||||
/* delete after time
|
/* delete after time
|
||||||
pref value */
|
pref value */
|
||||||
"default (%@)" = "по умолчанию (%@)";
|
"default (%@)" = "базовый (%@)";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"default (no)" = "по умолчанию (нет)";
|
"default (no)" = "базовый (нет)";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"default (yes)" = "по умолчанию (да)";
|
"default (yes)" = "базовый (да)";
|
||||||
|
|
||||||
/* alert action
|
/* alert action
|
||||||
swipe action */
|
swipe action */
|
||||||
|
@ -1705,7 +1705,7 @@ swipe action */
|
||||||
"Delete messages" = "Удалить сообщения";
|
"Delete messages" = "Удалить сообщения";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Delete messages after" = "Удалять сообщения через";
|
"Delete messages after" = "Удалять сообщения";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Delete old database" = "Удалить предыдущую версию данных";
|
"Delete old database" = "Удалить предыдущую версию данных";
|
||||||
|
@ -4633,7 +4633,7 @@ chat item action */
|
||||||
"Send questions and ideas" = "Отправьте вопросы и идеи";
|
"Send questions and ideas" = "Отправьте вопросы и идеи";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Send receipts" = "Отправлять отчёты о доставке";
|
"Send receipts" = "Отчёты о доставке";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур.";
|
"Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур.";
|
||||||
|
|
|
@ -68,7 +68,7 @@ private fun VerifyCodeLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QRCode(connectionCode, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF))
|
QRCode(connectionCode, small = true, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF))
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(Modifier.weight(2f))
|
Spacer(Modifier.weight(2f))
|
||||||
|
|
|
@ -84,7 +84,7 @@ fun QRCode(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.widthIn(max = 400.dp)
|
.widthIn(max = 400.dp)
|
||||||
.fillMaxWidth(if (small) 0.67f else 1f)
|
.fillMaxWidth(if (small) 0.63f else 1f)
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
.clickable {
|
.clickable {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue