mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 05:55:01 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+1
-19
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+92
-138
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+36
-34
@@ -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 & 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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user