core, ui: replace map of network statuses with subscription status of current chat (#6353)

* core: subscription status wip

* update

* update

* update

* remove statuses core

* cleanup ios

* comment

* plans

* remove NetworkStatus

* ios wip

* contact sub status

* Revert "contact sub status"

This reverts commit 50cf94beed.

* sub status

* set on connected

* kotlin

* rename

* layout

* member status

* kotlin

* fix chat subscription status

* string

* core: update simplexmq

* client notices

* update simplexmq

* update alert

* update simplexmq

* android/desktop

* formatting

* fix tests

* update plans and API docs

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
spaced4ndy
2025-10-18 21:53:47 +00:00
committed by GitHub
parent 0dc33708a3
commit a65151ba6d
74 changed files with 529 additions and 527 deletions
+3 -49
View File
@@ -158,7 +158,6 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
// WebRTC calls /
case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?)
case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64])
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
@@ -359,7 +358,6 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))"
case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(onOff(unreadChat))"
@@ -534,7 +532,6 @@ enum ChatCommand: ChatCmdProtocol {
case .apiEndCall: return "apiEndCall"
case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus"
case .apiGetNetworkStatuses: return "apiGetNetworkStatuses"
case .apiChatRead: return "apiChatRead"
case .apiChatItemsRead: return "apiChatItemsRead"
case .apiChatUnread: return "apiChatUnread"
@@ -793,7 +790,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case userContactLinkDeleted(user: User)
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?)
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case newChatItems(user: UserRef, chatItems: [AChatItem])
case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set<Int64>, byUser: Bool, member_: GroupMember?)
case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?)
@@ -837,7 +833,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case .userContactLinkDeleted: "userContactLinkDeleted"
case .acceptingContactRequest: "acceptingContactRequest"
case .contactRequestRejected: "contactRequestRejected"
case .networkStatuses: "networkStatuses"
case .newChatItems: "newChatItems"
case .groupChatItemsDeleted: "groupChatItemsDeleted"
case .forwardPlan: "forwardPlan"
@@ -870,7 +865,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case .userContactLinkDeleted: return noDetails
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
case let .contactRequestRejected(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))")
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .newChatItems(u, chatItems):
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
return withUser(u, itemsString)
@@ -1059,8 +1053,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest, chat_: ChatData?)
case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
case networkStatus(networkStatus: NetworkStatus, connections: [String])
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case subscriptionStatus(subscriptionStatus: SubscriptionStatus, connections: [String])
case chatInfoUpdated(user: UserRef, chatInfo: ChatInfo)
case newChatItems(user: UserRef, chatItems: [AChatItem])
case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem])
@@ -1137,8 +1130,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case .receivedContactRequest: "receivedContactRequest"
case .contactUpdated: "contactUpdated"
case .groupMemberUpdated: "groupMemberUpdated"
case .networkStatus: "networkStatus"
case .networkStatuses: "networkStatuses"
case .subscriptionStatus: "subscriptionStatus"
case .chatInfoUpdated: "chatInfoUpdated"
case .newChatItems: "newChatItems"
case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated"
@@ -1210,8 +1202,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case let .receivedContactRequest(u, contactRequest, chat_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\nchat_: \(String(describing: chat_))")
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .subscriptionStatus(status, conns): return "subscriptionStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .chatInfoUpdated(u, chatInfo): return withUser(u, String(describing: chatInfo))
case let .newChatItems(u, chatItems):
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
@@ -1371,38 +1362,6 @@ enum ChatDeleteMode: Codable {
}
}
enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
case error(connectionError: String)
var statusString: LocalizedStringKey {
switch self {
case .connected: "connected"
case .error: "error"
default: "connecting"
}
}
var statusExplanation: LocalizedStringKey {
switch self {
case .connected: "You are connected to the server used to receive messages from this contact."
case let .error(err): "Trying to connect to the server used to receive messages from this contact (error: \(err))."
default: "Trying to connect to the server used to receive messages from this contact."
}
}
var imageName: String {
switch self {
case .unknown: "circle.dotted"
case .connected: "circle.fill"
case .disconnected: "ellipsis.circle.fill"
case .error: "exclamationmark.circle.fill"
}
}
}
enum ForwardConfirmation: Decodable, Hashable {
case filesNotAccepted(fileIds: [Int64])
case filesInProgress(filesCount: Int)
@@ -1410,11 +1369,6 @@ enum ForwardConfirmation: Decodable, Hashable {
case filesFailed(filesCount: Int)
}
struct ConnNetworkStatus: Decodable {
var agentConnId: String
var networkStatus: NetworkStatus
}
struct UserMsgReceiptSettings: Codable {
var enable: Bool
var clearOverrides: Bool
+2 -23
View File
@@ -283,29 +283,6 @@ class ChatTagsModel: ObservableObject {
}
}
class NetworkModel: ObservableObject {
// map of connections network statuses, key is agent connection id
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
static let shared = NetworkModel()
private init() { }
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
if let conn = contact.activeConn {
networkStatuses[conn.agentConnId] = status
}
}
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
if let conn = contact.activeConn {
networkStatuses[conn.agentConnId] ?? .unknown
} else {
.unknown
}
}
}
/// ChatItemWithMenu can depend on previous or next item for it's appearance
/// This dummy model is used to force an update of all chat items,
/// when they might have changed appearance.
@@ -374,6 +351,8 @@ final class ChatModel: ObservableObject {
@Published var deletedChats: Set<String> = []
// current chat
@Published var chatId: String?
@Published var chatAgentConnId: String?
@Published var chatSubStatus: SubscriptionStatus?
@Published var openAroundItemId: ChatItem.ID? = nil
@Published var chatToTop: String?
@Published var groupMembers: [GMember] = []
+39 -49
View File
@@ -15,8 +15,6 @@ import SwiftUI
private var chatController: chat_ctrl?
private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock")
enum TerminalItem: Identifiable {
case cmd(Date, ChatCommand)
case res(Date, ChatAPIResult)
@@ -1319,6 +1317,10 @@ func apiCreateUserAddress() async throws -> CreatedConnLink? {
let userId = try currentUserId("apiCreateUserAddress")
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiCreateMyAddress(userId: userId))
if case let .result(.userContactLinkCreated(_, connLink)) = r { return connLink }
if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r {
showClientNotice(server, preset, expires)
return nil
}
if let r { throw r.unexpected } else { return nil }
}
@@ -1640,7 +1642,6 @@ func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress:
} else {
ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat)
}
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
inProgress?.wrappedValue = false
}
if contact.sndReady {
@@ -1728,12 +1729,6 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
}
}
func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
let r: ChatResponse1 = try chatSendCmdSync(.apiGetNetworkStatuses)
if case let .networkStatuses(_, statuses) = r { return statuses }
throw r.unexpected
}
func markChatRead(_ im: ItemsModel, _ chat: Chat) async {
do {
if chat.chatStats.unreadCount > 0 {
@@ -1899,6 +1894,10 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? {
let r: APIResult<ChatResponse2>? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink }
if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r {
showClientNotice(server, preset, expires)
return nil
}
if let r { throw r.unexpected } else { return nil }
}
@@ -1955,7 +1954,6 @@ func acceptMemberContact(contactId: Int64, inProgress: Binding<Bool>? = nil) asy
if let contact = await apiAcceptMemberContact(contactId: contactId) {
await MainActor.run {
ChatModel.shared.updateContact(contact)
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
inProgress?.wrappedValue = false
}
if contact.sndReady {
@@ -2241,7 +2239,6 @@ class ChatReceiver {
func processReceivedMsg(_ res: ChatEvent) async {
let m = ChatModel.shared
let n = NetworkModel.shared
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .contactDeletedByContact(user, contact):
@@ -2258,14 +2255,15 @@ func processReceivedMsg(_ res: ChatEvent) async {
m.dismissConnReqView(conn.id)
m.removeChat(conn.id)
}
if contact.id == m.chatId, let conn = contact.activeConn {
m.chatAgentConnId = conn.agentConnId
m.chatSubStatus = .active
}
}
}
if contact.directOrUsed {
NtfManager.shared.notifyContactConnected(user, contact)
}
await MainActor.run {
n.setContactNetworkStatus(contact, .connected)
}
case let .contactConnecting(user, contact):
if active(user) && contact.directOrUsed {
await MainActor.run {
@@ -2286,9 +2284,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
}
}
}
await MainActor.run {
n.setContactNetworkStatus(contact, .connected)
}
case let .receivedContactRequest(user, contactRequest, chat_):
if active(user) {
await MainActor.run {
@@ -2327,30 +2322,10 @@ func processReceivedMsg(_ res: ChatEvent) async {
_ = m.upsertGroupMember(groupInfo, toMember)
}
}
case let .networkStatus(status, connections):
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = n.networkStatuses
// slow loop is on the background thread
for cId in connections {
ns[cId] = status
}
// fast model update is on the main thread
DispatchQueue.main.sync {
n.networkStatuses = ns
}
}
case let .networkStatuses(_, statuses): ()
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = n.networkStatuses
// slow loop is on the background thread
for s in statuses {
ns[s.agentConnId] = s.networkStatus
}
// fast model update is on the main thread
DispatchQueue.main.sync {
n.networkStatuses = ns
case let .subscriptionStatus(status, connections):
if let chatAgentConnId = m.chatAgentConnId, connections.contains(chatAgentConnId) {
await MainActor.run {
m.chatSubStatus = status
}
}
case let .chatInfoUpdated(user, chatInfo):
@@ -2554,11 +2529,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
_ = m.upsertGroupMember(groupInfo, member)
}
}
if let contact = memberContact {
await MainActor.run {
n.setContactNetworkStatus(contact, .connected)
}
}
case let .groupUpdated(user, toGroup):
if active(user) {
await MainActor.run {
@@ -2784,13 +2754,10 @@ func processReceivedMsg(_ res: ChatEvent) async {
func switchToLocalSession() {
let m = ChatModel.shared
let n = NetworkModel.shared
m.remoteCtrlSession = nil
do {
m.users = try listUsers()
try getUserChatData()
let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) }
n.networkStatuses = Dictionary(uniqueKeysWithValues: statuses)
} catch let error {
logger.debug("error updating chat data: \(responseError(error))")
}
@@ -2898,3 +2865,26 @@ private struct UserResponse: Decodable {
var user: User?
var error: String?
}
private func showClientNotice(_ server: String, _ preset: Bool, _ expiresAt: Date?) {
DispatchQueue.main.async {
var message = "Server: \(server).\nConditions of use violation notice received from \(preset ? "preset" : "this") server.\nNo IDs shared, see How it works."
if let expiresAt {
message += "\n\nNew addresses can be created after \(expiresAt.formatted(date: .abbreviated, time: .shortened))."
}
showAlert("Not allowed", message: message) {
let howItWorks = UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in
UIApplication.shared.open(contentModerationPostLink)
})
return preset
? [
okAlertAction,
UIAlertAction(title: NSLocalizedString("Conditions of use", comment: "alert button"), style: .default, handler: { _ in
UIApplication.shared.open(conditionsURL)
}),
howItWorks
]
: [okAlertAction, howItWorks]
}
}
}
@@ -12,6 +12,7 @@ import SimpleXChat
struct ChatInfoToolbar: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var m: ChatModel
@ObservedObject var chat: Chat
var imageSize: CGFloat = 32
@@ -56,6 +57,13 @@ struct ChatInfoToolbar: View {
.padding(.top, -2)
}
}
if let contact = chat.chatInfo.contact,
contact.ready && contact.active,
let chatSubStatus = m.chatSubStatus,
chatSubStatus != .active {
SubStatusView(status: chatSubStatus)
.padding(.leading, 4)
}
}
.foregroundColor(theme.colors.onBackground)
.frame(width: 220)
@@ -68,6 +76,30 @@ struct ChatInfoToolbar: View {
.baselineOffset(1)
.kerning(-2)
}
struct SubStatusView: View {
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@EnvironmentObject var theme: AppTheme
var status: SubscriptionStatus
var body: some View {
switch status {
case .active: EmptyView()
case .pending: ProgressView()
case .removed: subStatusError()
case .noSub: subStatusError()
}
}
@ViewBuilder private func subStatusError() -> some View {
let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize
Image(systemName: "exclamationmark.circle")
.resizable()
.scaledToFit()
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
.foregroundColor(theme.colors.secondary)
}
}
}
struct ChatInfoToolbar_Previews: PreviewProvider {
+35 -30
View File
@@ -92,7 +92,6 @@ struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var networkModel = NetworkModel.shared
@ObservedObject var chat: Chat
@State var contact: Contact
@State var localAlias: String
@@ -115,7 +114,7 @@ struct ChatInfoView: View {
enum ChatInfoViewAlert: Identifiable {
case clearChatAlert
case networkStatusAlert
case subStatusAlert(status: SubscriptionStatus)
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
@@ -126,7 +125,7 @@ struct ChatInfoView: View {
var id: String {
switch self {
case .clearChatAlert: return "clearChatAlert"
case .networkStatusAlert: return "networkStatusAlert"
case let .subStatusAlert(status): return "subStatusAlert \(status)"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
@@ -236,10 +235,12 @@ struct ChatInfoView: View {
if contact.ready && contact.active {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
networkStatusRow()
.onTapGesture {
alert = .networkStatusAlert
}
if let chatSubStatus = chatModel.chatSubStatus {
SubStatusRow(status: chatSubStatus)
.onTapGesture {
alert = .subStatusAlert(status: chatSubStatus)
}
}
if let connStats = connectionStats {
Button("Change receiving address") {
alert = .switchAddressAlert
@@ -325,7 +326,7 @@ struct ChatInfoView: View {
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .clearChatAlert: return clearChatAlert()
case .networkStatusAlert: return networkStatusAlert()
case let .subStatusAlert(status): return subStatusAlert(status)
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case .syncConnectionForceAlert:
@@ -546,26 +547,6 @@ struct ChatInfoView: View {
}
}
private func networkStatusRow() -> some View {
HStack {
Text("Network status")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
Spacer()
Text(networkModel.contactNetworkStatus(contact).statusString)
.foregroundColor(theme.colors.secondary)
serverImage()
}
}
private func serverImage() -> some View {
let status = networkModel.contactNetworkStatus(contact)
return Image(systemName: status.imageName)
.foregroundColor(status == .connected ? .green : theme.colors.secondary)
.font(.system(size: 12))
}
private func deleteContactButton() -> some View {
Button(role: .destructive) {
deleteContactDialog(
@@ -605,10 +586,10 @@ struct ChatInfoView: View {
)
}
private func networkStatusAlert() -> Alert {
private func subStatusAlert(_ status: SubscriptionStatus) -> Alert {
Alert(
title: Text("Network status"),
message: Text(networkModel.contactNetworkStatus(contact).statusExplanation)
message: Text(status.statusExplanation)
)
}
@@ -667,6 +648,30 @@ struct ChatInfoView: View {
}
}
struct SubStatusRow: View {
@EnvironmentObject var theme: AppTheme
var status: SubscriptionStatus
var body: some View {
HStack {
Text("Network status")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
Spacer()
Text(status.statusString)
.foregroundColor(theme.colors.secondary)
serverImage(status)
}
}
private func serverImage(_ status: SubscriptionStatus) -> some View {
return Image(systemName: status.imageName)
.foregroundColor(status == .active ? .green : theme.colors.secondary)
.font(.system(size: 12))
}
}
struct ChatTTLOption: View {
@ObservedObject var chat: Chat
@Binding var progressIndicator: Bool
+24 -10
View File
@@ -393,6 +393,8 @@ struct ChatView: View {
if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
chatModel.chatAgentConnId = nil
chatModel.chatSubStatus = nil
im.reversedChatItems = []
im.chatState.clear()
chatModel.groupMembers = []
@@ -657,18 +659,30 @@ struct ChatView: View {
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
if case .active = scenePhase,
case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
await MainActor.run {
if let s = stats {
chatModel.updateContactConnectionStats(contact, s)
if case .active = scenePhase {
if case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
await MainActor.run {
if let s = stats {
chatModel.updateContactConnectionStats(contact, s)
if let conn = contact.activeConn {
chatModel.chatAgentConnId = conn.agentConnId
chatModel.chatSubStatus = s.subStatus
}
}
}
} catch let error {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
} else {
Task {
await MainActor.run {
chatModel.chatAgentConnId = nil
chatModel.chatSubStatus = nil
}
} catch let error {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
}
@@ -788,7 +788,6 @@ struct ComposeView: View {
await MainActor.run {
self.chatModel.updateContact(contact)
clearState()
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
}
} else {
AlertManager.shared.showAlertMsg(title: "Empty message!")
@@ -827,7 +826,6 @@ struct ComposeView: View {
if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognito, msg: mc) {
await MainActor.run {
self.chatModel.updateContact(contact)
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
clearState()
}
} else {
@@ -160,7 +160,15 @@ struct GroupMemberInfoView: View {
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
if let subStatus = connStats.subStatus {
SubStatusRow(status: subStatus)
.onTapGesture {
showAlert(
NSLocalizedString("Network status", comment: "alert title"),
message: subStatus.statusExplanation
)
}
}
Button("Change receiving address") {
alert = .switchAddressAlert
}
@@ -396,7 +404,6 @@ struct GroupMemberInfoView: View {
ItemsModel.shared.loadOpenChat(memberContact.id) {
dismissAllSheets(animated: true)
}
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
@@ -460,12 +460,6 @@ struct ChatPreviewView: View {
@ViewBuilder private func chatStatusImage() -> some View {
let size = dynamicSize(userFont).incognitoSize
switch chat.chatInfo {
case let .direct(contact):
if contact.active, let status = contact.activeConn?.connStatus, status == .ready || status == .sndReady {
NetworkStatusView(contact: contact, size: size)
} else {
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
}
case .group:
if progressByTimeout {
ProgressView()
@@ -482,30 +476,6 @@ struct ChatPreviewView: View {
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
}
}
struct NetworkStatusView: View {
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@EnvironmentObject var theme: AppTheme
@ObservedObject var networkModel = NetworkModel.shared
let contact: Contact
let size: CGFloat
var body: some View {
let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize
switch (networkModel.contactNetworkStatus(contact)) {
case .connected: incognitoIcon(contact.contactConnIncognito, theme.colors.secondary, size: size)
case .error:
Image(systemName: "exclamationmark.circle")
.resizable()
.scaledToFit()
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
.foregroundColor(theme.colors.secondary)
default:
ProgressView()
}
}
}
}
@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color, size: CGFloat) -> some View {
@@ -196,12 +196,13 @@ struct UserAddressView: View {
progressIndicator = true
Task {
do {
if let connLinkContact = try await apiCreateUserAddress() {
DispatchQueue.main.async {
let connLinkContact = try await apiCreateUserAddress()
DispatchQueue.main.async {
if let connLinkContact {
chatModel.userAddress = UserContactLink(connLinkContact)
alert = .shareOnCreate
progressIndicator = false
}
progressIndicator = false
}
} catch let error {
logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))")
+8 -8
View File
@@ -178,8 +178,8 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
@@ -545,8 +545,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -708,8 +708,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -795,8 +795,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */,
);
path = Libraries;
sourceTree = "<group>";
+51 -4
View File
@@ -545,6 +545,7 @@ public struct ConnectionStats: Decodable, Hashable {
public var sndQueuesInfo: [SndQueueInfo]
public var ratchetSyncState: RatchetSyncState
public var ratchetSyncSupported: Bool
public var subStatus: SubscriptionStatus?
public var ratchetSyncAllowed: Bool {
ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState)
@@ -559,25 +560,28 @@ public struct ConnectionStats: Decodable, Hashable {
}
}
public struct RcvQueueInfo: Codable, Hashable {
public struct RcvQueueInfo: Decodable, Hashable {
public var rcvServer: String
public var status: QueueStatus
public var rcvSwitchStatus: RcvSwitchStatus?
public var canAbortSwitch: Bool
public var subStatus: SubscriptionStatus
}
public enum RcvSwitchStatus: String, Codable, Hashable {
public enum RcvSwitchStatus: String, Decodable, Hashable {
case switchStarted = "switch_started"
case sendingQADD = "sending_qadd"
case sendingQUSE = "sending_quse"
case receivedMessage = "received_message"
}
public struct SndQueueInfo: Codable, Hashable {
public struct SndQueueInfo: Decodable, Hashable {
public var sndServer: String
public var status: QueueStatus
public var sndSwitchStatus: SndSwitchStatus?
}
public enum SndSwitchStatus: String, Codable, Hashable {
public enum SndSwitchStatus: String, Decodable, Hashable {
case sendingQKEY = "sending_qkey"
case sendingQTEST = "sending_qtest"
}
@@ -606,6 +610,48 @@ public enum RatchetSyncState: String, Decodable {
case agreed
}
public enum QueueStatus: String, Decodable, Hashable {
case new
case confirmed
case secured
case active
case disabled
}
public enum SubscriptionStatus: Decodable, Hashable {
case active
case pending
case removed(subError: String)
case noSub
public var statusString: LocalizedStringKey {
switch self {
case .active: "connected"
case .pending: "connecting"
case .removed: "error"
case .noSub: "no subscription"
}
}
public var statusExplanation: String {
switch self {
case .active: NSLocalizedString("You are connected to the server used to receive messages from this connection.", comment: "subscription status explanation")
case .pending: NSLocalizedString("Trying to connect to the server used to receive messages from this connection.", comment: "subscription status explanation")
case let .removed(err): String.localizedStringWithFormat(NSLocalizedString("Error connecting to the server used to receive messages from this connection: %@", comment: "subscription status explanation"), err)
case .noSub: NSLocalizedString("You are not connected to the server used to receive messages from this connection (no subscription).", comment: "subscription status explanation")
}
}
public var imageName: String {
switch self {
case .active: "circle.fill"
case .pending: "ellipsis.circle.fill"
case .removed: "exclamationmark.circle.fill"
case .noSub: "circle.dotted"
}
}
}
public protocol SelectableItem: Identifiable, Equatable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
@@ -833,6 +879,7 @@ public enum AgentErrorType: Decodable, Hashable {
case RCP(rcpErr: RCErrorType)
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
case AGENT(agentErr: SMPAgentError)
case NOTICE(server: String, preset: Bool, expiresAt: Date?)
case INTERNAL(internalErr: String)
case CRITICAL(offerRestart: Bool, criticalErr: String)
case INACTIVE
@@ -96,11 +96,12 @@ object ChatModel {
val dbMigrationInProgress = mutableStateOf(false)
val incompleteInitializedDbRemoved = mutableStateOf(false)
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
val switchingUsersAndHosts = mutableStateOf(false)
// current chat
val chatId = mutableStateOf<String?>(null)
val chatAgentConnId = mutableStateOf<String?>(null)
val chatSubStatus = mutableStateOf<SubscriptionStatus?>(null)
val openAroundItemId: MutableState<Long?> = mutableStateOf(null)
val chatsContext = ChatsContext(null)
val secondaryChatsContext = mutableStateOf<ChatsContext?>(null)
@@ -1147,21 +1148,6 @@ object ChatModel {
showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true)
}
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
val conn = contact.activeConn
if (conn != null) {
networkStatuses[conn.agentConnId] = status
}
}
fun contactNetworkStatus(contact: Contact): NetworkStatus {
val conn = contact.activeConn
return if (conn != null)
networkStatuses[conn.agentConnId] ?: NetworkStatus.Unknown()
else
NetworkStatus.Unknown()
}
fun addTerminalItem(item: TerminalItem) {
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
if (terminalsVisible.isNotEmpty()) {
@@ -1721,30 +1707,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
}
@Serializable
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(MR.strings.server_connected)
is Error -> generalGetString(MR.strings.server_error)
else -> generalGetString(MR.strings.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError)
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus()
}
@Serializable
data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus)
@Serializable
data class Contact(
val contactId: Long,
@@ -13,6 +13,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -27,12 +28,14 @@ import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.item.contentModerationPostLink
import chat.simplex.common.views.chat.item.showContentBlockedAlert
import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.migration.MigrationFileLinkData
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.networkAndServers.defaultConditionsLink
import chat.simplex.common.views.usersettings.networkAndServers.serverHostname
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
@@ -46,12 +49,17 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Date
typealias ChatCtrl = Long
@@ -1709,6 +1717,11 @@ object ChatController {
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
val r = sendCmdWithRetry(rh, CC.ApiCreateMyAddress(userId))
if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact
if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) {
val e = r.err.agentError
showClientNoticeAlert(e.server, e.preset, e.expiresAt)
return null
}
if (r == null) return null
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r)
@@ -1842,13 +1855,6 @@ object ChatController {
return r.result is CR.CmdOk
}
suspend fun apiGetNetworkStatuses(rh: Long?): List<ConnNetworkStatus>? {
val r = sendCmd(rh, CC.ApiGetNetworkStatuses())
if (r is API.Result && r.res is CR.NetworkStatuses) return r.res.networkStatuses
Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean {
val r = sendCmd(rh, CC.ApiChatRead(type, id, scope = null))
if (r.result is CR.CmdOk) return true
@@ -2175,6 +2181,11 @@ object ChatController {
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? {
val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole))
if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink
if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) {
val e = r.err.agentError
showClientNoticeAlert(e.server, e.preset, e.expiresAt)
return null
}
if (r == null) return null
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r)
@@ -2546,12 +2557,15 @@ object ChatController {
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
chatModel.chatsContext.removeChat(rhId, conn.id)
}
if (r.contact.id == chatModel.chatId.value && conn != null) {
chatModel.chatAgentConnId.value = conn.agentConnId
chatModel.chatSubStatus.value = SubscriptionStatus.Active
}
}
}
if (r.contact.directOrUsed) {
ntfManager.notifyContactConnected(r.user, r.contact)
}
chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected())
}
is CR.ContactConnecting -> {
if (active(r.user) && r.contact.directOrUsed) {
@@ -2576,7 +2590,6 @@ object ChatController {
}
}
}
chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected())
}
is CR.ReceivedContactRequest -> {
val contactRequest = r.contactRequest
@@ -2618,18 +2631,12 @@ object ChatController {
}
}
}
// ContactsSubscribed, ContactsDisconnected are only used in CLI,
// They have to be used here for remote desktop to process these status updates.
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
is CR.NetworkStatusResp -> {
for (cId in r.connections) {
chatModel.networkStatuses[cId] = r.networkStatus
}
}
is CR.NetworkStatuses -> {
for (s in r.networkStatuses) {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
is CR.SubscriptionStatusEvt -> {
val chatAgentConnId = chatModel.chatAgentConnId.value
if (chatAgentConnId != null && r.connections.contains(chatAgentConnId)) {
withContext(Dispatchers.Main) {
chatModel.chatSubStatus.value = r.subscriptionStatus
}
}
}
is CR.ChatInfoUpdated ->
@@ -2916,9 +2923,6 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
if (r.memberContact != null) {
chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected())
}
}
is CR.GroupUpdated ->
if (active(r.user)) {
@@ -3231,12 +3235,6 @@ object ChatController {
m.users.clear()
m.users.addAll(users)
getUserChatData(null)
val statuses = apiGetNetworkStatuses(null)
if (statuses != null) {
chatModel.networkStatuses.clear()
val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap()
chatModel.networkStatuses.putAll(ss)
}
}
private fun activeUser(rhId: Long?, user: UserLike): Boolean =
@@ -3349,12 +3347,6 @@ object ChatController {
}
}
private fun updateContactsStatus(contactRefs: List<ContactRef>, status: NetworkStatus) {
for (c in contactRefs) {
chatModel.networkStatuses[c.agentConnId] = status
}
}
suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded {
// TODO lock the switch so that two switches can't run concurrently?
chatModel.chatId.value = null
@@ -3381,12 +3373,6 @@ object ChatController {
chatModel.secondaryChatsContext.value?.popChatCollector?.clear()
}
}
val statuses = apiGetNetworkStatuses(rhId)
if (statuses != null) {
chatModel.networkStatuses.clear()
val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap()
chatModel.networkStatuses.putAll(ss)
}
getUserChatData(rhId)
}
@@ -3645,7 +3631,6 @@ sealed class CC {
class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC()
class ApiEndCall(val contact: Contact): CC()
class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC()
class ApiGetNetworkStatuses(): CC()
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
@@ -3844,7 +3829,6 @@ sealed class CC {
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiGetNetworkStatuses -> "/_network_statuses"
is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}"
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}"
@@ -4019,7 +4003,6 @@ sealed class CC {
is ApiSendCallExtraInfo -> "apiSendCallExtraInfo"
is ApiEndCall -> "apiEndCall"
is ApiCallStatus -> "apiCallStatus"
is ApiGetNetworkStatuses -> "apiGetNetworkStatuses"
is ApiChatRead -> "apiChatRead"
is ApiChatItemsRead -> "apiChatItemsRead"
is ApiChatUnread -> "apiChatUnread"
@@ -6159,12 +6142,7 @@ sealed class CR {
@Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR()
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
@Serializable @SerialName("groupMemberUpdated") class GroupMemberUpdated(val user: UserRef, val groupInfo: GroupInfo, val fromMember: GroupMember, val toMember: GroupMember): CR()
// TODO remove below
@Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR()
@Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR()
// TODO remove above
@Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List<String>): CR()
@Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List<ConnNetworkStatus>): CR()
@Serializable @SerialName("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List<String>): CR()
@Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR()
@Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR()
@Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List<AChatItem>): CR()
@@ -6345,10 +6323,7 @@ sealed class CR {
is ContactRequestRejected -> "contactRequestRejected"
is ContactUpdated -> "contactUpdated"
is GroupMemberUpdated -> "groupMemberUpdated"
is ContactsSubscribed -> "contactsSubscribed"
is ContactsDisconnected -> "contactsDisconnected"
is NetworkStatusResp -> "networkStatus"
is NetworkStatuses -> "networkStatuses"
is SubscriptionStatusEvt -> "subscriptionStatus"
is ChatInfoUpdated -> "chatInfoUpdated"
is NewChatItems -> "newChatItems"
is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated"
@@ -6521,10 +6496,7 @@ sealed class CR {
is ContactRequestRejected -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}")
is ContactUpdated -> withUser(user, json.encodeToString(toContact))
is GroupMemberUpdated -> withUser(user, "groupInfo: $groupInfo\nfromMember: $fromMember\ntoMember: $toMember")
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections"
is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses))
is SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections"
is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo))
is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
@@ -6747,7 +6719,8 @@ class ConnectionStats(
val rcvQueuesInfo: List<RcvQueueInfo>,
val sndQueuesInfo: List<SndQueueInfo>,
val ratchetSyncState: RatchetSyncState,
val ratchetSyncSupported: Boolean
val ratchetSyncSupported: Boolean,
var subStatus: SubscriptionStatus?
) {
val ratchetSyncAllowed: Boolean get() =
ratchetSyncSupported && listOf(RatchetSyncState.Allowed, RatchetSyncState.Required).contains(ratchetSyncState)
@@ -6762,8 +6735,10 @@ class ConnectionStats(
@Serializable
class RcvQueueInfo(
val rcvServer: String,
var status: QueueStatus,
val rcvSwitchStatus: RcvSwitchStatus?,
var canAbortSwitch: Boolean
var canAbortSwitch: Boolean,
var subStatus: SubscriptionStatus
)
@Serializable
@@ -6777,6 +6752,7 @@ enum class RcvSwitchStatus {
@Serializable
class SndQueueInfo(
val sndServer: String,
var status: QueueStatus,
val sndSwitchStatus: SndSwitchStatus?
)
@@ -6814,6 +6790,39 @@ enum class RatchetSyncState {
@SerialName("agreed") Agreed
}
@Serializable
enum class QueueStatus {
@SerialName("new") New,
@SerialName("confirmed") Confirmed,
@SerialName("secured") Secured,
@SerialName("active") Active,
@SerialName("disabled") Disabled
}
@Serializable
sealed class SubscriptionStatus {
@Serializable @SerialName("active") object Active: SubscriptionStatus()
@Serializable @SerialName("pending") object Pending: SubscriptionStatus()
@Serializable @SerialName("removed") class Removed(val subError: String): SubscriptionStatus()
@Serializable @SerialName("noSub") object NoSub: SubscriptionStatus()
val statusString: String get() =
when (this) {
is Active -> generalGetString(MR.strings.server_connected)
is Pending -> generalGetString(MR.strings.server_connecting)
is Removed -> generalGetString(MR.strings.server_error)
is NoSub -> generalGetString(MR.strings.server_no_sub)
}
val statusExplanation: String get() =
when (this) {
is Active -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
is Pending -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
is Removed -> String.format(generalGetString(MR.strings.error_connecting_to_server_to_receive_messages), subError)
is NoSub -> generalGetString(MR.strings.not_connected_to_server_to_receive_messages_no_sub)
}
}
interface SimplexAddress {
val connLinkContact: CreatedConnLink
val shortLinkDataSet: Boolean
@@ -7306,6 +7315,7 @@ sealed class AgentErrorType {
is RCP -> "RCP ${rcpErr.string}"
is BROKER -> "BROKER ${brokerErr.string}"
is AGENT -> "AGENT ${agentErr.string}"
is NOTICE -> "NOTICE $server $expiresAt"
is INTERNAL -> "INTERNAL $internalErr"
is CRITICAL -> "CRITICAL $offerRestart $criticalErr"
is INACTIVE -> "INACTIVE"
@@ -7319,6 +7329,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType()
@Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType()
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
@Serializable @SerialName("NOTICE") class NOTICE(val server: String, val preset: Boolean, val expiresAt: Instant?): AgentErrorType()
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
@Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType()
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
@@ -8046,3 +8057,34 @@ enum class MsgType {
@SerialName("quota")
QUOTA
}
fun showClientNoticeAlert(server: String, preset: Boolean, expiresAt: Instant?) {
var message = "Server: $server.\nConditions of use violation notice received from ${if (preset) "preset" else "this"} server.\nNo ID shared, see How it works."
if (expiresAt != null) {
val tz = TimeZone.currentSystemDefault()
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
message += "\n\nNew addresses can be created after ${expiresAt.toLocalDateTime(tz).toJavaLocalDateTime().format(formatter)}."
}
AlertManager.shared.showAlertDialogButtonsColumn(title = "Not allowed", text = AnnotatedString(message)) {
val uriHandler = LocalUriHandler.current
Column {
SectionItemView({ AlertManager.shared.hideAlert() }) {
Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
if (preset) {
SectionItemView({
AlertManager.shared.hideAlert()
uriHandler.openUriCatching(defaultConditionsLink)
}) {
Text(generalGetString(MR.strings.operator_conditions_of_use), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
SectionItemView({
AlertManager.shared.hideAlert()
uriHandler.openUriCatching(contentModerationPostLink)
}) {
Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
}
@@ -72,9 +72,6 @@ fun ChatInfoView(
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null && currentUser != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
val chatRh = chat.remoteHostId
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) }
@@ -101,7 +98,6 @@ fun ChatInfoView(
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
connStats = connStats,
contactNetworkStatus.value,
customUserProfile,
localAlias,
connectionCode,
@@ -524,7 +520,6 @@ fun ChatInfoLayout(
chatItemTTL: MutableState<ChatItemTTL?>,
setChatItemTTL: (ChatItemTTL?) -> Unit,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
@@ -643,13 +638,16 @@ fun ChatInfoLayout(
if (contact.ready && contact.active) {
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
contactNetworkStatus.statusExplanation
)
}) {
NetworkStatusRow(contactNetworkStatus)
val chatSubStatus = chatModel.chatSubStatus.value
if (chatSubStatus != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
chatSubStatus.statusExplanation
)
}) {
SubStatusRow(chatSubStatus)
}
}
if (cStats != null) {
SwitchAddressButton(
@@ -1063,7 +1061,7 @@ fun InfoViewActionButton(
}
@Composable
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
fun SubStatusRow(subStatus: SubscriptionStatus) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -1086,25 +1084,24 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) {
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
networkStatus.statusString,
subStatus.statusString,
color = MaterialTheme.colors.secondary
)
ServerImage(networkStatus)
ServerImage(subStatus)
}
}
}
@Composable
private fun ServerImage(networkStatus: NetworkStatus) {
private fun ServerImage(subStatus: SubscriptionStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is NetworkStatus.Connected ->
when (subStatus) {
is SubscriptionStatus.Active ->
Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = Color.Green)
is NetworkStatus.Disconnected ->
is SubscriptionStatus.Pending ->
Icon(painterResource(MR.images.ic_pending_filled), stringResource(MR.strings.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
is NetworkStatus.Error ->
is SubscriptionStatus.Removed, SubscriptionStatus.NoSub ->
Icon(painterResource(MR.images.ic_error_filled), stringResource(MR.strings.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
else -> Icon(painterResource(MR.images.ic_circle), stringResource(MR.strings.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
}
}
}
@@ -1455,7 +1452,6 @@ fun PreviewChatInfoLayout() {
connectionCode = "123",
developerTools = false,
connStats = remember { mutableStateOf(null) },
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {},
customUserProfile = null,
openPreferences = {},
@@ -102,6 +102,7 @@ fun ChatView(
val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }
// They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..."
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
val chatRh = remoteHostId.value
val activeChat = remember { derivedStateOf {
var chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }
val chatInfo = chat?.chatInfo
@@ -120,6 +121,8 @@ fun ChatView(
if (chat == null || chatInfo == null || user == null) {
LaunchedEffect(Unit) {
chatModel.chatId.value = null
chatModel.chatAgentConnId.value = null
chatModel.chatSubStatus.value = null
ModalManager.end.closeModals()
}
} else {
@@ -168,6 +171,25 @@ fun ChatView(
showSearch.value = false
searchText.value = ""
selectedChatItems.value = null
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) {
withBGApi {
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
if (r != null) {
val contactStats = r.first
if (contactStats != null)
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateContactConnectionStats(chatRh, chat.chatInfo.contact, contactStats)
chatModel.chatAgentConnId.value = chat.chatInfo.contact.activeConn.agentConnId
chatModel.chatSubStatus.value = contactStats.subStatus
}
}
}
} else {
withContext(Dispatchers.Main) {
chatModel.chatAgentConnId.value = null
chatModel.chatSubStatus.value = null
}
}
}
}
}
@@ -184,7 +206,6 @@ fun ChatView(
}
}
val view = LocalMultiplatformView()
val chatRh = remoteHostId.value
// We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags)
val unreadCount = remember {
@@ -356,7 +377,7 @@ fun ChatView(
if (chatInfo is ChatInfo.Direct) {
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
var code: String? by remember { mutableStateOf(preloadedCode) }
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
KeyChangeEffect(chatInfo.id) {
contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
@@ -1281,9 +1302,51 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
)
}
}
val chatSubStatus = chatModel.chatSubStatus.value
if (
cInfo is ChatInfo.Direct &&
cInfo.contact.ready &&
cInfo.contact.active &&
chatSubStatus != null &&
chatSubStatus != SubscriptionStatus.Active
) {
Box(
Modifier.padding(start = 10.dp)
) {
SubStatusView(chatSubStatus)
}
}
}
}
@Composable
fun SubStatusView(status: SubscriptionStatus) {
when (status) {
SubscriptionStatus.Active ->
Box {}
SubscriptionStatus.Pending ->
SubProgressView()
is SubscriptionStatus.Removed, SubscriptionStatus.NoSub ->
Icon(
painterResource(MR.images.ic_error),
contentDescription = null,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(19.sp.toDp())
)
}
}
@Composable
private fun SubProgressView() {
CircularProgressIndicator(
Modifier
.size(15.sp.toDp()),
color = MaterialTheme.colors.secondary,
strokeWidth = 1.5.dp
)
}
@Composable
private fun SupportChatsCountToolbar(
chatInfo: ChatInfo,
@@ -162,7 +162,6 @@ fun acceptMemberContact(
chatModel.chatsContext.updateContact(rhId, contact)
inProgress?.value = false
}
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
close?.invoke(chat)
} else {
@@ -533,7 +533,6 @@ fun ComposeView(
withContext(Dispatchers.Main) {
chatsCtx.updateContact(chat.remoteHostId, contact)
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
} else {
composeState.value = composeState.value.copy(inProgress = false)
@@ -554,7 +553,6 @@ fun ComposeView(
withContext(Dispatchers.Main) {
chatsCtx.updateContact(chat.remoteHostId, contact)
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
} else {
composeState.value = composeState.value.copy(inProgress = false)
@@ -109,7 +109,6 @@ fun GroupMemberInfoView(
}
openDirectChat(rhId, memberContact.contactId)
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}
progressIndicator = false
}
@@ -495,6 +494,17 @@ fun GroupMemberInfoLayout(
if (cStats != null) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
val subStatus = cStats.subStatus
if (subStatus != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
subStatus.statusExplanation
)
}) {
SubStatusRow(subStatus)
}
}
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || !member.sendMsgEnabled,
switchAddress = switchMemberAddress
@@ -62,12 +62,11 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
when (chat.chatInfo) {
is ChatInfo.Direct -> {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
val defaultClickAction = { if (chatModel.chatId.value != chat.id) scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } }
ChatListNavLinkLayout(
chatLinkPreview = {
tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction)
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction)
}
},
click = defaultClickAction,
@@ -87,7 +86,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
ChatListNavLinkLayout(
chatLinkPreview = {
tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction)
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction)
}
},
click = defaultClickAction,
@@ -107,7 +106,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
ChatListNavLinkLayout(
chatLinkPreview = {
tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) {
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction)
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction)
}
},
click = defaultClickAction,
@@ -744,7 +743,6 @@ fun acceptContactRequest(
}
inProgress?.value = false
}
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
close?.invoke(chat)
} else {
inProgress?.value = false
@@ -1057,7 +1055,6 @@ fun PreviewChatListNavLinkDirect() {
null,
null,
null,
null,
disabled = false,
linkMode = SimplexLinkMode.DESCRIPTION,
inProgress = false,
@@ -1103,7 +1100,6 @@ fun PreviewChatListNavLinkGroup() {
null,
null,
null,
null,
disabled = false,
linkMode = SimplexLinkMode.DESCRIPTION,
inProgress = false,
@@ -42,7 +42,6 @@ fun ChatPreviewView(
chatModelDraft: ComposeState?,
chatModelDraftChatId: ChatId?,
currentUserProfileDisplayName: String?,
contactNetworkStatus: NetworkStatus?,
disabled: Boolean,
linkMode: SimplexLinkMode,
inProgress: Boolean,
@@ -349,33 +348,7 @@ fun ChatPreviewView(
@Composable
fun chatStatusImage() {
if (cInfo is ChatInfo.Direct) {
if (
cInfo.contact.active &&
(cInfo.contact.activeConn?.connStatus == ConnStatus.Ready || cInfo.contact.activeConn?.connStatus == ConnStatus.SndReady)
) {
val descr = contactNetworkStatus?.statusString
when (contactNetworkStatus) {
is NetworkStatus.Connected ->
IncognitoIcon(chat.chatInfo.incognito)
is NetworkStatus.Error ->
Icon(
painterResource(MR.images.ic_error),
contentDescription = descr,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(19.sp.toDp())
.offset(x = 2.sp.toDp())
)
else ->
progressView()
}
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
} else if (cInfo is ChatInfo.Group) {
if (cInfo is ChatInfo.Group) {
if (progressByTimeout) {
progressView()
} else if (chat.chatStats.reportsCount > 0) {
@@ -636,6 +609,6 @@ private data class ActiveVoicePreview(
@Composable
fun PreviewChatPreviewView() {
SimpleXTheme {
ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {})
ChatPreviewView(Chat.sampleData, true, null, null, "", disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {})
}
}
@@ -64,7 +64,7 @@ enum class SubscriptionColorType {
ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED
}
data class SubscriptionStatus(
data class AppSubscriptionStatus(
val color: SubscriptionColorType,
val variableValue: Float,
val statusPercent: Float
@@ -75,7 +75,7 @@ fun subscriptionStatusColorAndPercentage(
socksProxy: String?,
subs: SMPServerSubs,
hasSess: Boolean
): SubscriptionStatus {
): AppSubscriptionStatus {
fun roundedToQuarter(n: Float): Float = when {
n >= 1 -> 1f
n <= 0 -> 0f
@@ -83,25 +83,25 @@ fun subscriptionStatusColorAndPercentage(
}
val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE
val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f)
val noConnColorAndPercent = AppSubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f)
val activeSubsRounded = roundedToQuarter(subs.shareOfActive)
return if (!online)
noConnColorAndPercent
else if (subs.total == 0 && !hasSess)
// On freshly installed app (without chats) and on app start
SubscriptionStatus(activeColor, 0f, 0f)
AppSubscriptionStatus(activeColor, 0f, 0f)
else if (subs.ssActive == 0) {
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
else
noConnColorAndPercent
} else { // ssActive > 0
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
else
// This would mean implementation error
SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive)
AppSubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive)
}
}
@@ -612,13 +612,14 @@ private fun SingleOperatorUsageConditionsView(
}
}
val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
@Composable
fun ConditionsTextView(
rhId: Long?
) {
val conditionsData = remember { mutableStateOf<Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>?>(null) }
val failedToLoad = remember { mutableStateOf(false) }
val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
val scope = rememberCoroutineScope()
// can show conditions when animation between modals finishes to prevent glitches
val canShowConditionsAt = remember { System.currentTimeMillis() + 300 }
@@ -1131,7 +1131,7 @@
<string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string>
<string name="choose_file_title">اختيار ملف</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">إرسال غير مصرح به</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string>
<string name="la_notice_turn_on">تشغيل</string>
<string name="webrtc_ice_servers">خوادم WebRTC ICE</string>
<string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string>
@@ -32,9 +32,11 @@
<string name="server_connected">connected</string>
<string name="server_error">error</string>
<string name="server_connecting">connecting</string>
<string name="connected_to_server_to_receive_messages_from_contact">You are connected to the server used to receive messages from this contact.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Trying to connect to the server used to receive messages from this contact (error: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Trying to connect to the server used to receive messages from this contact.</string>
<string name="server_no_sub">no subscription</string>
<string name="connected_to_server_to_receive_messages_from_contact">You are connected to the server used to receive messages from this connection.</string>
<string name="trying_to_connect_to_server_to_receive_messages">Trying to connect to the server used to receive messages from this connection.</string>
<string name="error_connecting_to_server_to_receive_messages">Error connecting to the server used to receive messages from this connection: %1$s.</string>
<string name="not_connected_to_server_to_receive_messages_no_sub">You are not connected to the server used to receive messages from this connection (no subscription).</string>
<!-- Item Content - ChatModel.kt -->
<string name="deleted_description">deleted</string>
@@ -1182,7 +1182,7 @@
<string name="messages_section_description">Тази настройка се прилага за съобщения в текущия ви профил</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</string>
<string name="trying_to_connect_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s).</string>
<string name="error_smp_test_failed_at_step">Тестът е неуспешен на стъпка %s.</string>
<string name="database_initialization_error_desc">Базата данни не работи правилно. Докоснете, за да научите повече</string>
<string name="image_decoding_exception_desc">Изображението не може да бъде декодирано. Моля, опитайте с друго изображение или се свържете с разработчиците.</string>
@@ -1556,7 +1556,7 @@
<string name="error_parsing_uri_desc">Comproveu que l\'enllaç SimpleX sigui correcte.</string>
<string name="sending_files_not_yet_supported">l\'enviament de fitxers encara no està suportat</string>
<string name="trying_to_connect_to_server_to_receive_messages">Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s).</string>
<string name="connect_use_current_profile">Usar perfil actual</string>
<string name="connect_use_new_incognito_profile">Usar nou perfil incògnit</string>
<string name="app_was_crashed">Error aplicació</string>
@@ -493,7 +493,7 @@
<string name="you_will_join_group">Připojtíte se ke všem členům skupiny.</string>
<string name="connect_via_link_verb">Připojení</string>
<string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s).</string>
<string name="marked_deleted_description">označeno jako smazáno</string>
<string name="sending_files_not_yet_supported">Odesílání souborů zatím není podporováno</string>
<string name="receiving_files_not_yet_supported">přijímání souborů zatím není podporováno</string>
@@ -178,7 +178,7 @@
<string name="server_error">fejl</string>
<string name="server_connecting">forbinder</string>
<string name="connected_to_server_to_receive_messages_from_contact">Du har forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt (fejl: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt (fejl: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt.</string>
<string name="deleted_description">slettet</string>
<string name="marked_deleted_description">markeret som slettet</string>
@@ -14,7 +14,7 @@
<string name="server_error">Fehler</string>
<string name="server_connecting">Verbinde</string>
<string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
<!-- Item Content - ChatModel.kt -->
<string name="deleted_description">Gelöscht</string>
@@ -806,7 +806,7 @@
<string name="update_network_settings_question">¿Actualizar la configuración de red\?</string>
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
<string name="unknown_message_format">formato de mensaje desconocido</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s).</string>
<string name="error_smp_test_failed_at_step">Prueba no superada en el paso %s.</string>
<string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string>
<string name="share_message">Compartir mensaje…</string>
@@ -35,7 +35,7 @@
<string name="connect_via_link_verb">متصل شدن</string>
<string name="non_content_uri_alert_title">مسیر نامعتبر فایل</string>
<string name="app_was_crashed">برنامه از کار افتاد</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s).</string>
<string name="deleted_description">حذف شد</string>
<string name="marked_deleted_description">علامت گذاشته شده به عنوان حذف شده</string>
<string name="moderated_item_description">توسط %s حذف شد</string>
@@ -1036,7 +1036,7 @@
<string name="profile_will_be_sent_to_contact_sending_link">Profiilisi lähetetään kontaktille, jolta sait tämän linkin.</string>
<string name="you_will_join_group">Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s).</string>
<string name="sender_you_pronoun">sinä</string>
<string name="description_via_one_time_link">kertalinkillä</string>
<string name="simplex_link_connection">%1$s:n kautta</string>
@@ -52,7 +52,7 @@
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre.</string>
<string name="connection_error">Erreur de connexion</string>
<string name="error_adding_members">Erreur lors de l\'ajout de membre·s</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s).</string>
<string name="invalid_message_format">format de message invalide</string>
<string name="simplex_link_mode_full">Lien entier</string>
<string name="error_saving_smp_servers">Erreur lors de la sauvegarde des serveurs SMP</string>
@@ -1317,7 +1317,7 @@
<string name="image_decoding_exception_desc">A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel.</string>
<string name="non_content_uri_alert_text">Érvénytelen fájlelérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek.</string>
<string name="failed_to_create_user_duplicate_desc">Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s).</string>
<string name="stop_rcv_file__message">A fájl fogadása le fog állni.</string>
<string name="la_please_remember_to_store_password">Ne felejtse el, vagy tárolja biztonságosan az elveszett jelszót nem lehet visszaállítani!</string>
<string name="video_will_be_received_when_contact_completes_uploading">A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</string>
@@ -104,7 +104,7 @@
<string name="error_parsing_uri_title">Tautan tidak valid</string>
<string name="error_parsing_uri_desc">Periksa apakah tautan SimpleX sudah benar.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Anda terhubung ke server yang digunakan untuk menerima pesan dari kontak ini.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s).</string>
<string name="database_migration_in_progress">Migrasi basis data sedang berlangsung,
\nmemerlukan waktu beberapa menit.</string>
<string name="server_connecting">menghubungkan</string>
@@ -201,7 +201,7 @@
<string name="reset_verb">Ripristina</string>
<string name="ok">OK</string>
<string name="connect_via_contact_link">Connettere via indirizzo del contatto?</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s).</string>
<string name="you_will_join_group">Ti connetterai a tutti i membri del gruppo.</string>
<string name="connection_local_display_name">connessione %1$d</string>
<string name="simplex_link_mode_description">Descrizione</string>
@@ -1063,7 +1063,7 @@
<string name="to_verify_compare">כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם.</string>
<string name="your_chat_profiles">פרופילי צ׳אט</string>
<string name="update_network_session_mode_question">לעדכן מצב בידוד תעבורה\?</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s).</string>
<string name="unknown_message_format">פורמט הודעה לא ידוע</string>
<string name="simplex_link_mode_browser">דרך הדפדפן</string>
<string name="unmute_chat">בטל השתקה</string>
@@ -887,7 +887,7 @@
<string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string>
<string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに公開されます。</string>
<string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。</string>
<string name="error_connecting_to_server_to_receive_messages">このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。</string>
<string name="connection_error_auth_desc">使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。
\n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">接続を完了するには、連絡相手がオンラインになる必要があります。
@@ -1404,7 +1404,7 @@
<string name="profile_will_be_sent_to_contact_sending_link">Jūsų profilis bus išsiųstas kontaktui iš kurio gavote šią nuorodą.</string>
<string name="you_will_join_group">Prisijungsite prie visų grupės narių.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Esate prisijungę prie serverio skirto gauti žinutes iš šio kontakto.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto.</string>
<string name="no_details">nėra detalių</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Kad išlaikyti jūsų privatumą, vietoj tiesioginių pranešimų, programėlė turi <b>SimpleX fono tarnybą</b> - ji naudoja kelis procentus akumuliatoriaus per dieną.]]></string>
@@ -913,7 +913,7 @@
<string name="simplex_link_mode">SimpleX links</string>
<string name="simplex_link_invitation">Eenmalige SimpleX uitnodiging</string>
<string name="trying_to_connect_to_server_to_receive_messages">Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s).</string>
<string name="unknown_message_format">onbekend berichtformaat</string>
<string name="simplex_link_mode_browser">Via browser</string>
<string name="description_via_contact_address_link">via contact adres link</string>
@@ -26,7 +26,7 @@
<string name="sending_files_not_yet_supported">wysyłanie plików nie jest jeszcze obsługiwane</string>
<string name="receiving_files_not_yet_supported">odbieranie plików nie jest jeszcze obsługiwane</string>
<string name="trying_to_connect_to_server_to_receive_messages">Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s).</string>
<string name="unknown_message_format">nieznany format wiadomości</string>
<string name="app_name">SimpleX</string>
<string name="sender_you_pronoun">Ty</string>
@@ -971,7 +971,7 @@
<string name="tap_to_activate_profile">Toque para ativar o perfil.</string>
<string name="unhide_chat_profile">Mostrar perfil de chat</string>
<string name="unhide_profile">Mostrar perfil</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Tentando se conectar ao servidor utilizado para receber mensagens deste contato.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Você está conectado ao servidor usado para receber mensagens desse contato.</string>
<string name="smp_servers_your_server">Seu servidor</string>
@@ -2077,7 +2077,7 @@
<string name="image_decoding_exception_desc">Imaginea nu poate fi decodificată. Vă rugăm să încercați o altă imagine sau să contactați dezvoltatorii.</string>
<string name="xftp_servers_per_user">Serverele pentru fișierele noi ale profilului tău de conversații actual</string>
<string name="alert_message_no_group">Acest grup nu mai există.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s).</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Actualizarea setărilor va reconecta clientul la toate serverele.</string>
<string name="this_string_is_not_a_connection_link">Acest șir de caractere nu este un link!</string>
<string name="remote_ctrl_error_timeout">S-a atins timpul de expirare la conectarea la desktop</string>
@@ -14,7 +14,7 @@
<string name="server_error">ошибка</string>
<string name="server_connecting">соединяется</string>
<string name="connected_to_server_to_receive_messages_from_contact">Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта.</string>
<!-- Item Content - ChatModel.kt -->
<string name="deleted_description">удалено</string>
@@ -1043,7 +1043,7 @@
<string name="you_are_invited_to_group">คุณได้รับเชิญให้เข้าร่วมกลุ่ม</string>
<string name="incognito_random_profile">โปรไฟล์แบบสุ่มของคุณ</string>
<string name="theme">ธีม</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s)</string>
<string name="error_connecting_to_server_to_receive_messages">กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s)</string>
<string name="app_name">SimpleX</string>
<string name="connected_to_server_to_receive_messages_from_contact">คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</string>
<string name="profile_will_be_sent_to_contact_sending_link">โปรไฟล์ของคุณจะถูกส่งไปยังผู้ติดต่อที่ส่งลิงก์นี้มาให้คุณ</string>
@@ -1023,7 +1023,7 @@
<string name="v4_6_hidden_chat_profiles_descr">Sohbet profillerini parola ile koru!</string>
<string name="v4_5_reduced_battery_usage">Daha az pil kullanımı</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Gizliliği korumak için, SimpleX her bir konuşma için farklı bir ID kullanır.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s).</string>
<string name="v4_4_live_messages_desc">Alıcılar güncellemeleri siz yazdıkça görürler.</string>
<string name="auth_log_in_using_credential">Bilgilerinizi kullanarak giriş yapın</string>
<string name="markdown_in_messages">Mesajlarda Markdown</string>
@@ -124,7 +124,7 @@
<string name="server_error">помилка</string>
<string name="server_connecting">підключення</string>
<string name="connected_to_server_to_receive_messages_from_contact">Ви підключені до сервера для отримання повідомлень від цього контакту.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s).</string>
<string name="deleted_description">видалено</string>
<string name="trying_to_connect_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього контакту.</string>
<string name="marked_deleted_description">відзначено як видалено</string>
@@ -2034,7 +2034,7 @@
<string name="la_notice_turn_on">Bật</string>
<string name="servers_info_subscriptions_total">Tổng</string>
<string name="group_member_status_unknown_short">không xác định</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này (lỗi: %1$s).</string>
<string name="error_connecting_to_server_to_receive_messages">Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này (lỗi: %1$s).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này.</string>
<string name="to_verify_compare">Để xác minh mã hóa đầu cuối với liên hệ của bạn, so sánh (hoặc quét) mã trên các thiết bị của các bạn.</string>
<string name="network_session_mode_transport_isolation">Cách ly truyền tải</string>
@@ -589,7 +589,7 @@
<string name="rcv_group_event_member_deleted">已删除 %1$s</string>
<string name="snd_group_event_member_deleted">你删除了 %1$s</string>
<string name="profile_will_be_sent_to_contact_sending_link">你的个人资料将发送给你收到此链接的联系人。</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。</string>
<string name="error_connecting_to_server_to_receive_messages">正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。</string>
<string name="connected_to_server_to_receive_messages_from_contact">你已连接到用于接收该联系人消息的服务器。</string>
<string name="description_you_shared_one_time_link">你分享了一次性链接</string>
<string name="message_delivery_error_desc">很可能此联系人已经删除了与你的联系。</string>
@@ -238,7 +238,7 @@
<string name="server_error">錯誤</string>
<string name="server_connecting">連接中</string>
<string name="connected_to_server_to_receive_messages_from_contact">你已連接到此聯絡人使用的伺服器以接收訊息。</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。</string>
<string name="error_connecting_to_server_to_receive_messages">嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。</string>
<string name="trying_to_connect_to_server_to_receive_messages">正在嘗試連接到用於接收此聯絡人訊息伺服器。</string>
<string name="deleted_description">已刪除</string>
<string name="marked_deleted_description">已標記為已刪除</string>
+16
View File
@@ -47,6 +47,7 @@ This file is generated automatically.
- [ChatType](#chattype)
- [ChatWallpaper](#chatwallpaper)
- [ChatWallpaperScale](#chatwallpaperscale)
- [ClientNotice](#clientnotice)
- [Color](#color)
- [CommandError](#commanderror)
- [CommandErrorType](#commanderrortype)
@@ -288,6 +289,12 @@ AGENT:
- type: "AGENT"
- agentErr: [SMPAgentError](#smpagenterror)
NOTICE:
- type: "NOTICE"
- server: string
- preset: bool
- expiresAt: UTCTime?
INTERNAL:
- type: "INTERNAL"
- internalErr: string
@@ -315,6 +322,7 @@ INACTIVE:
**Record type**:
- reason: [BlockingReason](#blockingreason)
- notice: [ClientNotice](#clientnotice)?
---
@@ -1378,6 +1386,14 @@ self == 'direct' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' // J
- "repeat"
---
## ClientNotice
**Record type**:
- ttl: int64?
---
## Color
-1
View File
@@ -349,7 +349,6 @@ undocumentedCommands =
"APIGetContactCode",
"APIGetGroupMemberCode",
"APIGetNetworkConfig",
"APIGetNetworkStatuses",
"APIGetNtfConns",
"APIGetNtfToken",
"APIGetReactionMembers",
+1 -1
View File
@@ -180,7 +180,7 @@ undocumentedEvents =
"CEvtGroupMemberSwitch",
"CEvtHostConnected",
"CEvtHostDisconnected",
"CEvtNetworkStatus",
"CEvtSubscriptionStatus",
"CEvtNewRemoteHost",
"CEvtNoMemberContactCreating",
"CEvtNtfMessage",
-1
View File
@@ -167,7 +167,6 @@ undocumentedResponses =
"CRMemberSupportChatDeleted",
"CRMemberSupportChats",
"CRNetworkConfig",
"CRNetworkStatuses",
"CRNewMemberContact",
"CRNewMemberContactSentInv",
"CRMemberContactAccepted",
+3
View File
@@ -42,6 +42,7 @@ import Simplex.Messaging.Client
import Simplex.Messaging.Crypto.File
import Simplex.Messaging.Parsers (dropPrefix, fstToLower)
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..))
import Simplex.Messaging.Protocol.Types (ClientNotice (..))
import Simplex.Messaging.Transport
import Simplex.RemoteControl.Types
import System.Console.ANSI.Types (Color (..))
@@ -228,6 +229,7 @@ chatTypesDocsData =
(sti @CIMentionMember, STRecord, "", [], "", ""),
(sti @CIReactionCount, STRecord, "", [], "", ""),
(sti @CITimed, STRecord, "", [], "", ""),
(sti @ClientNotice, STRecord, "", [], "", ""),
(sti @Color, STEnum, "", [], "", ""),
(sti @CommandError, STUnion, "", [], "", ""),
(sti @CommandErrorType, STUnion, "", [], "", ""),
@@ -413,6 +415,7 @@ deriving instance Generic CIMention
deriving instance Generic CIMentionMember
deriving instance Generic CIReactionCount
deriving instance Generic CITimed
deriving instance Generic ClientNotice
deriving instance Generic Color
deriving instance Generic CommandError
deriving instance Generic CommandErrorType
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 1dbc15b2e6225c0e254564747bc8412970273e85
tag: 1329fc726ffb2e773935ad10f024a137dd887867
source-repository-package
type: git
@@ -65,6 +65,7 @@ export type AgentErrorType =
| AgentErrorType.RCP
| AgentErrorType.BROKER
| AgentErrorType.AGENT
| AgentErrorType.NOTICE
| AgentErrorType.INTERNAL
| AgentErrorType.CRITICAL
| AgentErrorType.INACTIVE
@@ -82,6 +83,7 @@ export namespace AgentErrorType {
| "RCP"
| "BROKER"
| "AGENT"
| "NOTICE"
| "INTERNAL"
| "CRITICAL"
| "INACTIVE"
@@ -152,6 +154,13 @@ export namespace AgentErrorType {
agentErr: SMPAgentError
}
export interface NOTICE extends Interface {
type: "NOTICE"
server: string
preset: boolean
expiresAt?: string // ISO-8601 timestamp
}
export interface INTERNAL extends Interface {
type: "INTERNAL"
internalErr: string
@@ -174,6 +183,7 @@ export interface AutoAccept {
export interface BlockingInfo {
reason: BlockingReason
notice?: ClientNotice
}
export enum BlockingReason {
@@ -1587,6 +1597,10 @@ export enum ChatWallpaperScale {
Repeat = "repeat",
}
export interface ClientNotice {
ttl?: number // int64
}
export enum Color {
Black = "black",
Red = "red",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."1dbc15b2e6225c0e254564747bc8412970273e85" = "03hmlynixssyp0720h984slw4lkrzn3kr63k3mah50lbyxzsmnrs";
"https://github.com/simplex-chat/simplexmq.git"."1329fc726ffb2e773935ad10f024a137dd887867" = "0wlpwr464i8dif5a94mfx31y3fm44gkc3h357dx8l1ii9q3sy05i";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
-1
View File
@@ -87,7 +87,6 @@ library
Simplex.Chat.Types.Preferences
Simplex.Chat.Types.Shared
Simplex.Chat.Types.UITheme
Simplex.Chat.Types.Util
Simplex.Chat.Util
if !flag(client_library)
exposed-modules:
+1 -3
View File
@@ -149,7 +149,6 @@ newChatController
eventSeq <- newTVarIO 0
inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize
connNetworkStatuses <- TM.emptyIO
subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO
entityLocks <- TM.emptyIO
@@ -191,7 +190,6 @@ newChatController
eventSeq,
inputQ,
outputQ,
connNetworkStatuses,
subscriptionMode,
chatLock,
entityLocks,
@@ -252,7 +250,7 @@ newChatController
ops <- getUpdateServerOperators db presetOps (null users)
let opDomains = operatorDomains $ mapMaybe snd ops
(smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains}
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains, presetServers = L.toList allPresetServers}
where
optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))]
optServers srvs overrides_ = case L.nonEmpty overrides_ of
+2 -9
View File
@@ -231,7 +231,6 @@ data ChatController = ChatController
eventSeq :: TVar Int,
inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent),
connNetworkStatuses :: TMap AgentConnId NetworkStatus,
subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock,
entityLocks :: TMap ChatLockEntity Lock,
@@ -354,7 +353,6 @@ data ChatCommand
| APIEndCall ContactId
| APIGetCallInvitations
| APICallStatus ContactId WebRTCCallStatus
| APIGetNetworkStatuses
| APIUpdateProfile {userId :: UserId, profile :: Profile}
| APISetContactPrefs {contactId :: ContactId, preferences :: Preferences}
| APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias}
@@ -729,7 +727,6 @@ data ChatResponse
| CRGroupAliasUpdated {user :: User, toGroup :: GroupInfo}
| CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection}
| CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRMemberAccepted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRMemberSupportChatRead {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
@@ -829,7 +826,7 @@ data ChatEvent
| CEvtContactAnotherClient {user :: User, contact :: Contact}
| CEvtConnectionsDiff {userIds :: DatabaseDiff AgentUserId, connIds :: DatabaseDiff AgentConnId}
| CEvtSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity}
| CEvtNetworkStatus {server :: SMPServer, networkStatus :: NetworkStatus, connections :: [AgentConnId]}
| CEvtSubscriptionStatus {server :: SMPServer, subscriptionStatus :: SubscriptionStatus, connections :: [AgentConnId]}
| CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
@@ -909,7 +906,7 @@ allowRemoteEvent = \case
logEventToFile :: ChatEvent -> Bool
logEventToFile = \case
CEvtNetworkStatus {} -> True
CEvtSubscriptionStatus {} -> True
CEvtHostConnected {} -> True
CEvtHostDisconnected {} -> True
CEvtConnectionDisabled {} -> True
@@ -1472,10 +1469,6 @@ chatModifyVar' :: (ChatController -> TVar a) -> (a -> a) -> CM' ()
chatModifyVar' f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
{-# INLINE chatModifyVar' #-}
setContactNetworkStatus :: Contact -> NetworkStatus -> CM' ()
setContactNetworkStatus Contact {activeConn = Nothing} _ = pure ()
setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar' connNetworkStatuses $ M.insert agentConnId status
onChatError :: CM a -> CM b -> CM a
a `onChatError` onErr = a `catchAllErrors` \e -> onErr >> throwError e
{-# INLINE onChatError #-}
+6 -10
View File
@@ -1368,8 +1368,6 @@ processChatCommand vr nm = \case
user <- getUserByContactId db contactId
contact <- getContact db vr user contactId
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs}
APIGetNetworkStatuses -> withUser $ \_ ->
CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses
APICallStatus contactId receivedStatus ->
withCurrentCall contactId $ \user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
@@ -1777,7 +1775,7 @@ processChatCommand vr nm = \case
subMode <- chatReadVar subscriptionMode
let userData = contactShortLinkData (userProfileDirect user incognitoProfile Nothing True) Nothing
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation (Just userData) Nothing IKPQOn subMode
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
-- TODO PQ pass minVersion from the current range
conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn
@@ -1819,7 +1817,7 @@ processChatCommand vr nm = \case
| short = Just $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing
| otherwise = Nothing
-- TODO [certs rcv]
(agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True SCMInvitation userData_ Nothing IKPQOn subMode
(agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userData_ Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
conn' <- withFastStore' $ \db -> do
deleteConnectionRecord db user connId
@@ -2011,7 +2009,7 @@ processChatCommand vr nm = \case
subMode <- chatReadVar subscriptionMode
let userData = contactShortLinkData (userProfileDirect user Nothing Nothing True) Nothing
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) Nothing IKPQOn subMode
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode
pure $ CRUserContactLinkCreated user ccLink'
@@ -2231,7 +2229,7 @@ processChatCommand vr nm = \case
gVar <- asks random
subMode <- chatReadVar subscriptionMode
-- TODO [certs rcv]
(agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode
(agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode
member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode
sendInvitation member cReq
pure $ CRSentGroupInvitation user gInfo contact member
@@ -2635,7 +2633,7 @@ processChatCommand vr nm = \case
let userData = encodeShortLinkData $ GroupShortLinkData groupProfile
crClientData = encodeJSON $ CRDataGroup groupLinkId
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) (Just crClientData) IKPQOff subMode
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) (Just crClientData) IKPQOff subMode
ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink
gVar <- asks random
gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode
@@ -2676,12 +2674,11 @@ processChatCommand vr nm = \case
subMode <- chatReadVar subscriptionMode
-- TODO PQ should negotitate contact connection with PQSupportOn?
-- TODO [certs rcv]
(connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode
(connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode
-- [incognito] reuse membership incognito profile
ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
-- TODO not sure it is correct to set connections status here?
lift $ setContactNetworkStatus ct NSConnected
pure $ CRNewMemberContact user ct g m
_ -> throwChatError CEGroupMemberNotActive
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
@@ -4392,7 +4389,6 @@ chatCommandP =
"/_call end @" *> (APIEndCall <$> A.decimal),
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
"/_call get" $> APIGetCallInvitations,
"/_network_statuses" $> APIGetNetworkStatuses,
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
+4 -10
View File
@@ -131,19 +131,16 @@ processAgentMessageNoConn :: AEvent 'AENone -> CM ()
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CEvtHostConnected p h
DISCONNECT p h -> hostEvent $ CEvtHostDisconnected p h
DOWN srv conns -> serverEvent srv NSDisconnected conns
UP srv conns -> serverEvent srv NSConnected conns
DOWN srv conns -> serverEvent srv SSPending conns
UP srv conns -> serverEvent srv SSActive conns
SUSPENDED -> toView CEvtChatSuspended
DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId
ERRS cErrs -> errsEvent $ L.toList cErrs
where
hostEvent :: ChatEvent -> CM ()
hostEvent = whenM (asks $ hostEvents . config) . toView
serverEvent srv nsStatus conns = do
chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds
toView $ CEvtNetworkStatus srv nsStatus connIds
where
connIds = map AgentConnId conns
serverEvent :: SMPServer -> SubscriptionStatus -> [ConnId] -> CM ()
serverEvent srv nsStatus conns = toView $ CEvtSubscriptionStatus srv nsStatus $ map AgentConnId conns
errsEvent :: [(ConnId, AgentErrorType)] -> CM ()
errsEvent = toView . CEvtChatErrors . map (\(cId, e) -> ChatErrorAgent e (AgentConnId cId) Nothing)
@@ -559,7 +556,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ct' = ct {activeConn = Just conn'} :: Contact
-- [incognito] print incognito profile used for this contact
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
lift $ setContactNetworkStatus ct' NSConnected
toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile)
let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing
-- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)?
@@ -640,7 +636,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- [async agent commands] continuation on receiving JOINED
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData ->
when (directOrUsed ct && sqSecured) $ do
lift $ setContactNetworkStatus ct NSConnected
toView $ CEvtContactSndReady user ct
when (connChatVersion >= batchSend2Version) $ forM_ viaUserContactLink $ \userContactLinkId -> do
(ucl, _) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
@@ -1455,7 +1450,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
notifyMemberConnected gInfo m ct_ = do
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
memberConnectedChatItem gInfo' scopeInfo m'
lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_
toView $ CEvtConnectedToGroupMember user gInfo' m' ct_
probeMatchingMembers :: Contact -> IncognitoEnabled -> CM ()
-1
View File
@@ -49,7 +49,6 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util (textParseJSON)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..))
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Crypto.File (CryptoFile (..))
-1
View File
@@ -46,7 +46,6 @@ import Data.Time.Clock (UTCTime, nominalDay)
import Language.Haskell.TH.Syntax (lift)
import Simplex.Chat.Operators.Conditions
import Simplex.Chat.Types (User)
import Simplex.Chat.Types.Util (textParseJSON)
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
import Simplex.Messaging.Agent.Store.Entity
@@ -395,7 +395,7 @@ Query:
JOIN connections c ON q.conn_id = c.conn_id
WHERE c.deleted = 0 AND q.deleted = 0
Plan:
SCAN q USING INDEX idx_rcv_queues_link_id
SCAN q USING INDEX idx_rcv_queues_client_notice_id
SEARCH c USING PRIMARY KEY (conn_id=?)
SEARCH s USING PRIMARY KEY (host=? AND port=?)
USE TEMP B-TREE FOR DISTINCT
@@ -477,6 +477,16 @@ Query:
Plan:
SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?)
Query:
SELECT n.host, n.port, n.entity_id, COALESCE(n.server_key_hash, s.key_hash), n.created_at, n.notice_ttl
FROM client_notices n
JOIN servers s ON n.host = s.host AND n.port = s.port
WHERE n.protocol = 'smp'
Plan:
SEARCH n USING INDEX idx_client_notices_entity (protocol=?)
SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query:
SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status
FROM snd_messages s
@@ -793,7 +803,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
@@ -808,7 +818,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
@@ -823,7 +833,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?)
Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
@@ -838,7 +848,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?)
Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
@@ -853,7 +863,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
@@ -867,7 +877,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
SEARCH c USING PRIMARY KEY (conn_id=?)
Query:
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs,
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port
@@ -879,7 +889,7 @@ SEARCH q USING PRIMARY KEY (host=? AND port=?)
SEARCH c USING PRIMARY KEY (conn_id=?)
Query:
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs,
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port
-25
View File
@@ -49,7 +49,6 @@ import Data.Word (Word16)
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId)
@@ -1789,26 +1788,6 @@ serializeIntroStatus = \case
GMIntroToConnected -> "to-con"
GMIntroConnected -> "con"
data NetworkStatus
= NSUnknown
| NSConnected
| NSDisconnected
| NSError {connectionError :: String}
deriving (Eq, Ord, Show)
netStatusStr :: NetworkStatus -> String
netStatusStr = \case
NSUnknown -> "unknown"
NSConnected -> "subscribed"
NSDisconnected -> "disconnected"
NSError e -> "error: " <> e
data ConnNetworkStatus = ConnNetworkStatus
{ agentConnId :: AgentConnId,
networkStatus :: NetworkStatus
}
deriving (Show)
type CommandId = Int64
aCorrId :: CommandId -> ACorrId
@@ -1998,10 +1977,6 @@ $(JQ.deriveJSON defaultJSON ''GroupMemberSettings)
$(JQ.deriveJSON defaultJSON ''SecurityCode)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "NS") ''NetworkStatus)
$(JQ.deriveJSON defaultJSON ''ConnNetworkStatus)
$(JQ.deriveJSON defaultJSON ''Connection)
$(JQ.deriveJSON defaultJSON ''PendingContactConnection)
-1
View File
@@ -14,7 +14,6 @@ import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
-8
View File
@@ -1,8 +0,0 @@
module Simplex.Chat.Types.Util where
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as JT
import Simplex.Messaging.Encoding.String
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
+8 -8
View File
@@ -232,7 +232,6 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRRcvStandaloneFileCreated u ft -> ttyUser u $ receivingFileStandalone "started" ft
CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m
CRMemberAccepted u g m -> ttyUser u $ viewMemberAccepted g m
CRMemberSupportChatRead u g m -> ttyUser u $ viewSupportChatRead g m
@@ -457,7 +456,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
CEvtSubscriptionEnd u acEntity ->
let Connection {connId} = entityConnection acEntity
in ttyUser u [sShow connId <> ": END"]
CEvtNetworkStatus srv status conns -> [plain $ netStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv]
CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv]
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CEvtUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m
@@ -1174,12 +1173,6 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
viewNetworkStatuses :: [ConnNetworkStatus] -> [StyledString]
viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortOn netStatus
where
netStatus ConnNetworkStatus {networkStatus} = networkStatus
viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s)
viewUserJoinedGroup :: GroupInfo -> [StyledString]
viewUserJoinedGroup g@GroupInfo {membership} =
case incognitoMembershipProfile g of
@@ -1464,6 +1457,13 @@ viewConnDiffIds userDiff connDiff
where
showIds = plain . T.intercalate ", " . map (tshow . unwrapId)
subStatusStr :: SubscriptionStatus -> String
subStatusStr = \case
SSActive -> "subscribed"
SSPending -> "disconnected"
SSRemoved e -> "removed: " <> e
SSNoSub -> "no subscription"
viewUserServers :: UserOperatorServers -> [StyledString]
viewUserServers (UserOperatorServers _ [] []) = []
viewUserServers UserOperatorServers {operator, smpServers, xftpServers} =
+2 -17
View File
@@ -180,8 +180,6 @@ chatDirectTests = do
testReqVRange vr11 supportedChatVRange
testReqVRange vr11 vr11
it "update peer version range on received messages" testUpdatePeerChatVRange
describe "network statuses" $ do
it "should get network statuses" testGetNetworkStatuses
where
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
@@ -349,6 +347,7 @@ testRetryConnectingClientTimeout ps = do
messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
}
}
ChatConfig {presetServers = presetSrvs@PresetServers {netCfg}} = testCfg
cfgZeroTimeout =
(testCfg :: ChatConfig)
{ agentConfig =
@@ -356,9 +355,7 @@ testRetryConnectingClientTimeout ps = do
{ quotaExceededTimeout = 1,
messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
},
presetServers =
let def@PresetServers {netCfg} = presetServers testCfg
in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}}
presetServers = presetSrvs {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}}
}
opts' =
testOpts
@@ -3324,18 +3321,6 @@ testUpdatePeerChatVRange ps =
where
cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
testGetNetworkStatuses :: HasCallStack => TestParams -> IO ()
testGetNetworkStatuses ps = do
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
connectUsers alice bob
alice ##> "/_network_statuses"
alice <## "1 connections subscribed"
withTestChat ps "alice" $ \alice ->
withTestChat ps "bob" $ \bob -> do
alice <## "subscribed 1 connections on server localhost"
bob <## "subscribed 1 connections on server localhost"
vr11 :: VersionRangeChat
vr11 = mkVersionRange (VersionChat 1) (VersionChat 1)
-6
View File
@@ -28,12 +28,6 @@ chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}"
chatStartedTagged :: LB.ByteString
chatStartedTagged = "{\"result\":{\"type\":\"chatStarted\"}}"
networkStatusesSwift :: LB.ByteString
networkStatusesSwift = "{\"result\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
networkStatusesTagged :: LB.ByteString
networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
userJSON :: LB.ByteString
userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}"
-1
View File
@@ -25,7 +25,6 @@ owsf2TaggedJSONTest = do
activeUserExistsSwift `to` activeUserExistsTagged
activeUserSwift `to` activeUserTagged
chatStartedSwift `to` chatStartedTagged
networkStatusesSwift `to` networkStatusesTagged
parsedMarkdownSwift `to` parsedMarkdownTagged
where
to :: LB.ByteString -> LB.ByteString -> IO ()