diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 65631954e5..305ad0a601 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -11,12 +11,10 @@ import SimpleXChat private enum NoticesSheet: Identifiable { case whatsNew(updatedConditions: Bool) - case updatedConditions var id: String { switch self { case .whatsNew: return "whatsNew" - case .updatedConditions: return "updatedConditions" } } } @@ -278,10 +276,8 @@ struct ContentView: View { let showWhatsNew = shouldShowWhatsNew() let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false noticesShown = showWhatsNew || showUpdatedConditions - if showWhatsNew { + if showWhatsNew || showUpdatedConditions { noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) - } else if showUpdatedConditions { - noticesSheetItem = .updatedConditions } } } @@ -300,13 +296,6 @@ struct ContentView: View { .if(updatedConditions) { v in v.task { await setConditionsNotified_() } } - case .updatedConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]) - ) - .modifier(ThemedBackground(grouped: true)) - .task { await setConditionsNotified_() } } } if chatModel.setDeliveryReceipts { diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 8523336d2b..45ef186671 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -43,26 +43,23 @@ struct OnboardingButtonStyle: ButtonStyle { } } -private enum ChooseServerOperatorsSheet: Identifiable { - case showInfo +private enum OnboardingConditionsViewSheet: Identifiable { case showConditions + case configureOperators var id: String { switch self { - case .showInfo: return "showInfo" case .showConditions: return "showConditions" + case .configureOperators: return "configureOperators" } } } -struct ChooseServerOperators: View { - @Environment(\.dismiss) var dismiss: DismissAction - @Environment(\.colorScheme) var colorScheme: ColorScheme +struct OnboardingConditionsView: View { @EnvironmentObject var theme: AppTheme - var onboarding: Bool @State private var serverOperators: [ServerOperator] = [] @State private var selectedOperatorIds = Set() - @State private var sheetItem: ChooseServerOperatorsSheet? = nil + @State private var sheetItem: OnboardingConditionsViewSheet? = nil @State private var notificationsModeNavLinkActive = false @State private var justOpened = true @@ -72,16 +69,192 @@ struct ChooseServerOperators: View { GeometryReader { g in ScrollView { VStack(alignment: .leading, spacing: 20) { - let title = Text("Server operators") + Text("Conditions of use") .font(.largeTitle) .bold() .frame(maxWidth: .infinity, alignment: .center) - - if onboarding { - title.padding(.top, 25) - } else { - title + .padding(.top, 25) + + Spacer() + + VStack(alignment: .leading, spacing: 20) { + Text("Private chats, groups and your contacts are not accessible to server operators.") + .lineSpacing(2) + .frame(maxWidth: .infinity, alignment: .leading) + Text(""" + By using SimpleX Chat you agree to: + - send only legal content in public groups. + - respect other users – no spam. + """) + .lineSpacing(2) + .frame(maxWidth: .infinity, alignment: .leading) + + Button("Privacy policy and conditions of use.") { + sheetItem = .showConditions + } + .frame(maxWidth: .infinity, alignment: .leading) } + .padding(.horizontal, 4) + + Spacer() + + VStack(spacing: 12) { + acceptConditionsButton() + + Button("Configure server operators") { + sheetItem = .configureOperators + } + .frame(minHeight: 40) + } + } + .frame(minHeight: g.size.height) + } + .onAppear { + if justOpened { + serverOperators = ChatModel.shared.conditions.serverOperators + selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) + justOpened = false + } + } + .sheet(item: $sheetItem) { item in + switch item { + case .showConditions: + SimpleConditionsView() + .modifier(ThemedBackground(grouped: true)) + case .configureOperators: + ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds) + .modifier(ThemedBackground()) + } + } + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxHeight: .infinity, alignment: .top) + .padding(25) + } + + private func continueToNextStep() { + onboardingStageDefault.set(.step4_SetNotificationsMode) + notificationsModeNavLinkActive = true + } + + func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View { + ZStack { + button() + + NavigationLink(isActive: $notificationsModeNavLinkActive) { + notificationsModeDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func notificationsModeDestinationView() -> some View { + SetNotificationsMode() + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground()) + } + + private func acceptConditionsButton() -> some View { + notificationsModeNavLinkButton { + Button { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let operatorIds = acceptForOperators.map { $0.operatorId } + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + } + if let enabledOperators = enabledOperators(r.serverOperators) { + let r2 = try await setServerOperators(operators: enabledOperators) + await MainActor.run { + ChatModel.shared.conditions = r2 + continueToNextStep() + } + } else { + await MainActor.run { + continueToNextStep() + } + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } label: { + Text("Accept") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + } + } + + private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { + var ops = operators + if !ops.isEmpty { + for i in 0.. + @State private var sheetItem: ChooseServerOperatorsSheet? = nil + + var body: some View { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Server operators") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 25) infoText() .frame(maxWidth: .infinity, alignment: .center) @@ -101,74 +274,25 @@ struct ChooseServerOperators: View { .padding(.horizontal, 16) Spacer() - - let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } - let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - + VStack(spacing: 8) { - if !reviewForOperators.isEmpty { - reviewConditionsButton() - } else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty { - setOperatorsButton() - } else { - continueButton() - } - if onboarding { - Group { - if reviewForOperators.isEmpty { - Button("Conditions of use") { - sheetItem = .showConditions - } - } else { - Text("Conditions of use") - .foregroundColor(.clear) - } - } - .font(.system(size: 17, weight: .semibold)) - .frame(minHeight: 40) - } - } - - if !onboarding && !reviewForOperators.isEmpty { - VStack(spacing: 8) { - reviewLaterButton() - ( - Text("Conditions will be accepted for enabled operators after 30 days.") - + textSpace - + Text("You can configure operators in Network & servers settings.") - ) - .multilineTextAlignment(.center) - .font(.footnote) - .padding(.horizontal, 32) - } - .frame(maxWidth: .infinity) - .disabled(!canReviewLater) - .padding(.bottom) + setOperatorsButton() + onboardingButtonPlaceholder() } } .frame(minHeight: g.size.height) } - .onAppear { - if justOpened { - serverOperators = ChatModel.shared.conditions.serverOperators - selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - justOpened = false - } - } .sheet(item: $sheetItem) { item in switch item { case .showInfo: ChooseServerOperatorsInfoView() - case .showConditions: - SimpleConditionsView() - .modifier(ThemedBackground(grouped: true)) } } .frame(maxHeight: .infinity, alignment: .top) } .frame(maxHeight: .infinity, alignment: .top) - .padding(onboarding ? 25 : 16) + .padding(25) + .interactiveDismissDisabled(selectedOperatorIds.isEmpty) } private func infoText() -> some View { @@ -213,181 +337,15 @@ struct ChooseServerOperators: View { } } - private func reviewConditionsButton() -> some View { - NavigationLink("Review conditions") { - reviewConditionsView() - .navigationTitle("Conditions of use") - .navigationBarTitleDisplayMode(.large) - .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } - .modifier(ThemedBackground(grouped: true)) + private func setOperatorsButton() -> some View { + Button { + dismiss() + } label: { + Text("OK") } .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) .disabled(selectedOperatorIds.isEmpty) } - - private func setOperatorsButton() -> some View { - notificationsModeNavLinkButton { - Button { - Task { - if let enabledOperators = enabledOperators(serverOperators) { - let r = try await setServerOperators(operators: enabledOperators) - await MainActor.run { - ChatModel.shared.conditions = r - continueToNextStep() - } - } else { - await MainActor.run { - continueToNextStep() - } - } - } - } label: { - Text("Update") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - } - } - - private func continueButton() -> some View { - notificationsModeNavLinkButton { - Button { - continueToNextStep() - } label: { - Text("Continue") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - } - } - - private func reviewLaterButton() -> some View { - notificationsModeNavLinkButton { - Button { - continueToNextStep() - } label: { - Text("Review later") - } - .buttonStyle(.borderless) - } - } - - private func continueToNextStep() { - if onboarding { - onboardingStageDefault.set(.step4_SetNotificationsMode) - notificationsModeNavLinkActive = true - } else { - dismiss() - } - } - - func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View { - ZStack { - button() - - NavigationLink(isActive: $notificationsModeNavLinkActive) { - notificationsModeDestinationView() - } label: { - EmptyView() - } - .frame(width: 1, height: 1) - .hidden() - } - } - - private func notificationsModeDestinationView() -> some View { - SetNotificationsMode() - .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground()) - } - - @ViewBuilder private func reviewConditionsView() -> some View { - let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } - let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - VStack(alignment: .leading, spacing: 20) { - if !operatorsWithConditionsAccepted.isEmpty { - Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") - Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") - } else { - Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") - } - ConditionsTextView() - .frame(maxHeight: .infinity) - acceptConditionsButton() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal, 25) - } - - private func acceptConditionsButton() -> some View { - notificationsModeNavLinkButton { - Button { - Task { - do { - let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId - let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - let operatorIds = acceptForOperators.map { $0.operatorId } - let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) - await MainActor.run { - ChatModel.shared.conditions = r - } - if let enabledOperators = enabledOperators(r.serverOperators) { - let r2 = try await setServerOperators(operators: enabledOperators) - await MainActor.run { - ChatModel.shared.conditions = r2 - continueToNextStep() - } - } else { - await MainActor.run { - continueToNextStep() - } - } - } catch let error { - await MainActor.run { - showAlert( - NSLocalizedString("Error accepting conditions", comment: "alert title"), - message: responseError(error) - ) - } - } - } - } label: { - Text("Accept conditions") - } - .buttonStyle(OnboardingButtonStyle()) - } - } - - private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { - var ops = operators - if !ops.isEmpty { - for i in 0.. some View { - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index b2b1b8fa68..8f448dc508 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -23,7 +23,7 @@ struct OnboardingView: View { case .step3_CreateSimpleXAddress: // deprecated CreateSimpleXAddress() case .step3_ChooseServerOperators: - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .step4_SetNotificationsMode: @@ -44,7 +44,7 @@ enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo case step2_CreateProfile // deprecated case step3_CreateSimpleXAddress // deprecated - case step3_ChooseServerOperators + case step3_ChooseServerOperators // changed to simplified conditions case step4_SetNotificationsMode case onboardingComplete diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index dbae3e9fb3..e55cc4037a 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -129,6 +129,7 @@ struct SimpleXInfo: View { NavigationLink(isActive: $createProfileNavLinkActive) { CreateFirstProfile() + .modifier(ThemedBackground()) } label: { EmptyView() } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index f7c7145dcc..f65a21623a 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -594,8 +594,6 @@ func shouldShowWhatsNew() -> Bool { } fileprivate struct NewOperatorsView: View { - @State private var showOperatorsSheet = false - var body: some View { VStack(alignment: .leading) { Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) @@ -606,16 +604,7 @@ fileprivate struct NewOperatorsView: View { .multilineTextAlignment(.leading) .lineLimit(10) HStack { - Button("Enable Flux") { - showOperatorsSheet = true - } - Text("for better metadata privacy.") - } - } - .sheet(isPresented: $showOperatorsSheet) { - NavigationView { - ChooseServerOperators(onboarding: false) - .modifier(ThemedBackground()) + Text("Enable Flux in Network & servers settings for better metadata privacy.") } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 6727643022..b7db01ec2b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -174,8 +174,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -531,8 +531,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -688,8 +688,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -774,8 +774,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 2456463910..600804a763 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -194,7 +194,7 @@ fun MainScreen() { OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) OnboardingStage.Step3_ChooseServerOperators -> { val modalData = remember { ModalData() } - modalData.ChooseServerOperators(true) + modalData.OnboardingConditionsView() if (appPlatform.isDesktop) { ModalManager.fullscreen.showInView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index a6774c6870..3538d41f01 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -127,31 +127,13 @@ fun ToggleChatListCard() { @Composable fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val rhId = chatModel.remoteHostId() LaunchedEffect(Unit) { val showWhatsNew = shouldShowWhatsNew(chatModel) val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false - if (showWhatsNew) { + if (showWhatsNew || showUpdatedConditions) { delay(1000L) ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) } - } else if (showUpdatedConditions) { - ModalManager.center.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - LaunchedEffect(Unit) { - val conditionsId = chatModel.conditions.value.currentConditions.conditionsId - try { - setConditionsNotified(rh = rhId, conditionsId = conditionsId) - } catch (e: Exception) { - Log.d(TAG, "UsageConditionsView setConditionsNotified error: ${e.message}") - } - } - UsageConditionsView( - userServers = mutableStateOf(emptyList()), - currUserServers = mutableStateOf(emptyList()), - close = close, - rhId = rhId - ) - } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 80cc977602..a14f163a91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -7,15 +7,18 @@ import SectionTextFooter import SectionView import TextIconSpaced import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -27,11 +30,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -fun ModalData.ChooseServerOperators( - onboarding: Boolean, - close: (() -> Unit) = { ModalManager.fullscreen.closeModals() }, - modalManager: ModalManager = ModalManager.fullscreen -) { +fun ModalData.OnboardingConditionsView() { LaunchedEffect(Unit) { prepareChatBeforeFinishingOnboarding() } @@ -41,6 +40,73 @@ fun ModalData.ChooseServerOperators( val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } } val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } } + ColumnWithScrollBar( + Modifier + .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), + maxIntrinsicSize = true + ) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), bottomPadding = DEFAULT_PADDING) + } + + Spacer(Modifier.weight(1f)) + Column( + (if (appPlatform.isDesktop) Modifier.width(450.dp).align(Alignment.CenterHorizontally) else Modifier) + .fillMaxWidth() + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + horizontalAlignment = Alignment.Start + ) { + Text( + stringResource(MR.strings.onboarding_conditions_private_chats_not_accessible), + style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_by_using_you_agree), + style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use), + style = TextStyle(fontSize = 17.sp), + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showModal(endButtons = { ConditionsLinkButton() }) { + SimpleConditionsView(rhId = null) + } + } + ) + } + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperators, selectedOperatorIds) + TextButtonBelowOnboardingButton(stringResource(MR.strings.onboarding_conditions_configure_server_operators)) { + ModalManager.fullscreen.showModalCloseable { close -> + ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + } + } + } + } + } +} + +@Composable +fun ModalData.ChooseServerOperators( + serverOperators: State>, + selectedOperatorIds: MutableState>, + close: (() -> Unit) +) { + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { ColumnWithScrollBar( Modifier .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), @@ -53,7 +119,7 @@ fun ModalData.ChooseServerOperators( Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingInformationButton( stringResource(MR.strings.how_it_helps_privacy), - onClick = { modalManager.showModal { ChooseServerOperatorsInfoView(modalManager) } } + onClick = { ModalManager.fullscreen.showModal { ChooseServerOperatorsInfoView() } } ) } @@ -77,37 +143,11 @@ fun ModalData.ChooseServerOperators( } Spacer(Modifier.weight(1f)) - val reviewForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } - val canReviewLater = reviewForOperators.all { it.conditionsAcceptance.usageAllowed } - val currEnabledOperatorIds = serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { val enabled = selectedOperatorIds.value.isNotEmpty() - when { - reviewForOperators.isNotEmpty() -> ReviewConditionsButton(enabled, onboarding, selectedOperators, selectedOperatorIds, modalManager) - selectedOperatorIds.value != currEnabledOperatorIds && enabled -> SetOperatorsButton(true, onboarding, serverOperators, selectedOperatorIds, close) - else -> ContinueButton(enabled, onboarding, close) - } - if (onboarding && reviewForOperators.isEmpty()) { - TextButtonBelowOnboardingButton(stringResource(MR.strings.operator_conditions_of_use)) { - modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - SimpleConditionsView(rhId = null) - } - } - } else if (onboarding || reviewForOperators.isEmpty()) { - // Reserve space - TextButtonBelowOnboardingButton("", null) - } - if (!onboarding && reviewForOperators.isNotEmpty()) { - ReviewLaterButton(canReviewLater, close) - SectionTextFooter( - annotatedStringResource(MR.strings.onboarding_network_operators_conditions_will_be_accepted) + - AnnotatedString(" ") + - annotatedStringResource(MR.strings.onboarding_network_operators_conditions_you_can_configure), - textAlign = TextAlign.Center - ) - SectionBottomSpacer() - } + SetOperatorsButton(enabled, close) + // Reserve space + TextButtonBelowOnboardingButton("", null) } } } @@ -162,115 +202,36 @@ private fun CircleCheckbox(checked: Boolean) { } @Composable -private fun ReviewConditionsButton( - enabled: Boolean, - onboarding: Boolean, - selectedOperators: State>, - selectedOperatorIds: State>, - modalManager: ModalManager -) { +private fun SetOperatorsButton(enabled: Boolean, close: () -> Unit) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.operator_review_conditions, + labelId = MR.strings.ok, onboarding = null, enabled = enabled, onclick = { - modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - ReviewConditionsView(onboarding, selectedOperators, selectedOperatorIds, close) - } + close() } ) } -@Composable -private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State>, selectedOperatorIds: State>, close: () -> Unit) { - OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.onboarding_network_operators_update, - onboarding = null, - enabled = enabled, - onclick = { - withBGApi { - val enabledOperators = enabledOperators(serverOperators.value, selectedOperatorIds.value) - if (enabledOperators != null) { - val r = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators) - if (r != null) { - chatModel.conditions.value = r - } - continueToNextStep(onboarding, close) - } - } - } - ) -} - -@Composable -private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) { - OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.onboarding_network_operators_continue, - onboarding = null, - enabled = enabled, - onclick = { - continueToNextStep(onboarding, close) - } - ) -} - -@Composable -private fun ReviewLaterButton(enabled: Boolean, close: () -> Unit) { - TextButtonBelowOnboardingButton( - stringResource(MR.strings.onboarding_network_operators_review_later), - onClick = if (!enabled) null else {{ continueToNextStep(false, close) }} - ) -} - -@Composable -private fun ReviewConditionsView( - onboarding: Boolean, - selectedOperators: State>, - selectedOperatorIds: State>, - close: () -> Unit -) { - // remembering both since we don't want to reload the view after the user accepts conditions - val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } - val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } } - ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = if (onboarding) DEFAULT_ONBOARDING_HORIZONTAL_PADDING else DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false, bottomPadding = DEFAULT_PADDING) - if (operatorsWithConditionsAccepted.isNotEmpty()) { - ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }) - ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ }) - } else { - ReadableText(MR.strings.operator_conditions_will_be_accepted_for_some, args = acceptForOperators.joinToString(", ") { it.legalName_ }) - } - Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF)) { - ConditionsTextView(chatModel.remoteHostId()) - } - Column(Modifier.padding(vertical = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close) - } - } -} - @Composable private fun AcceptConditionsButton( - onboarding: Boolean, + enabled: Boolean, selectedOperators: State>, - selectedOperatorIds: State>, - close: () -> Unit + selectedOperatorIds: State> ) { fun continueOnAccept() { - if (appPlatform.isDesktop || !onboarding) { - if (onboarding) { close() } - continueToNextStep(onboarding, close) + if (appPlatform.isDesktop) { + continueToNextStep() } else { continueToSetNotificationsAfterAccept() } } OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier, - labelId = MR.strings.accept_conditions, + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.onboarding_conditions_accept, onboarding = null, + enabled = enabled, onclick = { withBGApi { val conditionsId = chatModel.conditions.value.currentConditions.conditionsId @@ -295,12 +256,8 @@ private fun AcceptConditionsButton( ) } -private fun continueToNextStep(onboarding: Boolean, close: () -> Unit) { - if (onboarding) { +private fun continueToNextStep() { appPrefs.onboardingStage.set(if (appPlatform.isAndroid) OnboardingStage.Step4_SetNotificationsMode else OnboardingStage.OnboardingComplete) - } else { - close() - } } private fun continueToSetNotificationsAfterAccept() { @@ -339,9 +296,7 @@ private fun enabledOperators(operators: List, selectedOperatorId } @Composable -private fun ChooseServerOperatorsInfoView( - modalManager: ModalManager -) { +private fun ChooseServerOperatorsInfoView() { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) @@ -357,21 +312,20 @@ private fun ChooseServerOperatorsInfoView( SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { chatModel.conditions.value.serverOperators.forEach { op -> - ServerOperatorRow(op, modalManager) + ServerOperatorRow(op) } } SectionBottomSpacer() } } -@Composable() +@Composable private fun ServerOperatorRow( - operator: ServerOperator, - modalManager: ModalManager + operator: ServerOperator ) { SectionItemView( { - modalManager.showModalCloseable { close -> + ModalManager.fullscreen.showModalCloseable { close -> OperatorInfoView(operator) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index de9f909150..52eea3dd9d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -14,7 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatController.appPrefs @@ -161,10 +161,14 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool } if (updatedConditions) { - Row( + Text( + stringResource(MR.strings.view_updated_conditions), + color = MaterialTheme.colors.primary, modifier = Modifier - .clip(shape = CircleShape) - .clickable { + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { modalManager.showModalCloseable { close -> UsageConditionsView( userServers = mutableStateOf(emptyList()), @@ -174,15 +178,7 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool ) } } - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - stringResource(MR.strings.view_updated_conditions), - color = MaterialTheme.colors.primary - ) - } + ) } if (!viaSettings) { @@ -190,14 +186,21 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool Box( Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - Text( - generalGetString(MR.strings.ok), - modifier = Modifier.clickable(onClick = { - close() - }), - style = MaterialTheme.typography.h3, - color = MaterialTheme.colors.primary - ) + Box(Modifier.clip(RoundedCornerShape(20.dp))) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .clickable { close() } + .padding(8.dp) + ) { + Text( + generalGetString(MR.strings.ok), + style = MaterialTheme.typography.h3, + color = MaterialTheme.colors.primary + ) + } + } } Spacer(Modifier.fillMaxHeight().weight(1f)) } @@ -213,8 +216,17 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool fun ReadMoreButton(url: String) { val uriHandler = LocalUriHandler.current Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) { - Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { uriHandler.openUriCatching(url) }) + Text( + stringResource(MR.strings.whats_new_read_more), + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + uriHandler.openUriCatching(url) + } + ) Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) } } @@ -751,17 +763,7 @@ private val versionDescriptions: List = listOf( val src = (operatorsInfo[OperatorTag.Flux] ?: dummyOperatorInfo).largeLogo Image(painterResource(src), null, modifier = Modifier.height(48.dp)) Text(stringResource(MR.strings.v6_2_network_decentralization_descr), modifier = Modifier.padding(top = 8.dp)) - Row { - Text( - stringResource(MR.strings.v6_2_network_decentralization_enable_flux), - color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { - modalManager.showModalCloseable { close -> ChooseServerOperators(onboarding = false, close, modalManager) } - } - ) - Text(" ") - Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux_reason)) - } + Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux)) } } ), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 5e8d22cb99..d905ab71ea 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1162,6 +1162,11 @@ Use random passphrase + Private chats, groups and your contacts are not accessible to server operators. + By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam. + Privacy policy and conditions of use. + Accept + Configure server operators Server operators Network operators SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. @@ -2291,7 +2296,7 @@ Delete or moderate up to 200 messages. Network decentralization The second preset operator in the app! - Enable flux + Enable Flux in Network & servers settings for better metadata privacy. for better metadata privacy. Improved chat navigation - Open chat on the first unread message.\n- Jump to quoted messages.