Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 387faa0c27 | |||
| aa990da17c | |||
| a48c82f4a1 | |||
| f41c04735b | |||
| a8da9b9cd9 | |||
| 64a0f509f7 | |||
| c4f8a50f0d | |||
| 93a4c0854e | |||
| 49c29c74df | |||
| e6ee5df158 | |||
| f4be0278b6 | |||
| a9d2535292 | |||
| 3e623684bc | |||
| fd90b47194 | |||
| acd3467d10 | |||
| 71ce598355 | |||
| 63393eaf0b | |||
| f90de83215 | |||
| 1c10209a31 | |||
| 5d7abf31ce | |||
| 44c0861fe4 | |||
| 1e6dc8002c |
@@ -73,6 +73,7 @@ final class ChatModel: ObservableObject {
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GMember] = []
|
||||
@Published var groupMembersIndexes: Dictionary<Int64, Int> = [:] // groupMemberId to index in groupMembers list
|
||||
// items in the terminal view
|
||||
@Published var showingTerminal = false
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@@ -180,8 +181,18 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func populateGroupMembersIndexes() {
|
||||
groupMembersIndexes.removeAll()
|
||||
for (i, member) in groupMembers.enumerated() {
|
||||
groupMembersIndexes[member.groupMemberId] = i
|
||||
}
|
||||
}
|
||||
|
||||
func getGroupMember(_ groupMemberId: Int64) -> GMember? {
|
||||
groupMembers.first { $0.groupMemberId == groupMemberId }
|
||||
if let i = groupMembersIndexes[groupMemberId] {
|
||||
return groupMembers[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getChatIndex(_ id: String) -> Int? {
|
||||
@@ -667,14 +678,17 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// update current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
|
||||
if let i = groupMembersIndexes[member.groupMemberId] {
|
||||
withAnimation(.default) {
|
||||
self.groupMembers[i].wrapped = member
|
||||
self.groupMembers[i].created = Date.now
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation { groupMembers.append(GMember(member)) }
|
||||
withAnimation {
|
||||
groupMembers.append(GMember(member))
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.count - 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
@@ -702,7 +716,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow)
|
||||
return UnreadChatItemCounts(isNearBottom: totalBelow < 16, unreadBelow: unreadBelow)
|
||||
}
|
||||
|
||||
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
||||
@@ -740,7 +754,7 @@ struct NTFContactRequest {
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var totalBelow: Int
|
||||
var isNearBottom: Bool
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ 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,7 +22,6 @@ struct CIInvalidJSONView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
|
||||
@@ -132,7 +132,6 @@ struct CIRcvDecryptionError: View {
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -152,7 +151,6 @@ struct CIRcvDecryptionError: View {
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ struct DeletedItemView: View {
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
|
||||
@@ -70,7 +70,6 @@ struct CIMsgError: View {
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ 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))
|
||||
.cornerRadius(18)
|
||||
.modifier(ChatItemClipped())
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
Button {
|
||||
@@ -302,7 +302,7 @@ struct ChatItemInfoView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
.cornerRadius(18)
|
||||
.modifier(ChatItemClipped())
|
||||
.contextMenu {
|
||||
if qi.text != "" {
|
||||
Button {
|
||||
@@ -415,7 +415,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
LazyVStack(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, CIStatus, Bool?)] {
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, 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: CIStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
@@ -450,23 +450,19 @@ struct ChatItemInfoView: View {
|
||||
.foregroundColor(theme.colors.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
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:
|
||||
let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
|
||||
switch status {
|
||||
case .rcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.padding(.trailing, 6)
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(Color.secondary)
|
||||
default:
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ struct ChatView: View {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
chatModel.reversedChatItems = []
|
||||
chatModel.groupMembers = []
|
||||
chatModel.groupMembersIndexes.removeAll()
|
||||
membersLoaded = false
|
||||
}
|
||||
}
|
||||
@@ -255,6 +256,7 @@ struct ChatView: View {
|
||||
await MainActor.run {
|
||||
if chatModel.chatId == groupInfo.id {
|
||||
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
chatModel.populateGroupMembersIndexes()
|
||||
membersLoaded = true
|
||||
updateView()
|
||||
}
|
||||
@@ -410,7 +412,7 @@ struct ChatView: View {
|
||||
|
||||
init() {
|
||||
unreadChatItemCounts = UnreadChatItemCounts(
|
||||
totalBelow: .zero,
|
||||
isNearBottom: true,
|
||||
unreadBelow: .zero
|
||||
)
|
||||
events
|
||||
@@ -425,9 +427,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 +481,7 @@ struct ChatView: View {
|
||||
scrollModel.scrollToItem(id: latestUnreadItem.id)
|
||||
}
|
||||
}
|
||||
} else if counts.totalBelow > 16 {
|
||||
} else if !counts.isNearBottom {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
@@ -754,6 +756,7 @@ 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 {
|
||||
|
||||
@@ -207,6 +207,7 @@ struct GroupChatInfoView: View {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
chatModel.populateGroupMembersIndexes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,8 +233,7 @@ struct GroupChatInfoView: View {
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground)
|
||||
(member.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
let s = Text(member.memberStatus.shortText)
|
||||
(user ? Text ("you: ") + s : s)
|
||||
(user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member)))
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
@@ -266,6 +266,16 @@ 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,12 +141,6 @@ 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 {
|
||||
@@ -183,6 +177,10 @@ 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,6 +55,7 @@ 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, *) {
|
||||
@@ -180,6 +181,7 @@ 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,6 +36,7 @@ struct ServersSummaryView: View {
|
||||
viewBody()
|
||||
.navigationTitle("Servers info")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
shareButton()
|
||||
@@ -264,6 +265,7 @@ struct ServersSummaryView: View {
|
||||
)
|
||||
.navigationBarTitle("SMP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.smpServer))
|
||||
@@ -332,6 +334,7 @@ struct ServersSummaryView: View {
|
||||
)
|
||||
.navigationBarTitle("XFTP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.xftpServer))
|
||||
@@ -360,7 +363,7 @@ struct ServersSummaryView: View {
|
||||
Button {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Reset all servers statistics?"),
|
||||
title: Text("Reset all statistics?"),
|
||||
message: Text("Servers statistics will be reset - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Reset")) {
|
||||
Task {
|
||||
@@ -422,7 +425,7 @@ struct SubscriptionStatusPercentageView: View {
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||
Text("\(Int(floor(statusPercent * 100)))%")
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
@@ -470,6 +473,7 @@ struct SMPServerSummaryView: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
@@ -563,6 +567,7 @@ struct SMPStatsView: View {
|
||||
DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
@@ -590,21 +595,21 @@ struct DetailedSMPStatsView: View {
|
||||
infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts)
|
||||
infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts)
|
||||
Text("Send errors")
|
||||
indentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
|
||||
indentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
|
||||
indentedInfoRow("expired", numOrDash(stats._sentExpiredErrs))
|
||||
indentedInfoRow("other", numOrDash(stats._sentOtherErrs))
|
||||
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)
|
||||
}
|
||||
Section("Received messages") {
|
||||
infoRow("Received total", numOrDash(stats._recvMsgs))
|
||||
Text("Receive errors")
|
||||
indentedInfoRow("duplicates", numOrDash(stats._recvDuplicates))
|
||||
indentedInfoRow("decryption errors", numOrDash(stats._recvCryptoErrs))
|
||||
indentedInfoRow("other errors", numOrDash(stats._recvErrs))
|
||||
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)
|
||||
infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts)
|
||||
Text("Acknowledgement errors")
|
||||
indentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
|
||||
indentedInfoRow("other errors", numOrDash(stats._ackOtherErrs))
|
||||
infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24)
|
||||
infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24)
|
||||
}
|
||||
Section {
|
||||
infoRow("Created", numOrDash(stats._connCreated))
|
||||
@@ -613,7 +618,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("Subscription results ignored", numOrDash(stats._connSubIgnored))
|
||||
infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored))
|
||||
infoRow("Subscription errors", numOrDash(stats._connSubErrs))
|
||||
} header: {
|
||||
Text("Connections")
|
||||
@@ -626,29 +631,19 @@ struct DetailedSMPStatsView: View {
|
||||
|
||||
private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View {
|
||||
HStack {
|
||||
Text(title) + Text(" / ").font(.caption2) + Text(title2).font(.caption2)
|
||||
Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2)
|
||||
Spacer()
|
||||
Group {
|
||||
if value == 0 && value2 == 0 {
|
||||
Text("-")
|
||||
Text(verbatim: "-")
|
||||
} else {
|
||||
Text(numOrDash(value)) + Text(" / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
|
||||
Text(numOrDash(value)) + Text(verbatim: " / ").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
|
||||
@@ -662,6 +657,7 @@ struct XFTPServerSummaryView: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
@@ -692,6 +688,7 @@ struct XFTPStatsView: View {
|
||||
DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
@@ -725,8 +722,8 @@ struct DetailedXFTPStatsView: View {
|
||||
infoRow("Size", prettySize(stats._downloadsSize))
|
||||
infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts)
|
||||
Text("Download errors")
|
||||
indentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
|
||||
indentedInfoRow("other", numOrDash(stats._downloadErrs))
|
||||
infoRow(Text(verbatim: "AUTH"), numOrDash(stats._downloadAuthErrs)).padding(.leading, 24)
|
||||
infoRow("other", numOrDash(stats._downloadErrs)).padding(.leading, 24)
|
||||
} header: {
|
||||
Text("Downloaded files")
|
||||
} footer: {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ struct AddGroupView: View {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
m.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
m.populateGroupMembersIndexes()
|
||||
}
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
|
||||
|
||||
@@ -305,13 +305,16 @@ 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: 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))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
|
||||
.modifier(ChatItemClipped())
|
||||
.frame(alignment: .trailing)
|
||||
}
|
||||
} else {
|
||||
@@ -747,7 +750,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 mode", selection: $themeUserDest) {
|
||||
Picker("Apply to", selection: $themeUserDest) {
|
||||
ForEach(values, id: \.0) { (_, text) in
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "bg",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "cs",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "de",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "en",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "es",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "fi",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "fr",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "hu",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "it",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "ja",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "nl",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "pl",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "ru",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "th",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "tr",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "uk",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "zh-Hans",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -195,6 +195,7 @@
|
||||
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 */; };
|
||||
@@ -202,11 +203,12 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -501,16 +503,17 @@
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -549,13 +552,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E52FF8DE2C34676700BF81EB /* libHSsimplex-chat-5.8.2.0-D50x9PQRTdqAAO9AQK2dA7.a in Frameworks */,
|
||||
E52FF8DB2C34676700BF81EB /* libgmpxx.a in Frameworks */,
|
||||
E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */,
|
||||
E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd 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 */,
|
||||
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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -622,11 +626,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 */,
|
||||
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 */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -683,6 +687,7 @@
|
||||
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */,
|
||||
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */,
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
|
||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -1062,6 +1067,7 @@
|
||||
);
|
||||
name = SimpleXChat;
|
||||
packageProductDependencies = (
|
||||
E50581052C3DDD9D009C3F71 /* Yams */,
|
||||
);
|
||||
productName = SimpleXChat;
|
||||
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
|
||||
@@ -1225,6 +1231,7 @@
|
||||
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 */,
|
||||
@@ -1608,7 +1615,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1633,7 +1640,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1657,7 +1664,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1682,7 +1689,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1743,7 +1750,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -1758,7 +1765,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1780,7 +1787,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -1795,7 +1802,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1817,7 +1824,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1843,7 +1850,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1868,7 +1875,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 226;
|
||||
CURRENT_PROJECT_VERSION = 227;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1894,7 +1901,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.8.2;
|
||||
MARKETING_VERSION = 6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2031,6 +2038,11 @@
|
||||
package = D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */;
|
||||
productName = LZString;
|
||||
};
|
||||
E50581052C3DDD9D009C3F71 /* Yams */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */;
|
||||
productName = Yams;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 5CA059BE279559F40002BEB4 /* Project object */;
|
||||
|
||||
@@ -1611,11 +1611,12 @@ 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
|
||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter
|
||||
}
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
@@ -1624,6 +1625,10 @@ 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
|
||||
}
|
||||
@@ -1637,7 +1642,8 @@ public struct Connection: Decodable, Hashable {
|
||||
viaGroupLink: false,
|
||||
pqSupport: false,
|
||||
pqEncryption: false,
|
||||
authErrCounter: 0
|
||||
authErrCounter: 0,
|
||||
quotaErrCounter: 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2846,6 +2852,62 @@ 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?)
|
||||
@@ -4012,6 +4074,6 @@ public struct ChatItemVersion: Decodable, Hashable {
|
||||
|
||||
public struct MemberDeliveryStatus: Decodable, Hashable {
|
||||
public var groupMemberId: Int64
|
||||
public var memberDeliveryStatus: CIStatus
|
||||
public var memberDeliveryStatus: GroupSndStatus
|
||||
public var sentViaProxy: Bool?
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ 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.*
|
||||
@@ -107,7 +108,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
|
||||
editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
|
||||
val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!!
|
||||
DrawableCompat.setTint(drawable, tintColor.toArgb())
|
||||
editText.background = drawable
|
||||
@@ -135,7 +136,7 @@ actual fun PlatformTextField(
|
||||
editText
|
||||
}) {
|
||||
it.setTextColor(textColor.toArgb())
|
||||
it.textSize = textStyle.value.fontSize.value
|
||||
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
|
||||
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))
|
||||
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.onRightClick
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
if (!disabled) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
|
||||
@@ -137,6 +137,9 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = true)
|
||||
FontScaleSection()
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
@@ -36,6 +37,7 @@ 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>,
|
||||
@@ -333,38 +335,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 + 56.dp)) {
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) {
|
||||
StartPartOfScreen(settingsState)
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
SwitchingUsersView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).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).clipToBounds()) {
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen) {
|
||||
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH)
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
ModalManager.start.closeModals()
|
||||
scope.launch { settingsState.scaffoldState.drawerState.close() }
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
|
||||
@@ -780,6 +780,7 @@ interface SomeChat {
|
||||
val id: ChatId
|
||||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val chatDeleted: Boolean
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
@@ -860,6 +861,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contact.id
|
||||
override val apiId get() = contact.apiId
|
||||
override val ready get() = contact.ready
|
||||
override val chatDeleted get() = contact.chatDeleted
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
@@ -884,6 +886,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = groupInfo.id
|
||||
override val apiId get() = groupInfo.apiId
|
||||
override val ready get() = groupInfo.ready
|
||||
override val chatDeleted get() = groupInfo.chatDeleted
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
@@ -908,6 +911,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = noteFolder.id
|
||||
override val apiId get() = noteFolder.apiId
|
||||
override val ready get() = noteFolder.ready
|
||||
override val chatDeleted get() = noteFolder.chatDeleted
|
||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||
override val incognito get() = noteFolder.incognito
|
||||
@@ -932,6 +936,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactRequest.id
|
||||
override val apiId get() = contactRequest.apiId
|
||||
override val ready get() = contactRequest.ready
|
||||
override val chatDeleted get() = contactRequest.chatDeleted
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
@@ -956,6 +961,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactConnection.id
|
||||
override val apiId get() = contactConnection.apiId
|
||||
override val ready get() = contactConnection.ready
|
||||
override val chatDeleted get() = contactConnection.chatDeleted
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = contactConnection.incognito
|
||||
@@ -981,6 +987,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
@@ -1057,6 +1064,7 @@ data class Contact(
|
||||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean,
|
||||
override val chatDeleted: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
@@ -1130,6 +1138,7 @@ data class Contact(
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
contactGrpInvSent = false,
|
||||
chatDeleted = false,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
@@ -1138,7 +1147,8 @@ data class Contact(
|
||||
@Serializable
|
||||
enum class ContactStatus {
|
||||
@SerialName("active") Active,
|
||||
@SerialName("deleted") Deleted;
|
||||
@SerialName("deleted") Deleted,
|
||||
@SerialName("deletedByUser") DeletedByUser;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1172,18 +1182,22 @@ data class Connection(
|
||||
val pqSndEnabled: Boolean? = null,
|
||||
val pqRcvEnabled: Boolean? = null,
|
||||
val connectionStats: ConnectionStats? = null,
|
||||
val authErrCounter: Int
|
||||
val authErrCounter: Int,
|
||||
val quotaErrCounter: 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1277,6 +1291,7 @@ data class GroupInfo (
|
||||
override val id get() = "#$groupId"
|
||||
override val apiId get() = groupId
|
||||
override val ready get() = membership.memberActive
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||
override val incognito get() = membership.memberIncognito
|
||||
@@ -1582,6 +1597,7 @@ class NoteFolder(
|
||||
override val chatType get() = ChatType.Local
|
||||
override val id get() = "*$noteFolderId"
|
||||
override val apiId get() = noteFolderId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1618,6 +1634,7 @@ class UserContactRequest (
|
||||
override val chatType get() = ChatType.ContactRequest
|
||||
override val id get() = "<@$contactRequestId"
|
||||
override val apiId get() = contactRequestId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1657,6 +1674,7 @@ class PendingContactConnection(
|
||||
override val chatType get() = ChatType.ContactConnection
|
||||
override val id get () = ":$pccConnId"
|
||||
override val apiId get() = pccConnId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -2352,6 +2370,48 @@ 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()
|
||||
@@ -3466,7 +3526,7 @@ data class ChatItemVersion(
|
||||
@Serializable
|
||||
data class MemberDeliveryStatus(
|
||||
val groupMemberId: Long,
|
||||
val memberDeliveryStatus: CIStatus,
|
||||
val memberDeliveryStatus: GroupSndStatus,
|
||||
val sentViaProxy: Boolean?
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ 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(
|
||||
@@ -197,6 +198,8 @@ 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)
|
||||
@@ -210,13 +213,16 @@ class AppPreferences {
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true)
|
||||
val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true)
|
||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, false)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
get = fun() = settings.getInt(prefName, default),
|
||||
@@ -338,6 +344,7 @@ 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"
|
||||
@@ -369,6 +376,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||
@@ -378,6 +386,8 @@ 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"
|
||||
@@ -387,6 +397,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
@@ -411,6 +423,18 @@ 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) {
|
||||
@@ -569,20 +593,24 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR {
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR {
|
||||
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
if (log) {
|
||||
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)
|
||||
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")
|
||||
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))
|
||||
}
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
|
||||
r.resp
|
||||
}
|
||||
}
|
||||
@@ -920,6 +948,14 @@ 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) {
|
||||
@@ -1119,16 +1155,16 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
||||
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val cInfo = chat.chatInfo
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
|
||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean {
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val success = when {
|
||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||
@@ -1149,6 +1185,22 @@ object ChatController {
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? {
|
||||
val type = ChatType.Direct
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val contact = when {
|
||||
r is CR.ContactDeleted -> r.contact
|
||||
else -> {
|
||||
val titleId = MR.strings.error_deleting_contact
|
||||
apiErrorAlert("apiDeleteChat", generalGetString(titleId), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
chatModel.deletedChats.value -= rh to type.type + id
|
||||
return contact
|
||||
}
|
||||
|
||||
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||
@@ -1887,6 +1939,10 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
chatModel.updateContact(rhId, updatedContact)
|
||||
}
|
||||
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
@@ -2577,6 +2633,8 @@ 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()
|
||||
@@ -2598,7 +2656,7 @@ sealed class CC {
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
|
||||
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||
class ApiListContacts(val userId: Long): CC()
|
||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||
@@ -2648,6 +2706,8 @@ 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
|
||||
@@ -2724,6 +2784,8 @@ 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"
|
||||
@@ -2745,11 +2807,7 @@ sealed class CC {
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
is ApiDeleteChat -> if (notify != null) {
|
||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
||||
} else {
|
||||
"/_delete ${chatRef(type, id)}"
|
||||
}
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
@@ -2804,6 +2862,8 @@ 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) {
|
||||
@@ -2864,6 +2924,8 @@ 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"
|
||||
@@ -2933,6 +2995,8 @@ 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)
|
||||
@@ -2962,8 +3026,6 @@ sealed class CC {
|
||||
null
|
||||
}
|
||||
|
||||
private fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||
|
||||
companion object {
|
||||
@@ -2973,6 +3035,8 @@ sealed class CC {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
@@ -3203,7 +3267,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
|
||||
@@ -3428,6 +3492,154 @@ 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()
|
||||
@@ -4439,6 +4651,7 @@ 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()
|
||||
@@ -4598,6 +4811,7 @@ sealed class CR {
|
||||
is ContactPQAllowed -> "contactPQAllowed"
|
||||
is ContactPQEnabled -> "contactPQEnabled"
|
||||
is VersionInfo -> "versionInfo"
|
||||
is AgentServersSummary -> "agentServersSummary"
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
@@ -4776,6 +4990,7 @@ 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 })}"
|
||||
@@ -4801,6 +5016,19 @@ fun chatError(r: CR): ChatErrorType? {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ChatDeleteMode {
|
||||
@Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("messages") class Messages: ChatDeleteMode()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is ChatDeleteMode.Full -> "full notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Messages -> "messages"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ConnectionPlan {
|
||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||
@@ -5651,6 +5879,7 @@ data class AppSettings(
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
var uiThemes: List<ThemeOverrides>? = null,
|
||||
var oneHandUI: Boolean? = null
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
@@ -5680,6 +5909,7 @@ data class AppSettings(
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||
if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI }
|
||||
return empty
|
||||
}
|
||||
|
||||
@@ -5717,6 +5947,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||
oneHandUI?.let { def.oneHandUI.set(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -5747,6 +5978,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
uiThemes = null,
|
||||
oneHandUI = false
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
@@ -5778,6 +6010,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
uiThemes = def.themeOverrides.get(),
|
||||
oneHandUI = def.oneHandUI.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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
|
||||
@@ -777,6 +779,7 @@ 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.
|
||||
@@ -791,6 +794,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||
LocalAppColors provides rememberedAppColors,
|
||||
LocalAppWallpaper provides rememberedWallpaper,
|
||||
LocalDensity provides density,
|
||||
content = content)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,13 +12,17 @@ import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.*
|
||||
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.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import androidx.compose.ui.text.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -45,9 +49,21 @@ import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
|
||||
sealed class ContactDeleteMode {
|
||||
class Full: ContactDeleteMode()
|
||||
class Entity: ContactDeleteMode()
|
||||
|
||||
fun toChatDeleteMode(notify: Boolean): ChatDeleteMode =
|
||||
when (this) {
|
||||
is Full -> ChatDeleteMode.Full(notify)
|
||||
is Entity -> ChatDeleteMode.Entity(notify)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(
|
||||
chatModel: ChatModel,
|
||||
openedFromChatView: Boolean,
|
||||
contact: Contact,
|
||||
connectionStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
@@ -68,6 +84,7 @@ fun ChatInfoView(
|
||||
val chatRh = chat.remoteHostId
|
||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||
ChatInfoLayout(
|
||||
openedFromChatView = openedFromChatView,
|
||||
chat,
|
||||
contact,
|
||||
currentUser,
|
||||
@@ -166,7 +183,8 @@ fun ChatInfoView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -201,53 +219,127 @@ sealed class SendReceipts {
|
||||
|
||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
||||
buttons = {
|
||||
Column {
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) {
|
||||
// Delete and notify contact
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
buttons = {
|
||||
Column {
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = true)
|
||||
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Full())
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete
|
||||
if (!chatInfo.contact.chatDeleted) {
|
||||
// Delete contact, keep conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Entity())
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_contact_keep_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = false)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyDeleteContactDialog(
|
||||
chat: Chat,
|
||||
chatModel: ChatModel,
|
||||
close: (() -> Unit)? = null,
|
||||
contactDeleteMode: ContactDeleteMode = ContactDeleteMode.Full()
|
||||
) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactActive = chatInfo.contact.ready && chatInfo.contact.active
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (contactActive) generalGetString(MR.strings.notify_delete_contact_question) else generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = when (contactDeleteMode) {
|
||||
is ContactDeleteMode.Full -> generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)
|
||||
is ContactDeleteMode.Entity -> generalGetString(MR.strings.delete_contact_cannot_undo_warning)
|
||||
},
|
||||
buttons = {
|
||||
Column {
|
||||
if (contactActive) {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = true))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteContactNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.contact_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteContactNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||
if (ct != null) {
|
||||
when (chatDeleteMode) {
|
||||
is ChatDeleteMode.Full ->
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
is ChatDeleteMode.Entity ->
|
||||
chatModel.updateContact(chatRh, ct)
|
||||
is ChatDeleteMode.Messages ->
|
||||
chatModel.clearChat(chatRh, ChatInfo.Direct(ct))
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -280,6 +372,7 @@ fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) {
|
||||
|
||||
@Composable
|
||||
fun ChatInfoLayout(
|
||||
openedFromChatView: Boolean,
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
currentUser: User,
|
||||
@@ -300,6 +393,7 @@ fun ChatInfoLayout(
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -319,7 +413,31 @@ fun ChatInfoLayout(
|
||||
}
|
||||
|
||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (contact.activeConn == null && contact.profile.contactLink != null && contact.active) {
|
||||
ConnectButton(openedFromChatView, chat, contact, close)
|
||||
} else if (!contact.active && !contact.chatDeleted) {
|
||||
OpenButton(openedFromChatView, chat, contact, close)
|
||||
} else {
|
||||
MessageButton(openedFromChatView, chat, contact, close)
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
CallButton(chat, contact)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
VideoButton(chat, contact)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||
SectionItemViewSpaceBetween {
|
||||
@@ -535,6 +653,155 @@ fun LocalAliasEditor(
|
||||
}
|
||||
}
|
||||
|
||||
// when contact is a "contact card"
|
||||
@Composable
|
||||
private fun ConnectButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_connect_button),
|
||||
disabled = false,
|
||||
onClick = {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = false, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = true, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
},
|
||||
hostDevice = hostDevice(chat.remoteHostId),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun infoConnectContactViaAddress(openedFromChatView: Boolean, chat: Chat, contact: Contact, incognito: Boolean, close: () -> Unit) {
|
||||
withBGApi {
|
||||
val ok = connectContactViaAddress(chatModel, chat.remoteHostId, contact.contactId, incognito = incognito)
|
||||
if (ok) {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
if (contact.chatDeleted) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||
}
|
||||
close.invoke()
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpenButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_open_button),
|
||||
disabled = false,
|
||||
onClick = {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
close.invoke()
|
||||
withBGApi {
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = !contact.sendMsgEnabled,
|
||||
onClick = {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
if (contact.chatDeleted) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||
}
|
||||
close.invoke()
|
||||
withBGApi {
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallButton(chat: Chat, contact: Contact) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_call_filled),
|
||||
title = generalGetString(MR.strings.info_view_call_button),
|
||||
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||
onClick = {
|
||||
startChatCall(chat, CallMediaType.Audio)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoButton(chat: Chat, contact: Contact) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_videocam_filled),
|
||||
title = generalGetString(MR.strings.info_view_video_button),
|
||||
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||
onClick = {
|
||||
startChatCall(chat, CallMediaType.Video)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
Modifier
|
||||
.width(96.dp)
|
||||
.height(66.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
val modifier = if (disabled) Modifier else Modifier.clickable { onClick () }
|
||||
Column(
|
||||
modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(26.dp),
|
||||
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
|
||||
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
@@ -823,6 +1090,7 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
||||
fun PreviewChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
ChatInfoLayout(
|
||||
openedFromChatView = false,
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
@@ -847,6 +1115,7 @@ fun PreviewChatInfoLayout() {
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
close = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) {
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: GroupSndStatus, sentViaProxy: Boolean?) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(horizontal = 0.dp)
|
||||
) {
|
||||
@@ -355,7 +355,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
)
|
||||
}
|
||||
}
|
||||
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
|
||||
val (icon, statusColor) = 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,20 +367,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
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
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = statusColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,7 +511,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, CIStatus, Boolean?>> {
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, GroupSndStatus, Boolean?>> {
|
||||
return memberDeliveryStatuses.mapNotNull { mds ->
|
||||
chatModel.getGroupMember(mds.groupMemberId)?.let { mem ->
|
||||
Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
|
||||
@@ -189,7 +189,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
ChatInfoView(chatModel, openedFromChatView = true, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
@@ -306,18 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
startCall = out@{ media -> startChatCall(chat, media) },
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -521,6 +510,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
|
||||
fun startChatCall(chat: Chat, media: CallMediaType) {
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chat.remoteHostId, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatLayout(
|
||||
chat: Chat,
|
||||
@@ -821,9 +823,9 @@ fun ChatInfoToolbar(
|
||||
buttons = barButtons
|
||||
)
|
||||
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
menuItems.forEach { it() }
|
||||
}
|
||||
@@ -837,9 +839,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (cInfo.incognito) {
|
||||
IncognitoImage(size = 36.dp, Indigo)
|
||||
IncognitoImage(size = 36.dp * fontSizeSqrtMultiplier, Indigo)
|
||||
}
|
||||
ChatInfoImage(cInfo, size = imageSize, iconColor)
|
||||
ChatInfoImage(cInfo, size = imageSize * fontSizeSqrtMultiplier, iconColor)
|
||||
Column(
|
||||
Modifier.padding(start = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -865,7 +867,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).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).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 +1285,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
|
||||
ProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -13,10 +13,12 @@ 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
|
||||
@@ -884,7 +886,7 @@ fun ComposeView(
|
||||
&& !nextSendGrpInv.value
|
||||
IconButton(
|
||||
attachmentClicked,
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }),
|
||||
enabled = attachmentEnabled
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -15,10 +15,12 @@ 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.*
|
||||
@@ -99,7 +101,7 @@ fun SendMsgView(
|
||||
if (showDeleteTextButton.value) {
|
||||
DeleteTextButton(composeState)
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) {
|
||||
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 5.sp.toDp() } * fontSizeSqrtMultiplier)) {
|
||||
val sendButtonSize = remember { Animatable(36f) }
|
||||
val sendButtonAlpha = remember { Animatable(1f) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -390,6 +390,16 @@ 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,
|
||||
@@ -412,8 +422,8 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified
|
||||
)
|
||||
}
|
||||
val s = member.memberStatus.shortText
|
||||
val statusDescr = if (user) String.format(generalGetString(MR.strings.group_info_member_you), s) else s
|
||||
val statusDescr =
|
||||
if (user) String.format(generalGetString(MR.strings.group_info_member_you), member.memberStatus.shortText) else memberConnStatus()
|
||||
Text(
|
||||
statusDescr,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
|
||||
@@ -246,10 +246,10 @@ fun GroupMemberInfoLayout(
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
fun knownDirectChat(contactId: Long): Chat? {
|
||||
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||
val chat = getContactChat(contactId)
|
||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
chat
|
||||
chat to chat.chatInfo.contact
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -309,17 +309,37 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
|
||||
Spacer(Modifier.width(10.dp))
|
||||
CallButton(chat, contact)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
VideoButton(chat, contact)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call_filled), generalGetString(MR.strings.info_view_call_button), disabled = true, onClick = {})
|
||||
Spacer(Modifier.width(10.dp))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam_filled), generalGetString(MR.strings.info_view_video_button), disabled = true, onClick = {})
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
@@ -358,13 +378,6 @@ 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()
|
||||
@@ -401,6 +414,13 @@ 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)
|
||||
@@ -513,12 +533,11 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_chat),
|
||||
stringResource(MR.strings.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = false,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -32,7 +33,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
@@ -47,6 +48,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
val inProgress = remember { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inProgress.value) {
|
||||
progressByTimeout = if (inProgress.value) {
|
||||
delay(1000)
|
||||
@@ -75,6 +77,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
@@ -94,6 +97,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.Local -> {
|
||||
ChatListNavLinkLayout(
|
||||
@@ -112,6 +116,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
@@ -131,6 +136,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -151,6 +157,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -167,12 +174,13 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorChatListItem() {
|
||||
fun ErrorChatListItem() {
|
||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||
}
|
||||
@@ -407,13 +415,73 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat, chatModel)
|
||||
deleteContactConversationDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContactConversationDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactDeletedByUser = chatInfo.contact.contactStatus == ContactStatus.DeletedByUser
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_question) else generalGetString(MR.strings.delete_contact_question),
|
||||
text = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_all_messages_deleted_cannot_undo_warning) else null,
|
||||
buttons = {
|
||||
Column {
|
||||
if (contactDeletedByUser) {
|
||||
// Delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Full(notify = false))
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
notifyDeleteContactDialog(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Only delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages())
|
||||
if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) {
|
||||
showDeleteConversationNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteConversationNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.conversation_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteConversationNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
@@ -843,6 +911,7 @@ expect fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
)
|
||||
|
||||
@Preview/*(
|
||||
@@ -885,7 +954,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -930,7 +1000,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -952,7 +1023,8 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
@@ -10,8 +11,10 @@ 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.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.TextRange
|
||||
@@ -35,7 +38,10 @@ 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) {
|
||||
@@ -47,6 +53,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
@@ -69,7 +77,8 @@ 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(searchText, scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListTopBar(stopped) } },
|
||||
bottomBar = { Box(Modifier.padding(end = endPadding)) { ChatListBottomToolbar(scaffoldState.drawerState, userPickerState) } },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@@ -82,13 +91,20 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
var bottom = DEFAULT_PADDING
|
||||
if (oneHandUI.state.value) {
|
||||
bottom = DEFAULT_BOTTOM_PADDING
|
||||
} else {
|
||||
bottom -= 16.dp
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||
}
|
||||
},
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = bottom).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
@@ -98,18 +114,23 @@ 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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
var modifier = Modifier.padding(it).padding(end = endPadding)
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
|
||||
}
|
||||
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(stringResource(
|
||||
@@ -181,7 +202,9 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
private fun ChatListTopBar(stopped: Boolean) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
@@ -199,40 +222,38 @@ private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: Dr
|
||||
}
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) {
|
||||
Text(
|
||||
stringResource(MR.strings.your_chats),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.chats.size > 0) {
|
||||
val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } }
|
||||
if (enabled.value) {
|
||||
ToggleFilterEnabledButton()
|
||||
} else {
|
||||
ToggleFilterDisabledButton()
|
||||
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) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
@@ -243,28 +264,164 @@ private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: Dr
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsButton(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
val scope = rememberCoroutineScope()
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun toolbarIcon(icon: Painter) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListToolbarButton(icon: @Composable () -> Unit, title: String, onClick: () -> Unit) {
|
||||
Surface(
|
||||
Modifier
|
||||
.size(56.dp * fontSizeSqrtMultiplier),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color.Transparent,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.clickable { onClick () },
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
icon()
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp * fontSizeSqrtMultiplier),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListBottomToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(BottomAppBarHeight)
|
||||
.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
) {
|
||||
Divider()
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SettingsButton(drawerState, userPickerState)
|
||||
|
||||
ChatListToolbarButton(
|
||||
icon = { toolbarIcon(painterResource(MR.images.ic_chat_bubble_filled)) },
|
||||
title = generalGetString(MR.strings.your_chats),
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
ChatListToolbarButton(
|
||||
icon = {
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp,
|
||||
color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
size = 24.dp * fontSizeSqrtMultiplier,
|
||||
color = MaterialTheme.colors.secondaryVariant.mixWith(
|
||||
MaterialTheme.colors.onBackground,
|
||||
0.97f
|
||||
)
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
val h by remember { chatModel.currentRemoteHost }
|
||||
if (h != null) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
HostDisconnectButton {
|
||||
stopRemoteHostAndReloadHosts(h!!, true)
|
||||
}
|
||||
},
|
||||
onClick = onButtonClicked,
|
||||
title = generalGetString(MR.strings.toolbar_settings),
|
||||
|
||||
)
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
val h by remember { chatModel.currentRemoteHost }
|
||||
if (h != null) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
HostDisconnectButton {
|
||||
stopRemoteHostAndReloadHosts(h!!, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,32 +446,17 @@ 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.primary,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||
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 = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.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)
|
||||
.size(sp16)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -334,11 +476,17 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>, oneHandUI: SharedPreference<Boolean>) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
@@ -357,33 +505,11 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
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))
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
@@ -446,9 +572,35 @@ private fun ErrorSettingsView() {
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
enum class ScrollDirection {
|
||||
Up, Down, Idle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>, oneHandUI: SharedPreference<Boolean>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
@@ -469,7 +621,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -477,7 +631,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
@@ -485,11 +639,17 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
ChatListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && chatModel.chats.isNotEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
var modifier = Modifier.fillMaxSize();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -508,17 +668,18 @@ private fun filteredChats(
|
||||
} else {
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats
|
||||
chats.filter { chat -> !chat.chatInfo.chatDeleted }
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Direct -> !chat.chatInfo.chatDeleted && (
|
||||
if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
})
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
} else {
|
||||
|
||||
@@ -47,10 +47,11 @@ 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(18.dp).background(MaterialTheme.colors.background, CircleShape),
|
||||
Modifier.size(sp18).background(MaterialTheme.colors.background, CircleShape),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
@@ -87,10 +88,11 @@ fun ChatPreviewView(
|
||||
|
||||
@Composable
|
||||
fun VerifiedIcon() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
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)
|
||||
}
|
||||
|
||||
fun messageDraft(draft: ComposeState): Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>> {
|
||||
fun messageDraft(draft: ComposeState, sp20: Dp): 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
|
||||
@@ -115,12 +117,12 @@ fun ChatPreviewView(
|
||||
"editIcon" to InlineTextContent(
|
||||
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.primary)
|
||||
Icon(painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), 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, tint = MaterialTheme.colors.secondary)
|
||||
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
)
|
||||
return inlineContentBuilder to inlineContent
|
||||
@@ -167,8 +169,9 @@ 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) }
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft, sp20) }
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci.meta) to null
|
||||
}
|
||||
@@ -198,7 +201,7 @@ fun ChatPreviewView(
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
|
||||
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
||||
if (cInfo.contact.nextSendGrpInv) {
|
||||
@@ -220,10 +223,11 @@ fun ChatPreviewView(
|
||||
|
||||
@Composable
|
||||
fun progressView() {
|
||||
val sp15 = with(LocalDensity.current) { 15.sp.toDp() }
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(15.dp),
|
||||
.size(sp15),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 1.5.dp
|
||||
)
|
||||
@@ -231,6 +235,7 @@ 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
|
||||
@@ -244,7 +249,7 @@ fun ChatPreviewView(
|
||||
contentDescription = descr,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(19.dp)
|
||||
.size(sp19)
|
||||
)
|
||||
|
||||
else ->
|
||||
@@ -266,7 +271,7 @@ fun ChatPreviewView(
|
||||
|
||||
Row {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
ChatInfoImage(cInfo, size = 72.dp)
|
||||
ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier)
|
||||
Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) {
|
||||
chatPreviewImageOverlayIcon()
|
||||
}
|
||||
@@ -295,9 +300,13 @@ 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 = 24.dp),
|
||||
Modifier.padding(top = sp23, end = with(LocalDensity.current) { 3.sp.toDp() }),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
@@ -313,7 +322,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
} else if (showNtfsIcon) {
|
||||
Box(
|
||||
Modifier.padding(top = 24.dp),
|
||||
Modifier.padding(top = sp21),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -323,12 +332,12 @@ fun ChatPreviewView(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.size(17.dp)
|
||||
.size(sp17)
|
||||
)
|
||||
}
|
||||
} else if (chat.chatInfo.chatSettings?.favorite == true) {
|
||||
Box(
|
||||
Modifier.padding(top = 24.dp),
|
||||
Modifier.padding(top = sp21),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -338,12 +347,12 @@ fun ChatPreviewView(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.size(17.dp)
|
||||
.size(sp17)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.padding(top = 50.dp),
|
||||
Modifier.padding(top = sp46),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
chatStatusImage()
|
||||
@@ -355,12 +364,13 @@ 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(21.dp)
|
||||
.size(sp21)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,979 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -153,6 +153,8 @@ fun UserPicker(
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(bottom = BottomAppBarHeight)
|
||||
.widthIn(min = 260.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
@@ -309,7 +311,7 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp
|
||||
size = 54.dp * fontSizeSqrtMultiplier
|
||||
)
|
||||
Text(
|
||||
u.displayName,
|
||||
@@ -354,7 +356,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
h.hostDeviceName,
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
@@ -395,7 +397,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
stringResource(MR.strings.this_device),
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
@@ -409,7 +411,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
@@ -419,7 +421,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
@@ -429,7 +431,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
@@ -439,7 +441,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
@@ -449,7 +451,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), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
@@ -459,7 +461,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), enabled = onClick != null) {
|
||||
IconButton(onClick ?: {}, Modifier.requiredSize(20.dp * fontSizeSqrtMultiplier), 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,15 +22,13 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = AppBarHeight)
|
||||
.padding(horizontal = AppBarHorizontalPadding),
|
||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.padding(horizontal = AppBarHorizontalPadding)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 4.dp), // Like in DefaultAppBar
|
||||
content = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
|
||||
Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -15,7 +16,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
navigationButton: @Composable RowScope.() -> Unit,
|
||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||
title: (@Composable () -> Unit)?,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
showSearch: Boolean,
|
||||
@@ -44,10 +45,10 @@ fun DefaultTopAppBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) {
|
||||
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) {
|
||||
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor
|
||||
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +85,7 @@ private fun TopAppBar(
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight)
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
@@ -125,5 +126,6 @@ private fun TopAppBar(
|
||||
|
||||
val AppBarHeight = 56.dp
|
||||
val AppBarHorizontalPadding = 4.dp
|
||||
val BottomAppBarHeight = 60.dp
|
||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
||||
val TitleInsetWithIcon = 72.dp
|
||||
|
||||
@@ -8,12 +8,14 @@ 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(
|
||||
@@ -89,7 +91,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 + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH)
|
||||
desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()) + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +102,9 @@ 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) {
|
||||
SectionItemViewSpaceBetween {
|
||||
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) {
|
||||
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,6 +287,61 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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,6 +8,7 @@ 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
|
||||
@@ -519,6 +520,15 @@ 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,6 +17,7 @@ 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
|
||||
@@ -38,6 +39,7 @@ import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Serializable
|
||||
data class MigrationFileLinkData(
|
||||
@@ -426,7 +428,8 @@ 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) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2))
|
||||
(if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else Modifier.size(windowWidth() - DEFAULT_PADDING *
|
||||
2))
|
||||
.rotate(-90f),
|
||||
color = MaterialTheme.colors.primary,
|
||||
strokeWidth = 25.dp
|
||||
|
||||
@@ -44,6 +44,11 @@ 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()
|
||||
@@ -55,15 +60,17 @@ 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_group)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||
) {
|
||||
@@ -102,7 +109,7 @@ private fun NewChatSheetLayout(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val actions = remember { listOf(addContact, createGroup) }
|
||||
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||
val backgroundColor = if (isInDarkTheme())
|
||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||
else
|
||||
@@ -118,11 +125,11 @@ private fun NewChatSheetLayout(
|
||||
Box(contentAlignment = Alignment.CenterEnd) {
|
||||
Button(
|
||||
actions[index],
|
||||
shape = RoundedCornerShape(21.dp),
|
||||
shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier),
|
||||
colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor),
|
||||
elevation = null,
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF),
|
||||
modifier = Modifier.height(42.dp)
|
||||
modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
Text(
|
||||
stringResource(titles[index]),
|
||||
@@ -133,7 +140,7 @@ private fun NewChatSheetLayout(
|
||||
Icon(
|
||||
painterResource(icons[index]),
|
||||
stringResource(titles[index]),
|
||||
Modifier.size(42.dp),
|
||||
Modifier.size(42.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
@@ -145,7 +152,7 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { if (!stopped) closeNewChatSheet(true) },
|
||||
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + BottomAppBarHeight).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
@@ -157,11 +164,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 }
|
||||
Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.graphicsLayer { alpha = animatedFloat.value }
|
||||
Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -264,6 +271,7 @@ 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) { index ->
|
||||
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index ->
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
@@ -82,6 +83,66 @@ 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,
|
||||
@@ -225,8 +286,8 @@ object AppearanceScope {
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
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 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 rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt()
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(4),
|
||||
|
||||
@@ -43,6 +43,10 @@ fun DeveloperView(
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
|
||||
if (appPlatform.isAndroid) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_back_hand), stringResource(MR.strings.one_hand_ui), m.controller.appPrefs.oneHandUI)
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
|
||||
if (checked) {
|
||||
|
||||
@@ -42,6 +42,7 @@ 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) }
|
||||
@@ -53,6 +54,7 @@ fun NetworkAndServersView() {
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
networkShowSubscriptionPercentage = networkShowSubscriptionPercentage,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
@@ -117,6 +119,9 @@ fun NetworkAndServersView() {
|
||||
)
|
||||
}
|
||||
},
|
||||
toggleNetworkShowSubscriptionPercentage = { enable ->
|
||||
networkShowSubscriptionPercentage.value = enable
|
||||
},
|
||||
useOnion = {
|
||||
if (onionHosts.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = onionHosts.value
|
||||
@@ -230,12 +235,14 @@ 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,
|
||||
@@ -256,6 +263,7 @@ 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) {
|
||||
@@ -677,8 +685,10 @@ 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,11 +174,14 @@ 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)
|
||||
.padding(start = 4.dp, top = 8.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
NavigationButtonBack(closeSettings)
|
||||
val sp24 = with(LocalDensity.current) { 24.sp.toDp() }
|
||||
NavigationButtonBack(closeSettings, height = sp24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +324,11 @@
|
||||
<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>
|
||||
@@ -336,6 +341,7 @@
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Settings</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="member_contact_send_direct_message">send direct message</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
@@ -427,10 +433,28 @@
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="info_view_connect_button">connect</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_call_button">call</string>
|
||||
<string name="info_view_video_button">video</string>
|
||||
<string name="delete_contact_question">Delete contact?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_conversation_question">Delete conversation?</string>
|
||||
<string name="delete_conversation_all_messages_deleted_cannot_undo_warning">Conversation and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_conversation">Delete conversation</string>
|
||||
<string name="only_delete_conversation">Only delete conversation</string>
|
||||
<string name="delete_contact_keep_conversation">Delete contact, keep conversation</string>
|
||||
<string name="notify_delete_contact_question">Notify contact?</string>
|
||||
<string name="confirm_delete_contact_question">Confirm contact deletion?</string>
|
||||
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
||||
<string name="delete_without_notification">Delete without notification</string>
|
||||
<string name="button_delete_contact">Delete contact</string>
|
||||
<string name="conversation_deleted">Conversation deleted!</string>
|
||||
<string name="you_can_still_send_messages_to_contact">You can still send messages to %1$s from the Contacts tab.</string>
|
||||
<string name="contact_deleted">Contact deleted!</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the Chats tab.</string>
|
||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
@@ -617,6 +641,7 @@
|
||||
<!-- 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>
|
||||
@@ -685,6 +710,7 @@
|
||||
<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>
|
||||
@@ -1213,6 +1239,7 @@
|
||||
<string name="database_downgrade">Database downgrade</string>
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="one_hand_ui">One-hand UI</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
@@ -1459,6 +1486,8 @@
|
||||
<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>
|
||||
@@ -1604,6 +1633,8 @@
|
||||
<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>
|
||||
@@ -2075,4 +2106,85 @@
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M241.78-244.5 134-136.5q-13.5 13.5-31.25 6.52Q85-136.97 85-156.5V-818q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-840.97 875-818v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27H241.78Z"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 715 B |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 802 B |
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 492 B |
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -14,6 +14,7 @@ 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
|
||||
@@ -26,6 +27,7 @@ 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()
|
||||
@@ -195,7 +197,8 @@ 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, height = 768.dp)
|
||||
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height =
|
||||
768.dp)
|
||||
Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) {
|
||||
SimpleXTheme {
|
||||
TerminalView(ChatModel) { hiddenUntilRestart = true }
|
||||
|
||||
@@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!disabled) modifier = modifier
|
||||
|
||||
@@ -3,18 +3,29 @@ 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) {
|
||||
@@ -55,6 +66,56 @@ 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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=5.8.2
|
||||
android.version_code=223
|
||||
android.version_name=6.0-beta.0
|
||||
android.version_code=225
|
||||
|
||||
desktop.version_name=5.8.2
|
||||
desktop.version_code=55
|
||||
desktop.version_name=6.0-beta.0
|
||||
desktop.version_code=56
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
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 Hype’s Impact on Privacy
|
||||
|
||||
The exaggeration of AI’s 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 Meta’s 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 — 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 — 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 — 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.
|
||||
|
After Width: | Height: | Size: 196 KiB |