Compare commits

...

7 Commits

Author SHA1 Message Date
Evgeny Poberezkin 9bc4a9a19b icon 2024-07-03 14:31:40 +01:00
Evgeny Poberezkin e34faf56c7 Merge branch 'master' into lp/ios-translate-sheet 2024-07-03 14:13:56 +01:00
spaced4ndy 1180edf80e ios: servers summary (#4321)
* types

* chat list buttons

* update

* wip

* xftp summary

* sub status view

* reconnect server button

* reset stats button

* refactor

* subscription icon in server view

* getAgentSubsSummary api

* view

* subs indicator with timer

* rename

* variableValueAsPercentage

* encodePrettyPrinted

* totals, timer

* reduce interval

* show more/less stats

* refactor

* sort

* rework indicator wip

* change offline icon

* rework indicator

* show percent

* file progress

* fix

* onion

* user servers, icons

* dashes

* Revert "user servers, icons"

This reverts commit 805e7e9bd6.

* remove icon, remove total, remove session %

* exclude sessions

* move starting from

* file stats

* fix

* open server settings

* file in progress

* update

* wpi

* wip

* rework stats

* arrow for sessions

* texts

* reconnect all

* single user

* rework file stats

* update

* dont log terminal items

* center

* update

* _connSubIgnored

* rename

* update

* large titles

* refactor

* update

* text

* upd

* single dash
2024-07-03 13:59:46 +01:00
Levitating Pineapple 5dad4e22f5 Minor 2024-07-03 14:03:49 +02:00
spaced4ndy 4bd6517d19 core: servers stats improvements, fixes (#4358) 2024-07-03 15:58:37 +04:00
Levitating Pineapple f1290d7e38 Feat: Add system translate sheet to chat item menu 2024-07-03 13:47:46 +02:00
Evgeny Poberezkin 2585f4ecfd ios: ChatView performance improvements (#4353)
* feat: Add synthesized hashable conformance to chat and API types (#4348)

* UIKit ReverseList

* ReverseList - manual layout updates for external state

* Propagate ScrollModel; Disable async media width

* Filter chat items

* Remove UIKit menu wrapper

* Make chat item width calculation synchronous (#4371)

* Fix floating button regression

* Improve filter performance

* Fix page load the merged items exceed full page

* Resolve iOS15 compabibility

* Restore build config

* Add page-up scroll; Fix same item decrementing unread counter multiple times

* Fix: Chat not loading additional pages, if newest items are all merged and exceed page size

* Minor

* Fix item loading regression

* Fix item loading regression 2

* Fix unread regression

* refactor

---------

Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-07-03 10:24:26 +01:00
24 changed files with 2106 additions and 772 deletions
+10 -8
View File
@@ -332,12 +332,12 @@ final class ChatModel: ObservableObject {
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = getChatItemIndex(cItem) {
withAnimation {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
return false
} else {
withAnimation(itemAnimation()) {
withConditionalAnimation(itemAnimation()) {
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
@@ -357,7 +357,7 @@ final class ChatModel: ObservableObject {
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
withAnimation {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
} else if let status = status {
@@ -512,11 +512,13 @@ final class ChatModel: ObservableObject {
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update preview
decreaseUnreadCounter(cInfo)
// update current chat
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
markChatItemRead_(i)
if reversedChatItems[i].isRcvNew {
// update current chat
markChatItemRead_(i)
// update preview
decreaseUnreadCounter(cInfo)
}
}
}
@@ -723,7 +725,7 @@ struct NTFContactRequest {
var chatId: String
}
struct UnreadChatItemCounts {
struct UnreadChatItemCounts: Equatable {
var totalBelow: Int
var unreadBelow: Int
}
+32 -9
View File
@@ -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
}
@@ -309,8 +313,10 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
throw r
}
let loadItemsPerPage = 50
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
throw r
}
@@ -541,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))
}
@@ -1334,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
@@ -13,10 +13,9 @@ struct CIImageView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
let chatItem: ChatItem
let image: String
var preview: UIImage?
let maxWidth: CGFloat
@Binding var imgWidth: CGFloat?
@State var scrollProxy: ScrollViewProxy?
var imgWidth: CGFloat?
@State private var showFullScreenImage = false
var body: some View {
@@ -25,15 +24,14 @@ struct CIImageView: View {
if let uiImage = getLoadedImage(file) {
imageView(uiImage)
.fullScreenCover(isPresented: $showFullScreenImage) {
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage)
}
.onTapGesture { showFullScreenImage = true }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenImage = false
}
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
} else if let preview {
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
@@ -90,7 +88,6 @@ struct CIImageView: View {
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { imgWidth = w }
return ZStack(alignment: .topTrailing) {
if img.imageData == nil {
Image(uiImage: img)
@@ -15,14 +15,12 @@ struct CIVideoView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
private let chatItem: ChatItem
private let image: String
private let preview: UIImage?
@State private var duration: Int
@State private var progress: Int = 0
@State private var videoPlaying: Bool = false
private let maxWidth: CGFloat
@Binding private var videoWidth: CGFloat?
@State private var scrollProxy: ScrollViewProxy?
@State private var preview: UIImage? = nil
private var videoWidth: CGFloat?
@State private var player: AVPlayer?
@State private var fullPlayer: AVPlayer?
@State private var url: URL?
@@ -33,13 +31,12 @@ struct CIVideoView: View {
@State private var fullScreenTimeObserver: Any? = nil
@State private var publisher: AnyCancellable? = nil
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) {
self.chatItem = chatItem
self.image = image
self.preview = preview
self._duration = State(initialValue: duration)
self.maxWidth = maxWidth
self._videoWidth = videoWidth
self.scrollProxy = scrollProxy
self.videoWidth = videoWidth
if let url = getLoadedVideo(chatItem.file) {
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
self._urlDecrypted = State(initialValue: decrypted)
@@ -49,10 +46,6 @@ struct CIVideoView: View {
}
self._url = State(initialValue: url)
}
if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
self._preview = State(initialValue: uiImage)
}
}
var body: some View {
@@ -63,9 +56,8 @@ struct CIVideoView: View {
videoView(player, decrypted, file, preview, duration)
} else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
videoViewEncrypted(file, defaultPreview, duration)
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
} else if let preview {
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
@@ -152,7 +144,6 @@ struct CIVideoView: View {
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) {
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
@@ -252,7 +243,6 @@ struct CIVideoView: View {
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
Image(uiImage: img)
.resizable()
@@ -18,15 +18,16 @@ private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, o
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@Binding var revealed: Bool
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@State var msgWidth: CGFloat = 0
@State var imgWidth: CGFloat? = nil
@State var videoWidth: CGFloat? = nil
var imgWidth: CGFloat? = nil
var videoWidth: CGFloat? = nil
@State var metaColor = Color.secondary
@State var showFullScreenImage = false
@Binding var allowMenu: Bool
@@ -58,10 +59,9 @@ struct FramedItemView: View {
if let qi = chatItem.quotedItem {
ciQuoteView(qi)
.onTapGesture {
if let proxy = scrollProxy,
let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
if let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
withAnimation {
proxy.scrollTo(ci.viewId, anchor: .bottom)
scrollModel.scrollToItem(id: ci.id)
}
}
}
@@ -84,6 +84,7 @@ struct FramedItemView: View {
}
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
.background(Color(.systemBackground))
.cornerRadius(18)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
@@ -114,8 +115,8 @@ struct FramedItemView: View {
.padding(.bottom, 2)
} else {
switch (chatItem.content.msgContent) {
case let .image(text, image):
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
case let .image(text, _):
CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
@@ -127,8 +128,8 @@ struct FramedItemView: View {
} else {
ciMsgContentView(chatItem)
}
case let .video(text, image, duration):
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
case let .video(text, _, duration):
CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
@@ -181,7 +182,6 @@ struct FramedItemView: View {
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
.overlay(DetermineWidth())
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, colorScheme))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
} else {
@@ -13,12 +13,12 @@ import AVKit
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@State var chatItem: ChatItem
@State var image: UIImage?
@State var player: AVPlayer? = nil
@State var url: URL? = nil
@Binding var showView: Bool
@State var scrollProxy: ScrollViewProxy?
@State private var showNext = false
@State private var nextImage: UIImage?
@State private var nextPlayer: AVPlayer?
@@ -71,9 +71,7 @@ struct FullScreenMediaView: View {
let w = abs(t.width)
if t.height > 60 && t.height > w * 2 {
showView = false
if let proxy = scrollProxy {
proxy.scrollTo(chatItem.viewId)
}
scrollModel.scrollToItem(id: chatItem.id)
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
let previous = t.width > 0
scrolling = true
+31 -4
View File
@@ -13,7 +13,6 @@ struct ChatItemView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@Binding var revealed: Bool
@Binding var allowMenu: Bool
@Binding var audioPlayer: AudioPlayer?
@@ -24,7 +23,6 @@ struct ChatItemView: View {
chatItem: ChatItem,
showMember: Bool = false,
maxWidth: CGFloat = .infinity,
scrollProxy: ScrollViewProxy? = nil,
revealed: Binding<Bool>,
allowMenu: Binding<Bool> = .constant(false),
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
@@ -34,7 +32,6 @@ struct ChatItemView: View {
self.chat = chat
self.chatItem = chatItem
self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy)
_revealed = revealed
_allowMenu = allowMenu
_audioPlayer = audioPlayer
@@ -62,7 +59,37 @@ struct ChatItemView: View {
}
private func framedItemView() -> some View {
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
let preview = chatItem.content.msgContent
.flatMap {
switch $0 {
case let .image(_, image): image
case let .video(_, image, _): image
default: nil
}
}
.map { dropImagePrefix($0) }
.flatMap { Data(base64Encoded: $0) }
.flatMap { UIImage(data: $0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
} else {
maxWidth
}
}()
return FramedItemView(
chat: chat,
chatItem: chatItem,
preview: preview,
revealed: $revealed,
maxWidth: maxWidth,
imgWidth: adjustedMaxWidth,
videoWidth: adjustedMaxWidth,
allowMenu: $allowMenu,
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime
)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,270 @@
//
// ReverseList.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 11/06/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import Combine
/// A List, which displays it's items in reverse order - from bottom to top
struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
let items: Array<Item>
@Binding var scrollState: ReverseListScrollModel<Item>.State
/// Closure, that returns user interface for a given item
let content: (Item) -> Content
let loadPage: () -> Void
func makeUIViewController(context: Context) -> Controller {
Controller(representer: self)
}
func updateUIViewController(_ controller: Controller, context: Context) {
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
switch destination {
case .nextPage:
controller.scrollToNextPage()
case let .item(id):
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
case .bottom:
controller.scroll(to: .zero, position: .top)
}
} else {
controller.update(items: items)
}
}
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
private let representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
private var itemCount: Int = .zero
private var bag = Set<AnyCancellable>()
init(representer: ReverseList) {
self.representer = representer
super.init(style: .plain)
// 1. Style
tableView.separatorStyle = .none
tableView.transform = .verticalFlip
// 2. Register cells
if #available(iOS 16.0, *) {
tableView.register(
UITableViewCell.self,
forCellReuseIdentifier: cellReuseId
)
} else {
tableView.register(
HostingCell<Content>.self,
forCellReuseIdentifier: cellReuseId
)
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
self.representer.loadPage()
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
if #available(iOS 16.0, *) {
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) }
.margins(.all, .zero)
.minSize(height: 1) // Passing zero will result in system default of 44 points being used
} else {
if let cell = cell as? HostingCell<Content> {
cell.set(content: self.representer.content(item), parent: self)
} else {
fatalError("Unexpected Cell Type for: \(item)")
}
}
cell.transform = .verticalFlip
cell.selectionStyle = .none
return cell
}
// 4. External state changes will require manual layout updates
NotificationCenter.default
.addObserver(
self,
selector: #selector(updateLayout),
name: notificationName,
object: nil
)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
deinit { NotificationCenter.default.removeObserver(self) }
@objc private func updateLayout() {
if #available(iOS 16.0, *) {
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
} else {
tableView.reloadData()
}
}
/// Hides keyboard, when user begins to scroll.
/// Equivalent to `.scrollDismissesKeyboard(.immediately)`
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
UIApplication.shared
.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
/// Scrolls up
func scrollToNextPage() {
tableView.setContentOffset(
CGPoint(
x: tableView.contentOffset.x,
y: tableView.contentOffset.y + tableView.bounds.height
),
animated: true
)
Task { representer.scrollState = .atDestination }
}
/// Scrolls to Item at index path
/// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil`
func scroll(to index: Int?, position: UITableView.ScrollPosition) {
if let index {
var animated = false
if #available(iOS 16.0, *) {
animated = true
}
tableView.scrollToRow(
at: IndexPath(row: index, section: .zero),
at: position,
animated: animated
)
Task { representer.scrollState = .atDestination }
}
}
func update(items: Array<Item>) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.defaultRowAnimation = .none
dataSource.apply(
snapshot,
animatingDifferences: itemCount != .zero && abs(items.count - itemCount) == 1
)
itemCount = items.count
}
}
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
/// Implemented as a `UITableViewCell` that wraps and manages a generic `UIHostingController`
private final class HostingCell<Hosted: View>: UITableViewCell {
private let hostingController = UIHostingController<Hosted?>(rootView: nil)
/// Updates content of the cell
/// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/
func set(content: Hosted, parent: UIViewController) {
hostingController.rootView = content
if let hostingView = hostingController.view {
hostingView.invalidateIntrinsicContentSize()
if hostingController.parent != parent { parent.addChild(hostingController) }
if !contentView.subviews.contains(hostingController.view) {
contentView.addSubview(hostingController.view)
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.leadingAnchor
.constraint(equalTo: contentView.leadingAnchor),
hostingView.trailingAnchor
.constraint(equalTo: contentView.trailingAnchor),
hostingView.topAnchor
.constraint(equalTo: contentView.topAnchor),
hostingView.bottomAnchor
.constraint(equalTo: contentView.bottomAnchor)
])
}
if hostingController.parent != parent { hostingController.didMove(toParent: parent) }
} else {
fatalError("Hosting View not loaded \(hostingController)")
}
}
override func prepareForReuse() {
super.prepareForReuse()
hostingController.rootView = nil
}
}
}
/// Manages ``ReverseList`` scrolling
class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
/// Represents Scroll State of ``ReverseList``
enum State: Equatable {
enum Destination: Equatable {
case nextPage
case item(Item.ID)
case bottom
}
case scrollingTo(Destination)
case atDestination
}
@Published var state: State = .atDestination
func scrollToNextPage() {
state = .scrollingTo(.nextPage)
}
func scrollToBottom() {
state = .scrollingTo(.bottom)
}
func scrollToItem(id: Item.ID) {
state = .scrollingTo(.item(id))
}
}
fileprivate let cellReuseId = "hostingCell"
fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout")
fileprivate extension CGAffineTransform {
/// Transform that vertically flips the view, preserving it's location
static let verticalFlip = CGAffineTransform(scaleX: 1, y: -1)
}
extension NotificationCenter {
static func postReverseListNeedsLayout() {
NotificationCenter.default.post(
name: notificationName,
object: nil
)
}
}
/// Disable animation on iOS 15
func withConditionalAnimation<Result>(
_ animation: Animation? = .default,
_ body: () throws -> Result
) rethrows -> Result {
if #available(iOS 16.0, *) {
try withAnimation(animation, body)
} else {
try body()
}
}
@@ -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 {
@@ -220,9 +209,7 @@ struct ChatListView: View {
@ViewBuilder private func chatView() -> some View {
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
ChatView(chat: chat).onAppear {
loadChat(chat: chat)
}
ChatView(chat: chat)
}
}
@@ -274,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
@@ -282,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) {
@@ -301,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))
@@ -335,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 }
}
@@ -376,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,
@@ -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()
}
@@ -1,112 +0,0 @@
//
// ContextMenu2.swift
// SimpleX (iOS)
//
// Created by Evgeny on 09/08/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import UIKit
import SwiftUI
extension View {
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
Group {
if allowMenu.wrappedValue {
if hasImageOrVideo {
InteractionView(content:
self.environmentObject(ChatModel.shared)
.overlay(DetermineWidthImageVideoItem())
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.frame(maxWidth: itemWidth.wrappedValue)
} else {
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.fixedSize(horizontal: true, vertical: false)
}
} else {
self
}
}
}
}
private class HostingViewHolder: UIView {
var contentSize: CGSize = CGSizeMake(0, 0)
override var intrinsicContentSize: CGSize { get { contentSize } }
}
struct InteractionView<Content: View>: UIViewRepresentable {
let content: Content
var maxWidth: CGFloat
var itemWidth: Binding<CGFloat>
@Binding var menu: UIMenu
func makeUIView(context: Context) -> UIView {
let view = HostingViewHolder()
view.backgroundColor = .clear
let hostView = UIHostingController(rootView: content)
view.contentSize = hostView.view.intrinsicContentSize
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
]
view.addSubview(hostView.view)
view.addConstraints(constraints)
view.layer.cornerRadius = 18
hostView.view.layer.cornerRadius = 18
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
view.addInteraction(menuInteraction)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let was = (uiView as! HostingViewHolder).contentSize
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
if was != (uiView as! HostingViewHolder).contentSize {
uiView.invalidateIntrinsicContentSize()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let parent: InteractionView<Content>
init(_ parent: InteractionView<Content>) {
self.parent = parent
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(
identifier: nil,
previewProvider: nil,
actionProvider: { [weak self] _ in
guard let self = self else { return nil }
return self.parent.menu
}
)
}
// func contextMenuInteraction(
// _ interaction: UIContextMenuInteraction,
// willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
// animator: UIContextMenuInteractionCommitAnimating
// ) {
// animator.addCompletion {
// print("user tapped")
// }
// }
}
}
@@ -0,0 +1,26 @@
//
// TranslateSheet.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 03/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import Translation
extension View {
@ViewBuilder func translateSheet(text: Binding<String?>) -> some View {
if #available(iOS 17.4, *) {
translationPresentation(
isPresented: Binding(
get: { text.wrappedValue != nil },
set: { if !$0 { text.wrappedValue = nil } }
),
text: text.wrappedValue ?? String()
)
} else {
self
}
}
}
@@ -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
]
+12 -4
View File
@@ -18,7 +18,6 @@
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
@@ -178,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 */; };
@@ -186,6 +186,8 @@
8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; };
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
CE81DF912C355E3C001C0A02 /* TranslateSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE81DF902C355E3C001C0A02 /* TranslateSheet.swift */; };
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
@@ -267,7 +269,6 @@
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
@@ -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>"; };
@@ -483,6 +485,8 @@
8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = "<group>"; };
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
CE81DF902C355E3C001C0A02 /* TranslateSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateSheet.swift; sourceTree = "<group>"; };
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -591,6 +595,7 @@
5CE4407127ADB1D0007B033A /* Emoji.swift */,
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */,
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */,
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */,
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
@@ -651,7 +656,6 @@
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */,
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */,
5C6BA666289BD954009B8ECC /* DismissSheets.swift */,
5C00164328A26FBC0094D739 /* ContextMenu.swift */,
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */,
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */,
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
@@ -660,6 +664,7 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
8C05382D2B39887E006436DC /* VideoUtils.swift */,
CE81DF902C355E3C001C0A02 /* TranslateSheet.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -802,6 +807,7 @@
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
18415835CBD939A9ABDC108A /* UserPicker.swift */,
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -1134,9 +1140,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */,
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */,
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */,
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
CE81DF912C355E3C001C0A02 /* TranslateSheet.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */,
@@ -1176,7 +1184,6 @@
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */,
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */,
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */,
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
@@ -1256,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 */,
+225 -69
View File
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI
public let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder()
public let jsonEncoder = getJSONEncoder()
public enum ChatCommand {
case showActiveUser
@@ -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)
@@ -1010,20 +1026,20 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
}
}
public enum ConnectionPlan: Decodable {
public enum ConnectionPlan: Decodable, Hashable {
case invitationLink(invitationLinkPlan: InvitationLinkPlan)
case contactAddress(contactAddressPlan: ContactAddressPlan)
case groupLink(groupLinkPlan: GroupLinkPlan)
}
public enum InvitationLinkPlan: Decodable {
public enum InvitationLinkPlan: Decodable, Hashable {
case ok
case ownLink
case connecting(contact_: Contact?)
case known(contact: Contact)
}
public enum ContactAddressPlan: Decodable {
public enum ContactAddressPlan: Decodable, Hashable {
case ok
case ownLink
case connectingConfirmReconnect
@@ -1032,7 +1048,7 @@ public enum ContactAddressPlan: Decodable {
case contactViaAddress(contact: Contact)
}
public enum GroupLinkPlan: Decodable {
public enum GroupLinkPlan: Decodable, Hashable {
case ok
case ownLink(groupInfo: GroupInfo)
case connectingConfirmReconnect
@@ -1040,13 +1056,13 @@ public enum GroupLinkPlan: Decodable {
case known(groupInfo: GroupInfo)
}
struct NewUser: Encodable {
struct NewUser: Encodable, Hashable {
var profile: Profile?
var sameServers: Bool
var pastTimestamp: Bool
}
public enum ChatPagination {
public enum ChatPagination: Hashable {
case last(count: Int)
case after(chatItemId: Int64, count: Int)
case before(chatItemId: Int64, count: Int)
@@ -1268,7 +1284,7 @@ public struct ServerAddress: Decodable {
)
}
public struct NetCfg: Codable, Equatable {
public struct NetCfg: Codable, Equatable, Hashable {
public var socksProxy: String? = nil
var socksMode: SocksMode = .always
public var hostMode: HostMode = .publicHost
@@ -1314,18 +1330,18 @@ public struct NetCfg: Codable, Equatable {
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
}
public enum HostMode: String, Codable {
public enum HostMode: String, Codable, Hashable {
case onionViaSocks
case onionHost = "onion"
case publicHost = "public"
}
public enum SocksMode: String, Codable {
public enum SocksMode: String, Codable, Hashable {
case always = "always"
case onion = "onion"
}
public enum SMPProxyMode: String, Codable {
public enum SMPProxyMode: String, Codable, Hashable {
case always = "always"
case unknown = "unknown"
case unprotected = "unprotected"
@@ -1345,7 +1361,7 @@ public enum SMPProxyMode: String, Codable {
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
}
public enum SMPProxyFallback: String, Codable {
public enum SMPProxyFallback: String, Codable, Hashable {
case allow = "allow"
case allowProtected = "allowProtected"
case prohibit = "prohibit"
@@ -1363,7 +1379,7 @@ public enum SMPProxyFallback: String, Codable {
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
}
public enum OnionHosts: String, Identifiable {
public enum OnionHosts: String, Identifiable, Hashable {
case no
case prefer
case require
@@ -1397,7 +1413,7 @@ public enum OnionHosts: String, Identifiable {
public static let values: [OnionHosts] = [.no, .prefer, .require]
}
public enum TransportSessionMode: String, Codable, Identifiable {
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
case user
case entity
@@ -1413,7 +1429,7 @@ public enum TransportSessionMode: String, Codable, Identifiable {
public static let values: [TransportSessionMode] = [.user, .entity]
}
public struct KeepAliveOpts: Codable, Equatable {
public struct KeepAliveOpts: Codable, Equatable, Hashable {
public var keepIdle: Int // seconds
public var keepIntvl: Int // seconds
public var keepCnt: Int // times
@@ -1421,7 +1437,7 @@ public struct KeepAliveOpts: Codable, Equatable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
}
public enum NetworkStatus: Decodable, Equatable {
public enum NetworkStatus: Decodable, Equatable, Hashable {
case unknown
case connected
case disconnected
@@ -1459,12 +1475,12 @@ public enum NetworkStatus: Decodable, Equatable {
}
}
public struct ConnNetworkStatus: Decodable {
public struct ConnNetworkStatus: Decodable, Hashable {
public var agentConnId: String
public var networkStatus: NetworkStatus
}
public struct ChatSettings: Codable {
public struct ChatSettings: Codable, Hashable {
public var enableNtfs: MsgFilter
public var sendRcpts: Bool?
public var favorite: Bool
@@ -1478,13 +1494,13 @@ public struct ChatSettings: Codable {
public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false)
}
public enum MsgFilter: String, Codable {
public enum MsgFilter: String, Codable, Hashable {
case none
case all
case mentions
}
public struct UserMsgReceiptSettings: Codable {
public struct UserMsgReceiptSettings: Codable, Hashable {
public var enable: Bool
public var clearOverrides: Bool
@@ -1494,7 +1510,7 @@ public struct UserMsgReceiptSettings: Codable {
}
}
public struct ConnectionStats: Decodable {
public struct ConnectionStats: Decodable, Hashable {
public var connAgentVersion: Int
public var rcvQueuesInfo: [RcvQueueInfo]
public var sndQueuesInfo: [SndQueueInfo]
@@ -1510,30 +1526,30 @@ public struct ConnectionStats: Decodable {
}
}
public struct RcvQueueInfo: Codable {
public struct RcvQueueInfo: Codable, Hashable {
public var rcvServer: String
public var rcvSwitchStatus: RcvSwitchStatus?
public var canAbortSwitch: Bool
}
public enum RcvSwitchStatus: String, Codable {
public enum RcvSwitchStatus: String, Codable, Hashable {
case switchStarted = "switch_started"
case sendingQADD = "sending_qadd"
case sendingQUSE = "sending_quse"
case receivedMessage = "received_message"
}
public struct SndQueueInfo: Codable {
public struct SndQueueInfo: Codable, Hashable {
public var sndServer: String
public var sndSwitchStatus: SndSwitchStatus?
}
public enum SndSwitchStatus: String, Codable {
public enum SndSwitchStatus: String, Codable, Hashable {
case sendingQKEY = "sending_qkey"
case sendingQTEST = "sending_qtest"
}
public enum QueueDirection: String, Decodable {
public enum QueueDirection: String, Decodable, Hashable {
case rcv
case snd
}
@@ -1557,7 +1573,7 @@ public enum RatchetSyncState: String, Decodable {
case agreed
}
public struct UserContactLink: Decodable {
public struct UserContactLink: Decodable, Hashable {
public var connReqContact: String
public var autoAccept: AutoAccept?
@@ -1571,7 +1587,7 @@ public struct UserContactLink: Decodable {
}
}
public struct AutoAccept: Codable {
public struct AutoAccept: Codable, Hashable {
public var acceptIncognito: Bool
public var autoReply: MsgContent?
@@ -1593,7 +1609,7 @@ public protocol SelectableItem: Hashable, Identifiable {
static var values: [Self] { get }
}
public struct DeviceToken: Decodable {
public struct DeviceToken: Decodable, Hashable {
var pushProvider: PushProvider
var token: String
@@ -1607,12 +1623,12 @@ public struct DeviceToken: Decodable {
}
}
public enum PushEnvironment: String {
public enum PushEnvironment: String, Hashable {
case development
case production
}
public enum PushProvider: String, Decodable {
public enum PushProvider: String, Decodable, Hashable {
case apns_dev
case apns_prod
@@ -1626,7 +1642,7 @@ public enum PushProvider: String, Decodable {
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
// and .local for periodic background checks
public enum NotificationsMode: String, Decodable, SelectableItem {
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
case off = "OFF"
case periodic = "PERIODIC"
case instant = "INSTANT"
@@ -1644,7 +1660,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
}
public enum NotificationPreviewMode: String, SelectableItem, Codable {
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
case hidden
case contact
case message
@@ -1662,7 +1678,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable {
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
}
public struct RemoteCtrlInfo: Decodable {
public struct RemoteCtrlInfo: Decodable, Hashable {
public var remoteCtrlId: Int64
public var ctrlDeviceName: String
public var sessionState: RemoteCtrlSessionState?
@@ -1672,7 +1688,7 @@ public struct RemoteCtrlInfo: Decodable {
}
}
public enum RemoteCtrlSessionState: Decodable {
public enum RemoteCtrlSessionState: Decodable, Hashable {
case starting
case searching
case connecting
@@ -1687,17 +1703,17 @@ public enum RemoteCtrlStopReason: Decodable {
case disconnected
}
public struct CtrlAppInfo: Decodable {
public struct CtrlAppInfo: Decodable, Hashable {
public var appVersionRange: AppVersionRange
public var deviceName: String
}
public struct AppVersionRange: Decodable {
public struct AppVersionRange: Decodable, Hashable {
public var minVersion: String
public var maxVersion: String
}
public struct CoreVersionInfo: Decodable {
public struct CoreVersionInfo: Decodable, Hashable {
public var version: String
public var simplexmqVersion: String
public var simplexmqCommit: String
@@ -1719,7 +1735,7 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
encodeJSON(value).cString(using: .utf8)!
}
public enum ChatError: Decodable {
public enum ChatError: Decodable, Hashable {
case error(errorType: ChatErrorType)
case errorAgent(agentError: AgentErrorType)
case errorStore(storeError: StoreError)
@@ -1728,7 +1744,7 @@ public enum ChatError: Decodable {
case invalidJSON(json: String)
}
public enum ChatErrorType: Decodable {
public enum ChatErrorType: Decodable, Hashable {
case noActiveUser
case noConnectionUser(agentConnId: String)
case noSndFileUser(agentSndFileId: String)
@@ -1807,7 +1823,7 @@ public enum ChatErrorType: Decodable {
case exception(message: String)
}
public enum StoreError: Decodable {
public enum StoreError: Decodable, Hashable {
case duplicateName
case userNotFound(userId: Int64)
case userNotFoundByName(contactName: ContactName)
@@ -1867,7 +1883,7 @@ public enum StoreError: Decodable {
case noGroupSndStatus(itemId: Int64, groupMemberId: Int64)
}
public enum DatabaseError: Decodable {
public enum DatabaseError: Decodable, Hashable {
case errorEncrypted
case errorPlaintext
case errorNoFile(dbFile: String)
@@ -1875,12 +1891,12 @@ public enum DatabaseError: Decodable {
case errorOpen(sqliteError: SQLiteError)
}
public enum SQLiteError: Decodable {
public enum SQLiteError: Decodable, Hashable {
case errorNotADatabase
case error(String)
}
public enum AgentErrorType: Decodable {
public enum AgentErrorType: Decodable, Hashable {
case CMD(cmdErr: CommandErrorType)
case CONN(connErr: ConnectionErrorType)
case SMP(smpErr: ProtocolErrorType)
@@ -1894,7 +1910,7 @@ public enum AgentErrorType: Decodable {
case INACTIVE
}
public enum CommandErrorType: Decodable {
public enum CommandErrorType: Decodable, Hashable {
case PROHIBITED
case SYNTAX
case NO_CONN
@@ -1902,7 +1918,7 @@ public enum CommandErrorType: Decodable {
case LARGE
}
public enum ConnectionErrorType: Decodable {
public enum ConnectionErrorType: Decodable, Hashable {
case NOT_FOUND
case DUPLICATE
case SIMPLEX
@@ -1910,7 +1926,7 @@ public enum ConnectionErrorType: Decodable {
case NOT_AVAILABLE
}
public enum BrokerErrorType: Decodable {
public enum BrokerErrorType: Decodable, Hashable {
case RESPONSE(smpErr: String)
case UNEXPECTED
case NETWORK
@@ -1919,7 +1935,7 @@ public enum BrokerErrorType: Decodable {
case TIMEOUT
}
public enum ProtocolErrorType: Decodable {
public enum ProtocolErrorType: Decodable, Hashable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
@@ -1930,7 +1946,7 @@ public enum ProtocolErrorType: Decodable {
case INTERNAL
}
public enum XFTPErrorType: Decodable {
public enum XFTPErrorType: Decodable, Hashable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
@@ -1947,7 +1963,7 @@ public enum XFTPErrorType: Decodable {
case INTERNAL
}
public enum RCErrorType: Decodable {
public enum RCErrorType: Decodable, Hashable {
case `internal`(internalErr: String)
case identity
case noLocalAddress
@@ -1965,7 +1981,7 @@ public enum RCErrorType: Decodable {
case syntax(syntaxErr: String)
}
public enum ProtocolCommandError: Decodable {
public enum ProtocolCommandError: Decodable, Hashable {
case UNKNOWN
case SYNTAX
case PROHIBITED
@@ -1974,7 +1990,7 @@ public enum ProtocolCommandError: Decodable {
case NO_ENTITY
}
public enum ProtocolTransportError: Decodable {
public enum ProtocolTransportError: Decodable, Hashable {
case badBlock
case largeMsg
case badSession
@@ -1982,14 +1998,14 @@ public enum ProtocolTransportError: Decodable {
case handshake(handshakeErr: SMPHandshakeError)
}
public enum SMPHandshakeError: Decodable {
public enum SMPHandshakeError: Decodable, Hashable {
case PARSE
case VERSION
case IDENTITY
case BAD_AUTH
}
public enum SMPAgentError: Decodable {
public enum SMPAgentError: Decodable, Hashable {
case A_MESSAGE
case A_PROHIBITED
case A_VERSION
@@ -1998,12 +2014,12 @@ public enum SMPAgentError: Decodable {
case A_QUEUE(queueErr: String)
}
public enum ArchiveError: Decodable {
public enum ArchiveError: Decodable, Hashable {
case `import`(chatError: ChatError)
case importFile(file: String, chatError: ChatError)
}
public enum RemoteCtrlError: Decodable {
public enum RemoteCtrlError: Decodable, Hashable {
case inactive
case badState
case busy
@@ -2017,14 +2033,14 @@ public enum RemoteCtrlError: Decodable {
case protocolError
}
public struct MigrationFileLinkData: Codable {
public struct MigrationFileLinkData: Codable, Hashable {
let networkConfig: NetworkConfig?
public init(networkConfig: NetworkConfig) {
self.networkConfig = networkConfig
}
public struct NetworkConfig: Codable {
public struct NetworkConfig: Codable, Hashable {
let socksProxy: String?
let hostMode: HostMode?
let requiredHostMode: Bool?
@@ -2056,7 +2072,7 @@ public struct MigrationFileLinkData: Codable {
}
}
public struct AppSettings: Codable, Equatable {
public struct AppSettings: Codable, Equatable, Hashable {
public var networkConfig: NetCfg? = nil
public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
@@ -2130,7 +2146,7 @@ public struct AppSettings: Codable, Equatable {
}
}
public enum AppSettingsNotificationMode: String, Codable {
public enum AppSettingsNotificationMode: String, Codable, Hashable {
case off
case periodic
case instant
@@ -2158,13 +2174,13 @@ public enum AppSettingsNotificationMode: String, Codable {
// case message
//}
public enum AppSettingsLockScreenCalls: String, Codable {
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
case disable
case show
case accept
}
public struct UserNetworkInfo: Codable, Equatable {
public struct UserNetworkInfo: Codable, Equatable, Hashable {
public let networkType: UserNetworkType
public let online: Bool
@@ -2174,7 +2190,7 @@ public struct UserNetworkInfo: Codable, Equatable {
}
}
public enum UserNetworkType: String, Codable {
public enum UserNetworkType: String, Codable, Hashable {
case none
case cellular
case wifi
@@ -2192,7 +2208,7 @@ public enum UserNetworkType: String, Codable {
}
}
public struct RcvMsgInfo: Codable {
public struct RcvMsgInfo: Codable, Hashable {
var msgId: Int64
var msgDeliveryId: Int64
var msgDeliveryStatus: String
@@ -2200,7 +2216,7 @@ public struct RcvMsgInfo: Codable {
var agentMsgMeta: String
}
public struct QueueInfo: Codable {
public struct QueueInfo: Codable, Hashable {
var qiSnd: Bool
var qiNtf: Bool
var qiSub: QSub?
@@ -2208,25 +2224,165 @@ public struct QueueInfo: Codable {
var qiMsg: MsgInfo?
}
public struct QSub: Codable {
public struct QSub: Codable, Hashable {
var qSubThread: QSubThread
var qDelivered: String?
}
public enum QSubThread: String, Codable {
public enum QSubThread: String, Codable, Hashable {
case noSub
case subPending
case subThread
case prohibitSub
}
public struct MsgInfo: Codable {
public struct MsgInfo: Codable, Hashable {
var msgId: String
var msgTs: Date
var msgType: MsgType
}
public enum MsgType: String, Codable {
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
}
+131 -116
View File
@@ -9,8 +9,9 @@
import Foundation
import SwiftUI
public struct User: Identifiable, Decodable, UserLike, NamedChat {
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 {
public static let sampleData = User(
userId: 1,
agentUserId: "abc",
userContactId: 1,
localDisplayName: "alice",
profile: LocalProfile.sampleData,
@@ -52,7 +54,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat {
)
}
public struct UserRef: Identifiable, Decodable, UserLike {
public struct UserRef: Identifiable, Decodable, UserLike, Hashable {
public var userId: Int64
public var localDisplayName: ContactName
@@ -63,12 +65,12 @@ public protocol UserLike: Identifiable {
var userId: Int64 { get }
}
public struct UserPwdHash: Decodable {
public struct UserPwdHash: Decodable, Hashable {
public var hash: String
public var salt: String
}
public struct UserInfo: Decodable, Identifiable {
public struct UserInfo: Decodable, Identifiable, Hashable {
public var user: User
public var unreadCount: Int
@@ -89,7 +91,7 @@ public typealias ContactName = String
public typealias GroupName = String
public struct Profile: Codable, NamedChat {
public struct Profile: Codable, NamedChat, Hashable {
public init(
displayName: String,
fullName: String,
@@ -121,7 +123,7 @@ public struct Profile: Codable, NamedChat {
)
}
public struct LocalProfile: Codable, NamedChat {
public struct LocalProfile: Codable, NamedChat, Hashable {
public init(
profileId: Int64,
displayName: String,
@@ -171,13 +173,13 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences)
}
public struct UserProfileUpdateSummary: Decodable {
public struct UserProfileUpdateSummary: Decodable, Hashable {
public var updateSuccesses: Int
public var updateFailures: Int
public var changedContacts: [Contact]
}
public enum ChatType: String {
public enum ChatType: String, Hashable {
case direct = "@"
case group = "#"
case local = "*"
@@ -202,7 +204,7 @@ extension NamedChat {
public typealias ChatId = String
public struct FullPreferences: Decodable, Equatable {
public struct FullPreferences: Decodable, Equatable, Hashable {
public var timedMessages: TimedMessagesPreference
public var fullDelete: SimplePreference
public var reactions: SimplePreference
@@ -232,7 +234,7 @@ public struct FullPreferences: Decodable, Equatable {
)
}
public struct Preferences: Codable {
public struct Preferences: Codable, Hashable {
public var timedMessages: TimedMessagesPreference?
public var fullDelete: SimplePreference?
public var reactions: SimplePreference?
@@ -308,11 +310,11 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac
)
}
public protocol Preference: Codable, Equatable {
public protocol Preference: Codable, Equatable, Hashable {
var allow: FeatureAllowed { get set }
}
public struct SimplePreference: Preference {
public struct SimplePreference: Preference, Hashable {
public var allow: FeatureAllowed
public init(allow: FeatureAllowed) {
@@ -320,7 +322,7 @@ public struct SimplePreference: Preference {
}
}
public struct TimedMessagesPreference: Preference {
public struct TimedMessagesPreference: Preference, Hashable {
public var allow: FeatureAllowed
public var ttl: Int?
@@ -334,7 +336,7 @@ public struct TimedMessagesPreference: Preference {
}
}
public enum CustomTimeUnit {
public enum CustomTimeUnit: Hashable {
case second
case minute
case hour
@@ -433,7 +435,7 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey {
return CustomTimeUnit.toShortText(seconds: seconds)
}
public struct ContactUserPreferences: Decodable {
public struct ContactUserPreferences: Decodable, Hashable {
public var timedMessages: ContactUserPreference<TimedMessagesPreference>
public var fullDelete: ContactUserPreference<SimplePreference>
public var reactions: ContactUserPreference<SimplePreference>
@@ -483,7 +485,7 @@ public struct ContactUserPreferences: Decodable {
)
}
public struct ContactUserPreference<P: Preference>: Decodable {
public struct ContactUserPreference<P: Preference>: Decodable, Hashable {
public var enabled: FeatureEnabled
public var userPreference: ContactUserPref<P>
public var contactPreference: P
@@ -495,7 +497,7 @@ public struct ContactUserPreference<P: Preference>: Decodable {
}
}
public struct FeatureEnabled: Decodable {
public struct FeatureEnabled: Decodable, Hashable {
public var forUser: Bool
public var forContact: Bool
@@ -526,7 +528,7 @@ public struct FeatureEnabled: Decodable {
}
}
public enum ContactUserPref<P: Preference>: Decodable {
public enum ContactUserPref<P: Preference>: Decodable, Hashable {
case contact(preference: P) // contact override is set
case user(preference: P) // global user default is used
@@ -547,7 +549,7 @@ public protocol Feature {
var text: String { get }
}
public enum ChatFeature: String, Decodable, Feature {
public enum ChatFeature: String, Decodable, Feature, Hashable {
case timedMessages
case fullDelete
case reactions
@@ -690,7 +692,7 @@ public enum ChatFeature: String, Decodable, Feature {
}
}
public enum GroupFeature: String, Decodable, Feature {
public enum GroupFeature: String, Decodable, Feature, Hashable {
case timedMessages
case directMessages
case fullDelete
@@ -890,7 +892,7 @@ public enum ContactFeatureAllowed: Identifiable, Hashable {
}
}
public struct ContactFeaturesAllowed: Equatable {
public struct ContactFeaturesAllowed: Equatable, Hashable {
public var timedMessagesAllowed: Bool
public var timedMessagesTTL: Int?
public var fullDelete: ContactFeatureAllowed
@@ -968,7 +970,7 @@ public func contactFeatureAllowedToPref(_ contactFeatureAllowed: ContactFeatureA
}
}
public enum FeatureAllowed: String, Codable, Identifiable {
public enum FeatureAllowed: String, Codable, Identifiable, Hashable {
case always
case yes
case no
@@ -986,7 +988,7 @@ public enum FeatureAllowed: String, Codable, Identifiable {
}
}
public struct FullGroupPreferences: Decodable, Equatable {
public struct FullGroupPreferences: Decodable, Equatable, Hashable {
public var timedMessages: TimedMessagesGroupPreference
public var directMessages: RoleGroupPreference
public var fullDelete: GroupPreference
@@ -1028,7 +1030,7 @@ public struct FullGroupPreferences: Decodable, Equatable {
)
}
public struct GroupPreferences: Codable {
public struct GroupPreferences: Codable, Hashable {
public var timedMessages: TimedMessagesGroupPreference?
public var directMessages: RoleGroupPreference?
public var fullDelete: GroupPreference?
@@ -1083,7 +1085,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
)
}
public struct GroupPreference: Codable, Equatable {
public struct GroupPreference: Codable, Equatable, Hashable {
public var enable: GroupFeatureEnabled
public var on: Bool {
@@ -1107,7 +1109,7 @@ public struct GroupPreference: Codable, Equatable {
}
}
public struct RoleGroupPreference: Codable, Equatable {
public struct RoleGroupPreference: Codable, Equatable, Hashable {
public var enable: GroupFeatureEnabled
public var role: GroupMemberRole?
@@ -1121,7 +1123,7 @@ public struct RoleGroupPreference: Codable, Equatable {
}
}
public struct TimedMessagesGroupPreference: Codable, Equatable {
public struct TimedMessagesGroupPreference: Codable, Equatable, Hashable {
public var enable: GroupFeatureEnabled
public var ttl: Int?
@@ -1135,7 +1137,7 @@ public struct TimedMessagesGroupPreference: Codable, Equatable {
}
}
public enum GroupFeatureEnabled: String, Codable, Identifiable {
public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable {
case on
case off
@@ -1158,7 +1160,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable {
}
}
public enum ChatInfo: Identifiable, Decodable, NamedChat {
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
case direct(contact: Contact)
case group(groupInfo: GroupInfo)
case local(noteFolder: NoteFolder)
@@ -1370,7 +1372,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public enum ShowEnableVoiceMessagesAlert {
public enum ShowEnableVoiceMessagesAlert: Hashable {
case userEnable
case askContact
case groupOwnerCan
@@ -1443,7 +1445,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public struct SampleData {
public struct SampleData: Hashable {
public var direct: ChatInfo
public var group: ChatInfo
public var local: ChatInfo
@@ -1460,7 +1462,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
)
}
public struct ChatData: Decodable, Identifiable {
public struct ChatData: Decodable, Identifiable, Hashable {
public var chatInfo: ChatInfo
public var chatItems: [ChatItem]
public var chatStats: ChatStats
@@ -1476,7 +1478,7 @@ public struct ChatData: Decodable, Identifiable {
}
}
public struct ChatStats: Decodable {
public struct ChatStats: Decodable, Hashable {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
self.unreadCount = unreadCount
self.minUnreadItemId = minUnreadItemId
@@ -1488,7 +1490,7 @@ public struct ChatStats: Decodable {
public var unreadChat: Bool = false
}
public struct Contact: Identifiable, Decodable, NamedChat {
public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
public var contactId: Int64
var localDisplayName: ContactName
public var profile: LocalProfile
@@ -1574,12 +1576,12 @@ public struct Contact: Identifiable, Decodable, NamedChat {
)
}
public enum ContactStatus: String, Decodable {
public enum ContactStatus: String, Decodable, Hashable {
case active = "active"
case deleted = "deleted"
}
public struct ContactRef: Decodable, Equatable {
public struct ContactRef: Decodable, Equatable, Hashable {
var contactId: Int64
public var agentConnId: String
var connId: Int64
@@ -1588,12 +1590,12 @@ public struct ContactRef: Decodable, Equatable {
public var id: ChatId { get { "@\(contactId)" } }
}
public struct ContactSubStatus: Decodable {
public struct ContactSubStatus: Decodable, Hashable {
public var contact: Contact
public var contactError: ChatError?
}
public struct Connection: Decodable {
public struct Connection: Decodable, Hashable {
public var connId: Int64
public var agentConnId: String
public var peerChatVRange: VersionRange
@@ -1637,7 +1639,7 @@ public struct Connection: Decodable {
)
}
public struct VersionRange: Decodable {
public struct VersionRange: Decodable, Hashable {
public init(minVersion: Int, maxVersion: Int) {
self.minVersion = minVersion
self.maxVersion = maxVersion
@@ -1651,7 +1653,7 @@ public struct VersionRange: Decodable {
}
}
public struct SecurityCode: Decodable, Equatable {
public struct SecurityCode: Decodable, Equatable, Hashable {
public init(securityCode: String, verifiedAt: Date) {
self.securityCode = securityCode
self.verifiedAt = verifiedAt
@@ -1661,7 +1663,7 @@ public struct SecurityCode: Decodable, Equatable {
public var verifiedAt: Date
}
public struct UserContact: Decodable {
public struct UserContact: Decodable, Hashable {
public var userContactLinkId: Int64
// public var connReqContact: String
public var groupId: Int64?
@@ -1679,7 +1681,7 @@ public struct UserContact: Decodable {
}
}
public struct UserContactRequest: Decodable, NamedChat {
public struct UserContactRequest: Decodable, NamedChat, Hashable {
var contactRequestId: Int64
public var userContactLinkId: Int64
public var cReqChatVRange: VersionRange
@@ -1708,7 +1710,7 @@ public struct UserContactRequest: Decodable, NamedChat {
)
}
public struct PendingContactConnection: Decodable, NamedChat {
public struct PendingContactConnection: Decodable, NamedChat, Hashable {
public var pccConnId: Int64
var pccAgentConnId: String
var pccConnStatus: ConnStatus
@@ -1798,7 +1800,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
}
}
public enum ConnStatus: String, Decodable {
public enum ConnStatus: String, Decodable, Hashable {
case new = "new"
case joined = "joined"
case requested = "requested"
@@ -1822,7 +1824,7 @@ public enum ConnStatus: String, Decodable {
}
}
public struct Group: Decodable {
public struct Group: Decodable, Hashable {
public var groupInfo: GroupInfo
public var members: [GroupMember]
@@ -1832,7 +1834,7 @@ public struct Group: Decodable {
}
}
public struct GroupInfo: Identifiable, Decodable, NamedChat {
public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
public var groupId: Int64
var localDisplayName: GroupName
public var groupProfile: GroupProfile
@@ -1878,12 +1880,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
)
}
public struct GroupRef: Decodable {
public struct GroupRef: Decodable, Hashable {
public var groupId: Int64
var localDisplayName: GroupName
}
public struct GroupProfile: Codable, NamedChat {
public struct GroupProfile: Codable, NamedChat, Hashable {
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
self.displayName = displayName
self.fullName = fullName
@@ -1905,7 +1907,7 @@ public struct GroupProfile: Codable, NamedChat {
)
}
public struct GroupMember: Identifiable, Decodable {
public struct GroupMember: Identifiable, Decodable, Hashable {
public var groupMemberId: Int64
public var groupId: Int64
public var memberId: String
@@ -2037,21 +2039,21 @@ public struct GroupMember: Identifiable, Decodable {
)
}
public struct GroupMemberSettings: Codable {
public struct GroupMemberSettings: Codable, Hashable {
public var showMessages: Bool
}
public struct GroupMemberRef: Decodable {
public struct GroupMemberRef: Decodable, Hashable {
var groupMemberId: Int64
var profile: Profile
}
public struct GroupMemberIds: Decodable {
public struct GroupMemberIds: Decodable, Hashable {
var groupMemberId: Int64
var groupId: Int64
}
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable {
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable, Hashable {
case observer = "observer"
case author = "author"
case member = "member"
@@ -2085,7 +2087,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod
}
}
public enum GroupMemberCategory: String, Decodable {
public enum GroupMemberCategory: String, Decodable, Hashable {
case userMember = "user"
case inviteeMember = "invitee"
case hostMember = "host"
@@ -2093,7 +2095,7 @@ public enum GroupMemberCategory: String, Decodable {
case postMember = "post"
}
public enum GroupMemberStatus: String, Decodable {
public enum GroupMemberStatus: String, Decodable, Hashable {
case memRemoved = "removed"
case memLeft = "left"
case memGroupDeleted = "deleted"
@@ -2142,7 +2144,7 @@ public enum GroupMemberStatus: String, Decodable {
}
}
public struct NoteFolder: Identifiable, Decodable, NamedChat {
public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable {
public var noteFolderId: Int64
public var favorite: Bool
public var unread: Bool
@@ -2175,18 +2177,18 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
)
}
public enum InvitedBy: Decodable {
public enum InvitedBy: Decodable, Hashable {
case contact(byContactId: Int64)
case user
case unknown
}
public struct MemberSubError: Decodable {
public struct MemberSubError: Decodable, Hashable {
var member: GroupMemberIds
var memberError: ChatError
}
public enum ConnectionEntity: Decodable {
public enum ConnectionEntity: Decodable, Hashable {
case rcvDirectMsgConnection(contact: Contact?)
case rcvGroupMsgConnection(groupInfo: GroupInfo, groupMember: GroupMember)
case sndFileConnection(sndFileTransfer: SndFileTransfer)
@@ -2217,12 +2219,12 @@ public enum ConnectionEntity: Decodable {
}
}
public struct NtfMsgInfo: Decodable {
public struct NtfMsgInfo: Decodable, Hashable {
public var msgId: String
public var msgTs: Date
}
public struct AChatItem: Decodable {
public struct AChatItem: Decodable, Hashable {
public var chatInfo: ChatInfo
public var chatItem: ChatItem
@@ -2234,19 +2236,19 @@ public struct AChatItem: Decodable {
}
}
public struct ACIReaction: Decodable {
public struct ACIReaction: Decodable, Hashable {
public var chatInfo: ChatInfo
public var chatReaction: CIReaction
}
public struct CIReaction: Decodable {
public struct CIReaction: Decodable, Hashable {
public var chatDir: CIDirection
public var chatItem: ChatItem
public var sentAt: Date
public var reaction: MsgReaction
}
public struct ChatItem: Identifiable, Decodable {
public struct ChatItem: Identifiable, Decodable, Hashable {
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) {
self.chatDir = chatDir
self.meta = meta
@@ -2596,7 +2598,7 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public enum CIMergeCategory {
public enum CIMergeCategory: Hashable {
case memberConnected
case rcvGroupEvent
case sndGroupEvent
@@ -2605,7 +2607,7 @@ public enum CIMergeCategory {
case chatFeature
}
public enum CIDirection: Decodable {
public enum CIDirection: Decodable, Hashable {
case directSnd
case directRcv
case groupSnd
@@ -2627,7 +2629,7 @@ public enum CIDirection: Decodable {
}
}
public struct CIMeta: Decodable {
public struct CIMeta: Decodable, Hashable {
public var itemId: Int64
public var itemTs: Date
var itemText: String
@@ -2690,7 +2692,7 @@ public struct CIMeta: Decodable {
}
}
public struct CITimed: Decodable {
public struct CITimed: Decodable, Hashable {
public var ttl: Int
public var deleteAt: Date?
}
@@ -2717,7 +2719,7 @@ private func recent(_ date: Date) -> Bool {
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
}
public enum CIStatus: Decodable {
public enum CIStatus: Decodable, Hashable {
case sndNew
case sndSent(sndProgress: SndCIStatusProgress)
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
@@ -2787,7 +2789,7 @@ public enum CIStatus: Decodable {
}
}
public enum SndError: Decodable {
public enum SndError: Decodable, Hashable {
case auth
case quota
case expired
@@ -2809,7 +2811,7 @@ public enum SndError: Decodable {
}
}
public enum SrvError: Decodable, Equatable {
public enum SrvError: Decodable, Hashable {
case host
case version
case other(srvError: String)
@@ -2831,17 +2833,17 @@ public enum SrvError: Decodable, Equatable {
}
}
public enum MsgReceiptStatus: String, Decodable {
public enum MsgReceiptStatus: String, Decodable, Hashable {
case ok
case badMsgHash
}
public enum SndCIStatusProgress: String, Decodable {
public enum SndCIStatusProgress: String, Decodable, Hashable {
case partial
case complete
}
public enum CIDeleted: Decodable {
public enum CIDeleted: Decodable, Hashable {
case deleted(deletedTs: Date?)
case blocked(deletedTs: Date?)
case blockedByAdmin(deletedTs: Date?)
@@ -2857,12 +2859,12 @@ public enum CIDeleted: Decodable {
}
}
public enum MsgDirection: String, Decodable {
public enum MsgDirection: String, Decodable, Hashable {
case rcv = "rcv"
case snd = "snd"
}
public enum CIForwardedFrom: Decodable {
public enum CIForwardedFrom: Decodable, Hashable {
case unknown
case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?)
case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?)
@@ -2882,7 +2884,7 @@ public enum CIForwardedFrom: Decodable {
}
}
public enum CIDeleteMode: String, Decodable {
public enum CIDeleteMode: String, Decodable, Hashable {
case cidmBroadcast = "broadcast"
case cidmInternal = "internal"
}
@@ -2891,7 +2893,7 @@ protocol ItemContent {
var text: String { get }
}
public enum CIContent: Decodable, ItemContent {
public enum CIContent: Decodable, ItemContent, Hashable {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)
case sndDeleted(deleteMode: CIDeleteMode) // legacy - since v4.3.0 itemDeleted field is used
@@ -3027,7 +3029,7 @@ public enum CIContent: Decodable, ItemContent {
}
}
public enum MsgDecryptError: String, Decodable {
public enum MsgDecryptError: String, Decodable, Hashable {
case ratchetHeader
case tooManySkipped
case ratchetEarlier
@@ -3045,7 +3047,7 @@ public enum MsgDecryptError: String, Decodable {
}
}
public struct CIQuote: Decodable, ItemContent {
public struct CIQuote: Decodable, ItemContent, Hashable {
public var chatDir: CIDirection?
public var itemId: Int64?
var sharedMsgId: String? = nil
@@ -3083,13 +3085,13 @@ public struct CIQuote: Decodable, ItemContent {
}
}
public struct CIReactionCount: Decodable {
public struct CIReactionCount: Decodable, Hashable {
public var reaction: MsgReaction
public var userReacted: Bool
public var totalReacted: Int
}
public enum MsgReaction: Hashable {
public enum MsgReaction: Hashable, Identifiable {
case emoji(emoji: MREmojiChar)
case unknown(type: String)
@@ -3110,9 +3112,16 @@ public enum MsgReaction: Hashable {
case type
case emoji
}
public var id: String {
switch self {
case let .emoji(emoji): emoji.rawValue
case let .unknown(unknown): unknown
}
}
}
public enum MREmojiChar: String, Codable, CaseIterable {
public enum MREmojiChar: String, Codable, CaseIterable, Hashable {
case thumbsup = "👍"
case thumbsdown = "👎"
case smile = "😀"
@@ -3153,7 +3162,7 @@ extension MsgReaction: Encodable {
}
}
public struct CIFile: Decodable {
public struct CIFile: Decodable, Hashable {
public var fileId: Int64
public var fileName: String
public var fileSize: Int64
@@ -3221,7 +3230,7 @@ public struct CIFile: Decodable {
}
}
public struct CryptoFile: Codable {
public struct CryptoFile: Codable, Hashable {
public var filePath: String // the name of the file, not a full path
public var cryptoArgs: CryptoFileArgs?
@@ -3268,22 +3277,28 @@ public struct CryptoFile: Codable {
static var decryptedUrls = Dictionary<String, URL>()
}
public struct CryptoFileArgs: Codable {
public struct CryptoFileArgs: Codable, Hashable {
public var fileKey: String
public var fileNonce: String
}
public struct CancelAction {
public struct CancelAction: Hashable {
public var uiAction: String
public var alert: AlertInfo
}
public struct AlertInfo {
public struct AlertInfo: Hashable {
public var title: LocalizedStringKey
public var message: LocalizedStringKey
public var confirm: LocalizedStringKey
}
extension LocalizedStringKey: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine("\(self)")
}
}
private var sndCancelAction = CancelAction(
uiAction: NSLocalizedString("Stop file", comment: "cancel file action"),
alert: AlertInfo(
@@ -3311,13 +3326,13 @@ private var rcvCancelAction = CancelAction(
)
)
public enum FileProtocol: String, Decodable {
public enum FileProtocol: String, Decodable, Hashable {
case smp = "smp"
case xftp = "xftp"
case local = "local"
}
public enum CIFileStatus: Decodable, Equatable {
public enum CIFileStatus: Decodable, Equatable, Hashable {
case sndStored
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
case sndComplete
@@ -3355,7 +3370,7 @@ public enum CIFileStatus: Decodable, Equatable {
}
}
public enum FileError: Decodable, Equatable {
public enum FileError: Decodable, Equatable, Hashable {
case auth
case noFile
case relay(srvError: SrvError)
@@ -3380,7 +3395,7 @@ public enum FileError: Decodable, Equatable {
}
}
public enum MsgContent: Equatable {
public enum MsgContent: Equatable, Hashable {
case text(String)
case link(text: String, preview: LinkPreview)
case image(text: String, image: String)
@@ -3547,7 +3562,7 @@ extension MsgContent: Encodable {
}
}
public struct FormattedText: Decodable {
public struct FormattedText: Decodable, Hashable {
public var text: String
public var format: Format?
@@ -3556,7 +3571,7 @@ public struct FormattedText: Decodable {
}
}
public enum Format: Decodable, Equatable {
public enum Format: Decodable, Equatable, Hashable {
case bold
case italic
case strikeThrough
@@ -3578,7 +3593,7 @@ public enum Format: Decodable, Equatable {
}
}
public enum SimplexLinkType: String, Decodable {
public enum SimplexLinkType: String, Decodable, Hashable {
case contact
case invitation
case group
@@ -3592,7 +3607,7 @@ public enum SimplexLinkType: String, Decodable {
}
}
public enum FormatColor: String, Decodable {
public enum FormatColor: String, Decodable, Hashable {
case red = "red"
case green = "green"
case blue = "blue"
@@ -3619,7 +3634,7 @@ public enum FormatColor: String, Decodable {
}
// Struct to use with simplex API
public struct LinkPreview: Codable, Equatable {
public struct LinkPreview: Codable, Equatable, Hashable {
public init(uri: URL, title: String, description: String = "", image: String) {
self.uri = uri
self.title = title
@@ -3634,7 +3649,7 @@ public struct LinkPreview: Codable, Equatable {
public var image: String
}
public enum NtfTknStatus: String, Decodable {
public enum NtfTknStatus: String, Decodable, Hashable {
case new = "NEW"
case registered = "REGISTERED"
case invalid = "INVALID"
@@ -3643,22 +3658,22 @@ public enum NtfTknStatus: String, Decodable {
case expired = "EXPIRED"
}
public struct SndFileTransfer: Decodable {
public struct SndFileTransfer: Decodable, Hashable {
}
public struct RcvFileTransfer: Decodable {
public struct RcvFileTransfer: Decodable, Hashable {
public let fileId: Int64
}
public struct FileTransferMeta: Decodable {
public struct FileTransferMeta: Decodable, Hashable {
public let fileId: Int64
public let fileName: String
public let filePath: String
public let fileSize: Int64
}
public enum CICallStatus: String, Decodable {
public enum CICallStatus: String, Decodable, Hashable {
case pending
case missed
case rejected
@@ -3690,7 +3705,7 @@ public func durationText(_ sec: Int) -> String {
: String(format: "%02d:%02d:%02d", m / 60, m % 60, s)
}
public enum MsgErrorType: Decodable {
public enum MsgErrorType: Decodable, Hashable {
case msgSkipped(fromMsgId: Int64, toMsgId: Int64)
case msgBadId(msgId: Int64)
case msgBadHash
@@ -3707,7 +3722,7 @@ public enum MsgErrorType: Decodable {
}
}
public struct CIGroupInvitation: Decodable {
public struct CIGroupInvitation: Decodable, Hashable {
public var groupId: Int64
public var groupMemberId: Int64
public var localDisplayName: GroupName
@@ -3723,18 +3738,18 @@ public struct CIGroupInvitation: Decodable {
}
}
public enum CIGroupInvitationStatus: String, Decodable {
public enum CIGroupInvitationStatus: String, Decodable, Hashable {
case pending
case accepted
case rejected
case expired
}
public struct E2EEInfo: Decodable {
public struct E2EEInfo: Decodable, Hashable {
public var pqEnabled: Bool
}
public enum RcvDirectEvent: Decodable {
public enum RcvDirectEvent: Decodable, Hashable {
case contactDeleted
case profileUpdated(fromProfile: Profile, toProfile: Profile)
@@ -3763,7 +3778,7 @@ public enum RcvDirectEvent: Decodable {
}
}
public enum RcvGroupEvent: Decodable {
public enum RcvGroupEvent: Decodable, Hashable {
case memberAdded(groupMemberId: Int64, profile: Profile)
case memberConnected
case memberLeft
@@ -3819,7 +3834,7 @@ public enum RcvGroupEvent: Decodable {
}
}
public enum SndGroupEvent: Decodable {
public enum SndGroupEvent: Decodable, Hashable {
case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole)
case userRole(role: GroupMemberRole)
case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool)
@@ -3847,7 +3862,7 @@ public enum SndGroupEvent: Decodable {
}
}
public enum RcvConnEvent: Decodable {
public enum RcvConnEvent: Decodable, Hashable {
case switchQueue(phase: SwitchPhase)
case ratchetSync(syncStatus: RatchetSyncState)
case verificationCodeReset
@@ -3884,7 +3899,7 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
}
}
public enum SndConnEvent: Decodable {
public enum SndConnEvent: Decodable, Hashable {
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
case pqEnabled(enabled: Bool)
@@ -3921,14 +3936,14 @@ public enum SndConnEvent: Decodable {
}
}
public enum SwitchPhase: String, Decodable {
public enum SwitchPhase: String, Decodable, Hashable {
case started
case confirmed
case secured
case completed
}
public enum ChatItemTTL: Hashable, Identifiable, Comparable {
public enum ChatItemTTL: Identifiable, Comparable, Hashable {
case day
case week
case month
@@ -3978,13 +3993,13 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
}
}
public struct ChatItemInfo: Decodable {
public struct ChatItemInfo: Decodable, Hashable {
public var itemVersions: [ChatItemVersion]
public var memberDeliveryStatuses: [MemberDeliveryStatus]?
public var forwardedFromChatItem: AChatItem?
}
public struct ChatItemVersion: Decodable {
public struct ChatItemVersion: Decodable, Hashable {
public var chatItemVersionId: Int64
public var msgContent: MsgContent
public var formattedText: [FormattedText]?
@@ -3992,7 +4007,7 @@ public struct ChatItemVersion: Decodable {
public var createdAt: Date
}
public struct MemberDeliveryStatus: Decodable {
public struct MemberDeliveryStatus: Decodable, Hashable {
public var groupMemberId: Int64
public var memberDeliveryStatus: CIStatus
public var sentViaProxy: Bool?
+1 -1
View File
@@ -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 -1
View File
@@ -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";
+2
View File
@@ -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,
+1
View File
@@ -507,6 +507,7 @@ data ChatCommand
| DebugLocks
| DebugEvent ChatResponse
| GetAgentServersSummary UserId
| ResetAgentServersStats
| GetAgentSubs
| GetAgentSubsDetails
| GetAgentWorkers
+100 -74
View File
@@ -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)