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:
Evgeny 2025-05-26 16:57:18 +01:00 committed by GitHub
parent cbaab06975
commit 686145ba36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 167 additions and 180 deletions

View file

@ -633,9 +633,6 @@ struct GroupChatInfoView: View {
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile
)
.navigationBarTitle("Group profile")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Edit group profile", systemImage: "pencil")
}

View file

@ -26,6 +26,7 @@ struct GroupProfileView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var groupInfo: GroupInfo
@State var groupProfile: GroupProfile
@State private var currentProfileHash: Int?
@State private var showChooseSource = false
@State private var showImagePicker = false
@State private var showTakePhoto = false
@ -34,60 +35,40 @@ struct GroupProfileView: View {
@FocusState private var focusDisplayName
var body: some View {
return VStack(alignment: .leading) {
Text("Group profile is stored on members' devices, not on the servers.")
.padding(.vertical)
List {
EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource)
.if(!focusDisplayName) { $0.padding(.top) }
ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) {
profileImageView(groupProfile.image)
if groupProfile.image != nil {
Button {
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() {
Section {
HStack {
TextField("Group display name", text: $groupProfile.displayName)
.focused($focusDisplayName)
if !validNewProfileName {
Button {
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
} label: {
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
if fullName != "" && fullName != groupProfile.displayName {
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
.padding(.bottom)
}
HStack(spacing: 20) {
Button("Cancel") { dismiss() }
Button("Save group profile") { saveProfile() }
.disabled(!canUpdateProfile())
TextField("Group full name (optional)", text: $groupProfile.fullName)
}
} footer: {
Text("Group profile is stored on members' devices, not on the servers.")
}
.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) {
Button("Take picture") {
showTakePhoto = true
@ -95,6 +76,11 @@ struct GroupProfileView: View {
Button("Choose from library") {
showImagePicker = true
}
if UIPasteboard.general.hasImages {
Button("Paste image") {
chosenImage = UIPasteboard.general.image
}
}
}
.fullScreenCover(isPresented: $showTakePhoto) {
ZStack {
@ -120,8 +106,20 @@ struct GroupProfileView: View {
}
}
.onAppear {
currentProfileHash = groupProfile.hashValue
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
@ -135,30 +133,30 @@ struct GroupProfileView: View {
return createInvalidNameAlert(name, $groupProfile.displayName)
}
}
.contentShape(Rectangle())
.onTapGesture { hideKeyboard() }
.navigationBarTitle("Group profile")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large)
}
private func canUpdateProfile() -> Bool {
groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName()
private var canUpdateProfile: Bool {
currentProfileHash != groupProfile.hashValue &&
groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
validNewProfileName
}
private func validNewProfileName() -> Bool {
private var validNewProfileName: Bool {
groupProfile.displayName == groupInfo.groupProfile.displayName
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
}
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name)
.padding(.leading, 32)
}
func saveProfile() {
Task {
do {
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
groupProfile.fullName = groupProfile.fullName.trimmingCharacters(in: .whitespaces)
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run {
currentProfileHash = groupProfile.hashValue
groupInfo = gInfo
chatModel.updateGroup(gInfo)
dismiss()

View file

@ -24,85 +24,70 @@ struct VerifyCodeView: View {
}
private func verifyCodeView(_ code: String) -> some View {
ScrollView {
let splitCode = splitToParts(code, length: 24)
VStack(alignment: .leading) {
Group {
let splitCode = splitToParts(code, length: 24)
return List {
Section {
QRCode(uri: code, small: true)
Text(splitCode)
.multilineTextAlignment(.leading)
.font(.body.monospaced())
.lineLimit(20)
.frame(maxWidth: .infinity, alignment: .center)
} header: {
if connectionVerified {
HStack {
if connectionVerified {
Image(systemName: "checkmark.shield")
.foregroundColor(theme.colors.secondary)
Text("\(displayName) is verified")
} else {
Text("\(displayName) is not verified")
}
Image(systemName: "checkmark.shield").foregroundColor(theme.colors.secondary)
Text("\(displayName) is verified").textCase(.none)
}
.frame(height: 24)
QRCode(uri: code)
.padding(.horizontal)
Text(splitCode)
.multilineTextAlignment(.leading)
.font(.body.monospaced())
.lineLimit(20)
.padding(.bottom, 8)
} else {
Text("\(displayName) is not verified").textCase(.none)
}
.frame(maxWidth: .infinity, alignment: .center)
} footer: {
Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.")
.padding(.bottom)
}
Group {
if connectionVerified {
Button {
verifyCode(nil)
} label: {
Label("Clear verification", systemImage: "shield")
}
.padding()
} else {
HStack {
NavigationLink {
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Scan code")
.modifier(ThemedBackground())
} label: {
Label("Scan code", systemImage: "qrcode")
}
.padding()
Button {
verifyCode(code) { verified in
if !verified { showCodeError = true }
}
} label: {
Label("Mark verified", systemImage: "checkmark.shield")
}
.padding()
.alert(isPresented: $showCodeError) {
Alert(title: Text("Incorrect security code!"))
}
}
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Section {
if connectionVerified {
Button {
showShareSheet(items: [splitCode])
verifyCode(nil)
} label: {
Image(systemName: "square.and.arrow.up")
Label("Clear verification", systemImage: "shield")
}
} else {
NavigationLink {
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Scan code")
.modifier(ThemedBackground())
} label: {
Label("Scan code", systemImage: "qrcode")
}
Button {
verifyCode(code) { verified in
if !verified { showCodeError = true }
}
} label: {
Label("Mark verified", systemImage: "checkmark.shield")
}
.alert(isPresented: $showCodeError) {
Alert(title: Text("Incorrect security code!"))
}
}
}
.onChange(of: connectionVerified) { _ in
if connectionVerified { dismiss() }
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showShareSheet(items: [splitCode])
} label: {
Image(systemName: "square.and.arrow.up")
}
}
}
.onChange(of: connectionVerified) { _ in
if connectionVerified { dismiss() }
}
}
private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) {

View file

@ -21,7 +21,7 @@ enum DatabaseAlert: Identifiable {
case deleteLegacyDatabase
case deleteFilesAndMedia
case setChatItemTTL(ttl: ChatItemTTL)
case error(title: LocalizedStringKey, error: String = "")
case error(title: String, error: String = "")
var id: String {
switch self {
@ -456,7 +456,7 @@ struct DatabaseView: View {
}
} catch let error {
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
}
}
@ -492,10 +492,10 @@ struct DatabaseView: View {
return migration
}
} 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 {
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 {
showAlert("Error accessing database file")
@ -513,7 +513,7 @@ struct DatabaseView: View {
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
return true
} 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
}
}
@ -522,7 +522,7 @@ struct DatabaseView: View {
if removeLegacyDatabaseAndFiles() {
legacyDatabase = false
} 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()
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
} else if case let .error(title, error) = dbAlert {
showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] })
showAlert(title, message: error, actions: { [okAlertActionWaiting] })
} else {
alert.wrappedValue = dbAlert
cont.resume()
@ -567,7 +567,7 @@ struct DatabaseView: View {
}
} catch {
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
afterSetCiTTL()
}

View file

@ -125,7 +125,7 @@ struct NewChatSheet: View {
}
NavigationLink {
AddGroupView()
.navigationTitle("Create secret group")
.navigationTitle("Create group")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {

View file

@ -25,28 +25,8 @@ struct UserProfile: View {
var body: some View {
List {
Group {
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)
.contentShape(Rectangle())
EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource)
.padding(.top)
Section {
HStack {
@ -133,25 +113,6 @@ struct UserProfile: View {
.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 {
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
}
@ -189,8 +150,54 @@ struct UserProfile: View {
}
}
func profileImageView(_ imageStr: String?) -> some View {
ProfileImage(imageStr: imageStr, size: 192)
struct EditProfileImage: View {
@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 {

View file

@ -1596,13 +1596,13 @@ set passcode view */
/* delete after time
pref value */
"default (%@)" = "по умолчанию (%@)";
"default (%@)" = "базовый (%@)";
/* No comment provided by engineer. */
"default (no)" = "по умолчанию (нет)";
"default (no)" = "базовый (нет)";
/* No comment provided by engineer. */
"default (yes)" = "по умолчанию (да)";
"default (yes)" = "базовый (да)";
/* alert action
swipe action */
@ -1705,7 +1705,7 @@ swipe action */
"Delete messages" = "Удалить сообщения";
/* No comment provided by engineer. */
"Delete messages after" = "Удалять сообщения через";
"Delete messages after" = "Удалять сообщения";
/* No comment provided by engineer. */
"Delete old database" = "Удалить предыдущую версию данных";
@ -4633,7 +4633,7 @@ chat item action */
"Send questions and ideas" = "Отправьте вопросы и идеи";
/* No comment provided by engineer. */
"Send receipts" = "Отправлять отчёты о доставке";
"Send receipts" = "Отчёты о доставке";
/* No comment provided by engineer. */
"Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур.";

View file

@ -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) {
Spacer(Modifier.weight(2f))

View file

@ -84,7 +84,7 @@ fun QRCode(
Modifier
.padding(padding)
.widthIn(max = 400.dp)
.fillMaxWidth(if (small) 0.67f else 1f)
.fillMaxWidth(if (small) 0.63f else 1f)
.aspectRatio(1f)
.then(modifier)
.clickable {