mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
Merge branch 'master' into lp/ios-translate-sheet
This commit is contained in:
commit
e34faf56c7
14 changed files with 1139 additions and 123 deletions
|
@ -92,18 +92,22 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) -> ChatResponse {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
if log {
|
||||||
|
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||||
|
}
|
||||||
let start = Date.now
|
let start = Date.now
|
||||||
let resp = bgTask
|
let resp = bgTask
|
||||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
||||||
: sendSimpleXCmd(cmd, ctrl)
|
: sendSimpleXCmd(cmd, ctrl)
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
if log {
|
||||||
if case let .response(_, json) = resp {
|
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
if case let .response(_, json) = resp {
|
||||||
}
|
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||||
Task {
|
}
|
||||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
Task {
|
||||||
|
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
@ -543,6 +547,11 @@ func reconnectAllServers() async throws {
|
||||||
try await sendCommandOkResp(.reconnectAllServers)
|
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 {
|
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
|
||||||
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
||||||
}
|
}
|
||||||
|
@ -1336,6 +1345,18 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAgentServersSummary() throws -> PresentedServersSummary {
|
||||||
|
let userId = try currentUserId("getAgentServersSummary")
|
||||||
|
let r = chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false)
|
||||||
|
if case let .agentServersSummary(_, serversSummary) = r { return serversSummary }
|
||||||
|
logger.error("getAgentServersSummary error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAgentServersStats() async throws {
|
||||||
|
try await sendCommandOkResp(.resetAgentServersStats)
|
||||||
|
}
|
||||||
|
|
||||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||||
if let userId = ChatModel.shared.currentUser?.userId {
|
if let userId = ChatModel.shared.currentUser?.userId {
|
||||||
return userId
|
return userId
|
||||||
|
|
|
@ -115,9 +115,7 @@ struct ChatListView: View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text("Chats")
|
Text("Chats")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
if chatModel.chats.count > 0 {
|
SubsStatusIndicator()
|
||||||
toggleFilterButton()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.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 {
|
@ViewBuilder private var chatList: some View {
|
||||||
let cs = filteredChats()
|
let cs = filteredChats()
|
||||||
ZStack {
|
ZStack {
|
||||||
|
@ -272,6 +261,75 @@ struct ChatListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SubsStatusIndicator: View {
|
||||||
|
@State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs
|
||||||
|
@State private var sess: ServerSessions = ServerSessions.newServerSessions
|
||||||
|
@State private var timer: Timer? = nil
|
||||||
|
@State private var timerCounter = 0
|
||||||
|
@State private var showServersSummary = false
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||||
|
|
||||||
|
// Constants for the intervals
|
||||||
|
let initialInterval: TimeInterval = 1.0
|
||||||
|
let regularInterval: TimeInterval = 3.0
|
||||||
|
let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
showServersSummary = true
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
SubscriptionStatusIndicatorView(subs: subs, sess: sess)
|
||||||
|
if showSubscriptionPercentage {
|
||||||
|
SubscriptionStatusPercentageView(subs: subs, sess: sess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
startInitialTimer()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showServersSummary) {
|
||||||
|
ServersSummaryView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startInitialTimer() {
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in
|
||||||
|
getServersSummary()
|
||||||
|
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
|
||||||
|
getServersSummary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getServersSummary() {
|
||||||
|
do {
|
||||||
|
let summ = try getAgentServersSummary()
|
||||||
|
(subs, sess) = (summ.allUsersSMP.smpTotals.subs, summ.allUsersSMP.smpTotals.sessions)
|
||||||
|
} catch let error {
|
||||||
|
logger.error("getAgentServersSummary error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatListSearchBar: View {
|
struct ChatListSearchBar: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@Binding var searchMode: Bool
|
@Binding var searchMode: Bool
|
||||||
|
@ -280,9 +338,9 @@ struct ChatListSearchBar: View {
|
||||||
@Binding var searchShowingSimplexLink: Bool
|
@Binding var searchShowingSimplexLink: Bool
|
||||||
@Binding var searchChatFilteredBySimplexLink: String?
|
@Binding var searchChatFilteredBySimplexLink: String?
|
||||||
@State private var ignoreSearchTextChange = false
|
@State private var ignoreSearchTextChange = false
|
||||||
@State private var showScanCodeSheet = false
|
|
||||||
@State private var alert: PlanAndConnectAlert?
|
@State private var alert: PlanAndConnectAlert?
|
||||||
@State private var sheet: PlanAndConnectActionSheet?
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
|
@ -299,26 +357,6 @@ struct ChatListSearchBar: View {
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
searchText = ""
|
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))
|
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||||
|
@ -333,14 +371,12 @@ struct ChatListSearchBar: View {
|
||||||
searchText = ""
|
searchText = ""
|
||||||
searchFocussed = false
|
searchFocussed = false
|
||||||
}
|
}
|
||||||
|
} else if m.chats.count > 0 {
|
||||||
|
toggleFilterButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
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
|
.onChange(of: searchFocussed) { sf in
|
||||||
withAnimation { searchMode = sf }
|
withAnimation { searchMode = sf }
|
||||||
}
|
}
|
||||||
|
@ -374,6 +410,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) {
|
private func connect(_ link: String) {
|
||||||
planAndConnect(
|
planAndConnect(
|
||||||
link,
|
link,
|
||||||
|
|
741
apps/ios/Shared/Views/ChatList/ServersSummaryView.swift
Normal file
741
apps/ios/Shared/Views/ChatList/ServersSummaryView.swift
Normal file
|
@ -0,0 +1,741 @@
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@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 timer: Timer? = nil
|
||||||
|
@State private var alert: SomeAlert?
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||||
|
|
||||||
|
enum PresentedUserCategory {
|
||||||
|
case currentUser
|
||||||
|
case allUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PresentedServerType {
|
||||||
|
case smp
|
||||||
|
case xftp
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
viewBody()
|
||||||
|
.navigationTitle("Servers info")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
shareButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count == 1 {
|
||||||
|
selectedUserCategory = .currentUser
|
||||||
|
}
|
||||||
|
getServersSummary()
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
.alert(item: $alert) { $0.alert }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTimer() {
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||||
|
getServersSummary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func viewBody() -> some View {
|
||||||
|
if let summ = serversSummary {
|
||||||
|
List {
|
||||||
|
Group {
|
||||||
|
if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
|
||||||
|
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("Messages").tag(PresentedServerType.smp)
|
||||||
|
Text("Files").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):
|
||||||
|
let smpSumm = summ.allUsersSMP
|
||||||
|
let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers)
|
||||||
|
|
||||||
|
SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||||
|
|
||||||
|
smpSubsSection(totals)
|
||||||
|
|
||||||
|
if curr.count > 0 {
|
||||||
|
smpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||||
|
}
|
||||||
|
if prev.count > 0 {
|
||||||
|
smpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||||
|
}
|
||||||
|
if prox.count > 0 {
|
||||||
|
smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSessionsView(sess: totals.sessions)
|
||||||
|
case (.currentUser, .smp):
|
||||||
|
let smpSumm = summ.currentUserSMP
|
||||||
|
let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers)
|
||||||
|
|
||||||
|
SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||||
|
|
||||||
|
smpSubsSection(totals)
|
||||||
|
|
||||||
|
if curr.count > 0 {
|
||||||
|
smpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||||
|
}
|
||||||
|
if prev.count > 0 {
|
||||||
|
smpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||||
|
}
|
||||||
|
if prox.count > 0 {
|
||||||
|
smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSessionsView(sess: totals.sessions)
|
||||||
|
case (.allUsers, .xftp):
|
||||||
|
let xftpSumm = summ.allUsersXFTP
|
||||||
|
let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers)
|
||||||
|
|
||||||
|
XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||||
|
|
||||||
|
if curr.count > 0 {
|
||||||
|
xftpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||||
|
}
|
||||||
|
if prev.count > 0 {
|
||||||
|
xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSessionsView(sess: totals.sessions)
|
||||||
|
case (.currentUser, .xftp):
|
||||||
|
let xftpSumm = summ.currentUserXFTP
|
||||||
|
let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers)
|
||||||
|
|
||||||
|
XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||||
|
|
||||||
|
if curr.count > 0 {
|
||||||
|
xftpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||||
|
}
|
||||||
|
if prev.count > 0 {
|
||||||
|
xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSessionsView(sess: totals.sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
reconnectAllButton()
|
||||||
|
resetStatsButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No info, try to reload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smpSubsSection(_ totals: SMPTotals) -> some View {
|
||||||
|
Section {
|
||||||
|
infoRow("Connections subscribed", numOrDash(totals.subs.ssActive))
|
||||||
|
infoRow("Total", numOrDash(totals.subs.total))
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Message subscriptions")
|
||||||
|
SubscriptionStatusIndicatorView(subs: totals.subs, sess: totals.sessions)
|
||||||
|
if showSubscriptionPercentage {
|
||||||
|
SubscriptionStatusPercentageView(subs: totals.subs, sess: totals.sessions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reconnectAllButton() -> some View {
|
||||||
|
Button {
|
||||||
|
alert = SomeAlert(
|
||||||
|
alert: Alert(
|
||||||
|
title: Text("Reconnect all servers?"),
|
||||||
|
message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."),
|
||||||
|
primaryButton: .default(Text("Ok")) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await reconnectAllServers()
|
||||||
|
} catch let error {
|
||||||
|
alert = SomeAlert(
|
||||||
|
alert: mkAlert(
|
||||||
|
title: "Error reconnecting servers",
|
||||||
|
message: "\(responseError(error))"
|
||||||
|
),
|
||||||
|
id: "error reconnecting servers"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
),
|
||||||
|
id: "reconnect servers question"
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
Text("Reconnect all servers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func smpServersListView(
|
||||||
|
_ servers: [SMPServerSummary],
|
||||||
|
_ statsStartedAt: Date,
|
||||||
|
_ header: LocalizedStringKey? = nil,
|
||||||
|
_ footer: LocalizedStringKey? = nil
|
||||||
|
) -> some View {
|
||||||
|
let sortedServers = servers.sorted {
|
||||||
|
$0.hasSubs == $1.hasSubs
|
||||||
|
? serverAddress($0.smpServer) < serverAddress($1.smpServer)
|
||||||
|
: $0.hasSubs && !$1.hasSubs
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
ForEach(sortedServers) { server in
|
||||||
|
smpServerView(server, statsStartedAt)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
if let header = header {
|
||||||
|
Text(header)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
if let footer = footer {
|
||||||
|
Text(footer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smpServerView(_ srvSumm: SMPServerSummary, _ statsStartedAt: Date) -> some View {
|
||||||
|
NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) {
|
||||||
|
SMPServerSummaryView(
|
||||||
|
summary: srvSumm,
|
||||||
|
statsStartedAt: statsStartedAt
|
||||||
|
)
|
||||||
|
.navigationBarTitle("SMP server")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(serverAddress(srvSumm.smpServer))
|
||||||
|
.lineLimit(1)
|
||||||
|
if let subs = srvSumm.subs {
|
||||||
|
Spacer()
|
||||||
|
if showSubscriptionPercentage {
|
||||||
|
SubscriptionStatusPercentageView(subs: subs, sess: srvSumm.sessionsOrNew)
|
||||||
|
}
|
||||||
|
SubscriptionStatusIndicatorView(subs: subs, sess: srvSumm.sessionsOrNew)
|
||||||
|
} else if let sess = srvSumm.sessions {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "arrow.up.circle")
|
||||||
|
.symbolRenderingMode(.palette)
|
||||||
|
.foregroundStyle(sessIconColor(sess), Color.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverAddress(_ server: String) -> String {
|
||||||
|
parseServerAddress(server)?.hostnames.first ?? server
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sessIconColor(_ sess: ServerSessions) -> Color {
|
||||||
|
let online = m.networkInfo.online
|
||||||
|
return (
|
||||||
|
online && sess.ssConnected > 0
|
||||||
|
? sessionActiveColor
|
||||||
|
: Color(uiColor: .tertiaryLabel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sessionActiveColor: Color {
|
||||||
|
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||||
|
return onionHosts == .require ? .indigo : .accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func xftpServersListView(
|
||||||
|
_ servers: [XFTPServerSummary],
|
||||||
|
_ statsStartedAt: Date,
|
||||||
|
_ header: LocalizedStringKey? = nil,
|
||||||
|
_ footer: LocalizedStringKey? = nil
|
||||||
|
) -> some View {
|
||||||
|
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
|
||||||
|
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(_ srvSumm: XFTPServerSummary, _ statsStartedAt: Date) -> some View {
|
||||||
|
NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) {
|
||||||
|
XFTPServerSummaryView(
|
||||||
|
summary: srvSumm,
|
||||||
|
statsStartedAt: statsStartedAt
|
||||||
|
)
|
||||||
|
.navigationBarTitle("XFTP server")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(serverAddress(srvSumm.xftpServer))
|
||||||
|
.lineLimit(1)
|
||||||
|
if let inProgressIcon = inProgressIcon(srvSumm) {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: inProgressIcon)
|
||||||
|
.symbolRenderingMode(.palette)
|
||||||
|
.foregroundStyle(sessionActiveColor, Color.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inProgressIcon(_ srvSumm: XFTPServerSummary) -> String? {
|
||||||
|
switch (srvSumm.rcvInProgress, srvSumm.sndInProgress, srvSumm.delInProgress) {
|
||||||
|
case (false, false, false): nil
|
||||||
|
case (true, false, false): "arrow.down.circle"
|
||||||
|
case (false, true, false): "arrow.up.circle"
|
||||||
|
case (false, false, true): "trash.circle"
|
||||||
|
default: "arrow.up.arrow.down.circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetStatsButton() -> some View {
|
||||||
|
Button {
|
||||||
|
alert = SomeAlert(
|
||||||
|
alert: Alert(
|
||||||
|
title: Text("Reset all 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 all statistics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getServersSummary() {
|
||||||
|
do {
|
||||||
|
serversSummary = try getAgentServersSummary()
|
||||||
|
} catch let error {
|
||||||
|
logger.error("getAgentServersSummary error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SubscriptionStatusIndicatorView: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
var subs: SMPServerSubs
|
||||||
|
var sess: ServerSessions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||||
|
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
|
||||||
|
.foregroundColor(color)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "dot.radiowaves.up.forward")
|
||||||
|
.foregroundColor(color.opacity(opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SubscriptionStatusPercentageView: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
var subs: SMPServerSubs
|
||||||
|
var sess: ServerSessions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||||
|
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||||
|
Text("\(Int(floor(statusPercent * 100)))%")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ sess: ServerSessions) -> (Color, Double, Double, Double) {
|
||||||
|
func roundedToQuarter(_ n: Double) -> Double {
|
||||||
|
n >= 1 ? 1
|
||||||
|
: n <= 0 ? 0
|
||||||
|
: (n * 4).rounded() / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
|
||||||
|
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
|
||||||
|
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||||
|
|
||||||
|
return online && subs.total > 0
|
||||||
|
? (
|
||||||
|
subs.ssActive == 0
|
||||||
|
? (
|
||||||
|
sess.ssConnected == 0 ? noConnColorAndPercent : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||||
|
)
|
||||||
|
: ( // ssActive > 0
|
||||||
|
sess.ssConnected == 0
|
||||||
|
? (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error
|
||||||
|
: (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: noConnColorAndPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SMPServerSummaryView: View {
|
||||||
|
var summary: SMPServerSummary
|
||||||
|
var statsStartedAt: Date
|
||||||
|
@State private var alert: SomeAlert?
|
||||||
|
|
||||||
|
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Server address") {
|
||||||
|
Text(summary.smpServer)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
if summary.known == true {
|
||||||
|
NavigationLink {
|
||||||
|
ProtocolServersView(serverProtocol: .smp)
|
||||||
|
.navigationTitle("Your SMP servers")
|
||||||
|
} label: {
|
||||||
|
Text("Open server settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stats = summary.stats {
|
||||||
|
SMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let subs = summary.subs {
|
||||||
|
smpSubsSection(subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sess = summary.sessions {
|
||||||
|
ServerSessionsView(sess: sess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(item: $alert) { $0.alert }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smpSubsSection(_ subs: SMPServerSubs) -> some View {
|
||||||
|
Section {
|
||||||
|
infoRow("Connections subscribed", numOrDash(subs.ssActive))
|
||||||
|
infoRow("Pending", numOrDash(subs.ssPending))
|
||||||
|
infoRow("Total", numOrDash(subs.total))
|
||||||
|
reconnectButton()
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Message subscriptions")
|
||||||
|
SubscriptionStatusIndicatorView(subs: subs, sess: summary.sessionsOrNew)
|
||||||
|
if showSubscriptionPercentage {
|
||||||
|
SubscriptionStatusPercentageView(subs: subs, sess: summary.sessionsOrNew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reconnectButton() -> some View {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerSessionsView: View {
|
||||||
|
var sess: ServerSessions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section("Transport sessions") {
|
||||||
|
infoRow("Connected", numOrDash(sess.ssConnected))
|
||||||
|
infoRow("Errors", numOrDash(sess.ssErrors))
|
||||||
|
infoRow("Connecting", numOrDash(sess.ssConnecting))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SMPStatsView: View {
|
||||||
|
var stats: AgentSMPServerStatsData
|
||||||
|
var statsStartedAt: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
infoRow("Messages sent", numOrDash(stats._sentDirect + stats._sentViaProxy))
|
||||||
|
infoRow("Messages received", numOrDash(stats._recvMsgs))
|
||||||
|
NavigationLink {
|
||||||
|
DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||||
|
.navigationTitle("Detailed statistics")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
Text("Details")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Statistics")
|
||||||
|
} footer: {
|
||||||
|
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func numOrDash(_ n: Int) -> String {
|
||||||
|
n == 0 ? "-" : "\(n)"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DetailedSMPStatsView: View {
|
||||||
|
var stats: AgentSMPServerStatsData
|
||||||
|
var statsStartedAt: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Sent messages") {
|
||||||
|
infoRow("Sent total", numOrDash(stats._sentDirect + stats._sentViaProxy))
|
||||||
|
infoRowTwoValues("Sent directly", "attempts", stats._sentDirect, stats._sentDirectAttempts)
|
||||||
|
infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts)
|
||||||
|
infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts)
|
||||||
|
Text("Send errors")
|
||||||
|
indentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
|
||||||
|
indentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
|
||||||
|
indentedInfoRow("expired", numOrDash(stats._sentExpiredErrs))
|
||||||
|
indentedInfoRow("other", numOrDash(stats._sentOtherErrs))
|
||||||
|
}
|
||||||
|
Section("Received messages") {
|
||||||
|
infoRow("Received total", numOrDash(stats._recvMsgs))
|
||||||
|
Text("Receive errors")
|
||||||
|
indentedInfoRow("duplicates", numOrDash(stats._recvDuplicates))
|
||||||
|
indentedInfoRow("decryption errors", numOrDash(stats._recvCryptoErrs))
|
||||||
|
indentedInfoRow("other errors", numOrDash(stats._recvErrs))
|
||||||
|
infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts)
|
||||||
|
Text("Acknowledgement errors")
|
||||||
|
indentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
|
||||||
|
indentedInfoRow("other errors", numOrDash(stats._ackOtherErrs))
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
infoRow("Created", numOrDash(stats._connCreated))
|
||||||
|
infoRow("Secured", numOrDash(stats._connCreated))
|
||||||
|
infoRow("Completed", numOrDash(stats._connCompleted))
|
||||||
|
infoRowTwoValues("Deleted", "attempts", stats._connDeleted, stats._connDelAttempts)
|
||||||
|
infoRow("Deletion errors", numOrDash(stats._connDelErrs))
|
||||||
|
infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts)
|
||||||
|
infoRow("Subscription results ignored", numOrDash(stats._connSubIgnored))
|
||||||
|
infoRow("Subscription errors", numOrDash(stats._connSubErrs))
|
||||||
|
} header: {
|
||||||
|
Text("Connections")
|
||||||
|
} footer: {
|
||||||
|
Text("Starting from \(localTimestamp(statsStartedAt)).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(title) + Text(" / ").font(.caption2) + Text(title2).font(.caption2)
|
||||||
|
Spacer()
|
||||||
|
Group {
|
||||||
|
if value == 0 && value2 == 0 {
|
||||||
|
Text("-")
|
||||||
|
} else {
|
||||||
|
Text(numOrDash(value)) + Text(" / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func indentedInfoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.padding(.leading, 24)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XFTPServerSummaryView: View {
|
||||||
|
var summary: XFTPServerSummary
|
||||||
|
var statsStartedAt: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Server address") {
|
||||||
|
Text(summary.xftpServer)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
if summary.known == true {
|
||||||
|
NavigationLink {
|
||||||
|
ProtocolServersView(serverProtocol: .xftp)
|
||||||
|
.navigationTitle("Your XFTP servers")
|
||||||
|
} label: {
|
||||||
|
Text("Open server settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stats = summary.stats {
|
||||||
|
XFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sess = summary.sessions {
|
||||||
|
ServerSessionsView(sess: sess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XFTPStatsView: View {
|
||||||
|
var stats: AgentXFTPServerStatsData
|
||||||
|
var statsStartedAt: Date
|
||||||
|
@State private var expanded = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
infoRow("Uploaded", prettySize(stats._uploadsSize))
|
||||||
|
infoRow("Downloaded", prettySize(stats._downloadsSize))
|
||||||
|
NavigationLink {
|
||||||
|
DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||||
|
.navigationTitle("Detailed statistics")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
Text("Details")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Statistics")
|
||||||
|
} footer: {
|
||||||
|
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func prettySize(_ sizeInKB: Int64) -> String {
|
||||||
|
let kb: Int64 = 1024
|
||||||
|
return sizeInKB == 0 ? "-" : ByteCountFormatter.string(fromByteCount: sizeInKB * kb, countStyle: .binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DetailedXFTPStatsView: View {
|
||||||
|
var stats: AgentXFTPServerStatsData
|
||||||
|
var statsStartedAt: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Uploaded files") {
|
||||||
|
infoRow("Size", prettySize(stats._uploadsSize))
|
||||||
|
infoRowTwoValues("Chunks uploaded", "attempts", stats._uploads, stats._uploadAttempts)
|
||||||
|
infoRow("Upload errors", numOrDash(stats._uploadErrs))
|
||||||
|
infoRowTwoValues("Chunks deleted", "attempts", stats._deletions, stats._deleteAttempts)
|
||||||
|
infoRow("Deletion errors", numOrDash(stats._deleteErrs))
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
infoRow("Size", prettySize(stats._downloadsSize))
|
||||||
|
infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts)
|
||||||
|
Text("Download errors")
|
||||||
|
indentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
|
||||||
|
indentedInfoRow("other", numOrDash(stats._downloadErrs))
|
||||||
|
} header: {
|
||||||
|
Text("Downloaded files")
|
||||||
|
} footer: {
|
||||||
|
Text("Starting from \(localTimestamp(statsStartedAt)).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ServersSummaryView()
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
||||||
|
|
||||||
enum NewChatMenuOption: Identifiable {
|
enum NewChatMenuOption: Identifiable {
|
||||||
case newContact
|
case newContact
|
||||||
|
case scanPaste
|
||||||
case newGroup
|
case newGroup
|
||||||
|
|
||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
|
@ -25,6 +26,11 @@ struct NewChatMenuButton: View {
|
||||||
} label: {
|
} label: {
|
||||||
Text("Add contact")
|
Text("Add contact")
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
newChatMenuOption = .scanPaste
|
||||||
|
} label: {
|
||||||
|
Text("Scan / Paste link")
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
newChatMenuOption = .newGroup
|
newChatMenuOption = .newGroup
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -39,6 +45,7 @@ struct NewChatMenuButton: View {
|
||||||
.sheet(item: $newChatMenuOption) { opt in
|
.sheet(item: $newChatMenuOption) { opt in
|
||||||
switch opt {
|
switch opt {
|
||||||
case .newContact: NewChatView(selection: .invite)
|
case .newContact: NewChatView(selection: .invite)
|
||||||
|
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||||
case .newGroup: AddGroupView()
|
case .newGroup: AddGroupView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ private enum NetworkAlert: Identifiable {
|
||||||
struct NetworkAndServers: View {
|
struct NetworkAndServers: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true
|
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||||
|
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||||
@State private var cfgLoaded = false
|
@State private var cfgLoaded = false
|
||||||
@State private var currentNetCfg = NetCfg.defaults
|
@State private var currentNetCfg = NetCfg.defaults
|
||||||
@State private var netCfg = NetCfg.defaults
|
@State private var netCfg = NetCfg.defaults
|
||||||
|
@ -58,6 +59,8 @@ struct NetworkAndServers: View {
|
||||||
Text("XFTP servers")
|
Text("XFTP servers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle("Subscription percentage", isOn: $showSubscriptionPercentage)
|
||||||
|
|
||||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||||
|
let DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE = "showSubscriptionPercentage"
|
||||||
|
|
||||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ let appDefaults: [String: Any] = [
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
||||||
|
DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE: false,
|
||||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,7 @@
|
||||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
||||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||||
|
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
|
||||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
||||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
|
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
|
||||||
|
@ -475,6 +476,7 @@
|
||||||
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
||||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
|
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
|
||||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||||
|
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
|
||||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||||
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
||||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||||
|
@ -805,6 +807,7 @@
|
||||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
||||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
|
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
|
||||||
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
||||||
|
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */,
|
||||||
);
|
);
|
||||||
path = ChatList;
|
path = ChatList;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1260,6 +1263,7 @@
|
||||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
||||||
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
|
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
|
||||||
|
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */,
|
||||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
||||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public let jsonDecoder = getJSONDecoder()
|
public let jsonDecoder = getJSONDecoder()
|
||||||
let jsonEncoder = getJSONEncoder()
|
public let jsonEncoder = getJSONEncoder()
|
||||||
|
|
||||||
public enum ChatCommand {
|
public enum ChatCommand {
|
||||||
case showActiveUser
|
case showActiveUser
|
||||||
|
@ -78,6 +78,7 @@ public enum ChatCommand {
|
||||||
case apiGetNetworkConfig
|
case apiGetNetworkConfig
|
||||||
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
|
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
|
||||||
case reconnectAllServers
|
case reconnectAllServers
|
||||||
|
case reconnectServer(userId: Int64, smpServer: String)
|
||||||
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
||||||
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
|
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
|
||||||
case apiContactInfo(contactId: Int64)
|
case apiContactInfo(contactId: Int64)
|
||||||
|
@ -122,6 +123,7 @@ public enum ChatCommand {
|
||||||
case apiEndCall(contact: Contact)
|
case apiEndCall(contact: Contact)
|
||||||
case apiGetCallInvitations
|
case apiGetCallInvitations
|
||||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||||
|
// WebRTC calls /
|
||||||
case apiGetNetworkStatuses
|
case apiGetNetworkStatuses
|
||||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||||
|
@ -142,6 +144,8 @@ public enum ChatCommand {
|
||||||
case apiStandaloneFileInfo(url: String)
|
case apiStandaloneFileInfo(url: String)
|
||||||
// misc
|
// misc
|
||||||
case showVersion
|
case showVersion
|
||||||
|
case getAgentServersSummary(userId: Int64)
|
||||||
|
case resetAgentServersStats
|
||||||
case string(String)
|
case string(String)
|
||||||
|
|
||||||
public var cmdString: String {
|
public var cmdString: String {
|
||||||
|
@ -226,6 +230,7 @@ public enum ChatCommand {
|
||||||
case .apiGetNetworkConfig: return "/network"
|
case .apiGetNetworkConfig: return "/network"
|
||||||
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
|
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
|
||||||
case .reconnectAllServers: return "/reconnect"
|
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 .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 .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
|
||||||
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
|
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
|
||||||
|
@ -301,6 +306,8 @@ public enum ChatCommand {
|
||||||
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
||||||
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
||||||
case .showVersion: return "/version"
|
case .showVersion: return "/version"
|
||||||
|
case let .getAgentServersSummary(userId): return "/get servers summary \(userId)"
|
||||||
|
case .resetAgentServersStats: return "/reset servers stats"
|
||||||
case let .string(str): return str
|
case let .string(str): return str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,6 +382,7 @@ public enum ChatCommand {
|
||||||
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
|
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
|
||||||
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
|
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
|
||||||
case .reconnectAllServers: return "reconnectAllServers"
|
case .reconnectAllServers: return "reconnectAllServers"
|
||||||
|
case .reconnectServer: return "reconnectServer"
|
||||||
case .apiSetChatSettings: return "apiSetChatSettings"
|
case .apiSetChatSettings: return "apiSetChatSettings"
|
||||||
case .apiSetMemberSettings: return "apiSetMemberSettings"
|
case .apiSetMemberSettings: return "apiSetMemberSettings"
|
||||||
case .apiContactInfo: return "apiContactInfo"
|
case .apiContactInfo: return "apiContactInfo"
|
||||||
|
@ -435,6 +443,8 @@ public enum ChatCommand {
|
||||||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||||
case .showVersion: return "showVersion"
|
case .showVersion: return "showVersion"
|
||||||
|
case .getAgentServersSummary: return "getAgentServersSummary"
|
||||||
|
case .resetAgentServersStats: return "resetAgentServersStats"
|
||||||
case .string: return "console command"
|
case .string: return "console command"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -663,6 +673,8 @@ public enum ChatResponse: Decodable, Error {
|
||||||
// misc
|
// misc
|
||||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||||
case cmdOk(user: UserRef?)
|
case cmdOk(user: UserRef?)
|
||||||
|
case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary)
|
||||||
|
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
|
||||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||||
case chatError(user_: UserRef?, chatError: ChatError)
|
case chatError(user_: UserRef?, chatError: ChatError)
|
||||||
case archiveImported(archiveErrors: [ArchiveError])
|
case archiveImported(archiveErrors: [ArchiveError])
|
||||||
|
@ -821,6 +833,8 @@ public enum ChatResponse: Decodable, Error {
|
||||||
case .contactPQEnabled: return "contactPQEnabled"
|
case .contactPQEnabled: return "contactPQEnabled"
|
||||||
case .versionInfo: return "versionInfo"
|
case .versionInfo: return "versionInfo"
|
||||||
case .cmdOk: return "cmdOk"
|
case .cmdOk: return "cmdOk"
|
||||||
|
case .agentServersSummary: return "agentServersSummary"
|
||||||
|
case .agentSubsSummary: return "agentSubsSummary"
|
||||||
case .chatCmdError: return "chatCmdError"
|
case .chatCmdError: return "chatCmdError"
|
||||||
case .chatError: return "chatError"
|
case .chatError: return "chatError"
|
||||||
case .archiveImported: return "archiveImported"
|
case .archiveImported: return "archiveImported"
|
||||||
|
@ -984,6 +998,8 @@ public enum ChatResponse: Decodable, Error {
|
||||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
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 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 .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 .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||||
case let .chatError(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)
|
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||||
|
@ -2230,3 +2246,143 @@ public enum MsgType: String, Codable, Hashable {
|
||||||
case message
|
case message
|
||||||
case quota
|
case quota
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct PresentedServersSummary: Codable {
|
||||||
|
public var statsStartedAt: Date
|
||||||
|
public var allUsersSMP: SMPServersSummary
|
||||||
|
public var allUsersXFTP: XFTPServersSummary
|
||||||
|
public var currentUserSMP: SMPServersSummary
|
||||||
|
public var currentUserXFTP: XFTPServersSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SMPServersSummary: Codable {
|
||||||
|
public var smpTotals: SMPTotals
|
||||||
|
public var currentlyUsedSMPServers: [SMPServerSummary]
|
||||||
|
public var previouslyUsedSMPServers: [SMPServerSummary]
|
||||||
|
public var onlyProxiedSMPServers: [SMPServerSummary]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SMPTotals: Codable {
|
||||||
|
public var sessions: ServerSessions
|
||||||
|
public var subs: SMPServerSubs
|
||||||
|
public var stats: AgentSMPServerStatsData
|
||||||
|
}
|
||||||
|
|
||||||
|
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 var hasSubs: Bool { subs != nil }
|
||||||
|
|
||||||
|
public var sessionsOrNew: ServerSessions { sessions ?? ServerSessions.newServerSessions }
|
||||||
|
|
||||||
|
public var subsOrNew: SMPServerSubs { subs ?? SMPServerSubs.newSMPServerSubs }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ServerSessions: Codable {
|
||||||
|
public var ssConnected: Int
|
||||||
|
public var ssErrors: Int
|
||||||
|
public var ssConnecting: Int
|
||||||
|
|
||||||
|
static public var newServerSessions = ServerSessions(
|
||||||
|
ssConnected: 0,
|
||||||
|
ssErrors: 0,
|
||||||
|
ssConnecting: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SMPServerSubs: Codable {
|
||||||
|
public var ssActive: Int
|
||||||
|
public var ssPending: Int
|
||||||
|
|
||||||
|
public init(ssActive: Int, ssPending: Int) {
|
||||||
|
self.ssActive = ssActive
|
||||||
|
self.ssPending = ssPending
|
||||||
|
}
|
||||||
|
|
||||||
|
static public var newSMPServerSubs = SMPServerSubs(
|
||||||
|
ssActive: 0,
|
||||||
|
ssPending: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
public var total: Int { ssActive + ssPending }
|
||||||
|
|
||||||
|
public var shareOfActive: Double {
|
||||||
|
guard total != 0 else { return 0.0 }
|
||||||
|
return Double(ssActive) / Double(total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _ackNoMsgErrs: Int
|
||||||
|
public var _ackOtherErrs: Int
|
||||||
|
public var _connCreated: Int
|
||||||
|
public var _connSecured: Int
|
||||||
|
public var _connCompleted: Int
|
||||||
|
public var _connDeleted: Int
|
||||||
|
public var _connDelAttempts: Int
|
||||||
|
public var _connDelErrs: Int
|
||||||
|
public var _connSubscribed: Int
|
||||||
|
public var _connSubAttempts: Int
|
||||||
|
public var _connSubIgnored: Int
|
||||||
|
public var _connSubErrs: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct XFTPServersSummary: Codable {
|
||||||
|
public var xftpTotals: XFTPTotals
|
||||||
|
public var currentlyUsedXFTPServers: [XFTPServerSummary]
|
||||||
|
public var previouslyUsedXFTPServers: [XFTPServerSummary]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct XFTPTotals: Codable {
|
||||||
|
public var sessions: ServerSessions
|
||||||
|
public var stats: AgentXFTPServerStatsData
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _uploadsSize: Int64
|
||||||
|
public var _uploadAttempts: Int
|
||||||
|
public var _uploadErrs: Int
|
||||||
|
public var _downloads: Int
|
||||||
|
public var _downloadsSize: Int64
|
||||||
|
public var _downloadAttempts: Int
|
||||||
|
public var _downloadAuthErrs: Int
|
||||||
|
public var _downloadErrs: Int
|
||||||
|
public var _deletions: Int
|
||||||
|
public var _deleteAttempts: Int
|
||||||
|
public var _deleteErrs: Int
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import SwiftUI
|
||||||
|
|
||||||
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
||||||
public var userId: Int64
|
public var userId: Int64
|
||||||
|
public var agentUserId: String
|
||||||
var userContactId: Int64
|
var userContactId: Int64
|
||||||
var localDisplayName: ContactName
|
var localDisplayName: ContactName
|
||||||
public var profile: LocalProfile
|
public var profile: LocalProfile
|
||||||
|
@ -41,6 +42,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
||||||
|
|
||||||
public static let sampleData = User(
|
public static let sampleData = User(
|
||||||
userId: 1,
|
userId: 1,
|
||||||
|
agentUserId: "abc",
|
||||||
userContactId: 1,
|
userContactId: 1,
|
||||||
localDisplayName: "alice",
|
localDisplayName: "alice",
|
||||||
profile: LocalProfile.sampleData,
|
profile: LocalProfile.sampleData,
|
||||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: f392ce0a9355cd3883400906ae6c361b77ca46ea
|
tag: ae8e1c5e9aa3155907f1bd075e9c69af5fce2bee
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."f392ce0a9355cd3883400906ae6c361b77ca46ea" = "0id9mg30kmhlfcpnn2np3f0a4bb4smdzvhrbw6km8vv26si1js60";
|
"https://github.com/simplex-chat/simplexmq.git"."ae8e1c5e9aa3155907f1bd075e9c69af5fce2bee" = "1k6phsn0xslqwd30g6l5bsg3ilghwjh2csav2g4bk6hb5a5ga2yk";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|
|
@ -2266,6 +2266,7 @@ processChatCommand' vr = \case
|
||||||
servers <- map (\ServerCfg {server} -> server) <$> withStore' (`getProtocolServers` users)
|
servers <- map (\ServerCfg {server} -> server) <$> withStore' (`getProtocolServers` users)
|
||||||
let srvs = if null servers then L.toList defServers else servers
|
let srvs = if null servers then L.toList defServers else servers
|
||||||
pure $ map protoServer srvs
|
pure $ map protoServer srvs
|
||||||
|
ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_
|
||||||
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
|
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
|
||||||
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
|
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
|
||||||
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
|
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
|
||||||
|
@ -7616,6 +7617,7 @@ chatCommandP =
|
||||||
"/debug locks" $> DebugLocks,
|
"/debug locks" $> DebugLocks,
|
||||||
"/debug event " *> (DebugEvent <$> jsonP),
|
"/debug event " *> (DebugEvent <$> jsonP),
|
||||||
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
|
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
|
||||||
|
"/reset servers stats" $> ResetAgentServersStats,
|
||||||
"/get subs" $> GetAgentSubs,
|
"/get subs" $> GetAgentSubs,
|
||||||
"/get subs details" $> GetAgentSubsDetails,
|
"/get subs details" $> GetAgentSubsDetails,
|
||||||
"/get workers" $> GetAgentWorkers,
|
"/get workers" $> GetAgentWorkers,
|
||||||
|
|
|
@ -507,6 +507,7 @@ data ChatCommand
|
||||||
| DebugLocks
|
| DebugLocks
|
||||||
| DebugEvent ChatResponse
|
| DebugEvent ChatResponse
|
||||||
| GetAgentServersSummary UserId
|
| GetAgentServersSummary UserId
|
||||||
|
| ResetAgentServersStats
|
||||||
| GetAgentSubs
|
| GetAgentSubs
|
||||||
| GetAgentSubsDetails
|
| GetAgentSubsDetails
|
||||||
| GetAgentWorkers
|
| GetAgentWorkers
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
module Simplex.Chat.Stats where
|
module Simplex.Chat.Stats where
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
|
||||||
import qualified Data.Aeson.TH as J
|
import qualified Data.Aeson.TH as J
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
|
@ -20,8 +19,10 @@ import Simplex.Messaging.Protocol
|
||||||
|
|
||||||
data PresentedServersSummary = PresentedServersSummary
|
data PresentedServersSummary = PresentedServersSummary
|
||||||
{ statsStartedAt :: UTCTime,
|
{ statsStartedAt :: UTCTime,
|
||||||
currentUserServers :: ServersSummary,
|
allUsersSMP :: SMPServersSummary,
|
||||||
allUsersServers :: ServersSummary
|
allUsersXFTP :: XFTPServersSummary,
|
||||||
|
currentUserSMP :: SMPServersSummary,
|
||||||
|
currentUserXFTP :: XFTPServersSummary
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
@ -29,8 +30,10 @@ data PresentedServersSummary = PresentedServersSummary
|
||||||
-- so users can differentiate currently used (connected) servers,
|
-- so users can differentiate currently used (connected) servers,
|
||||||
-- previously connected servers that were in use in previous sessions,
|
-- previously connected servers that were in use in previous sessions,
|
||||||
-- and servers that are only proxied (not connected directly).
|
-- and servers that are only proxied (not connected directly).
|
||||||
data ServersSummary = ServersSummary
|
data SMPServersSummary = SMPServersSummary
|
||||||
{ -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary;
|
{ -- SMP totals are calculated from all accounted SMP server summaries
|
||||||
|
smpTotals :: SMPTotals,
|
||||||
|
-- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary;
|
||||||
-- all other servers would fall either into previously used or only proxied servers category
|
-- all other servers would fall either into previously used or only proxied servers category
|
||||||
currentlyUsedSMPServers :: [SMPServerSummary],
|
currentlyUsedSMPServers :: [SMPServerSummary],
|
||||||
-- previously used SMP servers are those with Nothing in sessions and subs,
|
-- previously used SMP servers are those with Nothing in sessions and subs,
|
||||||
|
@ -40,13 +43,14 @@ data ServersSummary = ServersSummary
|
||||||
-- only proxied SMP servers are those that aren't (according to current state - sessions and subs)
|
-- only proxied SMP servers are those that aren't (according to current state - sessions and subs)
|
||||||
-- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs,
|
-- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs,
|
||||||
-- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats
|
-- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats
|
||||||
onlyProxiedSMPServers :: [SMPServerSummary],
|
onlyProxiedSMPServers :: [SMPServerSummary]
|
||||||
-- currently used XFTP servers are those with Just in sessions in XFTPServerSummary,
|
}
|
||||||
-- and/or have upload/download/deletion in progress;
|
deriving (Show)
|
||||||
-- all other servers would fall into previously used servers category
|
|
||||||
currentlyUsedXFTPServers :: [XFTPServerSummary],
|
data SMPTotals = SMPTotals
|
||||||
-- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress
|
{ sessions :: ServerSessions,
|
||||||
previouslyUsedXFTPServers :: [XFTPServerSummary]
|
subs :: SMPServerSubs,
|
||||||
|
stats :: AgentSMPServerStatsData
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
@ -68,6 +72,24 @@ data SMPServerSummary = SMPServerSummary
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
data XFTPServersSummary = XFTPServersSummary
|
||||||
|
{ -- XFTP totals are calculated from all accounted XFTP server summaries
|
||||||
|
xftpTotals :: XFTPTotals,
|
||||||
|
-- currently used XFTP servers are those with Just in sessions in XFTPServerSummary,
|
||||||
|
-- and/or have upload/download/deletion in progress;
|
||||||
|
-- all other servers would fall into previously used servers category
|
||||||
|
currentlyUsedXFTPServers :: [XFTPServerSummary],
|
||||||
|
-- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress
|
||||||
|
previouslyUsedXFTPServers :: [XFTPServerSummary]
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data XFTPTotals = XFTPTotals
|
||||||
|
{ sessions :: ServerSessions,
|
||||||
|
stats :: AgentXFTPServerStatsData
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
data XFTPServerSummary = XFTPServerSummary
|
data XFTPServerSummary = XFTPServerSummary
|
||||||
{ xftpServer :: XFTPServer,
|
{ xftpServer :: XFTPServer,
|
||||||
known :: Maybe Bool, -- same as for SMPServerSummary
|
known :: Maybe Bool, -- same as for SMPServerSummary
|
||||||
|
@ -87,34 +109,64 @@ data XFTPServerSummary = XFTPServerSummary
|
||||||
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary
|
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary
|
||||||
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do
|
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do
|
||||||
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
|
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
|
||||||
|
(userSMPTotals, allSMPTotals) = (accSMPTotals userSMPSrvsSumms, accSMPTotals allSMPSrvsSumms)
|
||||||
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
|
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
|
||||||
(allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms
|
(allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms
|
||||||
(userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries
|
(userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries
|
||||||
|
(userXFTPTotals, allXFTPTotals) = (accXFTPTotals userXFTPSrvsSumms, accXFTPTotals allXFTPSrvsSumms)
|
||||||
(userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms
|
(userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms
|
||||||
(allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms
|
(allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms
|
||||||
PresentedServersSummary
|
PresentedServersSummary
|
||||||
{ statsStartedAt,
|
{ statsStartedAt,
|
||||||
currentUserServers =
|
allUsersSMP =
|
||||||
ServersSummary
|
SMPServersSummary
|
||||||
{ currentlyUsedSMPServers = userSMPCurr,
|
{ smpTotals = allSMPTotals,
|
||||||
previouslyUsedSMPServers = userSMPPrev,
|
currentlyUsedSMPServers = allSMPCurr,
|
||||||
onlyProxiedSMPServers = userSMPProx,
|
|
||||||
currentlyUsedXFTPServers = userXFTPCurr,
|
|
||||||
previouslyUsedXFTPServers = userXFTPPrev
|
|
||||||
},
|
|
||||||
allUsersServers =
|
|
||||||
ServersSummary
|
|
||||||
{ currentlyUsedSMPServers = allSMPCurr,
|
|
||||||
previouslyUsedSMPServers = allSMPPrev,
|
previouslyUsedSMPServers = allSMPPrev,
|
||||||
onlyProxiedSMPServers = allSMPProx,
|
onlyProxiedSMPServers = allSMPProx
|
||||||
|
},
|
||||||
|
allUsersXFTP =
|
||||||
|
XFTPServersSummary
|
||||||
|
{ xftpTotals = allXFTPTotals,
|
||||||
currentlyUsedXFTPServers = allXFTPCurr,
|
currentlyUsedXFTPServers = allXFTPCurr,
|
||||||
previouslyUsedXFTPServers = allXFTPPrev
|
previouslyUsedXFTPServers = allXFTPPrev
|
||||||
|
},
|
||||||
|
currentUserSMP =
|
||||||
|
SMPServersSummary
|
||||||
|
{ smpTotals = userSMPTotals,
|
||||||
|
currentlyUsedSMPServers = userSMPCurr,
|
||||||
|
previouslyUsedSMPServers = userSMPPrev,
|
||||||
|
onlyProxiedSMPServers = userSMPProx
|
||||||
|
},
|
||||||
|
currentUserXFTP =
|
||||||
|
XFTPServersSummary
|
||||||
|
{ xftpTotals = userXFTPTotals,
|
||||||
|
currentlyUsedXFTPServers = userXFTPCurr,
|
||||||
|
previouslyUsedXFTPServers = userXFTPPrev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary
|
AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary
|
||||||
countUserInAll auId = auId == aUserId currentUser || auId `notElem` hiddenUserIds
|
countUserInAll auId = countUserInAllStats (AgentUserId auId) currentUser users
|
||||||
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
|
accSMPTotals :: Map SMPServer SMPServerSummary -> SMPTotals
|
||||||
|
accSMPTotals = M.foldr addTotals initialTotals
|
||||||
|
where
|
||||||
|
initialTotals = SMPTotals {sessions = ServerSessions 0 0 0, subs = SMPServerSubs 0 0, stats = newAgentSMPServerStatsData}
|
||||||
|
addTotals SMPServerSummary {sessions, subs, stats} SMPTotals {sessions = accSess, subs = accSubs, stats = accStats} =
|
||||||
|
SMPTotals
|
||||||
|
{ sessions = maybe accSess (accSess `addServerSessions`) sessions,
|
||||||
|
subs = maybe accSubs (accSubs `addSMPSubs`) subs,
|
||||||
|
stats = maybe accStats (accStats `addSMPStatsData`) stats
|
||||||
|
}
|
||||||
|
accXFTPTotals :: Map XFTPServer XFTPServerSummary -> XFTPTotals
|
||||||
|
accXFTPTotals = M.foldr addTotals initialTotals
|
||||||
|
where
|
||||||
|
initialTotals = XFTPTotals {sessions = ServerSessions 0 0 0, stats = newAgentXFTPServerStatsData}
|
||||||
|
addTotals XFTPServerSummary {sessions, stats} XFTPTotals {sessions = accSess, stats = accStats} =
|
||||||
|
XFTPTotals
|
||||||
|
{ sessions = maybe accSess (accSess `addServerSessions`) sessions,
|
||||||
|
stats = maybe accStats (accStats `addXFTPStatsData`) stats
|
||||||
|
}
|
||||||
smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary])
|
smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary])
|
||||||
smpSummsIntoCategories = foldr partitionSummary ([], [], [])
|
smpSummsIntoCategories = foldr partitionSummary ([], [], [])
|
||||||
where
|
where
|
||||||
|
@ -171,7 +223,7 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
||||||
addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary
|
addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary
|
||||||
addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs}
|
addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs}
|
||||||
addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary
|
addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary
|
||||||
addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStats`) stats}
|
addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStatsData`) stats}
|
||||||
accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
|
accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
|
||||||
accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats
|
accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats
|
||||||
where
|
where
|
||||||
|
@ -205,7 +257,7 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
||||||
addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary
|
addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary
|
||||||
addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
|
addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
|
||||||
addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary
|
addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary
|
||||||
addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStats`) stats}
|
addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStatsData`) stats}
|
||||||
addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions
|
addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions
|
||||||
addServerSessions ss1 ss2 =
|
addServerSessions ss1 ss2 =
|
||||||
ServerSessions
|
ServerSessions
|
||||||
|
@ -213,56 +265,30 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
||||||
ssErrors = ssErrors ss1 + ssErrors ss2,
|
ssErrors = ssErrors ss1 + ssErrors ss2,
|
||||||
ssConnecting = ssConnecting ss1 + ssConnecting ss2
|
ssConnecting = ssConnecting ss1 + ssConnecting ss2
|
||||||
}
|
}
|
||||||
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
|
|
||||||
addSMPSubs ss1 ss2 =
|
countUserInAllStats :: AgentUserId -> User -> [User] -> Bool
|
||||||
SMPServerSubs
|
countUserInAllStats (AgentUserId auId) currentUser users =
|
||||||
{ ssActive = ssActive ss1 + ssActive ss2,
|
auId == aUserId currentUser || auId `notElem` hiddenUserIds
|
||||||
ssPending = ssPending ss1 + ssPending ss2
|
where
|
||||||
}
|
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
|
||||||
addSMPStats :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
|
|
||||||
addSMPStats sd1 sd2 =
|
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
|
||||||
AgentSMPServerStatsData
|
addSMPSubs ss1 ss2 =
|
||||||
{ _sentDirect = _sentDirect sd1 + _sentDirect sd2,
|
SMPServerSubs
|
||||||
_sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2,
|
{ ssActive = ssActive ss1 + ssActive ss2,
|
||||||
_sentProxied = _sentProxied sd1 + _sentProxied sd2,
|
ssPending = ssPending ss1 + ssPending ss2
|
||||||
_sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2,
|
}
|
||||||
_sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2,
|
|
||||||
_sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2,
|
$(J.deriveJSON defaultJSON ''SMPTotals)
|
||||||
_sentAuthErrs = _sentAuthErrs sd1 + _sentAuthErrs sd2,
|
|
||||||
_sentQuotaErrs = _sentQuotaErrs sd1 + _sentQuotaErrs sd2,
|
|
||||||
_sentExpiredErrs = _sentExpiredErrs sd1 + _sentExpiredErrs sd2,
|
|
||||||
_sentOtherErrs = _sentOtherErrs sd1 + _sentOtherErrs sd2,
|
|
||||||
_recvMsgs = _recvMsgs sd1 + _recvMsgs sd2,
|
|
||||||
_recvDuplicates = _recvDuplicates sd1 + _recvDuplicates sd2,
|
|
||||||
_recvCryptoErrs = _recvCryptoErrs sd1 + _recvCryptoErrs sd2,
|
|
||||||
_recvErrs = _recvErrs sd1 + _recvErrs sd2,
|
|
||||||
_connCreated = _connCreated sd1 + _connCreated sd2,
|
|
||||||
_connSecured = _connSecured sd1 + _connSecured sd2,
|
|
||||||
_connCompleted = _connCompleted sd1 + _connCompleted sd2,
|
|
||||||
_connDeleted = _connDeleted sd1 + _connDeleted sd2,
|
|
||||||
_connSubscribed = _connSubscribed sd1 + _connSubscribed sd2,
|
|
||||||
_connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2,
|
|
||||||
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2
|
|
||||||
}
|
|
||||||
addXFTPStats :: AgentXFTPServerStatsData -> AgentXFTPServerStatsData -> AgentXFTPServerStatsData
|
|
||||||
addXFTPStats sd1 sd2 =
|
|
||||||
AgentXFTPServerStatsData
|
|
||||||
{ _uploads = _uploads sd1 + _uploads sd2,
|
|
||||||
_uploadAttempts = _uploadAttempts sd1 + _uploadAttempts sd2,
|
|
||||||
_uploadErrs = _uploadErrs sd1 + _uploadErrs sd2,
|
|
||||||
_downloads = _downloads sd1 + _downloads sd2,
|
|
||||||
_downloadAttempts = _downloadAttempts sd1 + _downloadAttempts sd2,
|
|
||||||
_downloadAuthErrs = _downloadAuthErrs sd1 + _downloadAuthErrs sd2,
|
|
||||||
_downloadErrs = _downloadErrs sd1 + _downloadErrs sd2,
|
|
||||||
_deletions = _deletions sd1 + _deletions sd2,
|
|
||||||
_deleteAttempts = _deleteAttempts sd1 + _deleteAttempts sd2,
|
|
||||||
_deleteErrs = _deleteErrs sd1 + _deleteErrs sd2
|
|
||||||
}
|
|
||||||
|
|
||||||
$(J.deriveJSON defaultJSON ''SMPServerSummary)
|
$(J.deriveJSON defaultJSON ''SMPServerSummary)
|
||||||
|
|
||||||
|
$(J.deriveJSON defaultJSON ''SMPServersSummary)
|
||||||
|
|
||||||
|
$(J.deriveJSON defaultJSON ''XFTPTotals)
|
||||||
|
|
||||||
$(J.deriveJSON defaultJSON ''XFTPServerSummary)
|
$(J.deriveJSON defaultJSON ''XFTPServerSummary)
|
||||||
|
|
||||||
$(J.deriveJSON defaultJSON ''ServersSummary)
|
$(J.deriveJSON defaultJSON ''XFTPServersSummary)
|
||||||
|
|
||||||
$(J.deriveJSON defaultJSON ''PresentedServersSummary)
|
$(J.deriveJSON defaultJSON ''PresentedServersSummary)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue