mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29: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
|
||||
}
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) -> ChatResponse {
|
||||
if log {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
}
|
||||
let start = Date.now
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
||||
: sendSimpleXCmd(cmd, ctrl)
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||
if log {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
@ -543,6 +547,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))
|
||||
}
|
||||
|
@ -1336,6 +1345,18 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
|||
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 {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Binding var searchMode: Bool
|
||||
|
@ -280,9 +338,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) {
|
||||
|
@ -299,26 +357,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))
|
||||
|
@ -333,14 +371,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 }
|
||||
}
|
||||
|
@ -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) {
|
||||
planAndConnect(
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@ private enum NetworkAlert: Identifiable {
|
|||
struct NetworkAndServers: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@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 currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
|
@ -58,6 +59,8 @@ struct NetworkAndServers: View {
|
|||
Text("XFTP servers")
|
||||
}
|
||||
|
||||
Toggle("Subscription percentage", isOn: $showSubscriptionPercentage)
|
||||
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
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_AUTO = "connectRemoteViaMulticastAuto"
|
||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
let DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE = "showSubscriptionPercentage"
|
||||
|
||||
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_AUTO: true,
|
||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
||||
DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE: false,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||
]
|
||||
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.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 */; };
|
||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -805,6 +807,7 @@
|
|||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
|
||||
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */,
|
||||
);
|
||||
path = ChatList;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1260,6 +1263,7 @@
|
|||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
||||
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
|
||||
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */,
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
public let jsonDecoder = getJSONDecoder()
|
||||
let jsonEncoder = getJSONEncoder()
|
||||
public let jsonEncoder = getJSONEncoder()
|
||||
|
||||
public enum ChatCommand {
|
||||
case showActiveUser
|
||||
|
@ -78,6 +78,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 +123,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 +144,8 @@ public enum ChatCommand {
|
|||
case apiStandaloneFileInfo(url: String)
|
||||
// misc
|
||||
case showVersion
|
||||
case getAgentServersSummary(userId: Int64)
|
||||
case resetAgentServersStats
|
||||
case string(String)
|
||||
|
||||
public var cmdString: String {
|
||||
|
@ -226,6 +230,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 +306,8 @@ 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 .string(str): return str
|
||||
}
|
||||
}
|
||||
|
@ -375,6 +382,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 +443,8 @@ public enum ChatCommand {
|
|||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||
case .showVersion: return "showVersion"
|
||||
case .getAgentServersSummary: return "getAgentServersSummary"
|
||||
case .resetAgentServersStats: return "resetAgentServersStats"
|
||||
case .string: return "console command"
|
||||
}
|
||||
}
|
||||
|
@ -663,6 +673,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 +833,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 +998,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)
|
||||
|
@ -2230,3 +2246,143 @@ public enum MsgType: String, Codable, Hashable {
|
|||
case message
|
||||
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 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, Hashable {
|
|||
|
||||
public static let sampleData = User(
|
||||
userId: 1,
|
||||
agentUserId: "abc",
|
||||
userContactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: LocalProfile.sampleData,
|
||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: f392ce0a9355cd3883400906ae6c361b77ca46ea
|
||||
tag: ae8e1c5e9aa3155907f1bd075e9c69af5fce2bee
|
||||
|
||||
source-repository-package
|
||||
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/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"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)
|
||||
let srvs = if null servers then L.toList defServers else servers
|
||||
pure $ map protoServer srvs
|
||||
ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_
|
||||
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
|
||||
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
|
||||
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
|
||||
|
@ -7616,6 +7617,7 @@ chatCommandP =
|
|||
"/debug locks" $> DebugLocks,
|
||||
"/debug event " *> (DebugEvent <$> jsonP),
|
||||
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
|
||||
"/reset servers stats" $> ResetAgentServersStats,
|
||||
"/get subs" $> GetAgentSubs,
|
||||
"/get subs details" $> GetAgentSubsDetails,
|
||||
"/get workers" $> GetAgentWorkers,
|
||||
|
|
|
@ -507,6 +507,7 @@ data ChatCommand
|
|||
| DebugLocks
|
||||
| DebugEvent ChatResponse
|
||||
| GetAgentServersSummary UserId
|
||||
| ResetAgentServersStats
|
||||
| GetAgentSubs
|
||||
| GetAgentSubsDetails
|
||||
| GetAgentWorkers
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
module Simplex.Chat.Stats where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
|
@ -20,8 +19,10 @@ import Simplex.Messaging.Protocol
|
|||
|
||||
data PresentedServersSummary = PresentedServersSummary
|
||||
{ statsStartedAt :: UTCTime,
|
||||
currentUserServers :: ServersSummary,
|
||||
allUsersServers :: ServersSummary
|
||||
allUsersSMP :: SMPServersSummary,
|
||||
allUsersXFTP :: XFTPServersSummary,
|
||||
currentUserSMP :: SMPServersSummary,
|
||||
currentUserXFTP :: XFTPServersSummary
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
@ -29,8 +30,10 @@ data PresentedServersSummary = PresentedServersSummary
|
|||
-- so users can differentiate currently used (connected) servers,
|
||||
-- previously connected servers that were in use in previous sessions,
|
||||
-- and servers that are only proxied (not connected directly).
|
||||
data ServersSummary = ServersSummary
|
||||
{ -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary;
|
||||
data SMPServersSummary = SMPServersSummary
|
||||
{ -- 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
|
||||
currentlyUsedSMPServers :: [SMPServerSummary],
|
||||
-- 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)
|
||||
-- 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
|
||||
onlyProxiedSMPServers :: [SMPServerSummary],
|
||||
-- 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]
|
||||
onlyProxiedSMPServers :: [SMPServerSummary]
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data SMPTotals = SMPTotals
|
||||
{ sessions :: ServerSessions,
|
||||
subs :: SMPServerSubs,
|
||||
stats :: AgentSMPServerStatsData
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
@ -68,6 +72,24 @@ data SMPServerSummary = SMPServerSummary
|
|||
}
|
||||
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
|
||||
{ xftpServer :: XFTPServer,
|
||||
known :: Maybe Bool, -- same as for SMPServerSummary
|
||||
|
@ -87,34 +109,64 @@ data XFTPServerSummary = XFTPServerSummary
|
|||
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary
|
||||
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do
|
||||
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
|
||||
(userSMPTotals, allSMPTotals) = (accSMPTotals userSMPSrvsSumms, accSMPTotals allSMPSrvsSumms)
|
||||
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
|
||||
(allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms
|
||||
(userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries
|
||||
(userXFTPTotals, allXFTPTotals) = (accXFTPTotals userXFTPSrvsSumms, accXFTPTotals allXFTPSrvsSumms)
|
||||
(userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms
|
||||
(allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms
|
||||
PresentedServersSummary
|
||||
{ statsStartedAt,
|
||||
currentUserServers =
|
||||
ServersSummary
|
||||
{ currentlyUsedSMPServers = userSMPCurr,
|
||||
previouslyUsedSMPServers = userSMPPrev,
|
||||
onlyProxiedSMPServers = userSMPProx,
|
||||
currentlyUsedXFTPServers = userXFTPCurr,
|
||||
previouslyUsedXFTPServers = userXFTPPrev
|
||||
},
|
||||
allUsersServers =
|
||||
ServersSummary
|
||||
{ currentlyUsedSMPServers = allSMPCurr,
|
||||
allUsersSMP =
|
||||
SMPServersSummary
|
||||
{ smpTotals = allSMPTotals,
|
||||
currentlyUsedSMPServers = allSMPCurr,
|
||||
previouslyUsedSMPServers = allSMPPrev,
|
||||
onlyProxiedSMPServers = allSMPProx,
|
||||
onlyProxiedSMPServers = allSMPProx
|
||||
},
|
||||
allUsersXFTP =
|
||||
XFTPServersSummary
|
||||
{ xftpTotals = allXFTPTotals,
|
||||
currentlyUsedXFTPServers = allXFTPCurr,
|
||||
previouslyUsedXFTPServers = allXFTPPrev
|
||||
},
|
||||
currentUserSMP =
|
||||
SMPServersSummary
|
||||
{ smpTotals = userSMPTotals,
|
||||
currentlyUsedSMPServers = userSMPCurr,
|
||||
previouslyUsedSMPServers = userSMPPrev,
|
||||
onlyProxiedSMPServers = userSMPProx
|
||||
},
|
||||
currentUserXFTP =
|
||||
XFTPServersSummary
|
||||
{ xftpTotals = userXFTPTotals,
|
||||
currentlyUsedXFTPServers = userXFTPCurr,
|
||||
previouslyUsedXFTPServers = userXFTPPrev
|
||||
}
|
||||
}
|
||||
where
|
||||
AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary
|
||||
countUserInAll auId = auId == aUserId currentUser || auId `notElem` hiddenUserIds
|
||||
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
|
||||
countUserInAll auId = countUserInAllStats (AgentUserId auId) currentUser 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 = foldr partitionSummary ([], [], [])
|
||||
where
|
||||
|
@ -171,7 +223,7 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
|||
addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary
|
||||
addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs}
|
||||
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 = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats
|
||||
where
|
||||
|
@ -205,7 +257,7 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
|||
addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary
|
||||
addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
|
||||
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 ss1 ss2 =
|
||||
ServerSessions
|
||||
|
@ -213,56 +265,30 @@ toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrv
|
|||
ssErrors = ssErrors ss1 + ssErrors ss2,
|
||||
ssConnecting = ssConnecting ss1 + ssConnecting ss2
|
||||
}
|
||||
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
|
||||
addSMPSubs ss1 ss2 =
|
||||
SMPServerSubs
|
||||
{ ssActive = ssActive ss1 + ssActive ss2,
|
||||
ssPending = ssPending ss1 + ssPending ss2
|
||||
}
|
||||
addSMPStats :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
|
||||
addSMPStats sd1 sd2 =
|
||||
AgentSMPServerStatsData
|
||||
{ _sentDirect = _sentDirect sd1 + _sentDirect sd2,
|
||||
_sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2,
|
||||
_sentProxied = _sentProxied sd1 + _sentProxied sd2,
|
||||
_sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2,
|
||||
_sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2,
|
||||
_sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2,
|
||||
_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
|
||||
}
|
||||
|
||||
countUserInAllStats :: AgentUserId -> User -> [User] -> Bool
|
||||
countUserInAllStats (AgentUserId auId) currentUser users =
|
||||
auId == aUserId currentUser || auId `notElem` hiddenUserIds
|
||||
where
|
||||
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
|
||||
|
||||
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
|
||||
addSMPSubs ss1 ss2 =
|
||||
SMPServerSubs
|
||||
{ ssActive = ssActive ss1 + ssActive ss2,
|
||||
ssPending = ssPending ss1 + ssPending ss2
|
||||
}
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SMPTotals)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SMPServerSummary)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SMPServersSummary)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''XFTPTotals)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''XFTPServerSummary)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''ServersSummary)
|
||||
$(J.deriveJSON defaultJSON ''XFTPServersSummary)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''PresentedServersSummary)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue