Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2025-03-29 20:48:36 +00:00
19 changed files with 487 additions and 538 deletions
+1 -1
View File
@@ -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:
+1 -12
View File
@@ -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 {
@@ -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<Int64>()
@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..<ops.count {
var op = ops[i]
op.enabled = selectedOperatorIds.contains(op.operatorId)
ops[i] = op
}
let haveSMPStorage = ops.contains(where: { $0.enabled && $0.smpRoles.storage })
let haveSMPProxy = ops.contains(where: { $0.enabled && $0.smpRoles.proxy })
let haveXFTPStorage = ops.contains(where: { $0.enabled && $0.xftpRoles.storage })
let haveXFTPProxy = ops.contains(where: { $0.enabled && $0.xftpRoles.proxy })
if haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy {
return ops
} else if let firstEnabledIndex = ops.firstIndex(where: { $0.enabled }) {
var op = ops[firstEnabledIndex]
if !haveSMPStorage { op.smpRoles.storage = true }
if !haveSMPProxy { op.smpRoles.proxy = true }
if !haveXFTPStorage { op.xftpRoles.storage = true }
if !haveXFTPProxy { op.xftpRoles.proxy = true }
ops[firstEnabledIndex] = op
return ops
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
return nil
}
} else {
return nil
}
}
}
private enum ChooseServerOperatorsSheet: Identifiable {
case showInfo
var id: String {
switch self {
case .showInfo: return "showInfo"
}
}
}
struct ChooseServerOperators: View {
@Environment(\.dismiss) var dismiss: DismissAction
@Environment(\.colorScheme) var colorScheme: ColorScheme
@EnvironmentObject var theme: AppTheme
var serverOperators: [ServerOperator]
@Binding var selectedOperatorIds: Set<Int64>
@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..<ops.count {
var op = ops[i]
op.enabled = selectedOperatorIds.contains(op.operatorId)
ops[i] = op
}
let haveSMPStorage = ops.contains(where: { $0.enabled && $0.smpRoles.storage })
let haveSMPProxy = ops.contains(where: { $0.enabled && $0.smpRoles.proxy })
let haveXFTPStorage = ops.contains(where: { $0.enabled && $0.xftpRoles.storage })
let haveXFTPProxy = ops.contains(where: { $0.enabled && $0.xftpRoles.proxy })
if haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy {
return ops
} else if let firstEnabledIndex = ops.firstIndex(where: { $0.enabled }) {
var op = ops[firstEnabledIndex]
if !haveSMPStorage { op.smpRoles.storage = true }
if !haveSMPProxy { op.smpRoles.proxy = true }
if !haveXFTPStorage { op.xftpRoles.storage = true }
if !haveXFTPProxy { op.xftpRoles.proxy = true }
ops[firstEnabledIndex] = op
return ops
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
return nil
}
} else {
return nil
}
}
}
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
@@ -444,5 +402,5 @@ struct ChooseServerOperatorsInfoView: View {
}
#Preview {
ChooseServerOperators(onboarding: true)
OnboardingConditionsView()
}
@@ -207,7 +207,7 @@ struct CreateFirstProfile: View {
}
private func nextStepDestinationView() -> some View {
ChooseServerOperators(onboarding: true)
OnboardingConditionsView()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
}
@@ -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
@@ -129,6 +129,7 @@ struct SimpleXInfo: View {
NavigationLink(isActive: $createProfileNavLinkActive) {
CreateFirstProfile()
.modifier(ThemedBackground())
} label: {
EmptyView()
}
@@ -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.")
}
}
}
+8 -8
View File
@@ -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 = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
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 = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.8-CNSVk2Y5c4gAUnxeodOxfm.a"; sourceTree = "<group>"; };
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 = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.1.0-IjBs5IcA9K0E6bK6RlYtK.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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()
}
@@ -127,31 +127,13 @@ fun ToggleChatListCard() {
@Composable
fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, 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
)
}
}
}
@@ -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<List<ServerOperator>>,
selectedOperatorIds: MutableState<Set<Long>>,
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<List<ServerOperator>>,
selectedOperatorIds: State<Set<Long>>,
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<List<ServerOperator>>, selectedOperatorIds: State<Set<Long>>, 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<List<ServerOperator>>,
selectedOperatorIds: State<Set<Long>>,
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<List<ServerOperator>>,
selectedOperatorIds: State<Set<Long>>,
close: () -> Unit
selectedOperatorIds: State<Set<Long>>
) {
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<ServerOperator>, 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)
}
}
@@ -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<VersionDescription> = 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))
}
}
),
@@ -1162,6 +1162,11 @@
<string name="use_random_passphrase">Use random passphrase</string>
<!-- ChooseServerOperators.kt -->
<string name="onboarding_conditions_private_chats_not_accessible">Private chats, groups and your contacts are not accessible to server operators.</string>
<string name="onboarding_conditions_by_using_you_agree">By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users no spam.</string>
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Privacy policy and conditions of use.</string>
<string name="onboarding_conditions_accept">Accept</string>
<string name="onboarding_conditions_configure_server_operators">Configure server operators</string>
<string name="onboarding_choose_server_operators">Server operators</string>
<string name="onboarding_network_operators">Network operators</string>
<string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</string>
@@ -2291,7 +2296,7 @@
<string name="v6_1_delete_many_messages_descr">Delete or moderate up to 200 messages.</string>
<string name="v6_2_network_decentralization">Network decentralization</string>
<string name="v6_2_network_decentralization_descr">The second preset operator in the app!</string>
<string name="v6_2_network_decentralization_enable_flux">Enable flux</string>
<string name="v6_2_network_decentralization_enable_flux">Enable Flux in Network &amp; servers settings for better metadata privacy.</string>
<string name="v6_2_network_decentralization_enable_flux_reason">for better metadata privacy.</string>
<string name="v6_2_improved_chat_navigation">Improved chat navigation</string>
<string name="v6_2_improved_chat_navigation_descr">- Open chat on the first unread message.\n- Jump to quoted messages.</string>
@@ -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)
+15 -37
View File
@@ -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
+1 -1
View File
@@ -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
+16 -10
View File
@@ -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) =
@@ -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
+87 -2
View File
@@ -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 </)
cath ##> "/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