diff --git a/README.md b/README.md index 1830228370..7e6e5d975d 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ You must: Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. -You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D) There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: 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 31c8ab0b47..d4b2100e62 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -179,8 +179,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 */; }; @@ -541,8 +541,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 = ""; }; @@ -698,8 +698,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; @@ -784,8 +784,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. diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 575c7ca738..054f261b4e 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -469,7 +469,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do - gli_ <- join <$> withDB' cc (\db -> getGroupLinkInfo db userId groupId) + gli_ <- join <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId) let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ gmId = groupMemberId' m sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case @@ -698,7 +698,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName case acceptance_ of Just a' | a /= a' -> do let d = toCustomData $ DirectoryGroupData a' - withDB' cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case Just () -> sendSettigns n a' " set to" Nothing -> sendReply $ "Error changing spam filter settings for group " <> n _ -> sendSettigns n a "" @@ -977,24 +977,24 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendComposedMessage cc ct Nothing $ MCText text getContact' :: ChatController -> User -> ContactId -> IO (Maybe Contact) -getContact' cc user ctId = withDB cc $ \db -> getContact db (vr cc) user ctId +getContact' cc user ctId = withDB "getContact" cc $ \db -> getContact db (vr cc) user ctId getGroup :: ChatController -> User -> GroupId -> IO (Maybe GroupInfo) -getGroup cc user gId = withDB cc $ \db -> getGroupInfo db (vr cc) user gId +getGroup cc user gId = withDB "getGroupInfo" cc $ \db -> getGroupInfo db (vr cc) user gId -withDB' :: ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) -withDB' cc a = withDB cc $ ExceptT . fmap Right . a +withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) +withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a -withDB :: ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) -withDB ChatController {chatStore} action = do +withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) +withDB cxt ChatController {chatStore} action = do r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors case r_ of Right r -> pure $ Just r - Left e -> Nothing <$ logError ("Database error: " <> tshow e) + Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e) getGroupAndSummary :: ChatController -> User -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) getGroupAndSummary cc user gId = - withDB cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) + withDB "getGroupAndSummary" cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) vr :: ChatController -> VersionRangeChat vr ChatController {config = ChatConfig {chatVRange}} = chatVRange @@ -1002,7 +1002,7 @@ vr ChatController {config = ChatConfig {chatVRange}} = chatVRange getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, ConnReqContact, GroupMemberRole)) getGroupLinkRole cc user gInfo = - withDB cc $ \db -> getGroupLink db user gInfo + withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact) setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) diff --git a/docs/SERVER.md b/docs/SERVER.md index 3bfd064c4e..4ddfb68e63 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -15,7 +15,7 @@ revision: 12.10.2024 - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) - [docker container](#docker-container) - [Linode marketplace](#linode-marketplace) -- [Verifying server binaries] +- [Verifying server binaries](#verifying-server-binaries) - [Configuration](#configuration) - [Interactively](#interactively) - [Via command line options](#via-command-line-options) @@ -1592,7 +1592,9 @@ To update your smp-server to latest version, choose your installation method and You can locally reproduce server binaries, following these instructions. -You must have: +If you are a security expert or researcher, you can help SimpleX network and users community by signing the release checksums – we will [publish your signature](https://github.com/simplex-chat/simplexmq/releases/tag/v6.3.1). Please reach out to us! + +To reproduce the build you must have: - Linux machine - `x86-64` architecture @@ -1613,52 +1615,28 @@ You must have: 3. Execute the script with the required tag: ```sh - ./reproduce-builds.sh 'v6.3.0' + ./reproduce-builds.sh 'v6.3.1' ``` + The script executes these steps (please review the script to confirm): + + 1) builds all server binaries for the release in docker container. + 2) downloads binaries from the same GitHub release and compares them with the built binaries. + 3) if they all match, generates _sha256sums file with their checksums. + This will take a while. -4. After compilation, you should see the following folders: +4. After compilation, you should see the folder named as the tag (e.g., `v6.3.1`) with two subfolders: ```sh - ls out* + ls v6.3.1 ``` ```sh - out-20.04: - ntf-server smp-server xftp xftp-server - - out-20.04-github: - ntf-server smp-server xftp xftp-server - - out-22.04: - ntf-server smp-server xftp xftp-server - - out-22.04-github: - ntf-server smp-server xftp xftp-server - - out-24.04: - ntf-server smp-server xftp xftp-server - - out-24.04-github: - ntf-server smp-server xftp xftp-server + from-source prebuilt _sha256sums ``` -5. Compare the hashes from github release with locally build binaries: - - ```sh - sha256sum out*-github/* - ``` - - ```sh - sha256sum out*[0-9]/* - ``` - - You can safely delete cloned repository: - - ```sh - cd ../ && rm -rf simplexmq - ``` + The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release. ## Configuring the app to use the server diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f2b3582722..f3095e7fce 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.3.1.0 +version: 6.3.1.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 22d2a7b1f5..bdd54c3f1e 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -492,15 +492,14 @@ getUserAddress db User {userId} = getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo) getUserContactLinkById db userId userContactLinkId = ExceptT . firstRow (\(ucl :. gli) -> (toUserContactLink ucl, toGroupLinkInfo gli)) SEUserContactLinkNotFound $ - DB.query db (groupLinkInfoQuery <> " AND user_contact_link_id = ?") (userId, userContactLinkId) - -groupLinkInfoQuery :: Query -groupLinkInfoQuery = - [sql| - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role - FROM user_contact_links - WHERE user_id = ? - |] + DB.query + db + [sql| + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND user_contact_link_id = ? + |] + (userId, userContactLinkId) toGroupLinkInfo :: (Maybe GroupId, Maybe GroupMemberRole) -> Maybe GroupLinkInfo toGroupLinkInfo (groupId_, mRole_) = @@ -510,7 +509,14 @@ toGroupLinkInfo (groupId_, mRole_) = getGroupLinkInfo :: DB.Connection -> UserId -> GroupId -> IO (Maybe GroupLinkInfo) getGroupLinkInfo db userId groupId = fmap join $ maybeFirstRow toGroupLinkInfo $ - DB.query db (groupLinkInfoQuery <> " AND group_id = ?") (userId, groupId) + DB.query + db + [sql| + SELECT group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 9cbd5965b7..6f1d243cf3 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -2950,6 +2950,14 @@ Query: Plan: SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +Query: + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND user_contact_link_id = ? + +Plan: +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, @@ -4642,14 +4650,6 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) -Query: - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role - FROM user_contact_links - WHERE user_id = ? - AND user_contact_link_id = ? -Plan: -SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) - Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index e9db100e8d..6601032a79 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -20,7 +20,7 @@ import Directory.Service import Directory.Store import GHC.IO.Handle (hClose) import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Core import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB @@ -64,6 +64,8 @@ directoryServiceTests = do it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval describe "list groups" $ do it "should list user's groups" testListUserGroups + describe "member admission" $ do + it "should ask member to pass captcha screen" testCapthaScreening describe "store log" $ do it "should restore directory service state" testRestoreDirectory describe "captcha" $ do @@ -954,6 +956,88 @@ testListUserGroups ps = groupNotFound cath "anonymity" listGroups superUser bob cath +testCapthaScreening :: HasCallStack => TestParams -> IO () +testCapthaScreening ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- check default role + bob #> "@SimpleX-Directory /role 1" + bob <# "SimpleX-Directory> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /role 1 observer to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + -- enable captcha + bob #> "@SimpleX-Directory /filter 1 captcha" + bob <# "SimpleX-Directory> > /filter 1 captcha" + bob <## " Spam filter settings for group privacy set to:" + bob <## "- reject long/inappropriate names: disabled" + bob <## "- pass captcha to join: enabled" + bob <## "" + bob <## "Use /filter 1 [name] [captcha] to enable and /filter 1 off to disable filter." + -- connect with captcha screen + _ <- join cath groupLink + cath #> "#privacy 123" -- sending incorrect captcha + cath <# "#privacy SimpleX-Directory!> > cath 123" + cath <## " Incorrect text, please try again." + captcha <- dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + cath #> "#privacy hello" + bob <# "#privacy cath> hello" + cath ##> "/l privacy" + cath <## "#privacy: you left the group" + cath <## "use /d #privacy to delete the group" + bob <## "#privacy: cath left the group" + cath ##> "/d #privacy" + cath <## "#privacy: you deleted the group" + -- change default role to observer + bob #> "@SimpleX-Directory /role 1 observer" + bob <# "SimpleX-Directory> > /role 1 observer" + bob <## " The initial member role for the group privacy is set to observer" + bob <## "" + bob <##. "Please note: it applies only to members joining via this link: https://" + -- connect with captcha screen again, as observer + captcha' <- join cath groupLink + sendCaptcha cath captcha' + -- message from cath that left + pastMember <- dropStrPrefix "#privacy: SimpleX-Directory forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath + cath <# ("#privacy " <> pastMember <> "> hello [>>]") + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath_1 (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath_1 is connected" + cath ##> "#privacy hello" + cath <## "#privacy: you don't have permission to send messages" + (bob "/ms privacy" + cath <## "cath (Catherine): observer, you, connected" + cath <## "SimpleX-Directory: admin, host, connected" + cath <## "bob (Bob): owner, connected" + cath <## (pastMember <> ": author, status unknown") + where + join cath groupLink = do + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy SimpleX-Directory> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha = do + cath #> ("#privacy " <> captcha) + cath <# ("#privacy SimpleX-Directory!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do testListUserGroups ps @@ -1137,7 +1221,8 @@ runDirectory cfg opts@DirectoryOpts {directoryLog} action = do where bot st = do env <- newServiceState opts - simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env + let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} + simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1