Compare commits

..

2 Commits

Author SHA1 Message Date
Evgeny Poberezkin 83fd6efed9 update 2024-11-06 09:24:06 +00:00
Evgeny Poberezkin c8936220b0 rfc: conversational agent 2024-07-04 12:10:47 +01:00
107 changed files with 759 additions and 11644 deletions
+2 -2
View File
@@ -702,7 +702,7 @@ final class ChatModel: ObservableObject {
}
i += 1
}
return UnreadChatItemCounts(isNearBottom: totalBelow < 16, unreadBelow: unreadBelow)
return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow)
}
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
@@ -740,7 +740,7 @@ struct NTFContactRequest {
}
struct UnreadChatItemCounts: Equatable {
var isNearBottom: Bool
var totalBelow: Int
var unreadBelow: Int
}
@@ -70,6 +70,7 @@ struct CIGroupInvitationView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
.onChange(of: inProgress) { inProgress in
@@ -22,6 +22,7 @@ struct CIInvalidJSONView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
.onTapGesture { showJSON = true }
.appSheet(isPresented: $showJSON) {
@@ -132,6 +132,7 @@ struct CIRcvDecryptionError: View {
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -151,6 +152,7 @@ struct CIRcvDecryptionError: View {
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -25,6 +25,7 @@ struct DeletedItemView: View {
.padding(.leading, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
}
}
@@ -76,6 +76,7 @@ struct FramedItemView: View {
}
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
.cornerRadius(18)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
@@ -70,6 +70,7 @@ struct CIMsgError: View {
.padding(.leading, 12)
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
.onTapGesture(perform: onTap)
}
@@ -23,6 +23,7 @@ struct MarkedDeletedItemView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -232,7 +232,7 @@ struct ChatItemInfoView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, theme))
.modifier(ChatItemClipped())
.cornerRadius(18)
.contextMenu {
if itemVersion.msgContent.text != "" {
Button {
@@ -302,7 +302,7 @@ struct ChatItemInfoView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, theme))
.modifier(ChatItemClipped())
.cornerRadius(18)
.contextMenu {
if qi.text != "" {
Button {
@@ -415,7 +415,7 @@ struct ChatItemInfoView: View {
}
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
LazyVStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
let mss = membersStatuses(memberDeliveryStatuses)
if !mss.isEmpty {
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
@@ -428,7 +428,7 @@ struct ChatItemInfoView: View {
}
}
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, Bool?)] {
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
memberDeliveryStatuses.compactMap({ mds in
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
@@ -438,7 +438,7 @@ struct ChatItemInfoView: View {
})
}
private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View {
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
HStack{
ProfileImage(imageStr: member.image, size: 30)
.padding(.trailing, 2)
@@ -450,19 +450,23 @@ struct ChatItemInfoView: View {
.foregroundColor(theme.colors.secondary).opacity(0.67)
}
let v = Group {
let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
switch status {
case .rcvd:
ZStack(alignment: .trailing) {
if let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) {
switch status {
case .sndRcvd:
ZStack(alignment: .trailing) {
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
.padding(.trailing, 6)
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
}
default:
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
.padding(.trailing, 6)
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
.foregroundColor(statusColor)
}
default:
Image(systemName: icon)
.foregroundColor(statusColor)
} else {
Image(systemName: "ellipsis")
.foregroundColor(Color.secondary)
}
}
+3 -4
View File
@@ -410,7 +410,7 @@ struct ChatView: View {
init() {
unreadChatItemCounts = UnreadChatItemCounts(
isNearBottom: true,
totalBelow: .zero,
unreadBelow: .zero
)
events
@@ -425,9 +425,9 @@ struct ChatView: View {
itemsInView
}
}
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) }
.removeDuplicates()
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.assign(to: \.unreadChatItemCounts, on: self)
.store(in: &bag)
}
@@ -479,7 +479,7 @@ struct ChatView: View {
scrollModel.scrollToItem(id: latestUnreadItem.id)
}
}
} else if !counts.isNearBottom {
} else if counts.totalBelow > 16 {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
@@ -754,7 +754,6 @@ struct ChatView: View {
playbackState: $playbackState,
playbackTime: $playbackTime
)
.modifier(ChatItemClipped(ci))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -232,7 +232,8 @@ struct GroupChatInfoView: View {
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground)
(member.verified ? memberVerifiedShield + t : t)
.lineLimit(1)
(user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member)))
let s = Text(member.memberStatus.shortText)
(user ? Text ("you: ") + s : s)
.lineLimit(1)
.font(.caption)
.foregroundColor(theme.colors.secondary)
@@ -265,16 +266,6 @@ struct GroupChatInfoView: View {
}
}
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
} else {
return member.memberStatus.shortText
}
}
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
if member.blocked {
Text("blocked")
@@ -141,6 +141,12 @@ struct GroupMemberInfoView: View {
} else {
infoRow("Role", member.memberRole.text)
}
// TODO invited by - need to get contact by contact id
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
}
if let connStats = connectionStats {
@@ -177,10 +183,6 @@ struct GroupMemberInfoView: View {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
@@ -55,7 +55,6 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
// 1. Style
tableView.separatorStyle = .none
tableView.transform = .verticalFlip
tableView.backgroundColor = .clear
// 2. Register cells
if #available(iOS 16.0, *) {
@@ -181,7 +180,6 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
/// Updates content of the cell
/// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/
func set(content: Hosted, parent: UIViewController) {
hostingController.view.backgroundColor = .clear
hostingController.rootView = content
if let hostingView = hostingController.view {
hostingView.invalidateIntrinsicContentSize()
@@ -36,7 +36,6 @@ struct ServersSummaryView: View {
viewBody()
.navigationTitle("Servers info")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
shareButton()
@@ -265,7 +264,6 @@ struct ServersSummaryView: View {
)
.navigationBarTitle("SMP server")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
} label: {
HStack {
Text(serverAddress(srvSumm.smpServer))
@@ -334,7 +332,6 @@ struct ServersSummaryView: View {
)
.navigationBarTitle("XFTP server")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
} label: {
HStack {
Text(serverAddress(srvSumm.xftpServer))
@@ -363,7 +360,7 @@ struct ServersSummaryView: View {
Button {
alert = SomeAlert(
alert: Alert(
title: Text("Reset all statistics?"),
title: Text("Reset all servers statistics?"),
message: Text("Servers statistics will be reset - this cannot be undone!"),
primaryButton: .destructive(Text("Reset")) {
Task {
@@ -425,7 +422,7 @@ struct SubscriptionStatusPercentageView: View {
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
Text("\(Int(floor(statusPercent * 100)))%")
.foregroundColor(.secondary)
.font(.caption)
}
@@ -473,7 +470,6 @@ struct SMPServerSummaryView: View {
NavigationLink {
ProtocolServersView(serverProtocol: .smp)
.navigationTitle("Your SMP servers")
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Open server settings")
}
@@ -567,7 +563,6 @@ struct SMPStatsView: View {
DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
.navigationTitle("Detailed statistics")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Details")
}
@@ -595,21 +590,21 @@ struct DetailedSMPStatsView: View {
infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts)
infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts)
Text("Send errors")
infoRow(Text(verbatim: "AUTH"), numOrDash(stats._sentAuthErrs)).padding(.leading, 24)
infoRow(Text(verbatim: "QUOTA"), numOrDash(stats._sentQuotaErrs)).padding(.leading, 24)
infoRow("expired", numOrDash(stats._sentExpiredErrs)).padding(.leading, 24)
infoRow("other", numOrDash(stats._sentOtherErrs)).padding(.leading, 24)
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")
infoRow("duplicates", numOrDash(stats._recvDuplicates)).padding(.leading, 24)
infoRow("decryption errors", numOrDash(stats._recvCryptoErrs)).padding(.leading, 24)
infoRow("other errors", numOrDash(stats._recvErrs)).padding(.leading, 24)
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")
infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24)
infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24)
indentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
indentedInfoRow("other errors", numOrDash(stats._ackOtherErrs))
}
Section {
infoRow("Created", numOrDash(stats._connCreated))
@@ -618,7 +613,7 @@ struct DetailedSMPStatsView: View {
infoRowTwoValues("Deleted", "attempts", stats._connDeleted, stats._connDelAttempts)
infoRow("Deletion errors", numOrDash(stats._connDelErrs))
infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts)
infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored))
infoRow("Subscription results ignored", numOrDash(stats._connSubIgnored))
infoRow("Subscription errors", numOrDash(stats._connSubErrs))
} header: {
Text("Connections")
@@ -631,19 +626,29 @@ struct DetailedSMPStatsView: View {
private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View {
HStack {
Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2)
Text(title) + Text(" / ").font(.caption2) + Text(title2).font(.caption2)
Spacer()
Group {
if value == 0 && value2 == 0 {
Text(verbatim: "-")
Text("-")
} else {
Text(numOrDash(value)) + Text(verbatim: " / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
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
@@ -657,7 +662,6 @@ struct XFTPServerSummaryView: View {
NavigationLink {
ProtocolServersView(serverProtocol: .xftp)
.navigationTitle("Your XFTP servers")
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Open server settings")
}
@@ -688,7 +692,6 @@ struct XFTPStatsView: View {
DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
.navigationTitle("Detailed statistics")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Details")
}
@@ -722,8 +725,8 @@ struct DetailedXFTPStatsView: View {
infoRow("Size", prettySize(stats._downloadsSize))
infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts)
Text("Download errors")
infoRow(Text(verbatim: "AUTH"), numOrDash(stats._downloadAuthErrs)).padding(.leading, 24)
infoRow("other", numOrDash(stats._downloadErrs)).padding(.leading, 24)
indentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
indentedInfoRow("other", numOrDash(stats._downloadErrs))
} header: {
Text("Downloaded files")
} footer: {
@@ -1,68 +0,0 @@
//
// ChatItemClipShape.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 04/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
/// Modifier, which provides clipping mask for ``ChatItemWithMenu`` view
/// and it's previews: (drag interaction, context menu, etc.)
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
struct ChatItemClipped: ViewModifier {
struct ClipShape: Shape {
let maxCornerRadius: Double
func path(in rect: CGRect) -> Path {
Path(
roundedRect: rect,
cornerRadius: min((rect.height / 2), maxCornerRadius),
style: .circular
)
}
}
init() {
clipShape = ClipShape(
maxCornerRadius: 18
)
}
init(_ chatItem: ChatItem) {
clipShape = ClipShape(
maxCornerRadius: {
switch chatItem.content {
case
.sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
.rcvGroupInvitation,
.sndGroupInvitation,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON: 18
default: 8
}
}()
)
}
private let clipShape: ClipShape
func body(content: Content) -> some View {
content
.contentShape(.dragPreview, clipShape)
.contentShape(.contextMenuPreview, clipShape)
.clipShape(clipShape)
}
}
@@ -305,16 +305,13 @@ struct ChatThemePreview: View {
let view = VStack {
if withMessages {
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack {
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
Spacer()
}
HStack {
Spacer()
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)), revealed: Binding.constant(false))
.frame(alignment: .trailing)
}
} else {
@@ -750,7 +747,7 @@ struct ThemeDestinationPicker: View {
let values = [(nil, "All profiles")] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) {
Picker("Apply to", selection: $themeUserDest) {
Picker("Apply to mode", selection: $themeUserDest) {
ForEach(values, id: \.0) { (_, text) in
Text(text)
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "bg",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "cs",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "de",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "en",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "es",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "fi",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "fr",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "hu",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "it",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "ja",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "nl",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "pl",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "ru",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "th",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "tr",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "uk",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj",
"targetLocale" : "zh-Hans",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.0"
},
"version" : "1.0"
}
+32 -44
View File
@@ -195,7 +195,6 @@
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; };
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.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 */; };
@@ -203,12 +202,11 @@
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FB2C3DDD7F009C3F71 /* libffi.a */; };
E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FC2C3DDD7F009C3F71 /* libgmp.a */; };
E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */; };
E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */; };
E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */; };
E50581062C3DDD9D009C3F71 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = E50581052C3DDD9D009C3F71 /* Yams */; };
E52FF8DA2C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E52FF8D52C34676600BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a */; };
E52FF8DB2C34676700BF81EB /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E52FF8D62C34676600BF81EB /* libgmpxx.a */; };
E52FF8DC2C34676700BF81EB /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E52FF8D72C34676700BF81EB /* libffi.a */; };
E52FF8DD2C34676700BF81EB /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E52FF8D82C34676700BF81EB /* libgmp.a */; };
E52FF8DE2C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E52FF8D92C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -503,17 +501,16 @@
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.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>"; };
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.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; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E50580FB2C3DDD7F009C3F71 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E50580FC2C3DDD7F009C3F71 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a"; sourceTree = "<group>"; };
E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a"; sourceTree = "<group>"; };
E52FF8D52C34676600BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a"; sourceTree = "<group>"; };
E52FF8D62C34676600BF81EB /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E52FF8D72C34676700BF81EB /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E52FF8D82C34676700BF81EB /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E52FF8D92C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -552,14 +549,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */,
E52FF8DE2C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a in Frameworks */,
E52FF8DB2C34676700BF81EB /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */,
E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */,
E50581062C3DDD9D009C3F71 /* Yams in Frameworks */,
E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */,
E52FF8DC2C34676700BF81EB /* libffi.a in Frameworks */,
E52FF8DD2C34676700BF81EB /* libgmp.a in Frameworks */,
E52FF8DA2C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -626,11 +622,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E50580FB2C3DDD7F009C3F71 /* libffi.a */,
E50580FC2C3DDD7F009C3F71 /* libgmp.a */,
E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */,
E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */,
E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */,
E52FF8D72C34676700BF81EB /* libffi.a */,
E52FF8D82C34676700BF81EB /* libgmp.a */,
E52FF8D62C34676600BF81EB /* libgmpxx.a */,
E52FF8D52C34676600BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7-ghc9.6.3.a */,
E52FF8D92C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -687,7 +683,6 @@
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */,
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */,
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -1067,7 +1062,6 @@
);
name = SimpleXChat;
packageProductDependencies = (
E50581052C3DDD9D009C3F71 /* Yams */,
);
productName = SimpleXChat;
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
@@ -1231,7 +1225,6 @@
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */,
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */,
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */,
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
@@ -1615,7 +1608,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1640,7 +1633,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1664,7 +1657,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1689,7 +1682,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1750,7 +1743,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1765,7 +1758,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1787,7 +1780,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1802,7 +1795,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1824,7 +1817,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1850,7 +1843,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1875,7 +1868,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 227;
CURRENT_PROJECT_VERSION = 226;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1901,7 +1894,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0;
MARKETING_VERSION = 5.8.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2038,11 +2031,6 @@
package = D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */;
productName = LZString;
};
E50581052C3DDD9D009C3F71 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */;
productName = Yams;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 5CA059BE279559F40002BEB4 /* Project object */;
+3 -65
View File
@@ -1611,12 +1611,11 @@ public struct Connection: Decodable, Hashable {
public var pqSndEnabled: Bool?
public var pqRcvEnabled: Bool?
public var authErrCounter: Int
public var quotaErrCounter: Int
public var connectionStats: ConnectionStats? = nil
private enum CodingKeys: String, CodingKey {
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter
}
public var id: ChatId { get { ":\(connId)" } }
@@ -1625,10 +1624,6 @@ public struct Connection: Decodable, Hashable {
authErrCounter >= 10 // authErrDisableCount in core
}
public var connInactive: Bool {
quotaErrCounter >= 5 // quotaErrInactiveCount in core
}
public var connPQEnabled: Bool {
pqSndEnabled == true && pqRcvEnabled == true
}
@@ -1642,8 +1637,7 @@ public struct Connection: Decodable, Hashable {
viaGroupLink: false,
pqSupport: false,
pqEncryption: false,
authErrCounter: 0,
quotaErrCounter: 0
authErrCounter: 0
)
}
@@ -2852,62 +2846,6 @@ public enum SndCIStatusProgress: String, Decodable, Hashable {
case complete
}
public enum GroupSndStatus: Decodable, Hashable {
case new
case forwarded
case inactive
case sent
case rcvd(msgRcptStatus: MsgReceiptStatus)
case error(agentError: SndError)
case warning(agentError: SndError)
case invalid(text: String)
public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color) {
switch self {
case .new: return ("ellipsis", metaColor)
case .forwarded: return ("chevron.forward.2", metaColor)
case .inactive: return ("person.badge.minus", metaColor)
case .sent: return ("checkmark", metaColor)
case let .rcvd(msgRcptStatus):
switch msgRcptStatus {
case .ok: return ("checkmark", metaColor)
case .badMsgHash: return ("checkmark", .red)
}
case .error: return ("multiply", .red)
case .warning: return ("exclamationmark.triangle.fill", .orange)
case .invalid: return ("questionmark", metaColor)
}
}
public var statusInfo: (String, String)? {
switch self {
case .new: return nil
case .forwarded: return (
NSLocalizedString("Message forwarded", comment: "item status text"),
NSLocalizedString("No direct connection yet, message is forwarded by admin.", comment: "item status description")
)
case .inactive: return (
NSLocalizedString("Member inactive", comment: "item status text"),
NSLocalizedString("Message may be delivered later if member becomes active.", comment: "item status description")
)
case .sent: return nil
case .rcvd: return nil
case let .error(agentError): return (
NSLocalizedString("Message delivery error", comment: "item status text"),
agentError.errorInfo
)
case let .warning(agentError): return (
NSLocalizedString("Message delivery warning", comment: "item status text"),
agentError.errorInfo
)
case let .invalid(text): return (
NSLocalizedString("Invalid status", comment: "item status text"),
text
)
}
}
}
public enum CIDeleted: Decodable, Hashable {
case deleted(deletedTs: Date?)
case blocked(deletedTs: Date?)
@@ -4074,6 +4012,6 @@ public struct ChatItemVersion: Decodable, Hashable {
public struct MemberDeliveryStatus: Decodable, Hashable {
public var groupMemberId: Int64
public var memberDeliveryStatus: GroupSndStatus
public var memberDeliveryStatus: CIStatus
public var sentViaProxy: Bool?
}
@@ -29,7 +29,6 @@ import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import chat.simplex.common.R
import chat.simplex.common.helpers.toURI
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.chat.*
@@ -108,7 +107,7 @@ actual fun PlatformTextField(
editText.maxLines = 16
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
editText.setTextColor(textColor.toArgb())
editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
editText.textSize = textStyle.value.fontSize.value
val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!!
DrawableCompat.setTint(drawable, tintColor.toArgb())
editText.background = drawable
@@ -136,7 +135,7 @@ actual fun PlatformTextField(
editText
}) {
it.setTextColor(textColor.toArgb())
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
it.textSize = textStyle.value.fontSize.value
DrawableCompat.setTint(it.background, tintColor.toArgb())
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
it.isFocusableInTouchMode = it.isFocusable
@@ -554,7 +554,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
}
} else {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
Spacer(Modifier.height(AppBarHeight))
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
@@ -137,9 +137,6 @@ fun AppearanceScope.AppearanceLayout(
}
}
SectionDividerSpaced(maxBottomPadding = true)
FontScaleSection()
SectionBottomSpacer()
}
}
@@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateFirstProfile
@@ -37,7 +36,6 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.math.sqrt
data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>,
@@ -335,38 +333,38 @@ fun EndPartOfScreen() {
fun DesktopScreen(settingsState: SettingsViewState) {
Box {
// 56.dp is a size of unused space of settings drawer
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) {
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) {
StartPartOfScreen(settingsState)
}
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
ModalManager.start.showInView()
SwitchingUsersView()
}
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
CenterPartOfScreen()
}
if (ModalManager.end.hasModalsOpen()) {
VerticalDivider()
}
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) {
EndPartOfScreen()
}
}
val (userPickerState, scaffoldState ) = settingsState
val scope = rememberCoroutineScope()
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
if (scaffoldState.drawerState.isOpen) {
Box(
Modifier
.fillMaxSize()
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
.padding(start = DEFAULT_START_MODAL_WIDTH)
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
ModalManager.start.closeModals()
scope.launch { settingsState.scaffoldState.drawerState.close() }
})
)
}
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
tryOrShowError("UserPicker", error = {}) {
UserPicker(chatModel, userPickerState) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
@@ -1172,22 +1172,18 @@ data class Connection(
val pqSndEnabled: Boolean? = null,
val pqRcvEnabled: Boolean? = null,
val connectionStats: ConnectionStats? = null,
val authErrCounter: Int,
val quotaErrCounter: Int
val authErrCounter: Int
) {
val id: ChatId get() = ":$connId"
val connDisabled: Boolean
get() = authErrCounter >= 10 // authErrDisableCount in core
val connInactive: Boolean
get() = quotaErrCounter >= 5 // quotaErrInactiveCount in core
val connPQEnabled: Boolean
get() = pqSndEnabled == true && pqRcvEnabled == true
companion object {
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0, quotaErrCounter = 0)
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0)
}
}
@@ -2356,48 +2352,6 @@ enum class SndCIStatusProgress {
@SerialName("complete") Complete;
}
@Serializable
sealed class GroupSndStatus {
@Serializable @SerialName("new") class New: GroupSndStatus()
@Serializable @SerialName("forwarded") class Forwarded: GroupSndStatus()
@Serializable @SerialName("inactive") class Inactive: GroupSndStatus()
@Serializable @SerialName("sent") class Sent: GroupSndStatus()
@Serializable @SerialName("rcvd") class Rcvd(val msgRcptStatus: MsgReceiptStatus): GroupSndStatus()
@Serializable @SerialName("error") class Error(val agentError: SndError): GroupSndStatus()
@Serializable @SerialName("warning") class Warning(val agentError: SndError): GroupSndStatus()
@Serializable @SerialName("invalid") class Invalid(val text: String): GroupSndStatus()
fun statusIcon(
primaryColor: Color,
metaColor: Color = CurrentColors.value.colors.secondary,
paleMetaColor: Color = CurrentColors.value.colors.secondary
): Pair<ImageResource, Color> =
when (this) {
is New -> MR.images.ic_more_horiz to metaColor
is Forwarded -> MR.images.ic_chevron_right_2 to metaColor
is Inactive -> MR.images.ic_person_off to metaColor
is Sent -> MR.images.ic_check_filled to metaColor
is Rcvd -> when(this.msgRcptStatus) {
MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
}
is Error -> MR.images.ic_close to Color.Red
is Warning -> MR.images.ic_warning_filled to WarningOrange
is Invalid -> MR.images.ic_question_mark to metaColor
}
val statusInto: Pair<String, String>? get() = when (this) {
is New -> null
is Forwarded -> generalGetString(MR.strings.message_forwarded_title) to generalGetString(MR.strings.message_forwarded_desc)
is Inactive -> generalGetString(MR.strings.member_inactive_title) to generalGetString(MR.strings.member_inactive_desc)
is Sent -> null
is Rcvd -> null
is Error -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo
is Warning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo
is Invalid -> "Invalid status" to this.text
}
}
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()
@@ -3512,7 +3466,7 @@ data class ChatItemVersion(
@Serializable
data class MemberDeliveryStatus(
val groupMemberId: Long,
val memberDeliveryStatus: GroupSndStatus,
val memberDeliveryStatus: CIStatus,
val sentViaProxy: Boolean?
)
@@ -127,7 +127,6 @@ class AppPreferences {
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false)
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
@@ -198,8 +197,6 @@ class AppPreferences {
}, settingsThemes)
val themeOverrides = mkThemeOverridesPreference()
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -341,7 +338,6 @@ class AppPreferences {
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE = "ShowSubscriptionPercentage"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode"
@@ -382,8 +378,6 @@ class AppPreferences {
private const val SHARED_PREFS_THEMES_OLD = "Themes"
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
@@ -417,18 +411,6 @@ object ChatController {
fun hasChatCtrl() = ctrl != -1L && ctrl != null
suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? {
val userId = currentUserId("getAgentServersSummary")
val r = sendCmd(rh, CC.GetAgentServersSummary(userId), log = false)
if (r is CR.AgentServersSummary) return r.serversSummary
Log.e(TAG, "getAgentServersSummary bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats())
private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock {
val userId = chatModel.currentUser.value?.userId
if (userId == null) {
@@ -587,24 +569,20 @@ object ChatController {
}
}
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR {
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR {
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
return withContext(Dispatchers.IO) {
val c = cmd.cmdString
if (log) {
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
}
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
val json = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c)
val r = APIResponse.decodeStr(json)
if (log) {
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
if (r.resp is CR.Response || r.resp is CR.Invalid) {
Log.d(TAG, "sendCmd response json $json")
}
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
if (r.resp is CR.Response || r.resp is CR.Invalid) {
Log.d(TAG, "sendCmd response json $json")
}
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
r.resp
}
}
@@ -942,14 +920,6 @@ object ChatController {
}
}
suspend fun reconnectServer(rh: Long?, server: String): Boolean {
val userId = currentUserId("reconnectServer")
return sendCommandOkResp(rh, CC.ReconnectServer(userId, server))
}
suspend fun reconnectAllServers(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ReconnectAllServers())
suspend fun apiSetSettings(rh: Long?, type: ChatType, id: Long, settings: ChatSettings): Boolean {
val r = sendCmd(rh, CC.APISetChatSettings(type, id, settings))
return when (r) {
@@ -2607,8 +2577,6 @@ sealed class CC {
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
class APIGetNetworkConfig: CC()
class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC()
class ReconnectServer(val userId: Long, val server: String): CC()
class ReconnectAllServers: CC()
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC()
class APIContactInfo(val contactId: Long): CC()
@@ -2680,8 +2648,6 @@ sealed class CC {
class ApiStandaloneFileInfo(val url: String): CC()
// misc
class ShowVersion(): CC()
class ResetAgentServersStats(): CC()
class GetAgentServersSummary(val userId: Long): CC()
val cmdString: String get() = when (this) {
is Console -> cmd
@@ -2758,8 +2724,6 @@ sealed class CC {
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
is APIGetNetworkConfig -> "/network"
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
is ReconnectServer -> "/reconnect $userId $server"
is ReconnectAllServers -> "/reconnect"
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
is APIContactInfo -> "/_info @$contactId"
@@ -2840,8 +2804,6 @@ sealed class CC {
is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}"
is ApiStandaloneFileInfo -> "/_download info $url"
is ShowVersion -> "/version"
is ResetAgentServersStats -> "/reset servers stats"
is GetAgentServersSummary -> "/get servers summary $userId"
}
val cmdType: String get() = when (this) {
@@ -2902,8 +2864,6 @@ sealed class CC {
is APISetNetworkConfig -> "apiSetNetworkConfig"
is APIGetNetworkConfig -> "apiGetNetworkConfig"
is APISetNetworkInfo -> "apiSetNetworkInfo"
is ReconnectServer -> "reconnectServer"
is ReconnectAllServers -> "reconnectAllServers"
is APISetChatSettings -> "apiSetChatSettings"
is ApiSetMemberSettings -> "apiSetMemberSettings"
is APIContactInfo -> "apiContactInfo"
@@ -2973,8 +2933,6 @@ sealed class CC {
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
is ShowVersion -> "showVersion"
is ResetAgentServersStats -> "resetAgentServersStats"
is GetAgentServersSummary -> "getAgentServersSummary"
}
class ItemRange(val from: Long, val to: Long)
@@ -3245,7 +3203,7 @@ data class NetCfg(
val tcpKeepAlive: KeepAliveOpts?,
val smpPingInterval: Long, // microseconds
val smpPingCount: Int,
val logTLSErrors: Boolean = false,
val logTLSErrors: Boolean = false
) {
val useSocksProxy: Boolean get() = socksProxy != null
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
@@ -3470,154 +3428,6 @@ data class TimedMessagesPreference(
}
}
@Serializable
data class PresentedServersSummary(
val statsStartedAt: Instant,
val allUsersSMP: SMPServersSummary,
val allUsersXFTP: XFTPServersSummary,
val currentUserSMP: SMPServersSummary,
val currentUserXFTP: XFTPServersSummary
)
@Serializable
data class SMPServersSummary(
val smpTotals: SMPTotals,
val currentlyUsedSMPServers: List<SMPServerSummary>,
val previouslyUsedSMPServers: List<SMPServerSummary>,
val onlyProxiedSMPServers: List<SMPServerSummary>
)
@Serializable
data class SMPTotals(
val sessions: ServerSessions,
val subs: SMPServerSubs,
val stats: AgentSMPServerStatsData
)
@Serializable
data class SMPServerSummary(
val smpServer: String,
val known: Boolean? = null,
val sessions: ServerSessions? = null,
val subs: SMPServerSubs? = null,
val stats: AgentSMPServerStatsData? = null
) {
val hasSubs: Boolean
get() = subs != null
val sessionsOrNew: ServerSessions
get() = sessions ?: ServerSessions.newServerSessions
val subsOrNew: SMPServerSubs
get() = subs ?: SMPServerSubs.newSMPServerSubs
}
@Serializable
data class ServerSessions(
val ssConnected: Int,
val ssErrors: Int,
val ssConnecting: Int
) {
companion object {
val newServerSessions = ServerSessions(
ssConnected = 0,
ssErrors = 0,
ssConnecting = 0
)
}
}
@Serializable
data class SMPServerSubs(
val ssActive: Int,
val ssPending: Int
) {
companion object {
val newSMPServerSubs = SMPServerSubs(
ssActive = 0,
ssPending = 0
)
}
val total: Int
get() = ssActive + ssPending
val shareOfActive: Float
get() = if (total != 0) ssActive.toFloat() / total else 0f
}
@Serializable
data class AgentSMPServerStatsData(
val _sentDirect: Int,
val _sentViaProxy: Int,
val _sentProxied: Int,
val _sentDirectAttempts: Int,
val _sentViaProxyAttempts: Int,
val _sentProxiedAttempts: Int,
val _sentAuthErrs: Int,
val _sentQuotaErrs: Int,
val _sentExpiredErrs: Int,
val _sentOtherErrs: Int,
val _recvMsgs: Int,
val _recvDuplicates: Int,
val _recvCryptoErrs: Int,
val _recvErrs: Int,
val _ackMsgs: Int,
val _ackAttempts: Int,
val _ackNoMsgErrs: Int,
val _ackOtherErrs: Int,
val _connCreated: Int,
val _connSecured: Int,
val _connCompleted: Int,
val _connDeleted: Int,
val _connDelAttempts: Int,
val _connDelErrs: Int,
val _connSubscribed: Int,
val _connSubAttempts: Int,
val _connSubIgnored: Int,
val _connSubErrs: Int
)
@Serializable
data class XFTPServersSummary(
val xftpTotals: XFTPTotals,
val currentlyUsedXFTPServers: List<XFTPServerSummary>,
val previouslyUsedXFTPServers: List<XFTPServerSummary>
)
@Serializable
data class XFTPTotals(
val sessions: ServerSessions,
val stats: AgentXFTPServerStatsData
)
@Serializable
data class XFTPServerSummary(
val xftpServer: String,
val known: Boolean? = null,
val sessions: ServerSessions? = null,
val stats: AgentXFTPServerStatsData? = null,
val rcvInProgress: Boolean,
val sndInProgress: Boolean,
val delInProgress: Boolean
) {}
@Serializable
data class AgentXFTPServerStatsData(
val _uploads: Int,
val _uploadsSize: Long,
val _uploadAttempts: Int,
val _uploadErrs: Int,
val _downloads: Int,
val _downloadsSize: Long,
val _downloadAttempts: Int,
val _downloadAuthErrs: Int,
val _downloadErrs: Int,
val _deletions: Int,
val _deleteAttempts: Int,
val _deleteErrs: Int
)
sealed class CustomTimeUnit {
object Second: CustomTimeUnit()
object Minute: CustomTimeUnit()
@@ -4629,7 +4439,6 @@ sealed class CR {
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
@Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR()
// general
@Serializable class Response(val type: String, val json: String): CR()
@Serializable class Invalid(val str: String): CR()
@@ -4789,7 +4598,6 @@ sealed class CR {
is ContactPQAllowed -> "contactPQAllowed"
is ContactPQEnabled -> "contactPQEnabled"
is VersionInfo -> "versionInfo"
is AgentServersSummary -> "agentServersSummary"
is CmdOk -> "cmdOk"
is ChatCmdError -> "chatCmdError"
is ChatRespError -> "chatError"
@@ -4968,7 +4776,6 @@ sealed class CR {
is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason"
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary))
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
@@ -6,8 +6,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatController.appPrefs
@@ -779,7 +777,6 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
typography = Typography,
shapes = Shapes,
content = {
val density = Density(LocalDensity.current.density * desktopDensityScaleMultiplier, LocalDensity.current.fontScale * fontSizeMultiplier)
val rememberedAppColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [appColors]
// provided, and overwrite the values set in it.
@@ -794,7 +791,6 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
LocalContentColor provides MaterialTheme.colors.onBackground,
LocalAppColors provides rememberedAppColors,
LocalAppWallpaper provides rememberedWallpaper,
LocalDensity provides density,
content = content)
}
)
@@ -330,7 +330,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
@Composable
fun MemberDeliveryStatusView(member: GroupMember, status: GroupSndStatus, sentViaProxy: Boolean?) {
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) {
SectionItemView(
padding = PaddingValues(horizontal = 0.dp)
) {
@@ -355,7 +355,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
)
}
}
val (icon, statusColor) = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp))
val info = status.statusInto
if (info != null) {
@@ -367,11 +367,20 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
Box(modifier, contentAlignment = Alignment.Center) {
Icon(
painterResource(icon),
contentDescription = null,
tint = statusColor
)
if (statusIcon != null) {
val (icon, statusColor) = statusIcon
Icon(
painterResource(icon),
contentDescription = null,
tint = statusColor
)
} else {
Icon(
painterResource(MR.images.ic_more_horiz),
contentDescription = null,
tint = CurrentColors.value.colors.secondary
)
}
}
}
}
@@ -511,7 +520,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, GroupSndStatus, Boolean?>> {
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, CIStatus, Boolean?>> {
return memberDeliveryStatuses.mapNotNull { mds ->
chatModel.getGroupMember(mds.groupMemberId)?.let { mem ->
Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy)
@@ -821,9 +821,9 @@ fun ChatInfoToolbar(
buttons = barButtons
)
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
Divider(Modifier.padding(top = AppBarHeight))
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) {
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
DefaultDropdownMenu(showMenu) {
menuItems.forEach { it() }
}
@@ -837,9 +837,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
verticalAlignment = Alignment.CenterVertically
) {
if (cInfo.incognito) {
IncognitoImage(size = 36.dp * fontSizeSqrtMultiplier, Indigo)
IncognitoImage(size = 36.dp, Indigo)
}
ChatInfoImage(cInfo, size = imageSize * fontSizeSqrtMultiplier, iconColor)
ChatInfoImage(cInfo, size = imageSize, iconColor)
Column(
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -865,7 +865,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
@Composable
private fun ContactVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
}
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
@@ -1283,7 +1283,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
@Composable
fun MemberImage(member: GroupMember) {
ProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
}
@Composable
@@ -13,12 +13,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.filesToDelete
@@ -886,7 +884,7 @@ fun ComposeView(
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }),
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
enabled = attachmentEnabled
) {
Icon(
@@ -15,12 +15,10 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
@@ -101,7 +99,7 @@ fun SendMsgView(
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 5.sp.toDp() } * fontSizeSqrtMultiplier)) {
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
@@ -390,16 +390,6 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
}
}
fun memberConnStatus(): String {
return if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_disabled)
} else if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_inactive)
} else {
member.memberStatus.shortText
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -422,8 +412,8 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
}
val statusDescr =
if (user) String.format(generalGetString(MR.strings.group_info_member_you), member.memberStatus.shortText) else memberConnStatus()
val s = member.memberStatus.shortText
val statusDescr = if (user) String.format(generalGetString(MR.strings.group_info_member_you), s) else s
Text(
statusDescr,
color = MaterialTheme.colors.secondary,
@@ -358,6 +358,13 @@ fun GroupMemberInfoLayout(
} else {
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
}
val conn = member.activeConn
if (conn != null) {
val connLevelDesc =
if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct)
else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc)
}
}
if (cStats != null) {
SectionDividerSpaced()
@@ -394,13 +401,6 @@ fun GroupMemberInfoLayout(
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString())
val conn = member.activeConn
if (conn != null) {
val connLevelDesc =
if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct)
else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc)
}
SectionItemView({
withBGApi {
val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId)
@@ -35,10 +35,7 @@ import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.json.Json
import java.net.URI
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Composable
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
@@ -72,7 +69,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val scope = rememberCoroutineScope()
val (userPickerState, scaffoldState ) = settingsState
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} },
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} },
scaffoldState = scaffoldState,
drawerContent = {
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
@@ -91,7 +88,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
}
},
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp).size(AppBarHeight * fontSizeSqrtMultiplier),
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
@@ -101,7 +98,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
contentColor = Color.White
) {
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier))
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group))
}
}
}
@@ -184,10 +181,8 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
}
@Composable
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
if (stopped) {
barButtons.add {
IconButton(onClick = {
@@ -205,7 +200,6 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
}
}
val scope = rememberCoroutineScope()
val clipboard = LocalClipboardManager.current
DefaultTopAppBar(
navigationButton = {
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
@@ -225,33 +219,20 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
}
},
title = {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
stringResource(MR.strings.your_chats),
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold,
)
SubscriptionStatusIndicator(
serversSummary = serversSummary,
click = {
ModalManager.start.closeModals()
ModalManager.start.showModalCloseable(
endButtons = {
val summary = serversSummary.value
if (summary != null) {
ShareButton {
val json = Json {
prettyPrint = true
}
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
clipboard.shareText(text)
}
}
}
) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) }
if (chatModel.chats.size > 0) {
val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } }
if (enabled.value) {
ToggleFilterEnabledButton()
} else {
ToggleFilterDisabledButton()
}
)
}
}
},
onTitleClick = null,
@@ -259,54 +240,7 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
onSearchValueChanged = {},
buttons = barButtons
)
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
}
@Composable
fun SubscriptionStatusIndicator(serversSummary: MutableState<PresentedServersSummary?>, click: (() -> Unit)) {
var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) }
var sess by remember { mutableStateOf(ServerSessions.newServerSessions) }
var timer: Job? by remember { mutableStateOf(null) }
val fetchInterval: Duration = 1.seconds
val scope = rememberCoroutineScope()
fun setServersSummary() {
withBGApi {
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
serversSummary.value?.let {
subs = it.allUsersSMP.smpTotals.subs
sess = it.allUsersSMP.smpTotals.sessions
}
}
}
LaunchedEffect(Unit) {
setServersSummary()
timer = timer ?: scope.launch {
while (true) {
delay(fetchInterval.inWholeMilliseconds)
setServersSummary()
}
}
}
fun stopTimer() {
timer?.cancel()
timer = null
}
DisposableEffect(Unit) {
onDispose {
stopTimer()
}
}
SimpleButtonFrame(click = click) {
SubscriptionStatusIndicatorView(subs = subs, sess = sess)
}
Divider(Modifier.padding(top = AppBarHeight))
}
@Composable
@@ -316,7 +250,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
Box {
ProfileImage(
image = image,
size = 37.dp * fontSizeSqrtMultiplier,
size = 37.dp,
color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)
)
if (!allRead) {
@@ -355,17 +289,32 @@ private fun BoxScope.unreadBadge(text: String? = "") {
private fun ToggleFilterEnabledButton() {
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
IconButton(onClick = { pref.set(!pref.get()) }) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
Icon(
painterResource(MR.images.ic_filter_list),
null,
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary,
modifier = Modifier
.padding(3.dp)
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
.padding(3.dp)
.size(sp16)
.size(16.dp)
)
}
}
@Composable
private fun ToggleFilterDisabledButton() {
IconButton({}, enabled = false) {
Icon(
painterResource(MR.images.ic_filter_list),
null,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.padding(3.dp)
.border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50))
.padding(3.dp)
.size(16.dp)
)
}
}
@@ -389,7 +338,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
Row(verticalAlignment = Alignment.CenterVertically) {
val focusRequester = remember { FocusRequester() }
var focused by remember { mutableStateOf(false) }
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary)
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
SearchTextField(
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
@@ -408,11 +357,33 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
hideSearchOnBack()
}
} else {
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
if (chatModel.chats.size > 0) {
ToggleFilterEnabledButton()
Row {
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
val clipboard = LocalClipboardManager.current
val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value
if (clipboardHasText) {
IconButton(
onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) },
Modifier.size(30.dp).desktopPointerHoverIconHand()
) {
Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary)
}
}
Spacer(Modifier.width(padding))
IconButton(
onClick = {
val fixedRhId = chatModel.currentRemoteHost.value
ModalManager.center.closeModals()
ModalManager.center.showModalCloseable { close ->
NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close)
}
},
Modifier.size(30.dp).desktopPointerHoverIconHand()
) {
Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary)
}
Spacer(Modifier.width(padding))
}
Spacer(Modifier.width(padding))
}
val focusManager = LocalFocusManager.current
val keyboardState = getKeyboardState()
@@ -47,11 +47,10 @@ fun ChatPreviewView(
@Composable
fun inactiveIcon() {
val sp18 = with(LocalDensity.current) { 18.sp.toDp() }
Icon(
painterResource(MR.images.ic_cancel_filled),
stringResource(MR.strings.icon_descr_group_inactive),
Modifier.size(sp18).background(MaterialTheme.colors.background, CircleShape),
Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape),
tint = MaterialTheme.colors.secondary
)
}
@@ -88,11 +87,10 @@ fun ChatPreviewView(
@Composable
fun VerifiedIcon() {
val sp19 = with(LocalDensity.current) { 19.sp.toDp() }
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(sp19).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
}
fun messageDraft(draft: ComposeState, sp20: Dp): Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>> {
fun messageDraft(draft: ComposeState): Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>> {
fun attachment(): Pair<ImageResource, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName
@@ -117,12 +115,12 @@ fun ChatPreviewView(
"editIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.primary)
Icon(painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.primary)
},
"attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.secondary)
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.secondary)
}
)
return inlineContentBuilder to inlineContent
@@ -169,9 +167,8 @@ fun ChatPreviewView(
val ci = chat.chatItems.lastOrNull()
if (ci != null) {
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft, sp20) }
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> markedDeletedText(ci.meta) to null
}
@@ -223,11 +220,10 @@ fun ChatPreviewView(
@Composable
fun progressView() {
val sp15 = with(LocalDensity.current) { 15.sp.toDp() }
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(sp15),
.size(15.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 1.5.dp
)
@@ -235,7 +231,6 @@ fun ChatPreviewView(
@Composable
fun chatStatusImage() {
val sp19 = with(LocalDensity.current) { 19.sp.toDp() }
if (cInfo is ChatInfo.Direct) {
if (cInfo.contact.active && cInfo.contact.activeConn != null) {
val descr = contactNetworkStatus?.statusString
@@ -249,7 +244,7 @@ fun ChatPreviewView(
contentDescription = descr,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(sp19)
.size(19.dp)
)
else ->
@@ -271,7 +266,7 @@ fun ChatPreviewView(
Row {
Box(contentAlignment = Alignment.BottomEnd) {
ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier)
ChatInfoImage(cInfo, size = 72.dp)
Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) {
chatPreviewImageOverlayIcon()
}
@@ -300,13 +295,9 @@ fun ChatPreviewView(
)
val n = chat.chatStats.unreadCount
val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group)
val sp17 = with(LocalDensity.current) { 17.sp.toDp() }
val sp21 = with(LocalDensity.current) { 21.sp.toDp() }
val sp23 = with(LocalDensity.current) { 23.sp.toDp() }
val sp46 = with(LocalDensity.current) { 46.sp.toDp() }
if (n > 0 || chat.chatStats.unreadChat) {
Box(
Modifier.padding(top = sp23, end = with(LocalDensity.current) { 3.sp.toDp() }),
Modifier.padding(top = 24.dp),
contentAlignment = Alignment.Center
) {
Text(
@@ -322,7 +313,7 @@ fun ChatPreviewView(
}
} else if (showNtfsIcon) {
Box(
Modifier.padding(top = sp21),
Modifier.padding(top = 24.dp),
contentAlignment = Alignment.Center
) {
Icon(
@@ -332,12 +323,12 @@ fun ChatPreviewView(
modifier = Modifier
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.size(sp17)
.size(17.dp)
)
}
} else if (chat.chatInfo.chatSettings?.favorite == true) {
Box(
Modifier.padding(top = sp21),
Modifier.padding(top = 24.dp),
contentAlignment = Alignment.Center
) {
Icon(
@@ -347,12 +338,12 @@ fun ChatPreviewView(
modifier = Modifier
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.size(sp17)
.size(17.dp)
)
}
}
Box(
Modifier.padding(top = sp46),
Modifier.padding(top = 50.dp),
contentAlignment = Alignment.Center
) {
chatStatusImage()
@@ -364,13 +355,12 @@ fun ChatPreviewView(
@Composable
fun IncognitoIcon(incognito: Boolean) {
if (incognito) {
val sp21 = with(LocalDensity.current) { 21.sp.toDp() }
Icon(
painterResource(MR.images.ic_theater_comedy),
contentDescription = null,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(sp21)
.size(21.dp)
)
}
}
@@ -1,979 +0,0 @@
package chat.simplex.common.views.chatlist
import InfoRow
import InfoRowTwoValues
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.AgentSMPServerStatsData
import chat.simplex.common.model.AgentXFTPServerStatsData
import chat.simplex.common.model.ChatController.chatModel
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.PresentedServersSummary
import chat.simplex.common.model.RemoteHostInfo
import chat.simplex.common.model.SMPServerSubs
import chat.simplex.common.model.SMPServerSummary
import chat.simplex.common.model.SMPTotals
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.common.model.ServerProtocol
import chat.simplex.common.model.ServerSessions
import chat.simplex.common.model.XFTPServerSummary
import chat.simplex.common.model.localTimestamp
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.ProtocolServersView
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import numOrDash
import java.text.DecimalFormat
import kotlin.math.floor
import kotlin.math.roundToInt
enum class SubscriptionColorType {
ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED
}
data class SubscriptionStatus(
val color: SubscriptionColorType,
val variableValue: Float,
val opacity: Float,
val statusPercent: Float
)
fun subscriptionStatusColorAndPercentage(
online: Boolean,
socksProxy: String?,
subs: SMPServerSubs,
sess: ServerSessions
): SubscriptionStatus {
fun roundedToQuarter(n: Float): Float = when {
n >= 1 -> 1f
n <= 0 -> 0f
else -> (n * 4).roundToInt() / 4f
}
val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE
val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 1f, 0f)
val activeSubsRounded = roundedToQuarter(subs.shareOfActive)
return if (online && subs.total > 0) {
if (subs.ssActive == 0) {
if (sess.ssConnected == 0)
noConnColorAndPercent
else
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
} else { // ssActive > 0
if (sess.ssConnected == 0)
// This would mean implementation error
SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
else
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
}
} else noConnColorAndPercent
}
@Composable
private fun SubscriptionStatusIndicatorPercentage(percentageText: String) {
Text(
percentageText,
color = MaterialTheme.colors.secondary,
fontSize = 12.sp,
style = MaterialTheme.typography.caption
)
}
@Composable
fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, sess: ServerSessions, leadingPercentage: Boolean = false) {
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, sess)
val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage }
val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%"
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)
) {
if (pref.state.value && leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText)
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
SubscriptionStatusIcon(
color = when(statusColorAndPercentage.color) {
SubscriptionColorType.ACTIVE -> MaterialTheme.colors.primary
SubscriptionColorType.ACTIVE_SOCKS_PROXY -> Indigo
SubscriptionColorType.ACTIVE_DISCONNECTED -> WarningOrange
SubscriptionColorType.DISCONNECTED -> MaterialTheme.colors.secondary
},
modifier = Modifier.size(sp16),
variableValue = statusColorAndPercentage.variableValue)
if (pref.state.value && !leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText)
}
}
enum class PresentedUserCategory {
CURRENT_USER, ALL_USERS
}
enum class PresentedServerType {
SMP, XFTP
}
@Composable
private fun ServerSessionsView(sess: ServerSessions) {
SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header).uppercase()) {
InfoRow(
generalGetString(MR.strings.servers_info_sessions_connected),
numOrDash(sess.ssConnected)
)
InfoRow(
generalGetString(MR.strings.servers_info_sessions_errors),
numOrDash(sess.ssErrors)
)
InfoRow(
generalGetString(MR.strings.servers_info_sessions_connecting),
numOrDash(sess.ssConnecting)
)
}
}
private fun serverAddress(server: String): String {
val address = parseServerAddress(server)
return address?.hostnames?.first() ?: server
}
@Composable
private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
SectionItemView(
click = {
ModalManager.start.showCustomModal { close ->
SMPServerSummaryView(
rh = rh,
close = close,
summary = srvSumm,
statsStartedAt = statsStartedAt
)
}
}
) {
Text(
serverAddress(srvSumm.smpServer),
modifier = Modifier.weight(10f, fill = true)
)
if (srvSumm.subs != null) {
Spacer(Modifier.fillMaxWidth().weight(1f))
SubscriptionStatusIndicatorView(subs = srvSumm.subs, sess = srvSumm.sessionsOrNew, leadingPercentage = true)
} else if (srvSumm.sessions != null) {
Spacer(Modifier.fillMaxWidth().weight(1f))
Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions))
}
}
}
@Composable
private fun SessIconColor(sess: ServerSessions): Color {
val online = chatModel.networkInfo.value.online
return if (online && sess.ssConnected > 0) SessionActiveColor() else MaterialTheme.colors.secondary
}
@Composable
private fun SessionActiveColor(): Color {
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
return if (netCfg.value.socksProxy != null) Indigo else MaterialTheme.colors.primary
}
@Composable
private fun SMPServersListView(servers: List<SMPServerSummary>, statsStartedAt: Instant, header: String? = null, footer: String? = null, rh: RemoteHostInfo?) {
val sortedServers = servers.sortedWith(compareBy<SMPServerSummary> { !it.hasSubs }
.thenBy { serverAddress(it.smpServer) })
SectionView(header) {
sortedServers.map { svr -> SMPServerView(srvSumm = svr, statsStartedAt = statsStartedAt, rh = rh) }
}
if (footer != null) {
SectionTextFooter(
footer
)
}
}
fun prettySize(sizeInKB: Long): String {
if (sizeInKB == 0L) {
return "-"
}
val sizeInBytes = sizeInKB * 1024
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
var size = sizeInBytes.toDouble()
var unitIndex = 0
while (size >= 1024 && unitIndex < units.size - 1) {
size /= 1024
unitIndex++
}
val formatter = DecimalFormat("#,##0.#")
return "${formatter.format(size)} ${units[unitIndex]}"
}
@Composable
private fun XFTPServerView(srvSumm: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
SectionItemView(
click = {
ModalManager.start.showCustomModal { close ->
XFTPServerSummaryView(
rh = rh,
close = close,
summary = srvSumm,
statsStartedAt = statsStartedAt
)
}
}
) {
Text(
serverAddress(srvSumm.xftpServer),
modifier = Modifier.weight(10f, fill = true)
)
if (srvSumm.rcvInProgress || srvSumm.sndInProgress || srvSumm.delInProgress) {
Spacer(Modifier.fillMaxWidth().weight(1f))
XFTPServerInProgressIcon(srvSumm)
}
}
}
@Composable
private fun XFTPServerInProgressIcon(srvSumm: XFTPServerSummary) {
return when {
srvSumm.rcvInProgress && !srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_downward),"download", tint = SessionActiveColor())
!srvSumm.rcvInProgress && srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_upward), "upload", tint = SessionActiveColor())
!srvSumm.rcvInProgress && !srvSumm.sndInProgress && srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_delete), "deletion", tint = SessionActiveColor())
else -> Icon(painterResource(MR.images.ic_expand_all), "upload and download", tint = SessionActiveColor())
}
}
@Composable
private fun XFTPServersListView(servers: List<XFTPServerSummary>, statsStartedAt: Instant, header: String? = null, rh: RemoteHostInfo?) {
val sortedServers = servers.sortedBy { serverAddress(it.xftpServer) }
SectionView(header) {
sortedServers.map { svr -> XFTPServerView(svr, statsStartedAt, rh) }
}
}
@Composable
private fun SMPStatsView(stats: AgentSMPServerStatsData, statsStartedAt: Instant, remoteHostInfo: RemoteHostInfo?) {
SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) {
InfoRow(
generalGetString(MR.strings.servers_info_messages_sent),
numOrDash(stats._sentDirect + stats._sentViaProxy)
)
InfoRow(
generalGetString(MR.strings.servers_info_messages_received),
numOrDash(stats._recvMsgs)
)
SectionItemView(
click = {
ModalManager.start.showCustomModal { close -> DetailedSMPStatsView(
rh = remoteHostInfo,
close = close,
stats = stats,
statsStartedAt = statsStartedAt)
}
}
) {
Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground)
}
}
SectionTextFooter(
String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt))
)
}
@Composable
private fun SMPSubscriptionsSection(totals: SMPTotals) {
Column {
Row(
Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2)
) {
Text(
generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(),
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.body2,
fontSize = 12.sp
)
SubscriptionStatusIndicatorView(totals.subs, totals.sessions)
}
Column(Modifier.padding(PaddingValues()).fillMaxWidth()) {
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed),
numOrDash(totals.subs.ssActive)
)
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_total),
numOrDash(totals.subs.total)
)
}
}
}
@Composable
private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSummary, rh: RemoteHostInfo?) {
Column {
Row(
Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2)
) {
Text(
generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(),
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.body2,
fontSize = 12.sp
)
SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew)
}
Column(Modifier.padding(PaddingValues()).fillMaxWidth()) {
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed),
numOrDash(subs.ssActive)
)
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_connections_pending),
numOrDash(subs.ssPending)
)
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_total),
numOrDash(subs.total)
)
ReconnectServerButton(rh, summary.smpServer)
}
}
}
@Composable
private fun ReconnectServerButton(rh: RemoteHostInfo?, server: String) {
SectionItemView(click = { reconnectServerAlert(rh, server) }) {
Text(
stringResource(MR.strings.reconnect),
color = MaterialTheme.colors.primary
)
}
}
private fun reconnectServerAlert(rh: RemoteHostInfo?, server: String) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.servers_info_reconnect_server_title),
text = generalGetString(MR.strings.servers_info_reconnect_server_message),
onConfirm = {
withBGApi {
val success = controller.reconnectServer(rh?.remoteHostId, server)
if (!success) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.servers_info_modal_error_title),
text = generalGetString(MR.strings.servers_info_reconnect_server_error)
)
}
}
}
)
}
@Composable
fun XFTPStatsView(stats: AgentXFTPServerStatsData, statsStartedAt: Instant, rh: RemoteHostInfo?) {
SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) {
InfoRow(
generalGetString(MR.strings.servers_info_uploaded),
prettySize(stats._uploadsSize)
)
InfoRow(
generalGetString(MR.strings.servers_info_downloaded),
prettySize(stats._downloadsSize)
)
SectionItemView (
click = {
ModalManager.start.showCustomModal { close -> DetailedXFTPStatsView(
rh = rh,
close = close,
stats = stats,
statsStartedAt = statsStartedAt)
}
}
) {
Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground)
}
}
SectionTextFooter(
String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt))
)
}
@Composable
private fun IndentedInfoRow(title: String, desc: String) {
InfoRow(title, desc, padding = PaddingValues(start = 24.dp + DEFAULT_PADDING, end = DEFAULT_PADDING))
}
@Composable
fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Instant) {
SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header).uppercase()) {
InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_total), numOrDash(stats._sentDirect + stats._sentViaProxy))
InfoRowTwoValues(generalGetString(MR.strings.sent_directly), generalGetString(MR.strings.attempts_label), stats._sentDirect, stats._sentDirectAttempts)
InfoRowTwoValues(generalGetString(MR.strings.sent_via_proxy), generalGetString(MR.strings.attempts_label), stats._sentViaProxy, stats._sentViaProxyAttempts)
InfoRowTwoValues(generalGetString(MR.strings.proxied), generalGetString(MR.strings.attempts_label), stats._sentProxied, stats._sentProxiedAttempts)
SectionItemView {
Text(generalGetString(MR.strings.send_errors), color = MaterialTheme.colors.onBackground)
}
IndentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
IndentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
IndentedInfoRow(generalGetString(MR.strings.expired_label), numOrDash(stats._sentExpiredErrs))
IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._sentOtherErrs))
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header).uppercase()) {
InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_received_total), numOrDash(stats._recvMsgs))
SectionItemView {
Text(generalGetString(MR.strings.servers_info_detailed_statistics_receive_errors), color = MaterialTheme.colors.onBackground)
}
IndentedInfoRow(generalGetString(MR.strings.duplicates_label), numOrDash(stats._recvDuplicates))
IndentedInfoRow(generalGetString(MR.strings.decryption_errors), numOrDash(stats._recvCryptoErrs))
IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._recvErrs))
InfoRowTwoValues(generalGetString(MR.strings.acknowledged), generalGetString(MR.strings.attempts_label), stats._ackMsgs, stats._ackAttempts)
SectionItemView {
Text(generalGetString(MR.strings.acknowledgement_errors), color = MaterialTheme.colors.onBackground)
}
IndentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._ackOtherErrs))
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.connections).uppercase()) {
InfoRow(generalGetString(MR.strings.created), numOrDash(stats._connCreated))
InfoRow(generalGetString(MR.strings.secured), numOrDash(stats._connSecured))
InfoRow(generalGetString(MR.strings.completed), numOrDash(stats._connCompleted))
InfoRowTwoValues(generalGetString(MR.strings.deleted), generalGetString(MR.strings.attempts_label), stats._connDeleted, stats._connDelAttempts)
InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._connDelErrs))
InfoRowTwoValues(generalGetString(MR.strings.subscribed), generalGetString(MR.strings.attempts_label), stats._connSubscribed, stats._connSubAttempts)
InfoRow(generalGetString(MR.strings.subscription_results_ignored), numOrDash(stats._connSubIgnored))
InfoRow(generalGetString(MR.strings.subscription_errors), numOrDash(stats._connSubErrs))
}
SectionTextFooter(
String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt))
)
SectionBottomSpacer()
}
@Composable
fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Instant) {
SectionView(generalGetString(MR.strings.uploaded_files).uppercase()) {
InfoRow(generalGetString(MR.strings.size), prettySize(stats._uploadsSize))
InfoRowTwoValues(generalGetString(MR.strings.chunks_uploaded), generalGetString(MR.strings.attempts_label), stats._uploads, stats._uploadAttempts)
InfoRow(generalGetString(MR.strings.upload_errors), numOrDash(stats._uploadErrs))
InfoRowTwoValues(generalGetString(MR.strings.chunks_deleted), generalGetString(MR.strings.attempts_label), stats._deletions, stats._deleteAttempts)
InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._deleteErrs))
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.downloaded_files).uppercase()) {
InfoRow(generalGetString(MR.strings.size), prettySize(stats._downloadsSize))
InfoRowTwoValues(generalGetString(MR.strings.chunks_downloaded), generalGetString(MR.strings.attempts_label), stats._downloads, stats._downloadAttempts)
SectionItemView {
Text(generalGetString(MR.strings.download_errors), color = MaterialTheme.colors.onBackground)
}
IndentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._downloadErrs))
}
SectionTextFooter(
String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt))
)
SectionBottomSpacer()
}
@Composable
fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
SectionView(generalGetString(MR.strings.server_address).uppercase()) {
SelectionContainer {
Text(
summary.xftpServer,
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
style = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
)
}
if (summary.known == true) {
SectionItemView(click = {
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) }
}) {
Text(generalGetString(MR.strings.open_server_settings_button))
}
}
if (summary.stats != null) {
SectionDividerSpaced()
XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt)
}
if (summary.sessions != null) {
SectionDividerSpaced()
ServerSessionsView(summary.sessions)
}
}
SectionBottomSpacer()
}
@Composable
fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
SectionView(generalGetString(MR.strings.server_address).uppercase()) {
SelectionContainer {
Text(
summary.smpServer,
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
style = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
)
}
if (summary.known == true) {
SectionItemView(click = {
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) }
}) {
Text(generalGetString(MR.strings.open_server_settings_button))
}
}
if (summary.stats != null) {
SectionDividerSpaced()
SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt)
}
if (summary.subs != null) {
SectionDividerSpaced()
SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh)
}
if (summary.sessions != null) {
SectionDividerSpaced()
ServerSessionsView(summary.sessions)
}
}
SectionBottomSpacer()
}
@Composable
fun ModalData.SMPServerSummaryView(
rh: RemoteHostInfo?,
close: () -> Unit,
summary: SMPServerSummary,
statsStartedAt: Instant
) {
ModalView(
close = close
) {
ColumnWithScrollBar(
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.smp_server),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
SMPServerSummaryLayout(summary, statsStartedAt, rh)
}
}
}
@Composable
fun ModalData.DetailedXFTPStatsView(
rh: RemoteHostInfo?,
close: () -> Unit,
stats: AgentXFTPServerStatsData,
statsStartedAt: Instant
) {
ModalView(
close = close
) {
ColumnWithScrollBar(
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.servers_info_detailed_statistics),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
DetailedXFTPStatsLayout(stats, statsStartedAt)
}
}
}
@Composable
fun ModalData.DetailedSMPStatsView(
rh: RemoteHostInfo?,
close: () -> Unit,
stats: AgentSMPServerStatsData,
statsStartedAt: Instant
) {
ModalView(
close = close
) {
ColumnWithScrollBar(
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.servers_info_detailed_statistics),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
DetailedSMPStatsLayout(stats, statsStartedAt)
}
}
}
@Composable
fun ModalData.XFTPServerSummaryView(
rh: RemoteHostInfo?,
close: () -> Unit,
summary: XFTPServerSummary,
statsStartedAt: Instant
) {
ModalView(
close = close
) {
ColumnWithScrollBar(
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.xftp_server),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
XFTPServerSummaryLayout(summary, statsStartedAt, rh)
}
}
}
@Composable
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
Column(
Modifier.fillMaxSize(),
) {
var showUserSelection by remember { mutableStateOf(false) }
val selectedUserCategory =
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
val selectedServerType =
remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1
) {
selectedUserCategory.value = PresentedUserCategory.CURRENT_USER
} else {
showUserSelection = true
}
}
Column(
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.servers_info),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
if (serversSummary.value == null) {
Box(
modifier = Modifier
.fillMaxSize()
) {
Text(generalGetString(MR.strings.servers_info_missing), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
}
} else {
val userOptions by remember {
mutableStateOf(
listOf(
PresentedUserCategory.ALL_USERS to generalGetString(MR.strings.all_users),
PresentedUserCategory.CURRENT_USER to generalGetString(MR.strings.current_user),
)
)
}
val serverTypeTabTitles = PresentedServerType.entries.map {
when (it) {
PresentedServerType.SMP ->
stringResource(MR.strings.messages_section_title)
PresentedServerType.XFTP ->
stringResource(MR.strings.servers_info_files_tab)
}
}
val serverTypePagerState = rememberPagerState(
initialPage = selectedServerType.value.ordinal,
initialPageOffsetFraction = 0f
) { PresentedServerType.entries.size }
KeyChangeEffect(serverTypePagerState.currentPage) {
selectedServerType.value = PresentedServerType.values()[serverTypePagerState.currentPage]
}
TabRow(
selectedTabIndex = serverTypePagerState.currentPage,
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colors.primary,
) {
serverTypeTabTitles.forEachIndexed { index, it ->
Tab(
selected = serverTypePagerState.currentPage == index,
onClick = {
scope.launch {
serverTypePagerState.animateScrollToPage(index)
}
},
text = { Text(it, fontSize = 13.sp) },
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.secondary,
)
}
}
HorizontalPager(
state = serverTypePagerState,
Modifier.fillMaxSize(),
verticalAlignment = Alignment.Top,
userScrollEnabled = appPlatform.isAndroid
) { index ->
ColumnWithScrollBar(
Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top
) {
Spacer(Modifier.height(DEFAULT_PADDING))
if (showUserSelection) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.servers_info_target),
userOptions,
selectedUserCategory,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = {
selectedUserCategory.value = it
}
)
SectionDividerSpaced()
}
when (index) {
PresentedServerType.SMP.ordinal -> {
serversSummary.value?.let {
val smpSummary =
if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserSMP else it.allUsersSMP;
val totals = smpSummary.smpTotals
val currentlyUsedSMPServers = smpSummary.currentlyUsedSMPServers
val previouslyUsedSMPServers = smpSummary.previouslyUsedSMPServers
val proxySMPServers = smpSummary.onlyProxiedSMPServers
val statsStartedAt = it.statsStartedAt
SMPStatsView(totals.stats, statsStartedAt, rh)
SectionDividerSpaced()
SMPSubscriptionsSection(totals)
SectionDividerSpaced()
if (currentlyUsedSMPServers.isNotEmpty()) {
SMPServersListView(
servers = currentlyUsedSMPServers,
statsStartedAt = statsStartedAt,
header = generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(),
rh = rh
)
SectionDividerSpaced()
}
if (previouslyUsedSMPServers.isNotEmpty()) {
SMPServersListView(
servers = previouslyUsedSMPServers,
statsStartedAt = statsStartedAt,
header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(),
rh = rh
)
SectionDividerSpaced()
}
if (proxySMPServers.isNotEmpty()) {
SMPServersListView(
servers = proxySMPServers,
statsStartedAt = statsStartedAt,
header = generalGetString(MR.strings.servers_info_proxied_servers_section_header).uppercase(),
footer = generalGetString(MR.strings.servers_info_proxied_servers_section_footer),
rh = rh
)
SectionDividerSpaced()
}
ServerSessionsView(totals.sessions)
}
}
PresentedServerType.XFTP.ordinal -> {
serversSummary.value?.let {
val xftpSummary =
if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserXFTP else it.allUsersXFTP
val totals = xftpSummary.xftpTotals
val statsStartedAt = it.statsStartedAt
val currentlyUsedXFTPServers = xftpSummary.currentlyUsedXFTPServers
val previouslyUsedXFTPServers = xftpSummary.previouslyUsedXFTPServers
XFTPStatsView(totals.stats, statsStartedAt, rh)
SectionDividerSpaced()
if (currentlyUsedXFTPServers.isNotEmpty()) {
XFTPServersListView(
currentlyUsedXFTPServers,
statsStartedAt,
generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(),
rh
)
SectionDividerSpaced()
}
if (previouslyUsedXFTPServers.isNotEmpty()) {
XFTPServersListView(
previouslyUsedXFTPServers,
statsStartedAt,
generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(),
rh
)
SectionDividerSpaced()
}
ServerSessionsView(totals.sessions)
}
}
}
SectionDividerSpaced()
SectionView {
ReconnectAllServersButton(rh)
ResetStatisticsButton(rh)
}
SectionBottomSpacer()
}
}
}
}
}
}
@Composable
private fun ReconnectAllServersButton(rh: RemoteHostInfo?) {
SectionItemView(click = { reconnectAllServersAlert(rh) }) {
Text(
stringResource(MR.strings.servers_info_reconnect_all_servers_button),
color = MaterialTheme.colors.primary
)
}
}
private fun reconnectAllServersAlert(rh: RemoteHostInfo?) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.servers_info_reconnect_servers_title),
text = generalGetString(MR.strings.servers_info_reconnect_servers_message),
onConfirm = {
withBGApi {
val success = controller.reconnectAllServers(rh?.remoteHostId)
if (!success) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.servers_info_modal_error_title),
text = generalGetString(MR.strings.servers_info_reconnect_servers_error)
)
}
}
}
)
}
@Composable
private fun ResetStatisticsButton(rh: RemoteHostInfo?) {
SectionItemView(click = { resetStatisticsAlert(rh) }) {
Text(
stringResource(MR.strings.servers_info_reset_stats),
color = MaterialTheme.colors.primary
)
}
}
private fun resetStatisticsAlert(rh: RemoteHostInfo?) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.servers_info_reset_stats_alert_title),
text = generalGetString(MR.strings.servers_info_reset_stats_alert_message),
confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm),
destructive = true,
onConfirm = {
withBGApi {
val success = controller.resetAgentServersStats(rh?.remoteHostId)
if (!success) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.servers_info_modal_error_title),
text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title)
)
}
}
}
)
}
@@ -309,7 +309,7 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr
) {
ProfileImage(
image = u.image,
size = 54.dp * fontSizeSqrtMultiplier
size = 54.dp
)
Text(
u.displayName,
@@ -354,7 +354,7 @@ fun RemoteHostRow(h: RemoteHostInfo) {
.padding(start = 17.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Text(
h.hostDeviceName,
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
@@ -395,7 +395,7 @@ fun LocalDeviceRow(active: Boolean) {
.padding(start = 17.dp, end = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically
) {
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Text(
stringResource(MR.strings.this_device),
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
@@ -409,7 +409,7 @@ fun LocalDeviceRow(active: Boolean) {
private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
@@ -419,7 +419,7 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
private fun LinkAMobilePickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.link_a_mobile)
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
@@ -429,7 +429,7 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) {
private fun CreateInitialProfile(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.create_chat_profile)
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
@@ -439,7 +439,7 @@ private fun CreateInitialProfile(onClick: () -> Unit) {
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
@@ -449,7 +449,7 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.cancel_verb)
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
@@ -459,7 +459,7 @@ private fun CancelPickerItem(onClick: () -> Unit) {
fun HostDisconnectButton(onClick: (() -> Unit)?) {
val interactionSource = remember { MutableInteractionSource() }
val hovered = interactionSource.collectIsHoveredAsState().value
IconButton(onClick ?: {}, Modifier.requiredSize(20.dp * fontSizeSqrtMultiplier), enabled = onClick != null) {
IconButton(onClick ?: {}, Modifier.requiredSize(20.dp), enabled = onClick != null) {
Icon(
painterResource(if (onClick == null) MR.images.ic_desktop else if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi),
null,
@@ -22,13 +22,15 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
Column(
Modifier
.fillMaxWidth()
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
.padding(horizontal = AppBarHorizontalPadding)
.heightIn(min = AppBarHeight)
.padding(horizontal = AppBarHorizontalPadding),
) {
Row(
Modifier
.padding(top = 4.dp), // Like in DefaultAppBar
content = {
Row(
Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier),
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.Dp
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -45,10 +44,10 @@ fun DefaultTopAppBar(
}
@Composable
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) {
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon(
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor
)
}
}
@@ -85,7 +84,7 @@ private fun TopAppBar(
Box(
modifier
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier)
.height(AppBarHeight)
.background(backgroundColor)
.padding(horizontal = 4.dp),
contentAlignment = Alignment.CenterStart,
@@ -8,14 +8,12 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
import kotlin.math.sqrt
@Composable
fun ModalView(
@@ -91,7 +89,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
if (placement == ModalPlacement.CENTER) {
ChatModel.chatId.value = null
} else if (placement == ModalPlacement.END) {
desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()) + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()))
desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH)
}
}
@@ -102,9 +100,6 @@ class ModalManager(private val placement: ModalPlacement? = null) {
fun hasModalsOpen() = modalCount.value > 0
val hasModalsOpen: Boolean
@Composable get () = remember { modalCount }.value > 0
fun closeModal() {
if (modalViews.isNotEmpty()) {
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
@@ -276,8 +276,8 @@ fun TextIconSpaced(extraPadding: Boolean = false) {
}
@Composable
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionItemViewSpaceBetween(padding = padding) {
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) {
SectionItemViewSpaceBetween {
Row {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary)
@@ -287,61 +287,6 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color
}
}
fun numOrDash(n: Number): String = if (n.toLong() == 0L) "-" else n.toString()
@Composable
fun InfoRowTwoValues(
title: String,
title2: String,
value: Int,
value2: Int,
textColor: Color = MaterialTheme.colors.onBackground
) {
SectionItemViewSpaceBetween {
Row(
verticalAlignment = Alignment.Bottom
) {
Text(
text = title,
color = textColor,
)
Text(
text = " / ",
fontSize = 12.sp,
)
Text(
text = title2,
color = textColor,
fontSize = 12.sp,
)
}
Row(verticalAlignment = Alignment.Bottom) {
if (value == 0 && value2 == 0) {
Text(
text = "-",
color = MaterialTheme.colors.secondary
)
} else {
Text(
text = numOrDash(value),
color = MaterialTheme.colors.secondary,
)
Text(
text = " / ",
color = MaterialTheme.colors.secondary,
fontSize = 12.sp,
)
Text(
text = numOrDash(value2),
color = MaterialTheme.colors.secondary,
fontSize = 12.sp,
)
}
}
}
}
@Composable
fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick) {
@@ -1,41 +0,0 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Icon
import androidx.compose.ui.graphics.Color
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun SubscriptionStatusIcon(
color: Color,
variableValue: Float,
modifier: Modifier = Modifier
) {
@Composable
fun ZeroIcon() {
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier)
}
when {
variableValue <= 0f -> ZeroIcon()
variableValue > 0f && variableValue <= 0.25f -> Box {
ZeroIcon()
Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier)
}
variableValue > 0.25f && variableValue <= 0.5f -> Box {
ZeroIcon()
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier)
}
variableValue > 0.5f && variableValue <= 0.75f -> Box {
ZeroIcon()
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier)
}
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier)
}
}
@@ -8,7 +8,6 @@ import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeOverrides
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
@@ -520,15 +519,6 @@ fun includeMoreFailedComposables() {
lastExecutedComposables.clear()
}
val fontSizeMultiplier: Float
@Composable get() = remember { appPrefs.fontScale.state }.value
val fontSizeSqrtMultiplier: Float
@Composable get() = sqrt(remember { appPrefs.fontScale.state }.value)
val desktopDensityScaleMultiplier: Float
@Composable get() = if (appPlatform.isDesktop) remember { appPrefs.densityScale.state }.value else 1f
@Composable
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
DisposableEffect(Unit) {
@@ -17,7 +17,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.startChat
import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase
@@ -39,7 +38,6 @@ import kotlinx.serialization.*
import java.io.File
import java.net.URLEncoder
import kotlin.math.max
import kotlin.math.sqrt
@Serializable
data class MigrationFileLinkData(
@@ -428,8 +426,7 @@ fun LargeProgressView(value: Float, title: String, description: String) {
Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = value,
(if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else Modifier.size(windowWidth() - DEFAULT_PADDING *
2))
(if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2))
.rotate(-90f),
color = MaterialTheme.colors.primary,
strokeWidth = 25.dp
@@ -44,11 +44,6 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
ModalManager.center.closeModals()
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
},
scanPaste = {
closeNewChatSheet(false)
ModalManager.center.closeModals()
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
},
createGroup = {
closeNewChatSheet(false)
ModalManager.center.closeModals()
@@ -60,17 +55,15 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
private val titles = listOf(
MR.strings.add_contact_tab,
MR.strings.scan_paste_link,
MR.strings.create_group_button
)
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_group)
@Composable
private fun NewChatSheetLayout(
newChatSheetState: StateFlow<AnimatedViewState>,
stopped: Boolean,
addContact: () -> Unit,
scanPaste: () -> Unit,
createGroup: () -> Unit,
closeNewChatSheet: (animated: Boolean) -> Unit,
) {
@@ -109,7 +102,7 @@ private fun NewChatSheetLayout(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.End
) {
val actions = remember { listOf(addContact, scanPaste, createGroup) }
val actions = remember { listOf(addContact, createGroup) }
val backgroundColor = if (isInDarkTheme())
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
else
@@ -125,11 +118,11 @@ private fun NewChatSheetLayout(
Box(contentAlignment = Alignment.CenterEnd) {
Button(
actions[index],
shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier),
shape = RoundedCornerShape(21.dp),
colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor),
elevation = null,
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF),
modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier)
modifier = Modifier.height(42.dp)
) {
Text(
stringResource(titles[index]),
@@ -140,7 +133,7 @@ private fun NewChatSheetLayout(
Icon(
painterResource(icons[index]),
stringResource(titles[index]),
Modifier.size(42.dp * fontSizeSqrtMultiplier),
Modifier.size(42.dp),
tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary
)
}
@@ -152,7 +145,7 @@ private fun NewChatSheetLayout(
}
FloatingActionButton(
onClick = { if (!stopped) closeNewChatSheet(true) },
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier),
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
@@ -164,11 +157,11 @@ private fun NewChatSheetLayout(
) {
Icon(
painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group),
Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }
)
Icon(
painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group),
Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
Modifier.graphicsLayer { alpha = animatedFloat.value }
)
}
}
@@ -271,7 +264,6 @@ private fun PreviewNewChatSheet() {
MutableStateFlow(AnimatedViewState.VISIBLE),
stopped = false,
addContact = {},
scanPaste = {},
createGroup = {},
closeNewChatSheet = {},
)
@@ -140,7 +140,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
}
}
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index ->
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top) { index ->
// LALAL SCROLLBAR DOESN'T WORK
ColumnWithScrollBar(
Modifier
@@ -19,7 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.*
import dev.icerock.moko.resources.compose.painterResource
@@ -83,66 +82,6 @@ object AppearanceScope {
}
}
@Composable
fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
.clip(RoundedCornerShape(percent = 22))
.clickable {
localFontScale.value = 1f
appPrefs.fontScale.set(localFontScale.value)
},
contentAlignment = Alignment.Center) {
CompositionLocalProvider(
LocalDensity provides Density(LocalDensity.current.density, localFontScale.value)
) {
Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground)
}
}
Spacer(Modifier.width(10.dp))
// Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp)
if (appPlatform.isAndroid) {
Slider(
localFontScale.value,
valueRange = 0.75f..1.25f,
steps = 11,
onValueChange = {
val diff = it % 0.05f
localFontScale.value = String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f
},
onValueChangeFinished = {
appPrefs.fontScale.set(localFontScale.value)
},
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
} else {
Slider(
localFontScale.value,
valueRange = 0.7f..1.5f,
steps = 9,
onValueChange = {
val diff = it % 0.1f
localFontScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f
},
onValueChangeFinished = {
appPrefs.fontScale.set(localFontScale.value)
},
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
}
}
}
@Composable
fun ChatThemePreview(
theme: DefaultTheme,
@@ -286,8 +225,8 @@ object AppearanceScope {
}
if (appPlatform.isDesktop) {
val itemWidth = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4
val itemHeight = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2) / 4
val itemWidth = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4
val itemHeight = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2) / 4
val rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt()
LazyVerticalGrid(
columns = GridCells.Fixed(4),
@@ -42,7 +42,6 @@ fun NetworkAndServersView() {
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
val netCfg = remember { chatModel.controller.getNetCfg() }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val networkShowSubscriptionPercentage: MutableState<Boolean> = remember { mutableStateOf(chatModel.controller.appPrefs.networkShowSubscriptionPercentage.get()) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
@@ -54,7 +53,6 @@ fun NetworkAndServersView() {
currentRemoteHost = currentRemoteHost,
developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy,
networkShowSubscriptionPercentage = networkShowSubscriptionPercentage,
onionHosts = onionHosts,
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
@@ -119,9 +117,6 @@ fun NetworkAndServersView() {
)
}
},
toggleNetworkShowSubscriptionPercentage = { enable ->
networkShowSubscriptionPercentage.value = enable
},
useOnion = {
if (onionHosts.value == it) return@NetworkAndServersLayout
val prevValue = onionHosts.value
@@ -235,14 +230,12 @@ fun NetworkAndServersView() {
currentRemoteHost: RemoteHostInfo?,
developerTools: Boolean,
networkUseSocksProxy: MutableState<Boolean>,
networkShowSubscriptionPercentage: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
toggleNetworkShowSubscriptionPercentage: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
@@ -263,7 +256,6 @@ fun NetworkAndServersView() {
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
if (currentRemoteHost == null) {
SettingsPreferenceItem(painterResource(MR.images.ic_radiowaves_up_forward_4_bar),stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage)
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
if (developerTools) {
@@ -685,10 +677,8 @@ fun PreviewNetworkAndServersLayout() {
currentRemoteHost = null,
developerTools = true,
networkUseSocksProxy = remember { mutableStateOf(true) },
networkShowSubscriptionPercentage = remember { mutableStateOf(false) },
proxyPort = remember { mutableStateOf(9050) },
toggleSocksProxy = {},
toggleNetworkShowSubscriptionPercentage = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
@@ -174,14 +174,11 @@ fun SettingsLayout(
Box(
Modifier
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier)
.background(MaterialTheme.colors.background)
.background(if (isInDarkTheme()) ToolbarDark else ToolbarLight)
.padding(start = 4.dp, top = 8.dp),
contentAlignment = Alignment.CenterStart
.padding(start = 4.dp, top = 8.dp)
) {
val sp24 = with(LocalDensity.current) { 24.sp.toDp() }
NavigationButtonBack(closeSettings, height = sp24)
NavigationButtonBack(closeSettings)
}
}
}
@@ -324,11 +324,6 @@
<string name="forward_chat_item">Forward</string>
<string name="download_file">Download</string>
<string name="message_forwarded_title">Message forwarded</string>
<string name="message_forwarded_desc">No direct connection yet, message is forwarded by admin.</string>
<string name="member_inactive_title">Member inactive</string>
<string name="member_inactive_desc">Message may be delivered later if member becomes active.</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">edited</string>
<string name="icon_descr_sent_msg_status_sent">sent</string>
@@ -622,7 +617,6 @@
<!-- NewChatView.kt -->
<string name="new_chat">New chat</string>
<string name="add_contact_tab">Add contact</string>
<string name="scan_paste_link">Scan / Paste link</string>
<string name="one_time_link">One-time invitation link</string>
<string name="one_time_link_short">1-time link</string>
<string name="simplex_address">SimpleX address</string>
@@ -691,7 +685,6 @@
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="smp_save_servers_question">Save servers?</string>
<string name="xftp_servers">XFTP servers</string>
<string name="subscription_percentage">Subscription percentage</string>
<string name="install_simplex_chat_for_terminal">Install SimpleX Chat for terminal</string>
<string name="star_on_github">Star on GitHub</string>
<string name="contribute">Contribute</string>
@@ -1466,8 +1459,6 @@
<string name="unblock_member_desc">Messages from %s will be shown!</string>
<string name="member_blocked_by_admin">Blocked by admin</string>
<string name="member_info_member_blocked">blocked</string>
<string name="member_info_member_disabled">disabled</string>
<string name="member_info_member_inactive">inactive</string>
<string name="member_info_section_title_member">MEMBER</string>
<string name="role_in_group">Role</string>
<string name="change_role">Change role</string>
@@ -1613,8 +1604,6 @@
<string name="color_wallpaper_background">Wallpaper background</string>
<string name="color_wallpaper_tint">Wallpaper accent</string>
<string name="theme_remove_image">Remove image</string>
<string name="appearance_font_size">Font size</string>
<string name="appearance_zoom">Zoom</string>
<!-- Wallpapers -->
<string name="wallpaper_preview_hello_alice">Good afternoon!</string>
@@ -2086,85 +2075,4 @@
<string name="network_type_network_wifi">WiFi</string>
<string name="network_type_ethernet">Wired ethernet</string>
<string name="network_type_other">Other</string>
<!-- ServersSummaryView.kt -->
<string name="servers_info">Servers info</string>
<string name="servers_info_files_tab">Files</string>
<string name="servers_info_missing">No info, try to reload</string>
<string name="servers_info_target">Showing info for</string>
<string name="all_users">All users</string>
<string name="current_user">Current user</string>
<string name="servers_info_transport_sessions_section_header">Transport sessions</string>
<string name="servers_info_sessions_connected">Connected</string>
<string name="servers_info_sessions_connecting">Connecting</string>
<string name="servers_info_sessions_errors">Errors</string>
<string name="servers_info_statistics_section_header">Statistics</string>
<string name="servers_info_messages_sent">Messages sent</string>
<string name="servers_info_messages_received">Messages received</string>
<string name="servers_info_details">Details</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is private to your device.</string>
<string name="servers_info_subscriptions_section_header">Messages subscriptions</string>
<string name="servers_info_subscriptions_connections_subscribed">Connections subscribed</string>
<string name="servers_info_subscriptions_connections_pending">Pending</string>
<string name="servers_info_subscriptions_total">Total</string>
<string name="servers_info_connected_servers_section_header">Connected servers</string>
<string name="servers_info_previously_connected_servers_section_header">Previously connected servers</string>
<string name="servers_info_proxied_servers_section_header">Proxied servers</string>
<string name="servers_info_proxied_servers_section_footer">You are not connected to these servers. Private routing is used to deliver messages to them.</string>
<string name="servers_info_reconnect_servers_title">Reconnect servers?</string>
<string name="servers_info_reconnect_servers_message">Reconnect all connected servers to force message delivery. It uses additional traffic.</string>
<string name="servers_info_reconnect_server_title">Reconnect server?</string>
<string name="servers_info_reconnect_server_message">Reconnect server to force message delivery. It uses additional traffic.</string>
<string name="servers_info_reconnect_servers_error">Error reconnecting servers</string>
<string name="servers_info_reconnect_server_error">Error reconnecting server</string>
<string name="servers_info_modal_error_title">Error</string>
<string name="servers_info_reconnect_all_servers_button">Reconnect all servers</string>
<string name="servers_info_reset_stats">Reset all statistics</string>
<string name="servers_info_reset_stats_alert_title">Reset all statistics?</string>
<string name="servers_info_reset_stats_alert_message">Servers statistics will be reset - this cannot be undone!</string>
<string name="servers_info_reset_stats_alert_confirm">Reset</string>
<string name="servers_info_reset_stats_alert_error_title">Error resetting statistics</string>
<string name="servers_info_uploaded">Uploaded</string>
<string name="servers_info_downloaded">Downloaded</string>
<string name="servers_info_detailed_statistics">Detailed statistics</string>
<string name="servers_info_detailed_statistics_sent_messages_header">Sent messages</string>
<string name="servers_info_detailed_statistics_sent_messages_total">Sent total</string>
<string name="servers_info_detailed_statistics_received_messages_header">Received messages</string>
<string name="servers_info_detailed_statistics_received_total">Received total</string>
<string name="servers_info_detailed_statistics_receive_errors">Receive errors</string>
<string name="servers_info_starting_from">Starting from %s.</string>
<string name="smp_server">SMP server</string>
<string name="xftp_server">XFTP server</string>
<string name="reconnect">Reconnect</string>
<string name="attempts_label">attempts</string>
<string name="sent_directly">Sent directly</string>
<string name="sent_via_proxy">Sent via proxy</string>
<string name="proxied">Proxied</string>
<string name="send_errors">Send errors</string>
<string name="expired_label">expired</string>
<string name="other_label">other</string>
<string name="duplicates_label">duplicates</string>
<string name="decryption_errors">decryption errors</string>
<string name="other_errors">other errors</string>
<string name="acknowledged">Acknowledged</string>
<string name="acknowledgement_errors">Acknowledgement errors</string>
<string name="connections">Connections</string>
<string name="created">Created</string>
<string name="secured">Secured</string>
<string name="completed">Completed</string>
<string name="deleted">Deleted</string>
<string name="deletion_errors">Deletion errors</string>
<string name="subscribed">Subscribed</string>
<string name="subscription_results_ignored">Subscriptions ignored</string>
<string name="subscription_errors">Subscription errors</string>
<string name="uploaded_files">Uploaded files</string>
<string name="size">Size</string>
<string name="chunks_uploaded">Chunks uploaded</string>
<string name="upload_errors">Upload errors</string>
<string name="chunks_deleted">Chunks deleted</string>
<string name="chunks_downloaded">Chunks downloaded</string>
<string name="downloaded_files">Downloaded files</string>
<string name="download_errors">Download errors</string>
<string name="server_address">Server address</string>
<string name="open_server_settings_button">Open server settings</string>
</resources>
@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 960" width="20px" height="20px">
<g transform="matrix(1, 0, 0, 1, -160, 960)">
<path
d="m592.5-482-314-314q-10.5-10.5-10.75-26.25t10.75-27.25q11-11.5 27-11.5t27.5 11.5L661-522q8.5 8.5 12.25 18.75T677-482q0 11-3.75 21.25T661-442L332.5-113.5q-11.5 11.5-27.25 11t-26.75-12q-11-11-11.25-26.5t11.25-27l314-314Z" />
</g>
<g transform="matrix(1, 0, 0, 1, 160, 960)">
<path
d="m592.5-482-314-314q-10.5-10.5-10.75-26.25t10.75-27.25q11-11.5 27-11.5t27.5 11.5L661-522q8.5 8.5 12.25 18.75T677-482q0 11-3.75 21.25T661-442L332.5-113.5q-11.5 11.5-27.25 11t-26.75-12q-11-11-11.25-26.5t11.25-27l314-314Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 715 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#000000"><path d="m790.5-88-79-78.5H222q-23.97 0-40.73-16.77Q164.5-200.03 164.5-224v-33.52q0-37.48 18.75-63.98t48.25-40q60-27 115.25-41.75T458-420L87.09-790.91q-8.59-8.59-8.34-20.59t9-20.5q8.75-8.5 20.75-8.5t20.61 8.61l702.78 703.28q8.61 8.43 8.61 20.27T831.75-88Q823-79.5 811-79.5T790.5-88ZM222-224h432L515.48-362.5q-8.48-.5-17.48-.75t-17.98-.25q-56.54 0-109.53 11.5T254.5-310q-14 7-23.25 21.73T222-257.26V-224Zm505.5-137.5q31 13.5 49.5 40.25T795.5-257v9.5L652-391q18 6 37 13.75t38.5 15.75ZM548-495l-46-45.8q30.23-7.2 49.11-31.2Q570-596 570-628q0-38-26-64t-64-26q-32 0-56 19t-31 49l-46-46q18.5-38 54.5-58.75T480-775.5q62 0 104.75 42.75T627.5-628q0 42.5-20.75 78.5T548-495Zm106 271H222h432ZM448-596Z"/></svg>

Before

Width:  |  Height:  |  Size: 802 B

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742189)">
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 492 B

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 787 B

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z M 10.714 -45.251 C 10.714 -43.771 11.962 -42.538 13.428 -42.538 C 31.216 -42.538 45.578 -28.161 45.578 -10.388 C 45.578 -8.908 46.826 -7.675 48.291 -7.675 C 49.756 -7.675 51.004 -8.908 51.004 -10.388 C 51.004 -31.141 34.147 -47.95 13.428 -47.95 C 11.962 -47.95 10.714 -46.75 10.714 -45.251 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
<path d="M15.1235-6.71093C18.0332-6.71093 20.3989-9.1289 20.3989-11.9863C20.3989-14.96 18.082-17.2954 15.1235-17.2954C12.2324-17.2954 9.76562-14.8623 9.76562-11.8887C9.76562-9.1289 12.2813-6.71093 15.1235-6.71093ZM10.7143-30.0659C10.7143-28.5854 11.9624-27.3525 13.4277-27.3525C22.7871-27.3525 30.3921-19.7324 30.3921-10.3882C30.3921-8.90771 31.6401-7.6748 33.1055-7.6748C34.5708-7.6748 35.8189-8.90771 35.8189-10.3882C35.8189-22.7456 25.7178-32.7642 13.4277-32.7642C11.9624-32.7642 10.7143-31.5649 10.7143-30.0659ZM10.7143-45.2515C10.7143-43.771 11.9624-42.5381 13.4277-42.5381C31.2158-42.5381 45.5776-28.1611 45.5776-10.3882C45.5776-8.90771 46.8257-7.6748 48.291-7.6748C49.7564-7.6748 51.0044-8.90771 51.0044-10.3882C51.0044-31.1406 34.1465-47.9497 13.4277-47.9497C11.9624-47.9497 10.7143-46.7505 10.7143-45.2515ZM10.7143-61.4136C10.7143-59.9331 11.9624-58.7002 13.4277-58.7002C40.0874-58.7002 61.7397-37.0327 61.7397-10.3882C61.7397-8.90771 62.9878-7.6748 64.4531-7.6748C65.9185-7.6748 67.1665-8.90771 67.1665-10.3882C67.1665-40.0273 43.0181-64.1118 13.4277-64.1118C11.9624-64.1118 10.7143-62.9126 10.7143-61.4136Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -14,7 +14,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme
@@ -27,7 +26,6 @@ import kotlinx.coroutines.*
import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener
import java.io.File
import kotlin.math.sqrt
import kotlin.system.exitProcess
val simplexWindowState = SimplexWindowState()
@@ -197,8 +195,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
if (remember { ChatController.appPrefs.developerTools.state }.value && remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") {
var hiddenUntilRestart by remember { mutableStateOf(false) }
if (!hiddenUntilRestart) {
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height =
768.dp)
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp)
Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) {
SimpleXTheme {
TerminalView(ChatModel) { hiddenUntilRestart = true }
@@ -3,29 +3,18 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import java.util.Locale
import kotlin.math.roundToInt
@Composable
actual fun AppearanceView(m: ChatModel) {
@@ -66,56 +55,6 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced(maxTopPadding = true)
ProfileImageSection()
SectionDividerSpaced(maxBottomPadding = true)
FontScaleSection()
SectionDividerSpaced(maxBottomPadding = true)
DensityScaleSection()
SectionBottomSpacer()
}
}
@Composable
fun DensityScaleSection() {
val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) }
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
.clip(RoundedCornerShape(percent = 22))
.clickable {
localDensityScale.value = 1f
appPrefs.densityScale.set(localDensityScale.value)
},
contentAlignment = Alignment.Center) {
CompositionLocalProvider(
LocalDensity provides Density(LocalDensity.current.density * localDensityScale.value, LocalDensity.current.fontScale)
) {
Text("${localDensityScale.value}",
color = if (localDensityScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground,
fontSize = 12.sp,
maxLines = 1
)
}
}
Spacer(Modifier.width(10.dp))
Slider(
localDensityScale.value,
valueRange = 1f..2f,
steps = 11,
onValueChange = {
val diff = it % 0.1f
localDensityScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f
},
onValueChangeFinished = {
appPrefs.densityScale.set(localDensityScale.value)
},
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
}
}
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.0-beta.0
android.version_code=225
android.version_name=5.8.2
android.version_code=223
desktop.version_name=6.0-beta.0
desktop.version_code=56
desktop.version_name=5.8.2
desktop.version_code=55
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
@@ -1,51 +0,0 @@
---
layout: layouts/article.html
title: "The Future of Privacy: Enforcing Privacy Standards"
date: 2024-07-04
previewBody: blog_previews/20240704.html
image: images/20240704-privacy.jpg
imageWide: true
permalink: "/blog/20240704-future-of-privacy-enforcing-privacy-standards.html"
---
# The Future of Privacy: Enforcing Privacy Standards
**Published:** Jul 4, 2024
Recent anti-privacy legislations and proposals in [Europe](https://www.theverge.com/2024/6/19/24181214/eu-chat-control-law-propose-scanning-encrypted-messages-csam), [the US](https://theconversation.com/section-702-foreign-surveillance-law-lives-on-but-privacy-fight-continues-229253) and [Australia](https://www.theguardian.com/technology/article/2024/jun/20/meredith-walker-signal-boss-government-encryption-laws) threaten to infringe our fundamental right to privacy and to create grave risks to the [safety](https://simplex.chat/blog/20240601-protecting-children-safety-requires-e2e-encryption.html) of children and vulnerable people. It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a right users must constantly fight to protect, and not something that users can be asked to consent away as a condition of access to a service.
Tech giants are trying to normalize surveillance capitalism, often with little to no consequences globally. These companies are contributing to a growing ecosystem where opting out of invasive data hoarding practices is becoming increasingly challenging, if not outright impossible. We are being gaslit by the technology executives who try to justify profiteering from AI theft, from [Microsoft](https://www.computing.co.uk/news/4330395/microsoft-ai-chief-makes-questionable-claims-about-copyright-online-content) claiming all our content is fair game for their exploitation to unethical startups like [Perplexity](https://www.theverge.com/2024/6/27/24187405/perplexity-ai-twitter-lie-plagiarism) turning the word “[privacy](https://x.com/perplexity_ai/status/1789007907092066559)” into a marketable farce.
## The AI Hypes Impact on Privacy
The exaggeration of AIs actual capabilities and the continuous promotion of its “intelligence” is creating a rat race where tech companies and well-funded startups are evading accountability, as they eagerly collect and exploit more data than ever. 
They're prioritizing AI development over user privacy and rights, setting a dangerous precedent for current and future online engagements. They've already normalized the use of AI to scan and analyze supposedly private communications - from emails to instant messages - repackaging this intrusion as "productivity tools”. Meanwhile, most consumers actually want [more data privacy](https://iapp.org/news/a/most-consumers-want-data-privacy-and-will-act-to-defend-it), not less, and are increasingly concerned by the lack of it.
The legal push towards “client-side scanning”, attacks on end-to-end encryption and the support for pro-surveillance legislation gives credibility to these highly intrusive practices that literally endanger lives. And we know that moral obligations mean nothing to corporations benefiting from these exploitative models, so we have to ensure that our demands for privacy are legally enforceable and non-negotiable. 
## Legal Action
We are encouraged to see more legal pressure on companies that exploit user data on a daily basis. For example, the European Center for Digital Rights (Noyb) [complaints](https://noyb.eu/en/noyb-urges-11-dpas-immediately-stop-metas-abuse-personal-data-ai) against Metas abuse of personal data to train their AI and, and the demands from the Norwegian Consumer Council to data protection authority to ensure that applicable laws are enforced against Meta considering there is “[no way to remove personal data from AI models once Meta has begun the training](https://www.forbrukerradet.no/side/legal-complaint-against-metas-use-of-personal-content-for-ai-training/)”.
Noyb is taking a strong stance against [other companies](https://noyb.eu/en/project/cases) with similar exploitative models, including facial recognition surveillance tools often misused by law enforcement agencies. Consider [supporting](https://noyb.eu/en/support) their ongoing efforts &mdash; we strongly believe legal action is one of the most effective means to hold these companies accountable for their persistent abuses, which are otherwise shielded by heavily funded self-serving lobby groups.
## Privacy as a Legal Obligation
We must shift from a defensive stance to a proactive one by proposing privacy legislation that puts users in direct control of their private data.
This legislation should:
1. Establish non-negotiable provider duties for protecting user privacy, with hefty fines and consequences for service operators who do not comply.
2. Prevent providers from circumventing these duties through user consent clauses &mdash; it should be legally prohibited to ask for a consent to share user data or to use it for anything other than providing a service.
3. Prevent providers from asking for any more personal information from the users than technically necessary and legally required. For example, asking for a phone number as a condition of access to a service should be made illegal in most cases &mdash; it does not provide a sufficient security, exposes users' private information and allows simple aggregation of users' data across multiple services.
4. Create a strong legal framework that cannot be resisted or modified
By codifying these principles into law, we can establish a strong technological framework that is built to create more value for end users, while protecting their privacy against data exploitation, criminal use and identity theft. We will continue the fight against illogical legislative proposals designed to normalize mass surveillance, but our efforts should equally gear towards creating and supporting new models and technological foundations that bring us far closer to the reality we urgently need.
## Collective Action
There is great work being done by advocacy organizations, and service providers need to contribute to this fight as well by shifting the narrative and reclaiming the term “privacy” from the tech giants who co-opted and corrupted it. We must play a bigger role in supporting users in setting stronger boundaries, making demands, and refusing anything less than genuine privacy and data ownership, while getting comfortable with holding providers accountable for any violations.
Privacy should be seen as a fundamental obligation of technology providers, and legislators must actively enforce this expectation. The more consumers make this demand, the more pressure we put on anti-privacy lobbyists with rogue motives, the easier it will be to hold abusers accountable, and the more likely we can collectively ensure that a privacy-first web becomes a reality.
You can support privacy today by signing [the petition](https://www.globalencryption.org/2024/05/joint-statement-on-the-dangers-of-the-may-2024-council-of-the-eu-compromise-proposal-on-eu-csam/) prepared by Global Encryption Coalition in support of communication privacy. You can also write to your elected representatives, explaining them how data privacy and encrypted communications protect children safety and reduce crime.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

+2 -2
View File
@@ -2,7 +2,7 @@ packages: .
-- packages: . ../simplexmq
-- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple
index-state: 2024-06-01T00:00:00Z
index-state: 2023-12-12T00:00:00Z
package cryptostore
flags: +use_crypton
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: d39100f7ea9cb2ea79df723d0040f484f58ae058
tag: 9d0774a58e3307892e0b38500fe96fb5d15d9def
source-repository-package
type: git
@@ -0,0 +1,68 @@
# Conversational agent
## Problem
SimpleX Chat has a different paradigm of connecting the users - instead of discovering friends, like it happens in most other apps, the users have to invite the friends. Also, many configuration options are complex and require explanations.
Historically, we tried different UX approaches with various amount of explanations texts, sheets that open when you press (i) icon, and external documentations.
All these approaches are ineffective, and we lose the new users during the onboarding, especially if they don't come via the introduction of the existing users - they simply fail to understand why the app design is what it is, why it cannot do what every other app is doing, and instead of using the onboarding as a promotion opportunity, we simply lose the users.
At this point, as the users are growing it appears to be the #1 blocker for the growth.
## Solution
We believe that the best approach to both onboard the users and to explain all the complex concepts while using the app is conversational assistant that offers scripted conversation with both multiple choice and free-form questions that users can interact with and both understand what options and choices best fit their needs and manage the app configuration and actions via conversational UX.
## Implementation
Possible options are:
## Just write code in the UI
Pros: it avoids any abstractions that can prove to be inadequate to the task as the requirements are discovered and evolve.
Cons: even though there are some differences between the platform, particularly around how the user connects to the network, there is a lot of overlap, and this approach would result in a lot of repetive code.
## Use JavaScript as a cross-platform language
Pros: the same code will run on all platforms.
Cons: requires complex integration and design of JS host environment to integrate both with the code code and with the UI.
While this option is very tempting, and JS engine can be used for other things - chat widgets and interactive activities - it is probably unnecessary for this problem.
## Programm it in Haskell
Pros: no new languages, easy to create high level DSL to describe conversational scenarios, and at the same time have direct access to conversation data in Haskell code, without the need for any additional API, as would be required with JavaScript.
Cons:
- UIs would have to support protocol for UI actions and settings.
- Harder to use to some developers in the team.
Overall, this appears to be the simplest and the most effective approach.
Conversation(s) with the agent would be stored in the usual database, and the agent would be interactive - it will be called by the code when the message is added to this conversation.
## Questions to consider
- UX:
- should it be one conversation for all user profiles or separate conversations per profile? The latter seems safer from privacy and security points of view.
- assistant should support navigating to other sections of the app.
- assistant should be able to present some interface elements in its UI.
- types:
- conversation type. Could use "direct" or may be a special type.
- chat item types. Currently we do not parameterize chat items by conversation types, even though some chat items are only possible in specific conversations.
- UI protocol:
- currently we do not have a pattern when the core executes commands in the UI. We could use the event with some correlation ID and another API call to pass the response from the UI.
- alternatively, because all UI changes will be triggered by the user action of choosing one of the multiple choice answers we may instead attach UI commands to the buttons to avoid round trip of sending this choice to the agent and then have the agent call UI command.
- Translations:
- we cannot have partially translated assistant messages, so it means:
- it has to be a separate translation component from the rest of the UI.
- we are likely to support much fewer languages.
- we have to work with some specific translators more closely so they align their translations with our releases.
- we would need to externalize strings into JSON files, using the same format as for website, and embed them into Haskell code.
- Other information about the app:
- Should assistant complement or should it replace the current guide (I think it should replace).
- It would still have the links to the external technical documents.
- Showing images.
-47
View File
@@ -1,47 +0,0 @@
# Group messages sending status
## Problem
Currently in UI chat item info:
- There's no differentiation between sent messages and pending messages.
- There's no differentiation between pending messages reasons (establishing connection or member inactivity).
- Since the former is usually not a case due to group forwarding, this can be ignored.
- Messages to be forwarded by admin are not accounted.
## Solution
Differentiate new statuses for group sending in chat item info:
- forwarded
- inactive / pending
Option 1 is to add statuses to CIStatus / ACIStatus types.
Pros:
- simple.
Cons:
- further muddies type of statuses for chat item with impossible states / different dimension, as it's not applicable directly to chat item but a technicality of group sending process.
Option 2 is to create a new type, GroupSndStatus.
```haskell
data GroupSndStatus
= GSSNew
| GSSForwarded
| GSSInactive
| GSSSent
| GSSRcvd {msgRcptStatus :: MsgReceiptStatus}
| GSSError {agentError :: SndError}
| GSSWarning {agentError :: SndError}
| GSSInvalid {text :: Text}
```
Most statuses repeat CIStatus sending statuses, with addition of forwarded and inactive for group sending process.
Pros:
- separates concerns of chat item presentation from group sending process.
- allows future extension without further muddying CIStatus types.
Cons:
- more work.
- requires backwards compatible decoding with ACIStatus to read previous data from db.
Generated
+76 -160
View File
@@ -123,11 +123,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -153,14 +153,51 @@
"type": "github"
}
},
"ghc98X": {
"flake": false,
"locked": {
"lastModified": 1696643148,
"narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=",
"ref": "ghc-9.8",
"rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6",
"revCount": 61642,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
},
"original": {
"ref": "ghc-9.8",
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
}
},
"ghc99": {
"flake": false,
"locked": {
"lastModified": 1697054644,
"narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=",
"ref": "refs/heads/master",
"rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a",
"revCount": 62040,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
},
"original": {
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
}
},
"hackage": {
"flake": false,
"locked": {
"lastModified": 1719621483,
"narHash": "sha256-x1JBNd46flORH4OEZJDjDH5SX+RuDmhvrbKRGVMuXU0=",
"lastModified": 1702513363,
"narHash": "sha256-kloro9uEe8aYhPMoMjVNq2rfrXNgMOZhOPwVH5DH2K0=",
"owner": "input-output-hk",
"repo": "hackage.nix",
"rev": "7f6056018cb0d9f3ad70bcdafeb0ac755a4a3ee5",
"rev": "a9d931d0398da67846fa257922a924829233cb91",
"type": "github"
},
"original": {
@@ -178,6 +215,8 @@
"cardano-shell": "cardano-shell",
"flake-compat": "flake-compat",
"ghc-8.6.5-iohk": "ghc-8.6.5-iohk",
"ghc98X": "ghc98X",
"ghc99": "ghc99",
"hackage": [
"hackage"
],
@@ -186,11 +225,6 @@
"hls-2.2": "hls-2.2",
"hls-2.3": "hls-2.3",
"hls-2.4": "hls-2.4",
"hls-2.5": "hls-2.5",
"hls-2.6": "hls-2.6",
"hls-2.7": "hls-2.7",
"hls-2.8": "hls-2.8",
"hls-2.9": "hls-2.9",
"hpc-coveralls": "hpc-coveralls",
"hydra": "hydra",
"iserv-proxy": "iserv-proxy",
@@ -204,22 +238,21 @@
"nixpkgs-2205": "nixpkgs-2205",
"nixpkgs-2211": "nixpkgs-2211",
"nixpkgs-2305": "nixpkgs-2305",
"nixpkgs-2311": "nixpkgs-2311",
"nixpkgs-2405": "nixpkgs-2405",
"nixpkgs-unstable": "nixpkgs-unstable",
"old-ghc-nix": "old-ghc-nix",
"stackage": "stackage"
},
"locked": {
"lastModified": 1721956799,
"narHash": "sha256-FU09PlekhkuocxDO2UN2aARdUflIGA36VP1EUra4b7c=",
"lastModified": 1701163700,
"narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "ccbd8ed7d4aff11e0507d19dc7c40601487c0bea",
"rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b",
"type": "github"
},
"original": {
"owner": "input-output-hk",
"ref": "armv7a",
"repo": "haskell.nix",
"type": "github"
}
@@ -295,101 +328,16 @@
"hls-2.4": {
"flake": false,
"locked": {
"lastModified": 1699862708,
"narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=",
"lastModified": 1696939266,
"narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57",
"rev": "362fdd1293efb4b82410b676ab1273479f6d17ee",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.4.0.1",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.5": {
"flake": false,
"locked": {
"lastModified": 1701080174,
"narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "27f8c3d3892e38edaef5bea3870161815c4d014c",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.5.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.6": {
"flake": false,
"locked": {
"lastModified": 1705325287,
"narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.6.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.7": {
"flake": false,
"locked": {
"lastModified": 1708965829,
"narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.7.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.8": {
"flake": false,
"locked": {
"lastModified": 1715153580,
"narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "dd1be1beb16700de59e0d6801957290bcf956a0a",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.8.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.9": {
"flake": false,
"locked": {
"lastModified": 1718469202,
"narHash": "sha256-THXSz+iwB1yQQsr/PY151+2GvtoJnTIB2pIQ4OzfjD4=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "40891bccb235ebacce020b598b083eab9dda80f1",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.9.0.0",
"ref": "2.4.0.0",
"repo": "haskell-language-server",
"type": "github"
}
@@ -436,18 +384,18 @@
"iserv-proxy": {
"flake": false,
"locked": {
"lastModified": 1717479972,
"narHash": "sha256-7vE3RQycHI1YT9LHJ1/fUaeln2vIpYm6Mmn8FTpYeVo=",
"owner": "stable-haskell",
"repo": "iserv-proxy",
"rev": "2ed34002247213fc435d0062350b91bab920626e",
"type": "github"
"lastModified": 1691634696,
"narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=",
"ref": "hkm/remote-iserv",
"rev": "43a979272d9addc29fbffc2e8542c5d96e993d73",
"revCount": 14,
"type": "git",
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
},
"original": {
"owner": "stable-haskell",
"ref": "iserv-syms",
"repo": "iserv-proxy",
"type": "github"
"ref": "hkm/remote-iserv",
"type": "git",
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
}
},
"lowdown-src": {
@@ -472,11 +420,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1715594531,
"narHash": "sha256-/1Eppd4g/30hU/LKG84VWOgjOfrWfesBqM7eNmnOf08=",
"lastModified": 1699767871,
"narHash": "sha256-kxeCUfwC/Vgh2FvVMlBUq0eVx1JvfHyN+5MPKUik9mE=",
"owner": "zw3rk",
"repo": "mobile-core-tools",
"rev": "90e0a52893288bfe85e5ed6bff9cee077e3d18c7",
"rev": "4dcb77d5ea896d749381806dfab5358851b08951",
"type": "github"
},
"original": {
@@ -604,11 +552,11 @@
},
"nixpkgs-2305": {
"locked": {
"lastModified": 1701362232,
"narHash": "sha256-GVdzxL0lhEadqs3hfRLuj+L1OJFGiL/L7gCcelgBlsw=",
"lastModified": 1695416179,
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d2332963662edffacfddfad59ff4f709dde80ffe",
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
"type": "github"
},
"original": {
@@ -618,38 +566,6 @@
"type": "github"
}
},
"nixpkgs-2311": {
"locked": {
"lastModified": 1701386440,
"narHash": "sha256-xI0uQ9E7JbmEy/v8kR9ZQan6389rHug+zOtZeZFiDJk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "293822e55ec1872f715a66d0eda9e592dc14419f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-23.11-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-2405": {
"locked": {
"lastModified": 1721931987,
"narHash": "sha256-1Zg8LY0T5EfXtv0Kf4M6SFnjH7Eto4VV+EKJ/YSnhiI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e21630230c77140bc6478a21cd71e8bb73706fce",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-24.05-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
@@ -686,11 +602,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1721933792,
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "github"
},
"original": {
@@ -741,18 +657,18 @@
"mac2ios": "mac2ios",
"nixpkgs": [
"haskellNix",
"nixpkgs-2405"
"nixpkgs-2305"
]
}
},
"stackage": {
"flake": false,
"locked": {
"lastModified": 1705709369,
"narHash": "sha256-HWgwM8v0g6ZBkRVCStsPaMUDf26WDOGV0q+AxyGlcBQ=",
"lastModified": 1699834215,
"narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=",
"owner": "input-output-hk",
"repo": "stackage.nix",
"rev": "f7e9bff9f7582d06c4237d183dafaccc33cfa5d4",
"rev": "47aacd04abcce6bad57f43cbbbd133538380248e",
"type": "github"
},
"original": {
+4 -4
View File
@@ -1,7 +1,7 @@
{
description = "nix flake for simplex-chat";
inputs.haskellNix.url = "github:input-output-hk/haskell.nix";
inputs.nixpkgs.follows = "haskellNix/nixpkgs-2405";
inputs.haskellNix.url = "github:input-output-hk/haskell.nix/armv7a";
inputs.nixpkgs.follows = "haskellNix/nixpkgs-2305";
inputs.mac2ios.url = "github:zw3rk/mobile-core-tools";
inputs.hackage = {
url = "github:input-output-hk/hackage.nix";
@@ -30,8 +30,8 @@
# `appendOverlays` with a singleton is identical to `extend`.
let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in
let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project {
compiler-nix-name = "ghc966";
index-state = "2024-06-01T00:00:00Z";
compiler-nix-name = "ghc963";
index-state = "2023-12-12T00:00:00Z";
# We need this, to specify we want the cabal project.
# If the stack.yaml was dropped, this would not be necessary.
projectFileName = "cabal.project";
+2 -2
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 6.0.0.0
version: 5.8.2.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -45,7 +45,7 @@ dependencies:
- stm == 2.5.*
- terminal == 0.2.*
- time == 1.12.*
- tls >= 2.0.6 && < 2.1
- tls >= 1.7.0 && < 1.8
- unliftio == 0.2.*
- unliftio-core == 0.2.*
- zip == 2.0.*
-5
View File
@@ -29,11 +29,6 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -
cd $BUILD_DIR/build
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it
# but libffi.so.8 is shipped as an external library with other libs
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
mkdir deps 2> /dev/null || true
ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/

Some files were not shown because too many files have changed in this diff Show More