Compare commits

...

18 Commits

Author SHA1 Message Date
spaced4ndy 1af4cb0dcf ios: add known server wip 2024-06-27 11:58:44 +04:00
spaced4ndy 1d83140c1d encodePrettyPrinted 2024-06-26 15:45:39 +04:00
spaced4ndy 14f79df72f variableValueAsPercentage 2024-06-26 15:07:37 +04:00
spaced4ndy 55a05ebe5c rename 2024-06-26 14:41:02 +04:00
spaced4ndy ada8dc659a subs indicator with timer 2024-06-26 14:29:11 +04:00
spaced4ndy 2457fa174e view 2024-06-26 14:08:31 +04:00
spaced4ndy 46bc947128 getAgentSubsSummary api 2024-06-26 13:53:24 +04:00
spaced4ndy 09b2819fe6 subscription icon in server view 2024-06-26 12:03:19 +04:00
spaced4ndy 792ee522b5 refactor 2024-06-26 11:56:12 +04:00
spaced4ndy b542c5d0b2 reset stats button 2024-06-26 11:50:52 +04:00
spaced4ndy fd11c9d0b3 reconnect server button 2024-06-26 10:58:26 +04:00
spaced4ndy 4f2838d329 sub status view 2024-06-25 21:15:31 +04:00
spaced4ndy db89757e11 xftp summary 2024-06-25 17:58:27 +04:00
spaced4ndy db331a7ff1 Merge branch 'master' into f/ios-servers-summary 2024-06-25 17:45:06 +04:00
spaced4ndy c9c47bd1a6 wip 2024-06-25 17:32:11 +04:00
spaced4ndy c2eb510b4a update 2024-06-25 13:32:05 +04:00
spaced4ndy 9d4e37a27c chat list buttons 2024-06-12 17:54:35 +04:00
spaced4ndy c71e40cc9d types 2024-06-12 17:21:34 +04:00
10 changed files with 937 additions and 86 deletions
+28
View File
@@ -487,6 +487,11 @@ func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg])
try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers))
}
func addKnownProtoServer(server: String) async throws {
let userId = try currentUserId("addKnownProtoServer")
try await sendCommandOkResp(.apiAddKnownProtoServer(userId: userId, server: server))
}
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
let userId = try currentUserId("testProtoServer")
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
@@ -541,6 +546,11 @@ func reconnectAllServers() async throws {
try await sendCommandOkResp(.reconnectAllServers)
}
func reconnectServer(smpServer: String) async throws {
let userId = try currentUserId("reconnectServer")
try await sendCommandOkResp(.reconnectServer(userId: userId, smpServer: smpServer))
}
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
}
@@ -1334,6 +1344,24 @@ func apiGetVersion() throws -> CoreVersionInfo {
throw r
}
func getAgentServersSummary() throws -> PresentedServersSummary {
let userId = try currentUserId("getAgentServersSummary")
let r = chatSendCmdSync(.getAgentServersSummary(userId: userId))
if case let .agentServersSummary(_, serversSummary) = r { return serversSummary }
throw r
}
func resetAgentServersStats() async throws {
try await sendCommandOkResp(.resetAgentServersStats)
}
func getAgentSubsSummary() throws -> SMPServerSubs {
let userId = try currentUserId("getAgentSubsSummary")
let r = chatSendCmdSync(.getAgentSubsSummary(userId: userId))
if case let .agentSubsSummary(_, subsSummary) = r { return subsSummary }
throw r
}
private func currentUserId(_ funcName: String) throws -> Int64 {
if let userId = ChatModel.shared.currentUser?.userId {
return userId
@@ -115,9 +115,7 @@ struct ChatListView: View {
HStack(spacing: 4) {
Text("Chats")
.font(.headline)
if chatModel.chats.count > 0 {
toggleFilterButton()
}
SubsStatusIndicator()
}
.frame(maxWidth: .infinity, alignment: .center)
}
@@ -131,15 +129,6 @@ struct ChatListView: View {
}
}
private func toggleFilterButton() -> some View {
Button {
showUnreadAndFavorites = !showUnreadAndFavorites
} label: {
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
.foregroundColor(.accentColor)
}
}
@ViewBuilder private var chatList: some View {
let cs = filteredChats()
ZStack {
@@ -274,6 +263,66 @@ struct ChatListView: View {
}
}
struct SubsStatusIndicator: View {
@State private var subs: SMPServerSubs = SMPServerSubs(ssActive: 0, ssPending: 0)
@State private var timer: Timer? = nil
@State private var timerCounter = 0
@State private var showServersSummary = false
// Constants for the intervals
let initialInterval: TimeInterval = 1.0
let regularInterval: TimeInterval = 5.0
let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds
var body: some View {
Button {
showServersSummary = true
} label: {
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending, variableValueAsPercentage: true)
}
.onAppear {
startInitialTimer()
}
.onDisappear {
stopTimer()
}
.sheet(isPresented: $showServersSummary) {
ServersSummaryView()
}
}
private func startInitialTimer() {
timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in
getSubsSummary()
timerCounter += 1
// Switch to the regular timer after the initial phase
if timerCounter * Int(initialInterval) >= Int(initialPhaseDuration) {
switchToRegularTimer()
}
}
}
func switchToRegularTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: regularInterval, repeats: true) { _ in
getSubsSummary()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
private func getSubsSummary() {
do {
subs = try getAgentSubsSummary()
} catch let error {
logger.error("getAgentSubsSummary error: \(responseError(error))")
}
}
}
struct ChatListSearchBar: View {
@EnvironmentObject var m: ChatModel
@Binding var searchMode: Bool
@@ -282,9 +331,9 @@ struct ChatListSearchBar: View {
@Binding var searchShowingSimplexLink: Bool
@Binding var searchChatFilteredBySimplexLink: String?
@State private var ignoreSearchTextChange = false
@State private var showScanCodeSheet = false
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
VStack(spacing: 12) {
@@ -301,26 +350,6 @@ struct ChatListSearchBar: View {
.onTapGesture {
searchText = ""
}
} else if !searchFocussed {
HStack(spacing: 24) {
if m.pasteboardHasStrings {
Image(systemName: "doc")
.onTapGesture {
if let str = UIPasteboard.general.string {
searchText = str
}
}
}
Image(systemName: "qrcode")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.onTapGesture {
showScanCodeSheet = true
}
}
.padding(.trailing, 2)
}
}
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
@@ -335,14 +364,12 @@ struct ChatListSearchBar: View {
searchText = ""
searchFocussed = false
}
} else if m.chats.count > 0 {
toggleFilterButton()
}
}
Divider()
}
.sheet(isPresented: $showScanCodeSheet) {
NewChatView(selection: .connect, showQRCodeScanner: true)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatListView affecting nested view
}
.onChange(of: searchFocussed) { sf in
withAnimation { searchMode = sf }
}
@@ -376,6 +403,21 @@ struct ChatListSearchBar: View {
}
}
private func toggleFilterButton() -> some View {
ZStack {
Color.clear
.frame(width: 22, height: 22)
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
.resizable()
.scaledToFit()
.foregroundColor(showUnreadAndFavorites ? .accentColor : .secondary)
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
.onTapGesture {
showUnreadAndFavorites = !showUnreadAndFavorites
}
}
}
private func connect(_ link: String) {
planAndConnect(
link,
@@ -0,0 +1,629 @@
//
// ServersSummaryView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 25.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ServersSummaryView: View {
@State private var serversSummary: PresentedServersSummary? = nil
@State private var selectedUserCategory: PresentedUserCategory = .allUsers
@State private var selectedServerType: PresentedServerType = .smp
@State private var selectedSMPServer: String? = nil
@State private var selectedXFTPServer: String? = nil
@State private var alert: SomeAlert?
enum PresentedUserCategory {
case currentUser
case allUsers
}
enum PresentedServerType {
case smp
case xftp
}
var body: some View {
NavigationView {
viewBody()
.navigationTitle("Servers info")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
reloadButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
shareButton()
}
}
}
.onAppear {
getServersSummary()
}
.alert(item: $alert) { $0.alert }
}
private func shareButton() -> some View {
Button {
if let serversSummary = serversSummary {
showShareSheet(items: [encodePrettyPrinted(serversSummary)])
}
} label: {
Image(systemName: "square.and.arrow.up")
}
.disabled(serversSummary == nil)
}
public func encodePrettyPrinted<T: Encodable>(_ value: T) -> String {
let encoder = jsonEncoder
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(value)
return String(decoding: data, as: UTF8.self)
}
private func reloadButton() -> some View {
Button {
getServersSummary()
} label: {
Image(systemName: "arrow.counterclockwise")
}
}
@ViewBuilder private func viewBody() -> some View {
if let summ = serversSummary {
List {
Group {
Picker("User selection", selection: $selectedUserCategory) {
Text("All users").tag(PresentedUserCategory.allUsers)
Text("Current user").tag(PresentedUserCategory.currentUser)
}
.pickerStyle(.segmented)
Picker("Server type", selection: $selectedServerType) {
Text("SMP").tag(PresentedServerType.smp)
Text("XFTP").tag(PresentedServerType.xftp)
}
.pickerStyle(.segmented)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
switch (selectedUserCategory, selectedServerType) {
case (.allUsers, .smp):
if summ.allUsedSMP.count > 0 || summ.allPrevSMP.count > 0 || summ.allProxSMP.count > 0 {
if summ.allUsedSMP.count > 0 {
smpServersListView(summ.allUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Current session")
}
if summ.allPrevSMP.count > 0 {
smpServersListView(summ.allPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used")
}
if summ.allProxSMP.count > 0 {
smpServersListView(summ.allProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.currentUser, .smp):
if summ.userUsedSMP.count > 0 || summ.userPrevSMP.count > 0 || summ.userProxSMP.count > 0 {
if summ.userUsedSMP.count > 0 {
smpServersListView(summ.userUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Current session")
}
if summ.userPrevSMP.count > 0 {
smpServersListView(summ.userPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used")
}
if summ.userProxSMP.count > 0 {
smpServersListView(summ.userProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.allUsers, .xftp):
if summ.allUsedXFTP.count > 0 || summ.allPrevXFTP.count > 0 {
if summ.allUsedXFTP.count > 0 {
xftpServersListView(summ.allUsedXFTP, summ.statsStartedAt, "Current session")
}
if summ.allPrevXFTP.count > 0 {
xftpServersListView(summ.allPrevXFTP, summ.statsStartedAt, "Previously used")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.currentUser, .xftp):
if summ.userUsedXFTP.count > 0 || summ.userPrevXFTP.count > 0 {
if summ.userUsedXFTP.count > 0 {
xftpServersListView(summ.userUsedXFTP, summ.statsStartedAt, "Current session")
}
if summ.userPrevXFTP.count > 0 {
xftpServersListView(summ.userPrevXFTP, summ.statsStartedAt, "Previously used")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
}
}
} else {
Text("No info, try to reload")
}
}
@ViewBuilder private func smpServersListView(
_ servers: [SMPServerSummary],
showReconnectButton: Bool,
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
_ footer: LocalizedStringKey? = nil
) -> some View {
let sortedServers = servers.sorted { serverAddress($0.smpServer).compare(serverAddress($1.smpServer)) == .orderedAscending }
Section {
ForEach(sortedServers) { server in
smpServerView(server, showReconnectButton, statsStartedAt)
}
} header: {
if let header = header {
Text(header)
}
} footer: {
if let footer = footer {
Text(footer)
}
}
}
private func smpServerView(_ server: SMPServerSummary, _ showReconnectButton: Bool, _ statsStartedAt: Date) -> some View {
NavigationLink(tag: server.id, selection: $selectedSMPServer) {
SMPServerSummaryView(
summary: server,
showReconnectButton: showReconnectButton,
statsStartedAt: statsStartedAt
)
.navigationBarTitle("SMP server")
.navigationBarTitleDisplayMode(.large)
} label: {
HStack {
if let subs = server.subs {
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending)
.frame(width: 16, alignment: .center)
.padding(.trailing, 4)
}
Text(serverAddress(server.smpServer))
.lineLimit(1)
}
}
}
private func serverAddress(_ server: String) -> String {
parseServerAddress(server)?.hostnames.first ?? server
}
@ViewBuilder private func xftpServersListView(
_ servers: [XFTPServerSummary],
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
_ footer: LocalizedStringKey? = nil
) -> some View {
let sortedServers = servers.sorted { serverAddress($0.xftpServer).compare(serverAddress($1.xftpServer)) == .orderedAscending }
Section {
ForEach(sortedServers) { server in
xftpServerView(server, statsStartedAt)
}
} header: {
if let header = header {
Text(header)
}
} footer: {
if let footer = footer {
Text(footer)
}
}
}
private func xftpServerView(_ server: XFTPServerSummary, _ statsStartedAt: Date) -> some View {
NavigationLink(tag: server.id, selection: $selectedXFTPServer) {
XFTPServerSummaryView(
summary: server,
statsStartedAt: statsStartedAt
)
.navigationBarTitle("XFTP server")
.navigationBarTitleDisplayMode(.large)
} label: {
Text(serverAddress(server.xftpServer))
.lineLimit(1)
}
}
private func noCategoryInfoText() -> some View {
ZStack {
Rectangle()
.aspectRatio(contentMode: .fill)
.foregroundColor(Color.clear)
Text("No info")
.foregroundColor(.secondary)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
private func resetStatsButtonSection() -> some View {
Section {
Button {
alert = SomeAlert(
alert: Alert(
title: Text("Reset servers statistics?"),
message: Text("Servers statistics will be reset - this cannot be undone!"),
primaryButton: .destructive(Text("Reset")) {
Task {
do {
try await resetAgentServersStats()
getServersSummary()
} catch let error {
alert = SomeAlert(
alert: mkAlert(
title: "Error resetting statistics",
message: "\(responseError(error))"
),
id: "error resetting statistics"
)
}
}
},
secondaryButton: .cancel()
),
id: "reset statistics question"
)
} label: {
Text("Reset statistics")
}
}
}
private func getServersSummary() {
do {
serversSummary = try getAgentServersSummary()
} catch let error {
logger.error("getAgentServersSummary error: \(responseError(error))")
}
}
}
struct SubscriptionStatusView: View {
@EnvironmentObject var m: ChatModel
var activeSubs: Int
var pendingSubs: Int
var variableValueAsPercentage: Bool = false
var body: some View {
let netInfo = m.networkInfo
if netInfo.online {
let (image, color, variableValue, opacity) = networkOnlineImage(netInfo.networkType)
if #available(iOS 16.0, *) {
Image(systemName: image, variableValue: variableValue)
.foregroundColor(color)
} else {
Image(systemName: image)
.foregroundColor(color.opacity(opacity))
}
} else {
Image(systemName: "wifi.slash")
.foregroundColor(.secondary)
}
}
func networkOnlineImage(_ networkType: UserNetworkType) -> (String, Color, Double, Double) {
switch networkType {
case .cellular:
let (color, variableValue, opacity) = cellularbarsColor
return ("cellularbars", color, variableValue, opacity)
default:
let (color, variableValue, opacity) = wifiColor
return ("wifi", color, variableValue, opacity)
}
}
// We manipulate variableValue so all "wifi" sections are filled only with 100% active subs,
// unless variableValueAsPercentage is true; same for cellularbarsColor
var wifiColor: (Color, Double, Double) {
if activeSubs > 0 {
let wifiVariableValue = (
variableValueAsPercentage ? activeSubsPercentage
: ( // "wifi" has 3 sections
activeSubsPercentage >= 1 ? 1
: (activeSubsPercentage >= 0.5 && activeSubsPercentage < 1) ? 0.6
: (activeSubsPercentage > 0 && activeSubsPercentage < 0.5) ? 0.3
: 0
)
)
return (.accentColor, wifiVariableValue, activeSubsPercentage)
} else {
return (.secondary, 1, 1)
}
}
var cellularbarsColor: (Color, Double, Double) {
if activeSubs > 0 {
let wifiVariableValue = (
variableValueAsPercentage ? activeSubsPercentage
: ( // "cellularbars" has 4 sections
activeSubsPercentage >= 1 ? 1
: (activeSubsPercentage >= 0.67 && activeSubsPercentage < 1) ? 0.7
: (activeSubsPercentage >= 0.33 && activeSubsPercentage < 0.67) ? 0.45
: (activeSubsPercentage > 0 && activeSubsPercentage < 0.33) ? 0.2
: 0
)
)
return (.accentColor, wifiVariableValue, activeSubsPercentage)
} else {
return (.secondary, 1, 1)
}
}
var activeSubsPercentage: Double {
let total = activeSubs + pendingSubs
guard total != 0 else { return 0.0 }
return Double(activeSubs) / Double(total)
}
}
struct SMPServerSummaryView: View {
var summary: SMPServerSummary
var showReconnectButton: Bool
var statsStartedAt: Date
@State private var alert: SomeAlert?
var body: some View {
List {
Section {
Text(summary.smpServer)
.textSelection(.enabled)
if let known = summary.known, !known {
Button {
addKnownServer()
} label: {
Text("Add as known")
}
}
} header: {
Text("Server address")
} footer: {
if let known = summary.known, known {
Text("Server is configured in **Settings** → **Network & servers**.")
}
}
if showReconnectButton {
reconnectButtonSection()
}
if let subs = summary.subs {
subsSection(subs)
}
if let sess = summary.sessions {
sessionsSection(sess)
}
if let stats = summary.stats {
statsSection(stats)
}
}
.alert(item: $alert) { $0.alert }
}
private func reconnectButtonSection() -> some View {
Section {
Button {
alert = SomeAlert(
alert: Alert(
title: Text("Reconnect server?"),
message: Text("Reconnect server to force message delivery. It uses additional traffic."),
primaryButton: .default(Text("Ok")) {
Task {
do {
try await reconnectServer(smpServer: summary.smpServer)
} catch let error {
alert = SomeAlert(
alert: mkAlert(
title: "Error reconnecting server",
message: "\(responseError(error))"
),
id: "error reconnecting server"
)
}
}
},
secondaryButton: .cancel()
),
id: "reconnect server question"
)
} label: {
Text("Reconnect")
}
}
}
private func subsSection(_ subs: SMPServerSubs) -> some View {
Section {
infoRow("Active", "\(subs.ssActive)")
infoRow("Pending", "\(subs.ssPending)")
} header: {
HStack {
Text("Subscriptions")
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending)
}
}
}
private func sessionsSection(_ sess: ServerSessions) -> some View {
Section("Sessions") {
infoRow("Connected", "\(sess.ssConnected)")
infoRow("Errors", "\(sess.ssErrors)")
infoRow("Connecting", "\(sess.ssConnecting)")
}
}
private func statsSection(_ stats: AgentSMPServerStatsData) -> some View {
Section("Statistics") {
infoRow("Starting from", localTimestamp(statsStartedAt))
infoRow("Messages sent directly", "\(stats._sentDirect)")
indentedInfoRow("attempts", "\(stats._sentDirectAttempts)")
infoRow("Messages sent via proxy", "\(stats._sentViaProxy)")
indentedInfoRow("attempts", "\(stats._sentViaProxyAttempts)")
infoRow("Messages sent to proxy", "\(stats._sentProxied)")
indentedInfoRow("attempts", "\(stats._sentProxiedAttempts)")
infoRow("Sending AUTH errors", "\(stats._sentAuthErrs)")
indentedInfoRow("QUOTA errors", "\(stats._sentQuotaErrs)")
indentedInfoRow("expired", "\(stats._sentExpiredErrs)")
indentedInfoRow("other errors", "\(stats._sentOtherErrs)")
infoRow("Messages received", "\(stats._recvMsgs)")
indentedInfoRow("duplicates", "\(stats._recvDuplicates)")
indentedInfoRow("decryption errors", "\(stats._recvCryptoErrs)")
indentedInfoRow("other errors", "\(stats._recvErrs)")
infoRow("Messages acknowledged", "\(stats._ackMsgs)")
indentedInfoRow("attempts", "\(stats._ackAttempts)")
infoRow("Connections created", "\(stats._connCreated)")
indentedInfoRow("secured", "\(stats._connSecured)")
indentedInfoRow("completed", "\(stats._connCompleted)")
infoRow("Connections deleted", "\(stats._connDeleted)")
infoRow("Connections subscribed", "\(stats._connSubscribed)")
indentedInfoRow("attempts", "\(stats._connSubAttempts)")
indentedInfoRow("errors", "\(stats._connSubErrs)")
}
}
func addKnownServer() {
Task {
do {
try await addKnownProtoServer(server: summary.smpServer)
await MainActor.run {
// TODO disable button, etc.
}
} catch let error {
await MainActor.run {
alert = errorAddingServerAlert(error)
}
}
}
}
}
private func indentedInfoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
HStack {
Text(title)
.padding(.leading, 24)
Spacer()
Text(value)
.foregroundStyle(.secondary)
}
}
private func errorAddingServerAlert(_ error: Error) -> SomeAlert {
SomeAlert(
alert: mkAlert(
title: "Error adding server",
message: "Make sure server address is in correct format (\(responseError(error)))."
),
id: "error saving server"
)
}
struct XFTPServerSummaryView: View {
var summary: XFTPServerSummary
var statsStartedAt: Date
@State private var alert: SomeAlert?
var body: some View {
List {
Section {
Text(summary.xftpServer)
.textSelection(.enabled)
if let known = summary.known, !known {
Button {
addKnownServer()
} label: {
Text("Add as known")
}
}
} header: {
Text("Server address")
} footer: {
if let known = summary.known, known {
Text("Server is configured in **Settings** → **Network & servers**.")
}
}
if let sess = summary.sessions {
sessionsSection(sess)
}
inProgressSection()
if let stats = summary.stats {
statsSection(stats)
}
}
.alert(item: $alert) { $0.alert }
}
private func sessionsSection(_ sess: ServerSessions) -> some View {
Section("Sessions") {
infoRow("Connected", "\(sess.ssConnected)")
infoRow("Errors", "\(sess.ssErrors)")
infoRow("Connecting", "\(sess.ssConnecting)")
}
}
private func inProgressSection() -> some View {
Section("In progress") {
localizedInfoRow("Download", boolYesNo(summary.rcvInProgress))
localizedInfoRow("Upload", boolYesNo(summary.sndInProgress))
localizedInfoRow("Deletion", boolYesNo(summary.delInProgress))
}
}
private func boolYesNo(_ b: Bool) -> LocalizedStringKey {
b ? "yes" : "no"
}
private func statsSection(_ stats: AgentXFTPServerStatsData) -> some View {
Section("Statistics") {
infoRow("Starting from", localTimestamp(statsStartedAt))
infoRow("Chunks uploaded", "\(stats._uploads)")
indentedInfoRow("attempts", "\(stats._uploadAttempts)")
indentedInfoRow("errors", "\(stats._uploadErrs)")
infoRow("Chunks downloaded", "\(stats._downloads)")
indentedInfoRow("attempts", "\(stats._downloadAttempts)")
indentedInfoRow("AUTH errors", "\(stats._downloadAuthErrs)")
indentedInfoRow("other errors", "\(stats._downloadErrs)")
infoRow("Chunks deleted", "\(stats._deletions)")
indentedInfoRow("attempts", "\(stats._deleteAttempts)")
indentedInfoRow("errors", "\(stats._deleteErrs)")
}
}
func addKnownServer() {
Task {
do {
try await addKnownProtoServer(server: summary.xftpServer)
await MainActor.run {
// TODO disable button, etc.
}
} catch let error {
await MainActor.run {
alert = errorAddingServerAlert(error)
}
}
}
}
}
#Preview {
ServersSummaryView()
}
@@ -10,6 +10,7 @@ import SwiftUI
enum NewChatMenuOption: Identifiable {
case newContact
case scanPaste
case newGroup
var id: Self { self }
@@ -25,6 +26,11 @@ struct NewChatMenuButton: View {
} label: {
Text("Add contact")
}
Button {
newChatMenuOption = .scanPaste
} label: {
Text("Scan / Paste link")
}
Button {
newChatMenuOption = .newGroup
} label: {
@@ -39,6 +45,7 @@ struct NewChatMenuButton: View {
.sheet(item: $newChatMenuOption) { opt in
switch opt {
case .newContact: NewChatView(selection: .invite)
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
case .newGroup: AddGroupView()
}
}
@@ -14,6 +14,7 @@ struct ProtocolServerView: View {
let serverProtocol: ServerProtocol
@Binding var server: ServerCfg
@State var serverToEdit: ServerCfg
@State var serverEnabled: Bool
@State private var showTestFailure = false
@State private var testing = false
@State private var testFailure: ProtocolTestFailure?
@@ -110,7 +111,10 @@ struct ProtocolServerView: View {
Spacer()
showTestStatus(server: serverToEdit)
}
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
Toggle("Use for new connections", isOn: $serverEnabled)
.onChange(of: serverEnabled) { enabled in
serverToEdit.enabled = enabled ? .enabled : .disabled
}
}
}
}
@@ -179,7 +183,8 @@ struct ProtocolServerView_Previews: PreviewProvider {
ProtocolServerView(
serverProtocol: .smp,
server: Binding.constant(ServerCfg.sampleData.custom),
serverToEdit: ServerCfg.sampleData.custom
serverToEdit: ServerCfg.sampleData.custom,
serverEnabled: true
)
}
}
@@ -159,7 +159,7 @@ struct ProtocolServersView: View {
}
private var allServersDisabled: Bool {
servers.allSatisfy { !$0.enabled }
servers.allSatisfy { $0.enabled != .enabled }
}
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
@@ -168,7 +168,8 @@ struct ProtocolServersView: View {
ProtocolServerView(
serverProtocol: serverProtocol,
server: server,
serverToEdit: srv
serverToEdit: srv,
serverEnabled: srv.enabled == .enabled
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.navigationBarTitleDisplayMode(.large)
@@ -181,7 +182,7 @@ struct ProtocolServersView: View {
invalidServer()
} else if !uniqueAddress(srv, address) {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
} else if !srv.enabled {
} else if srv.enabled != .enabled {
Image(systemName: "slash.circle").foregroundColor(.secondary)
} else {
showTestStatus(server: srv)
@@ -194,7 +195,7 @@ struct ProtocolServersView: View {
.padding(.trailing, 4)
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
if srv.enabled {
if srv.enabled == .enabled {
v
} else {
v.foregroundColor(.secondary)
@@ -235,7 +236,7 @@ struct ProtocolServersView: View {
private func addAllPresets() {
for srv in presetServers {
if !hasPreset(srv) {
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true))
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled))
}
}
}
@@ -260,7 +261,7 @@ struct ProtocolServersView: View {
private func resetTestStatus() {
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
servers[i].tested = nil
}
}
@@ -269,7 +270,7 @@ struct ProtocolServersView: View {
private func runServersTest() async -> [String: ProtocolTestFailure] {
var fs: [String: ProtocolTestFailure] = [:]
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
if let f = await testServerConnection(server: $servers[i]) {
fs[serverHostname(servers[i].server)] = f
}
@@ -40,7 +40,7 @@ struct ScanProtocolServer: View {
switch resp {
case let .success(r):
if parseServerAddress(r.string) != nil {
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: true))
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: .enabled))
dismiss()
} else {
showAddressError = true
+36 -32
View File
@@ -148,6 +148,12 @@
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; };
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
641753572C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; };
641753582C2AC158005415B4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753532C2AC158005415B4 /* libgmp.a */; };
641753592C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; };
6417535A2C2AC158005415B4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753552C2AC158005415B4 /* libgmpxx.a */; };
6417535B2C2AC158005415B4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753562C2AC158005415B4 /* libffi.a */; };
6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */; };
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
@@ -192,11 +198,6 @@
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; };
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3B2C22D78C00CBA347 /* libffi.a */; };
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; };
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3D2C22D78C00CBA347 /* libgmp.a */; };
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -443,6 +444,12 @@
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; };
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = "<group>"; };
641753532C2AC158005415B4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; path = "Libraries/libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = "<group>"; };
641753552C2AC158005415B4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
641753562C2AC158005415B4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
@@ -487,11 +494,6 @@
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = "<group>"; };
E5D68D3B2C22D78C00CBA347 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = "<group>"; };
E5D68D3D2C22D78C00CBA347 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -529,13 +531,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
6417535A2C2AC158005415B4 /* libgmpxx.a in Frameworks */,
641753592C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
641753572C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
6417535B2C2AC158005415B4 /* libffi.a in Frameworks */,
641753582C2AC158005415B4 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */,
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */,
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */,
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -601,11 +603,6 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5D68D3B2C22D78C00CBA347 /* libffi.a */,
E5D68D3D2C22D78C00CBA347 /* libgmp.a */,
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */,
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -672,6 +669,11 @@
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
5C764E5C279C70B7000C6508 /* Libraries */,
641753562C2AC158005415B4 /* libffi.a */,
641753532C2AC158005415B4 /* libgmp.a */,
641753552C2AC158005415B4 /* libgmpxx.a */,
641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */,
5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */,
5CA059DA279559F40002BEB4 /* Tests iOS */,
@@ -802,6 +804,7 @@
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
18415835CBD939A9ABDC108A /* UserPicker.swift */,
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -1256,6 +1259,7 @@
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
@@ -1552,7 +1556,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1577,7 +1581,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1601,7 +1605,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1626,7 +1630,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1687,7 +1691,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1702,7 +1706,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1724,7 +1728,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1739,7 +1743,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1761,7 +1765,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1787,7 +1791,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1812,7 +1816,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 224;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1838,7 +1842,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
+140 -7
View File
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI
public let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder()
public let jsonEncoder = getJSONEncoder()
public enum ChatCommand {
case showActiveUser
@@ -71,6 +71,7 @@ public enum ChatCommand {
case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent)
case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol)
case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg])
case apiAddKnownProtoServer(userId: Int64, server: String)
case apiTestProtoServer(userId: Int64, server: String)
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
case apiGetChatItemTTL(userId: Int64)
@@ -78,6 +79,7 @@ public enum ChatCommand {
case apiGetNetworkConfig
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
case reconnectAllServers
case reconnectServer(userId: Int64, smpServer: String)
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
case apiContactInfo(contactId: Int64)
@@ -122,6 +124,7 @@ public enum ChatCommand {
case apiEndCall(contact: Contact)
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
// WebRTC calls /
case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
@@ -142,6 +145,9 @@ public enum ChatCommand {
case apiStandaloneFileInfo(url: String)
// misc
case showVersion
case getAgentServersSummary(userId: Int64)
case resetAgentServersStats
case getAgentSubsSummary(userId: Int64)
case string(String)
public var cmdString: String {
@@ -219,6 +225,7 @@ public enum ChatCommand {
case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)"
case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)"
case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))"
case let .apiAddKnownProtoServer(userId, server): return "/_known server \(userId) \(server)"
case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)"
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
@@ -226,6 +233,7 @@ public enum ChatCommand {
case .apiGetNetworkConfig: return "/network"
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
case .reconnectAllServers: return "/reconnect"
case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)"
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
@@ -301,6 +309,9 @@ public enum ChatCommand {
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
case .showVersion: return "/version"
case let .getAgentServersSummary(userId): return "/get servers summary \(userId)"
case .resetAgentServersStats: return "/reset servers stats"
case let .getAgentSubsSummary(userId): return "/get subs summary \(userId)"
case let .string(str): return str
}
}
@@ -368,6 +379,7 @@ public enum ChatCommand {
case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation"
case .apiGetUserProtoServers: return "apiGetUserProtoServers"
case .apiSetUserProtoServers: return "apiSetUserProtoServers"
case .apiAddKnownProtoServer: return "apiAddKnownProtoServer"
case .apiTestProtoServer: return "apiTestProtoServer"
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
@@ -375,6 +387,7 @@ public enum ChatCommand {
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
case .reconnectAllServers: return "reconnectAllServers"
case .reconnectServer: return "reconnectServer"
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiSetMemberSettings: return "apiSetMemberSettings"
case .apiContactInfo: return "apiContactInfo"
@@ -435,6 +448,9 @@ public enum ChatCommand {
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
case .showVersion: return "showVersion"
case .getAgentServersSummary: return "getAgentServersSummary"
case .resetAgentServersStats: return "resetAgentServersStats"
case .getAgentSubsSummary: return "getAgentSubsSummary"
case .string: return "console command"
}
}
@@ -663,6 +679,8 @@ public enum ChatResponse: Decodable, Error {
// misc
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
case cmdOk(user: UserRef?)
case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary)
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
case chatCmdError(user_: UserRef?, chatError: ChatError)
case chatError(user_: UserRef?, chatError: ChatError)
case archiveImported(archiveErrors: [ArchiveError])
@@ -821,6 +839,8 @@ public enum ChatResponse: Decodable, Error {
case .contactPQEnabled: return "contactPQEnabled"
case .versionInfo: return "versionInfo"
case .cmdOk: return "cmdOk"
case .agentServersSummary: return "agentServersSummary"
case .agentSubsSummary: return "agentSubsSummary"
case .chatCmdError: return "chatCmdError"
case .chatError: return "chatError"
case .archiveImported: return "archiveImported"
@@ -984,6 +1004,8 @@ public enum ChatResponse: Decodable, Error {
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
case .cmdOk: return noDetails
case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary))
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary))
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
@@ -1109,13 +1131,13 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var server: String
public var preset: Bool
public var tested: Bool?
public var enabled: Bool
public var enabled: ServerEnabled
var createdAt = Date()
// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive?
// Even if we don't see the use case, it's probably better to allow it in the model
// In any case, "trusted/known" servers are out of scope of this change
public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) {
public init(server: String, preset: Bool, tested: Bool?, enabled: ServerEnabled) {
self.server = server
self.preset = preset
self.tested = tested
@@ -1128,7 +1150,7 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var id: String { "\(server) \(createdAt)" }
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: true)
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: .enabled)
public var isEmpty: Bool {
server.trimmingCharacters(in: .whitespaces) == ""
@@ -1145,19 +1167,19 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
server: "smp://abcd@smp8.simplex.im",
preset: true,
tested: true,
enabled: true
enabled: .enabled
),
custom: ServerCfg(
server: "smp://abcd@smp9.simplex.im",
preset: false,
tested: false,
enabled: false
enabled: .enabled
),
untested: ServerCfg(
server: "smp://abcd@smp10.simplex.im",
preset: false,
tested: nil,
enabled: true
enabled: .enabled
)
)
@@ -1169,6 +1191,12 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
}
}
public enum ServerEnabled: String, Codable {
case disabled
case enabled
case known
}
public enum ProtocolTestStep: String, Decodable, Equatable {
case connect
case disconnect
@@ -2224,3 +2252,108 @@ public enum MsgType: String, Codable {
case message
case quota
}
public struct PresentedServersSummary: Codable {
public var statsStartedAt: Date
public var currentUserServers: ServersSummary
public var allUsersServers: ServersSummary
public var allUsedSMP: [SMPServerSummary] { self.allUsersServers.currentlyUsedSMPServers }
public var allPrevSMP: [SMPServerSummary] { self.allUsersServers.previouslyUsedSMPServers }
public var allProxSMP: [SMPServerSummary] { self.allUsersServers.onlyProxiedSMPServers }
public var userUsedSMP: [SMPServerSummary] { self.currentUserServers.currentlyUsedSMPServers }
public var userPrevSMP: [SMPServerSummary] { self.currentUserServers.previouslyUsedSMPServers }
public var userProxSMP: [SMPServerSummary] { self.currentUserServers.onlyProxiedSMPServers }
public var allUsedXFTP: [XFTPServerSummary] { self.allUsersServers.currentlyUsedXFTPServers }
public var allPrevXFTP: [XFTPServerSummary] { self.allUsersServers.previouslyUsedXFTPServers }
public var userUsedXFTP: [XFTPServerSummary] { self.currentUserServers.currentlyUsedXFTPServers }
public var userPrevXFTP: [XFTPServerSummary] { self.currentUserServers.previouslyUsedXFTPServers }
}
public struct ServersSummary: Codable {
public var currentlyUsedSMPServers: [SMPServerSummary]
public var previouslyUsedSMPServers: [SMPServerSummary]
public var onlyProxiedSMPServers: [SMPServerSummary]
public var currentlyUsedXFTPServers: [XFTPServerSummary]
public var previouslyUsedXFTPServers: [XFTPServerSummary]
}
public struct SMPServerSummary: Codable, Identifiable {
public var smpServer: String
public var known: Bool?
public var sessions: ServerSessions?
public var subs: SMPServerSubs?
public var stats: AgentSMPServerStatsData?
public var id: String { smpServer }
}
public struct ServerSessions: Codable {
public var ssConnected: Int
public var ssErrors: Int
public var ssConnecting: Int
}
public struct SMPServerSubs: Codable {
public var ssActive: Int
public var ssPending: Int
public init(ssActive: Int, ssPending: Int) {
self.ssActive = ssActive
self.ssPending = ssPending
}
}
public struct AgentSMPServerStatsData: Codable {
public var _sentDirect: Int
public var _sentViaProxy: Int
public var _sentProxied: Int
public var _sentDirectAttempts: Int
public var _sentViaProxyAttempts: Int
public var _sentProxiedAttempts: Int
public var _sentAuthErrs: Int
public var _sentQuotaErrs: Int
public var _sentExpiredErrs: Int
public var _sentOtherErrs: Int
public var _recvMsgs: Int
public var _recvDuplicates: Int
public var _recvCryptoErrs: Int
public var _recvErrs: Int
public var _ackMsgs: Int
public var _ackAttempts: Int
public var _connCreated: Int
public var _connSecured: Int
public var _connCompleted: Int
public var _connDeleted: Int
public var _connSubscribed: Int
public var _connSubAttempts: Int
public var _connSubErrs: Int
}
public struct XFTPServerSummary: Codable, Identifiable {
public var xftpServer: String
public var known: Bool?
public var sessions: ServerSessions?
public var stats: AgentXFTPServerStatsData?
public var rcvInProgress: Bool
public var sndInProgress: Bool
public var delInProgress: Bool
public var id: String { xftpServer }
}
public struct AgentXFTPServerStatsData: Codable {
public var _uploads: Int
public var _uploadAttempts: Int
public var _uploadErrs: Int
public var _downloads: Int
public var _downloadAttempts: Int
public var _downloadAuthErrs: Int
public var _downloadErrs: Int
public var _deletions: Int
public var _deleteAttempts: Int
public var _deleteErrs: Int
}
+2
View File
@@ -11,6 +11,7 @@ import SwiftUI
public struct User: Identifiable, Decodable, UserLike, NamedChat {
public var userId: Int64
public var agentUserId: String
var userContactId: Int64
var localDisplayName: ContactName
public var profile: LocalProfile
@@ -41,6 +42,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat {
public static let sampleData = User(
userId: 1,
agentUserId: "abc",
userContactId: 1,
localDisplayName: "alice",
profile: LocalProfile.sampleData,