mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-09 06:35:54 +00:00
3d85480944
* ui: onboarding assets * android: fix gradle version check, pass assets dir to builds * desktop: pass assets dir to gradle builds * ui: new onboarding (#6872) * ios: improve onboarding * ios version condition * android strings * merge keys * refactor network conditions to old location * ios scroll headline * remove nav view * kotlin: refactor network commitments page to use existing view * remove unused keys * update why page * configure -> setup * padding for app bar in why page * fix why page * padding * copy translations from the website * export localizations * export again * kotlin: fix why page * fix * import localizations * custom layout * padding for system bars * paddings * more paddings * more padding 2 * update fonts * fonts * line height, padding * paddings * refactor notifications * refactor ios * notification icons in cards * restore profile field * padding * desktop layout create profile * fix * more layout * create profile layout * mobile padding * split mobile and desktop * layout * layout * background * refactor onboarding images * use DARK theme by default * page 3 and 4 layouts * restructure desktop onboarding to two panes * improve layout * improve * fonts, padding * link mobile on full page * fix, reduce noise * change to animation * fix animation * refactor * colors, animation * import * details * fix padding * fix icon * fix * button paddings * accept button on terms page * fix conditions button * close modal --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Co-authored-by: shum <github.shum@liber.li> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
524 lines
19 KiB
Swift
524 lines
19 KiB
Swift
//
|
|
// NetworkServersView.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Evgeny on 02/08/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
// Spec: spec/architecture.md
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
private enum NetworkAlert: Identifiable {
|
|
case error(err: String)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case let .error(err): return "error \(err)"
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum NetworkAndServersSheet: Identifiable {
|
|
case showConditions
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .showConditions: return "showConditions"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NetworkAndServers: View {
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
@EnvironmentObject var m: ChatModel
|
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
|
@EnvironmentObject var theme: AppTheme
|
|
@EnvironmentObject var ss: SaveableSettings
|
|
@State private var sheetItem: NetworkAndServersSheet? = nil
|
|
@State private var justOpened = true
|
|
@State private var showSaveDialog = false
|
|
|
|
var body: some View {
|
|
VStack {
|
|
List {
|
|
let conditionsAction = m.conditions.conditionsAction
|
|
let anyOperatorEnabled = ss.servers.userServers.contains(where: { $0.operator?.enabled ?? false })
|
|
Section {
|
|
ForEach(ss.servers.userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in
|
|
if let serverOperator = userOperatorServers.operator {
|
|
serverOperatorView(idx, serverOperator)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
|
|
if let conditionsAction = conditionsAction, anyOperatorEnabled {
|
|
conditionsButton(conditionsAction)
|
|
}
|
|
} header: {
|
|
Text("Preset servers")
|
|
.foregroundColor(theme.colors.secondary)
|
|
} footer: {
|
|
switch conditionsAction {
|
|
case let .review(_, deadline, _):
|
|
if let deadline = deadline, anyOperatorEnabled {
|
|
Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
default:
|
|
EmptyView()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
if let idx = ss.servers.userServers.firstIndex(where: { $0.operator == nil }) {
|
|
NavigationLink {
|
|
YourServersView(
|
|
userServers: $ss.servers.userServers,
|
|
serverErrors: $ss.servers.serverErrors,
|
|
serverWarnings: $ss.servers.serverWarnings,
|
|
operatorIndex: idx
|
|
)
|
|
.navigationTitle("Your servers")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
} label: {
|
|
HStack {
|
|
Text("Your servers")
|
|
|
|
if ss.servers.userServers[idx] != ss.servers.currUserServers[idx] {
|
|
Spacer()
|
|
unsavedChangesIndicator()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NavigationLink {
|
|
AdvancedNetworkSettings()
|
|
.navigationTitle("Advanced settings")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
} label: {
|
|
Text("Advanced network settings")
|
|
}
|
|
} header: {
|
|
Text("Messages & files")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
Section {
|
|
Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) })
|
|
.disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors))
|
|
} footer: {
|
|
if let errStr = globalServersError(ss.servers.serverErrors) {
|
|
ServersErrorView(errStr: errStr)
|
|
} else if !ss.servers.serverErrors.isEmpty {
|
|
ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error"))
|
|
}
|
|
if let warnStr = globalServersWarning(ss.servers.serverWarnings) {
|
|
ServersWarningView(warnStr: warnStr)
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) {
|
|
NavigationLink {
|
|
RTCServers()
|
|
.navigationTitle("Your ICE servers")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
} label: {
|
|
Text("WebRTC ICE servers")
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) {
|
|
HStack {
|
|
Text(m.networkInfo.networkType.text)
|
|
Spacer()
|
|
Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
// this condition is needed to prevent re-setting the servers when exiting single server view
|
|
if justOpened {
|
|
do {
|
|
ss.servers.currUserServers = try await getUserServers()
|
|
ss.servers.userServers = ss.servers.currUserServers
|
|
ss.servers.serverErrors = []
|
|
ss.servers.serverWarnings = []
|
|
validateServers_($ss.servers.userServers, $ss.servers.serverErrors, $ss.servers.serverWarnings)
|
|
} catch let error {
|
|
await MainActor.run {
|
|
showAlert(
|
|
NSLocalizedString("Error loading servers", comment: "alert title"),
|
|
message: responseError(error)
|
|
)
|
|
}
|
|
}
|
|
justOpened = false
|
|
}
|
|
}
|
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
|
if serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors) {
|
|
showSaveDialog = true
|
|
} else {
|
|
dismiss()
|
|
}
|
|
})
|
|
.confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) {
|
|
Button("Save") {
|
|
saveServers($ss.servers.currUserServers, $ss.servers.userServers)
|
|
dismiss()
|
|
}
|
|
Button("Exit without saving") { dismiss() }
|
|
}
|
|
.sheet(item: $sheetItem) { item in
|
|
switch item {
|
|
case .showConditions:
|
|
UsageConditionsView(
|
|
currUserServers: $ss.servers.currUserServers,
|
|
userServers: $ss.servers.userServers
|
|
)
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func serverOperatorView(_ operatorIndex: Int, _ serverOperator: ServerOperator) -> some View {
|
|
NavigationLink() {
|
|
OperatorView(
|
|
currUserServers: $ss.servers.currUserServers,
|
|
userServers: $ss.servers.userServers,
|
|
serverErrors: $ss.servers.serverErrors,
|
|
serverWarnings: $ss.servers.serverWarnings,
|
|
operatorIndex: operatorIndex,
|
|
useOperator: serverOperator.enabled
|
|
)
|
|
.navigationBarTitle("\(serverOperator.tradeName) servers")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
HStack {
|
|
Image(serverOperator.logo(colorScheme))
|
|
.resizable()
|
|
.scaledToFit()
|
|
.grayscale(serverOperator.enabled ? 0.0 : 1.0)
|
|
.frame(width: 24, height: 24)
|
|
Text(serverOperator.tradeName)
|
|
.foregroundColor(serverOperator.enabled ? theme.colors.onBackground : theme.colors.secondary)
|
|
|
|
if ss.servers.userServers[operatorIndex] != ss.servers.currUserServers[operatorIndex] {
|
|
Spacer()
|
|
unsavedChangesIndicator()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func unsavedChangesIndicator() -> some View {
|
|
Image(systemName: "pencil")
|
|
.foregroundColor(theme.colors.secondary)
|
|
.symbolRenderingMode(.monochrome)
|
|
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
|
|
}
|
|
|
|
private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View {
|
|
Button {
|
|
sheetItem = .showConditions
|
|
} label: {
|
|
switch conditionsAction {
|
|
case .review:
|
|
Text("Review conditions")
|
|
case .accepted:
|
|
Text("Accepted conditions")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UsageConditionsView: View {
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Binding var currUserServers: [UserOperatorServers]
|
|
@Binding var userServers: [UserOperatorServers]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
switch ChatModel.shared.conditions.conditionsAction {
|
|
|
|
case .none:
|
|
regularConditionsHeader()
|
|
.padding(.top)
|
|
.padding(.top)
|
|
ConditionsTextView()
|
|
.padding(.bottom)
|
|
.padding(.bottom)
|
|
|
|
case let .review(operators, deadline, _):
|
|
HStack {
|
|
Text("Updated conditions").font(.largeTitle).bold()
|
|
}
|
|
.padding(.top)
|
|
.padding(.top)
|
|
|
|
Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
|
ConditionsTextView()
|
|
VStack(spacing: 8) {
|
|
acceptConditionsButton(operators.map { $0.operatorId })
|
|
if let deadline = deadline {
|
|
Text("Conditions will be automatically accepted for enabled operators on: \(conditionsTimestamp(deadline)).")
|
|
.foregroundColor(theme.colors.secondary)
|
|
.font(.footnote)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.horizontal, 32)
|
|
conditionsDiffButton(.footnote)
|
|
} else {
|
|
conditionsDiffButton()
|
|
.padding(.top)
|
|
}
|
|
}
|
|
.padding(.bottom)
|
|
.padding(.bottom)
|
|
|
|
|
|
case let .accepted(operators):
|
|
regularConditionsHeader()
|
|
.padding(.top)
|
|
.padding(.top)
|
|
Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
|
ConditionsTextView()
|
|
.padding(.bottom)
|
|
.padding(.bottom)
|
|
}
|
|
}
|
|
.padding(.horizontal, 25)
|
|
.frame(maxHeight: .infinity)
|
|
}
|
|
|
|
private func acceptConditionsButton(_ operatorIds: [Int64]) -> some View {
|
|
Button {
|
|
acceptForOperators(operatorIds)
|
|
} label: {
|
|
Text("Accept conditions")
|
|
}
|
|
.buttonStyle(OnboardingButtonStyle())
|
|
}
|
|
|
|
func acceptForOperators(_ operatorIds: [Int64]) {
|
|
Task {
|
|
do {
|
|
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
|
|
let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)
|
|
await MainActor.run {
|
|
ChatModel.shared.conditions = r
|
|
updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators)
|
|
updateOperatorsConditionsAcceptance($userServers, r.serverOperators)
|
|
dismiss()
|
|
}
|
|
} catch let error {
|
|
await MainActor.run {
|
|
showAlert(
|
|
NSLocalizedString("Error accepting conditions", comment: "alert title"),
|
|
message: responseError(error)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View {
|
|
let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
|
|
if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
|
|
ExternalLink(destination: commitUrl) {
|
|
HStack {
|
|
Text("Open changes")
|
|
Image(systemName: "arrow.up.right.circle")
|
|
}
|
|
.font(font)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func regularConditionsHeader() -> some View {
|
|
HStack {
|
|
Text("Conditions of use").font(.largeTitle).bold()
|
|
Spacer()
|
|
conditionsLinkButton()
|
|
}
|
|
}
|
|
|
|
|
|
func validateServers_(
|
|
_ userServers: Binding<[UserOperatorServers]>,
|
|
_ serverErrors: Binding<[UserServersError]>,
|
|
_ serverWarnings: Binding<[UserServersWarning]>? = nil
|
|
) {
|
|
let userServersToValidate = userServers.wrappedValue
|
|
Task {
|
|
do {
|
|
let (errs, warns) = try await validateServers(userServers: userServersToValidate)
|
|
await MainActor.run {
|
|
serverErrors.wrappedValue = errs
|
|
serverWarnings?.wrappedValue = warns
|
|
}
|
|
} catch let error {
|
|
logger.error("validateServers error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
|
|
func serversCanBeSaved(
|
|
_ currUserServers: [UserOperatorServers],
|
|
_ userServers: [UserOperatorServers],
|
|
_ serverErrors: [UserServersError]
|
|
) -> Bool {
|
|
return userServers != currUserServers && serverErrors.isEmpty
|
|
}
|
|
|
|
struct ServersErrorView: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var errStr: String
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.circle")
|
|
.foregroundColor(.red)
|
|
Text(errStr)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ServersWarningView: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var warnStr: String
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.foregroundColor(.orange)
|
|
Text(warnStr)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
func globalServersError(_ serverErrors: [UserServersError]) -> String? {
|
|
for err in serverErrors {
|
|
if let errStr = err.globalError {
|
|
return errStr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? {
|
|
for warn in serverWarnings {
|
|
switch warn {
|
|
case let .noChatRelays(user):
|
|
let text = NSLocalizedString("No chat relays enabled.", comment: "servers warning")
|
|
if let user = user {
|
|
return String.localizedStringWithFormat(
|
|
NSLocalizedString("For chat profile %@:", comment: "servers warning"),
|
|
user.localDisplayName
|
|
) + " " + text
|
|
} else { return text }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bindingForChatRelays(_ userServers: Binding<[UserOperatorServers]>, _ opIndex: Int) -> Binding<[UserChatRelay]> {
|
|
Binding(
|
|
get: { userServers[opIndex].wrappedValue.chatRelays },
|
|
set: { userServers[opIndex].wrappedValue.chatRelays = $0 }
|
|
)
|
|
}
|
|
|
|
func globalSMPServersError(_ serverErrors: [UserServersError]) -> String? {
|
|
for err in serverErrors {
|
|
if let errStr = err.globalSMPError {
|
|
return errStr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func globalXFTPServersError(_ serverErrors: [UserServersError]) -> String? {
|
|
for err in serverErrors {
|
|
if let errStr = err.globalXFTPError {
|
|
return errStr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findDuplicateHosts(_ serverErrors: [UserServersError]) -> Set<String> {
|
|
let duplicateHostsList = serverErrors.compactMap { err in
|
|
if case let .duplicateServer(_, _, duplicateHost) = err {
|
|
return duplicateHost
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return Set(duplicateHostsList)
|
|
}
|
|
|
|
func findDuplicateRelayAddresses(_ serverErrors: [UserServersError]) -> Set<String> {
|
|
Set(serverErrors.compactMap { err in
|
|
if case let .duplicateChatRelayAddress(_, duplicateAddress) = err { return duplicateAddress }
|
|
else { return nil }
|
|
})
|
|
}
|
|
|
|
|
|
func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServers: Binding<[UserOperatorServers]>) {
|
|
let userServersToSave = userServers.wrappedValue
|
|
Task {
|
|
do {
|
|
try await setUserServers(userServers: userServersToSave)
|
|
// Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers)
|
|
do {
|
|
let updatedServers = try await getUserServers()
|
|
let updatedOperators = try await getServerOperators()
|
|
await MainActor.run {
|
|
ChatModel.shared.conditions = updatedOperators
|
|
currUserServers.wrappedValue = updatedServers
|
|
userServers.wrappedValue = updatedServers
|
|
}
|
|
} catch let error {
|
|
logger.error("saveServers getUserServers error: \(responseError(error))")
|
|
await MainActor.run {
|
|
currUserServers.wrappedValue = userServersToSave
|
|
}
|
|
}
|
|
} catch let error {
|
|
logger.error("saveServers setUserServers error: \(responseError(error))")
|
|
await MainActor.run {
|
|
showAlert(
|
|
NSLocalizedString("Error saving servers", comment: "alert title"),
|
|
message: responseError(error)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateOperatorsConditionsAcceptance(_ usvs: Binding<[UserOperatorServers]>, _ updatedOperators: [ServerOperator]) {
|
|
for i in 0..<usvs.wrappedValue.count {
|
|
if let updatedOperator = updatedOperators.first(where: { $0.operatorId == usvs.wrappedValue[i].operator?.operatorId }) {
|
|
usvs.wrappedValue[i].operator?.conditionsAcceptance = updatedOperator.conditionsAcceptance
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NetworkServersView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
NetworkAndServers()
|
|
}
|
|
}
|