diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 4218e94224..55d7b626fa 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -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") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 1617edd11f..4de3608735 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -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) -> 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() diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 7b01fe0300..373311073a 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -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) { diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 59eee1338b..a7e61b3105 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -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() } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index e5263813fa..98f43f49b2 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -125,7 +125,7 @@ struct NewChatSheet: View { } NavigationLink { AddGroupView() - .navigationTitle("Create secret group") + .navigationTitle("Create group") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 9aa42930bf..aa2c04ccaa 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -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 { diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index a6025879eb..70cd739531 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -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." = "Отправьте из галереи или из дополнительных клавиатур."; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index e670fae5ef..91f7af2b95 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -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)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index 0ed32f845b..74c9a55ecf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -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 {