Merge branch 'master' into av/multiplatform-merged-master
This commit is contained in:
@@ -14,9 +14,28 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
|
||||
application.registerForRemoteNotifications()
|
||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||
return true
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
private func trackKeyboard() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@objc func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
||||
ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@objc func keyboardWillHide(_ notification: Notification) {
|
||||
ChatModel.shared.keyboardHeight = 0
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
|
||||
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
|
||||
|
||||
@@ -57,6 +57,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
||||
@Published var draft: ComposeState?
|
||||
@Published var draftChatId: String?
|
||||
// tracks keyboard height via subscription in AppDelegate
|
||||
@Published var keyboardHeight: CGFloat = 0
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
@@ -133,6 +135,14 @@ final class ChatModel: ObservableObject {
|
||||
updateChat(.direct(contact: contact), addMissing: contact.directOrUsed)
|
||||
}
|
||||
|
||||
func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) {
|
||||
var updatedConn = contact.activeConn
|
||||
updatedConn.connectionStats = connectionStats
|
||||
var updatedContact = contact
|
||||
updatedContact.activeConn = updatedConn
|
||||
updateContact(updatedContact)
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
updateChat(.group(groupInfo: groupInfo))
|
||||
}
|
||||
@@ -521,6 +531,16 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
|
||||
if let conn = member.activeConn {
|
||||
var updatedConn = conn
|
||||
updatedConn.connectionStats = connectionStats
|
||||
var updatedMember = member
|
||||
updatedMember.activeConn = updatedConn
|
||||
_ = upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
|
||||
var i = 0
|
||||
var totalBelow = 0
|
||||
|
||||
@@ -478,9 +478,9 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
|
||||
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
|
||||
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -508,6 +508,18 @@ func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSyncContactRatchet(_ contactId: Int64, _ force: Bool) throws -> ConnectionStats {
|
||||
let r = chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force))
|
||||
if case let .contactRatchetSyncStarted(_, _, connectionStats) = r { return connectionStats }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSyncGroupMemberRatchet(_ groupId: Int64, _ groupMemberId: Int64, _ force: Bool) throws -> (GroupMember, ConnectionStats) {
|
||||
let r = chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force))
|
||||
if case let .groupMemberRatchetSyncStarted(_, _, member, connectionStats) = r { return (member, connectionStats) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
|
||||
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
|
||||
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
|
||||
@@ -1453,6 +1465,14 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ struct ChatInfoView: View {
|
||||
case networkStatusAlert
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
@@ -85,6 +86,7 @@ struct ChatInfoView: View {
|
||||
case .networkStatusAlert: return "networkStatusAlert"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -115,6 +117,12 @@ struct ChatInfoView: View {
|
||||
Section {
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
contactPreferencesButton()
|
||||
if let connStats = connectionStats,
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
} else if developerTools {
|
||||
synchronizeConnectionButtonForce()
|
||||
}
|
||||
}
|
||||
|
||||
if let contactLink = contact.contactLink {
|
||||
@@ -141,12 +149,18 @@ struct ChatInfoView: View {
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
}
|
||||
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
@@ -175,6 +189,7 @@ struct ChatInfoView: View {
|
||||
case .networkStatusAlert: return networkStatusAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
@@ -280,6 +295,24 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronizeConnectionButton() -> some View {
|
||||
Button {
|
||||
syncContactConnection(force: false)
|
||||
} label: {
|
||||
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronizeConnectionButtonForce() -> some View {
|
||||
Button {
|
||||
alert = .syncConnectionForceAlert
|
||||
} label: {
|
||||
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func networkStatusRow() -> some View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
@@ -370,6 +403,10 @@ struct ChatInfoView: View {
|
||||
do {
|
||||
let stats = try apiSwitchContact(contactId: contact.apiId)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateContactConnectionStats(contact, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
@@ -385,6 +422,9 @@ struct ChatInfoView: View {
|
||||
do {
|
||||
let stats = try apiAbortSwitchContact(contact.apiId)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateContactConnectionStats(contact, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error aborting address change")
|
||||
@@ -394,6 +434,25 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncContactConnection(force: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiSyncContactRatchet(contact.apiId, force)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateContactConnectionStats(contact, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||
@@ -414,6 +473,15 @@ func abortSwitchAddressAlert(_ abortSwitchAddress: @escaping () -> Void) -> Aler
|
||||
)
|
||||
}
|
||||
|
||||
func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Alert {
|
||||
Alert(
|
||||
title: Text("Renegotiate encryption?"),
|
||||
message: Text("The encryption is working and the new encryption agreement is not required. It may result in connection errors!"),
|
||||
primaryButton: .destructive(Text("Renegotiate"), action: syncConnectionForce),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(
|
||||
|
||||
@@ -12,25 +12,215 @@ import SimpleXChat
|
||||
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
|
||||
|
||||
struct CIRcvDecryptionError: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
@State private var alert: CIRcvDecryptionErrorAlert?
|
||||
|
||||
enum CIRcvDecryptionErrorAlert: Identifiable {
|
||||
case syncAllowedAlert(_ syncConnection: () -> Void)
|
||||
case syncNotSupportedContactAlert
|
||||
case syncNotSupportedMemberAlert
|
||||
case decryptionErrorAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .syncAllowedAlert: return "syncAllowedAlert"
|
||||
case .syncNotSupportedContactAlert: return "syncNotSupportedContactAlert"
|
||||
case .syncNotSupportedMemberAlert: return "syncNotSupportedMemberAlert"
|
||||
case .decryptionErrorAlert: return "decryptionErrorAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
CIMsgError(chatItem: chatItem, showMember: showMember) {
|
||||
var message: Text
|
||||
let why = Text(decryptErrorReason)
|
||||
let permanent = Text("This error is permanent for this connection, please re-connect.")
|
||||
switch msgDecryptError {
|
||||
case .ratchetHeader:
|
||||
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + Text("\n") + permanent
|
||||
case .tooManySkipped:
|
||||
message = Text("\(msgCount) messages skipped.") + Text("\n") + why + Text("\n") + permanent
|
||||
viewBody()
|
||||
.onAppear {
|
||||
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
|
||||
if case let .group(groupInfo) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir {
|
||||
do {
|
||||
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
|
||||
if let s = stats {
|
||||
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, member, s)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Decryption error"), message: message))
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case let .syncAllowedAlert(syncConnection): return syncAllowedAlert(syncConnection)
|
||||
case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message())
|
||||
case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message())
|
||||
case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message())
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func viewBody() -> some View {
|
||||
if case let .direct(contact) = chat.chatInfo,
|
||||
let contactStats = contact.activeConn.connectionStats {
|
||||
if contactStats.ratchetSyncAllowed {
|
||||
decryptionErrorItemFixButton(syncSupported: true) {
|
||||
alert = .syncAllowedAlert { syncContactConnection(contact) }
|
||||
}
|
||||
} else if !contactStats.ratchetSyncSupported {
|
||||
decryptionErrorItemFixButton(syncSupported: false) {
|
||||
alert = .syncNotSupportedContactAlert
|
||||
}
|
||||
} else {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
} else if case let .group(groupInfo) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir,
|
||||
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
|
||||
let memberStats = modelMember.activeConn?.connectionStats {
|
||||
if memberStats.ratchetSyncAllowed {
|
||||
decryptionErrorItemFixButton(syncSupported: true) {
|
||||
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
|
||||
}
|
||||
} else if !memberStats.ratchetSyncSupported {
|
||||
decryptionErrorItemFixButton(syncSupported: false) {
|
||||
alert = .syncNotSupportedMemberAlert
|
||||
}
|
||||
} else {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
} else {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
}
|
||||
|
||||
private func basicDecryptionErrorItem() -> some View {
|
||||
decryptionErrorItem { alert = .decryptionErrorAlert }
|
||||
}
|
||||
|
||||
private func decryptionErrorItemFixButton(syncSupported: Bool, _ onClick: @escaping (() -> Void)) -> some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ")
|
||||
}
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
}
|
||||
(
|
||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text("Fix connection")
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View {
|
||||
func text() -> Text {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
|
||||
}
|
||||
return ZStack(alignment: .bottomTrailing) {
|
||||
HStack {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ") + text()
|
||||
} else {
|
||||
text()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
private func message() -> Text {
|
||||
var message: Text
|
||||
let why = Text(decryptErrorReason)
|
||||
switch msgDecryptError {
|
||||
case .ratchetHeader:
|
||||
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
|
||||
case .tooManySkipped:
|
||||
message = Text("\(msgCount) messages skipped.") + Text("\n") + why
|
||||
case .ratchetEarlier:
|
||||
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
|
||||
case .other:
|
||||
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
private func syncMemberConnection(_ groupInfo: GroupInfo, _ member: GroupMember) {
|
||||
Task {
|
||||
do {
|
||||
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, mem, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncContactConnection(_ contact: Contact) {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiSyncContactRatchet(contact.apiId, false)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateContactConnectionStats(contact, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncAllowedAlert(_ syncConnection: @escaping () -> Void) -> Alert {
|
||||
Alert(
|
||||
title: Text("Fix connection?"),
|
||||
message: message(),
|
||||
primaryButton: .default(Text("Fix"), action: syncConnection),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//struct CIRcvDecryptionError_Previews: PreviewProvider {
|
||||
|
||||
@@ -22,7 +22,7 @@ struct ChatView: View {
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var keyboardVisible = false
|
||||
@State private var showDeleteMessage = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var customUserProfile: Profile?
|
||||
@@ -39,6 +39,16 @@ struct ChatView: View {
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.keyboardPadding()
|
||||
} else {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
|
||||
private var viewBody: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
return VStack(spacing: 0) {
|
||||
if searchMode {
|
||||
@@ -65,17 +75,14 @@ struct ChatView: View {
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
Task {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
}
|
||||
initChatView()
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil { dismiss() }
|
||||
.onChange(of: chatModel.chatId) { cId in
|
||||
if cId != nil {
|
||||
initChatView()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
VideoPlayerView.players.removeAll()
|
||||
@@ -185,6 +192,32 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func initChatView() {
|
||||
let cInfo = chat.chatInfo
|
||||
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)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiContactInfo error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
Task {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func searchToolbar() -> some View {
|
||||
HStack {
|
||||
HStack {
|
||||
@@ -616,7 +649,7 @@ struct ChatView: View {
|
||||
|
||||
private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu {
|
||||
UIMenu(
|
||||
title: NSLocalizedString("React...", comment: "chat item menu"),
|
||||
title: NSLocalizedString("React…", comment: "chat item menu"),
|
||||
image: UIImage(systemName: "face.smiling"),
|
||||
children: rs
|
||||
)
|
||||
|
||||
@@ -234,7 +234,7 @@ struct ComposeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var composeState: ComposeState
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@Binding var keyboardVisible: Bool
|
||||
|
||||
@State var linkUrl: URL? = nil
|
||||
@State var prevLinkUrl: URL? = nil
|
||||
@@ -943,19 +943,18 @@ struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
@State var composeState = ComposeState(message: "hello")
|
||||
@FocusState var keyboardVisible: Bool
|
||||
|
||||
return Group {
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible
|
||||
keyboardVisible: Binding.constant(true)
|
||||
)
|
||||
.environmentObject(ChatModel())
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible
|
||||
keyboardVisible: Binding.constant(true)
|
||||
)
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
@Binding var disableEditing: Bool
|
||||
let height: CGFloat
|
||||
let font: UIFont
|
||||
@FocusState.Binding var focused: Bool
|
||||
@Binding var focused: Bool
|
||||
let alignment: TextAlignment
|
||||
let onImagesAdded: ([UploadContent]) -> Void
|
||||
|
||||
@@ -144,13 +144,12 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
|
||||
struct NativeTextEditor_Previews: PreviewProvider{
|
||||
static var previews: some View {
|
||||
@FocusState var keyboardVisible: Bool
|
||||
return NativeTextEditor(
|
||||
text: Binding.constant("Hello, world!"),
|
||||
disableEditing: Binding.constant(false),
|
||||
height: 100,
|
||||
font: UIFont.preferredFont(forTextStyle: .body),
|
||||
focused: $keyboardVisible,
|
||||
focused: Binding.constant(false),
|
||||
alignment: TextAlignment.leading,
|
||||
onImagesAdded: { _ in }
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ struct SendMessageView: View {
|
||||
var onMediaAdded: ([UploadContent]) -> Void
|
||||
@State private var holdingVMR = false
|
||||
@Namespace var namespace
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@Binding var keyboardVisible: Bool
|
||||
@State private var teHeight: CGFloat = 42
|
||||
@State private var teFont: Font = .body
|
||||
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
@@ -401,7 +401,6 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
@State var composeStateNew = ComposeState()
|
||||
let ci = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
@State var composeStateEditing = ComposeState(editingItem: ci)
|
||||
@FocusState var keyboardVisible: Bool
|
||||
@State var sendEnabled: Bool = true
|
||||
|
||||
return Group {
|
||||
@@ -412,7 +411,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
composeState: $composeStateNew,
|
||||
sendMessage: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
keyboardVisible: Binding.constant(true)
|
||||
)
|
||||
}
|
||||
VStack {
|
||||
@@ -422,7 +421,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
composeState: $composeStateEditing,
|
||||
sendMessage: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
keyboardVisible: Binding.constant(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ struct GroupChatInfoView: View {
|
||||
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
.keyboardPadding()
|
||||
}
|
||||
|
||||
private func groupInfoHeader() -> some View {
|
||||
|
||||
@@ -27,6 +27,7 @@ struct GroupMemberInfoView: View {
|
||||
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case connRequestSentAlert(type: ConnReqType)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case other(alert: Alert)
|
||||
@@ -38,6 +39,7 @@ struct GroupMemberInfoView: View {
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .connRequestSentAlert: return "connRequestSentAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
case let .other(alert): return "other \(alert)"
|
||||
}
|
||||
@@ -77,6 +79,12 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
if let connStats = connectionStats,
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
} else if developerTools {
|
||||
synchronizeConnectionButtonForce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +137,18 @@ struct GroupMemberInfoView: View {
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
}
|
||||
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
@@ -162,7 +176,7 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
newRole = member.memberRole
|
||||
do {
|
||||
let stats = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
|
||||
member = mem
|
||||
connectionStats = stats
|
||||
@@ -185,6 +199,7 @@ struct GroupMemberInfoView: View {
|
||||
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
|
||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .connRequestSentAlert(type): return connReqSentAlert(type)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .other(alert): return alert
|
||||
@@ -291,7 +306,24 @@ struct GroupMemberInfoView: View {
|
||||
systemImage: member.verified ? "checkmark.shield" : "shield"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronizeConnectionButton() -> some View {
|
||||
Button {
|
||||
syncMemberConnection(force: false)
|
||||
} label: {
|
||||
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronizeConnectionButtonForce() -> some View {
|
||||
Button {
|
||||
alert = .syncConnectionForceAlert
|
||||
} label: {
|
||||
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeMemberButton(_ mem: GroupMember) -> some View {
|
||||
@@ -357,7 +389,11 @@ struct GroupMemberInfoView: View {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
connectionStats = stats
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
@@ -373,6 +409,9 @@ struct GroupMemberInfoView: View {
|
||||
do {
|
||||
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error aborting address change")
|
||||
@@ -382,6 +421,25 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncMemberConnection(force: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
|
||||
@@ -18,6 +18,14 @@ struct ChatListView: View {
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody.scrollDismissesKeyboard(.immediately)
|
||||
} else {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
|
||||
private var viewBody: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
NavStackCompat(
|
||||
isActive: Binding(
|
||||
@@ -76,7 +84,19 @@ struct ChatListView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
|
||||
.frame(width: 32, height: 32)
|
||||
.padding(.trailing, 4)
|
||||
let allRead = chatModel.users
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
.allSatisfy { u in u.unreadCount == 0 }
|
||||
if !allRead {
|
||||
unreadBadge(size: 12)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
@@ -84,19 +104,6 @@ struct ChatListView: View {
|
||||
} else {
|
||||
showSettings = true
|
||||
}
|
||||
} label: {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
|
||||
.frame(width: 32, height: 32)
|
||||
.padding(.trailing, 4)
|
||||
let allRead = chatModel.users
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
.allSatisfy { u in u.unreadCount == 0 }
|
||||
if !allRead {
|
||||
unreadBadge(size: 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
|
||||
@@ -65,7 +65,7 @@ struct MigrateToAppGroupView: View {
|
||||
case .exporting:
|
||||
center {
|
||||
ProgressView(value: 0.33)
|
||||
Text("Exporting database archive...")
|
||||
Text("Exporting database archive…")
|
||||
}
|
||||
migrationProgress()
|
||||
case .export_error:
|
||||
@@ -82,7 +82,7 @@ struct MigrateToAppGroupView: View {
|
||||
case .migrating:
|
||||
center {
|
||||
ProgressView(value: 0.67)
|
||||
Text("Migrating database archive...")
|
||||
Text("Migrating database archive…")
|
||||
}
|
||||
migrationProgress()
|
||||
case .migration_error:
|
||||
|
||||
9
apps/ios/Shared/Views/Helpers/Keyboard.swift
Normal file
9
apps/ios/Shared/Views/Helpers/Keyboard.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// Keyboard.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/07/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
21
apps/ios/Shared/Views/Helpers/KeyboardPadding.swift
Normal file
21
apps/ios/Shared/Views/Helpers/KeyboardPadding.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// KeyboardPadding.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/07/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@ViewBuilder func keyboardPadding() -> some View {
|
||||
if #available(iOS 17.0, *) {
|
||||
GeometryReader { g in
|
||||
self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom))
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ struct AddGroupView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createGroupView()
|
||||
createGroupView().keyboardPadding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ struct CreateProfile: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.keyboardPadding()
|
||||
}
|
||||
|
||||
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
|
||||
|
||||
@@ -18,7 +18,7 @@ struct TerminalView: View {
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State var composeState: ComposeState = ComposeState()
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var keyboardVisible = false
|
||||
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
@State private var terminalItem: TerminalItem?
|
||||
@State private var scrolled = false
|
||||
|
||||
@@ -115,11 +115,6 @@
|
||||
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
|
||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
|
||||
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
|
||||
5CCAA6DF2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCAA6DA2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a */; };
|
||||
5CCAA6E02A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCAA6DB2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a */; };
|
||||
5CCAA6E12A53713A00BAF93B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCAA6DC2A53713A00BAF93B /* libgmpxx.a */; };
|
||||
5CCAA6E22A53713A00BAF93B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCAA6DD2A53713A00BAF93B /* libgmp.a */; };
|
||||
5CCAA6E32A53713A00BAF93B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCAA6DE2A53713A00BAF93B /* libffi.a */; };
|
||||
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
|
||||
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
|
||||
@@ -146,6 +141,7 @@
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
|
||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
@@ -163,6 +159,11 @@
|
||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
|
||||
645041592A5C5749000221AD /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041542A5C5748000221AD /* libffi.a */; };
|
||||
6450415A2A5C5749000221AD /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041552A5C5748000221AD /* libgmp.a */; };
|
||||
6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */; };
|
||||
6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */; };
|
||||
6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041582A5C5748000221AD /* libgmpxx.a */; };
|
||||
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
|
||||
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
@@ -392,11 +393,6 @@
|
||||
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
|
||||
5CCAA6DA2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CCAA6DB2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a"; sourceTree = "<group>"; };
|
||||
5CCAA6DC2A53713A00BAF93B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CCAA6DD2A53713A00BAF93B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CCAA6DE2A53713A00BAF93B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
|
||||
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
|
||||
@@ -422,6 +418,7 @@
|
||||
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
@@ -438,6 +435,11 @@
|
||||
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
|
||||
645041542A5C5748000221AD /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
645041552A5C5748000221AD /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a"; sourceTree = "<group>"; };
|
||||
645041582A5C5748000221AD /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
|
||||
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
@@ -498,12 +500,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CCAA6E12A53713A00BAF93B /* libgmpxx.a in Frameworks */,
|
||||
5CCAA6DF2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a in Frameworks */,
|
||||
5CCAA6E02A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a in Frameworks */,
|
||||
5CCAA6E32A53713A00BAF93B /* libffi.a in Frameworks */,
|
||||
5CCAA6E22A53713A00BAF93B /* libgmp.a in Frameworks */,
|
||||
645041592A5C5749000221AD /* libffi.a in Frameworks */,
|
||||
6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */,
|
||||
6450415A2A5C5749000221AD /* libgmp.a in Frameworks */,
|
||||
6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -564,11 +566,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CCAA6DE2A53713A00BAF93B /* libffi.a */,
|
||||
5CCAA6DD2A53713A00BAF93B /* libgmp.a */,
|
||||
5CCAA6DC2A53713A00BAF93B /* libgmpxx.a */,
|
||||
5CCAA6DA2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i-ghc8.10.7.a */,
|
||||
5CCAA6DB2A53713A00BAF93B /* libHSsimplex-chat-5.2.0.0-H74s0RJkRXv7ArDExYHa6i.a */,
|
||||
645041542A5C5748000221AD /* libffi.a */,
|
||||
645041552A5C5748000221AD /* libgmp.a */,
|
||||
645041582A5C5748000221AD /* libgmpxx.a */,
|
||||
645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */,
|
||||
645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -619,6 +621,7 @@
|
||||
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
|
||||
64466DCB29FFE3E800E3D48D /* MailView.swift */,
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -1142,6 +1145,7 @@
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
@@ -1470,7 +1474,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 151;
|
||||
CURRENT_PROJECT_VERSION = 153;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1512,7 +1516,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 151;
|
||||
CURRENT_PROJECT_VERSION = 153;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1592,7 +1596,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 151;
|
||||
CURRENT_PROJECT_VERSION = 153;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1624,7 +1628,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 151;
|
||||
CURRENT_PROJECT_VERSION = 153;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
||||
@@ -74,6 +74,8 @@ public enum ChatCommand {
|
||||
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
|
||||
case apiAbortSwitchContact(contactId: Int64)
|
||||
case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
|
||||
case apiSyncContactRatchet(contactId: Int64, force: Bool)
|
||||
case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool)
|
||||
case apiGetContactCode(contactId: Int64)
|
||||
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
|
||||
case apiVerifyContact(contactId: Int64, connectionCode: String?)
|
||||
@@ -185,6 +187,16 @@ public enum ChatCommand {
|
||||
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
|
||||
case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)"
|
||||
case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)"
|
||||
case let .apiSyncContactRatchet(contactId, force): if force {
|
||||
return "/_sync @\(contactId) force=on"
|
||||
} else {
|
||||
return "/_sync @\(contactId)"
|
||||
}
|
||||
case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force {
|
||||
return "/_sync #\(groupId) \(groupMemberId) force=on"
|
||||
} else {
|
||||
return "/_sync #\(groupId) \(groupMemberId)"
|
||||
}
|
||||
case let .apiGetContactCode(contactId): return "/_get code @\(contactId)"
|
||||
case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)"
|
||||
case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)"
|
||||
@@ -294,6 +306,8 @@ public enum ChatCommand {
|
||||
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
|
||||
case .apiAbortSwitchContact: return "apiAbortSwitchContact"
|
||||
case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember"
|
||||
case .apiSyncContactRatchet: return "apiSyncContactRatchet"
|
||||
case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet"
|
||||
case .apiGetContactCode: return "apiGetContactCode"
|
||||
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
|
||||
case .apiVerifyContact: return "apiVerifyContact"
|
||||
@@ -410,6 +424,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case groupMemberSwitchStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
|
||||
case contactSwitchAborted(user: User, contact: Contact, connectionStats: ConnectionStats)
|
||||
case groupMemberSwitchAborted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
|
||||
case contactSwitch(user: User, contact: Contact, switchProgress: SwitchProgress)
|
||||
case groupMemberSwitch(user: User, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress)
|
||||
case contactRatchetSyncStarted(user: User, contact: Contact, connectionStats: ConnectionStats)
|
||||
case groupMemberRatchetSyncStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
|
||||
case contactRatchetSync(user: User, contact: Contact, ratchetSyncProgress: RatchetSyncProgress)
|
||||
case groupMemberRatchetSync(user: User, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress)
|
||||
case contactVerificationReset(user: User, contact: Contact)
|
||||
case groupMemberVerificationReset(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case contactCode(user: User, contact: Contact, connectionCode: String)
|
||||
case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
|
||||
case connectionVerified(user: User, verified: Bool, expectedCode: String)
|
||||
@@ -533,6 +555,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .groupMemberSwitchStarted: return "groupMemberSwitchStarted"
|
||||
case .contactSwitchAborted: return "contactSwitchAborted"
|
||||
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted"
|
||||
case .contactSwitch: return "contactSwitch"
|
||||
case .groupMemberSwitch: return "groupMemberSwitch"
|
||||
case .contactRatchetSyncStarted: return "contactRatchetSyncStarted"
|
||||
case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted"
|
||||
case .contactRatchetSync: return "contactRatchetSync"
|
||||
case .groupMemberRatchetSync: return "groupMemberRatchetSync"
|
||||
case .contactVerificationReset: return "contactVerificationReset"
|
||||
case .groupMemberVerificationReset: return "groupMemberVerificationReset"
|
||||
case .contactCode: return "contactCode"
|
||||
case .groupMemberCode: return "groupMemberCode"
|
||||
case .connectionVerified: return "connectionVerified"
|
||||
@@ -655,6 +685,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))")
|
||||
case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))")
|
||||
case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
|
||||
case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
|
||||
case let .contactVerificationReset(u, contact): return withUser(u, "contact: \(String(describing: contact))")
|
||||
case let .groupMemberVerificationReset(u, groupInfo, member): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))")
|
||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||
@@ -1106,9 +1144,20 @@ public struct ChatSettings: Codable {
|
||||
public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, favorite: false)
|
||||
}
|
||||
|
||||
public struct ConnectionStats: Codable {
|
||||
public struct ConnectionStats: Decodable {
|
||||
public var connAgentVersion: Int
|
||||
public var rcvQueuesInfo: [RcvQueueInfo]
|
||||
public var sndQueuesInfo: [SndQueueInfo]
|
||||
public var ratchetSyncState: RatchetSyncState
|
||||
public var ratchetSyncSupported: Bool
|
||||
|
||||
public var ratchetSyncAllowed: Bool {
|
||||
ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState)
|
||||
}
|
||||
|
||||
public var ratchetSyncSendProhibited: Bool {
|
||||
[.required, .started, .agreed].contains(ratchetSyncState)
|
||||
}
|
||||
}
|
||||
|
||||
public struct RcvQueueInfo: Codable {
|
||||
@@ -1134,6 +1183,30 @@ public enum SndSwitchStatus: String, Codable {
|
||||
case sendingQTEST = "sending_qtest"
|
||||
}
|
||||
|
||||
public enum QueueDirection: String, Decodable {
|
||||
case rcv
|
||||
case snd
|
||||
}
|
||||
|
||||
public struct SwitchProgress: Decodable {
|
||||
public var queueDirection: QueueDirection
|
||||
public var switchPhase: SwitchPhase
|
||||
public var connectionStats: ConnectionStats
|
||||
}
|
||||
|
||||
public struct RatchetSyncProgress: Decodable {
|
||||
public var ratchetSyncStatus: RatchetSyncState
|
||||
public var connectionStats: ConnectionStats
|
||||
}
|
||||
|
||||
public enum RatchetSyncState: String, Decodable {
|
||||
case ok
|
||||
case allowed
|
||||
case required
|
||||
case started
|
||||
case agreed
|
||||
}
|
||||
|
||||
public struct UserContactLink: Decodable {
|
||||
public var connReqContact: String
|
||||
public var autoAccept: AutoAccept?
|
||||
|
||||
@@ -1353,7 +1353,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public var id: ChatId { get { "@\(contactId)" } }
|
||||
public var apiId: Int64 { get { contactId } }
|
||||
public var ready: Bool { get { activeConn.connStatus == .ready } }
|
||||
public var sendMsgEnabled: Bool { get { true } }
|
||||
public var sendMsgEnabled: Bool { get { !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false) } }
|
||||
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
@@ -1426,6 +1426,12 @@ public struct Connection: Decodable {
|
||||
public var customUserProfileId: Int64?
|
||||
public var connectionCode: SecurityCode?
|
||||
|
||||
public var connectionStats: ConnectionStats? = nil
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
|
||||
}
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
static let sampleData = Connection(
|
||||
@@ -2456,11 +2462,15 @@ public enum CIContent: Decodable, ItemContent {
|
||||
public enum MsgDecryptError: String, Decodable {
|
||||
case ratchetHeader
|
||||
case tooManySkipped
|
||||
case ratchetEarlier
|
||||
case other
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .ratchetHeader: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
|
||||
case .tooManySkipped: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
|
||||
case .ratchetEarlier: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
|
||||
case .other: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3057,6 +3067,8 @@ public enum SndGroupEvent: Decodable {
|
||||
|
||||
public enum RcvConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase)
|
||||
case ratchetSync(syncStatus: RatchetSyncState)
|
||||
case verificationCodeReset
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
@@ -3064,25 +3076,51 @@ public enum RcvConnEvent: Decodable {
|
||||
if case .completed = phase {
|
||||
return NSLocalizedString("changed address for you", comment: "chat item text")
|
||||
}
|
||||
return NSLocalizedString("changing address...", comment: "chat item text")
|
||||
return NSLocalizedString("changing address…", comment: "chat item text")
|
||||
case let .ratchetSync(syncStatus):
|
||||
return ratchetSyncStatusToText(syncStatus)
|
||||
case .verificationCodeReset:
|
||||
return NSLocalizedString("security code changed", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
|
||||
switch ratchetSyncStatus {
|
||||
case .ok: return NSLocalizedString("encryption ok", comment: "chat item text")
|
||||
case .allowed: return NSLocalizedString("encryption re-negotiation allowed", comment: "chat item text")
|
||||
case .required: return NSLocalizedString("encryption re-negotiation required", comment: "chat item text")
|
||||
case .started: return NSLocalizedString("agreeing encryption…", comment: "chat item text")
|
||||
case .agreed: return NSLocalizedString("encryption agreed", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
||||
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .switchQueue(phase, member):
|
||||
if let name = member?.profile.profileViewName {
|
||||
return phase == .completed
|
||||
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
|
||||
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@…", comment: "chat item text"), name)
|
||||
}
|
||||
return phase == .completed
|
||||
? NSLocalizedString("you changed address", comment: "chat item text")
|
||||
: NSLocalizedString("changing address...", comment: "chat item text")
|
||||
? NSLocalizedString("you changed address", comment: "chat item text")
|
||||
: NSLocalizedString("changing address…", comment: "chat item text")
|
||||
case let .ratchetSync(syncStatus, member):
|
||||
if let name = member?.profile.profileViewName {
|
||||
switch syncStatus {
|
||||
case .ok: return String.localizedStringWithFormat(NSLocalizedString("encryption ok for %@", comment: "chat item text"), name)
|
||||
case .allowed: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation allowed for %@", comment: "chat item text"), name)
|
||||
case .required: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation required for %@", comment: "chat item text"), name)
|
||||
case .started: return String.localizedStringWithFormat(NSLocalizedString("agreeing encryption for %@…", comment: "chat item text"), name)
|
||||
case .agreed: return String.localizedStringWithFormat(NSLocalizedString("encryption agreed for %@", comment: "chat item text"), name)
|
||||
}
|
||||
}
|
||||
return ratchetSyncStatusToText(syncStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.ComposeState
|
||||
@@ -136,13 +137,36 @@ object ChatModel {
|
||||
|
||||
fun updateChatInfo(cInfo: ChatInfo) {
|
||||
val i = getChatIndex(cInfo.id)
|
||||
if (i >= 0) chats[i] = chats[i].copy(chatInfo = cInfo)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
|
||||
val currentStats = currentCInfo.contact.activeConn.connectionStats
|
||||
val newStats = newCInfo.contact.activeConn.connectionStats
|
||||
if (currentStats != null && newStats == null) {
|
||||
newCInfo = newCInfo.copy(
|
||||
contact = newCInfo.contact.copy(
|
||||
activeConn = newCInfo.contact.activeConn.copy(
|
||||
connectionStats = currentStats
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
@@ -441,6 +465,15 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.agentConnId] = status
|
||||
}
|
||||
@@ -752,7 +785,7 @@ data class Contact(
|
||||
override val id get() = "@$contactId"
|
||||
override val apiId get() = contactId
|
||||
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
||||
override val sendMsgEnabled get() = true
|
||||
override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
override val incognito get() = contactConnIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
@@ -832,7 +865,8 @@ data class Connection(
|
||||
val connLevel: Int,
|
||||
val viaGroupLink: Boolean,
|
||||
val customUserProfileId: Long? = null,
|
||||
val connectionCode: SecurityCode? = null
|
||||
val connectionCode: SecurityCode? = null,
|
||||
val connectionStats: ConnectionStats? = null
|
||||
) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
@@ -1773,11 +1807,15 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable
|
||||
enum class MsgDecryptError {
|
||||
@SerialName("ratchetHeader") RatchetHeader,
|
||||
@SerialName("tooManySkipped") TooManySkipped;
|
||||
@SerialName("tooManySkipped") TooManySkipped,
|
||||
@SerialName("ratchetEarlier") RatchetEarlier,
|
||||
@SerialName("other") Other;
|
||||
|
||||
val text: String get() = when (this) {
|
||||
RatchetHeader -> generalGetString(MR.strings.decryption_error)
|
||||
TooManySkipped -> generalGetString(MR.strings.decryption_error)
|
||||
RatchetEarlier -> generalGetString(MR.strings.decryption_error)
|
||||
Other -> generalGetString(MR.strings.decryption_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2333,18 +2371,33 @@ sealed class SndGroupEvent() {
|
||||
@Serializable
|
||||
sealed class RcvConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
|
||||
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent()
|
||||
@Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is SwitchQueue -> when (phase) {
|
||||
SwitchPhase.Completed -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_completed)
|
||||
else -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
is RatchetSync -> ratchetSyncStatusToText(syncStatus)
|
||||
is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset)
|
||||
}
|
||||
}
|
||||
|
||||
fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String {
|
||||
return when (ratchetSyncStatus) {
|
||||
RatchetSyncState.Ok -> generalGetString(MR.strings.conn_event_ratchet_sync_ok)
|
||||
RatchetSyncState.Allowed -> generalGetString(MR.strings.conn_event_ratchet_sync_allowed)
|
||||
RatchetSyncState.Required -> generalGetString(MR.strings.conn_event_ratchet_sync_required)
|
||||
RatchetSyncState.Started -> generalGetString(MR.strings.conn_event_ratchet_sync_started)
|
||||
RatchetSyncState.Agreed -> generalGetString(MR.strings.conn_event_ratchet_sync_agreed)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SndConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
|
||||
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
@@ -2360,6 +2413,19 @@ sealed class SndConnEvent {
|
||||
else -> generalGetString(MR.strings.snd_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
}
|
||||
|
||||
is RatchetSync -> {
|
||||
member?.profile?.profileViewName?.let {
|
||||
return when (syncStatus) {
|
||||
RatchetSyncState.Ok -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_ok), it)
|
||||
RatchetSyncState.Allowed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_allowed), it)
|
||||
RatchetSyncState.Required -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_required), it)
|
||||
RatchetSyncState.Started -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_started), it)
|
||||
RatchetSyncState.Agreed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_agreed), it)
|
||||
}
|
||||
}
|
||||
ratchetSyncStatusToText(syncStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -722,9 +722,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGroupMemberInfo(groupId: Long, groupMemberId: Long): ConnectionStats? {
|
||||
suspend fun apiGroupMemberInfo(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats?>? {
|
||||
val r = sendCmd(CC.APIGroupMemberInfo(groupId, groupMemberId))
|
||||
if (r is CR.GroupMemberInfo) return r.connectionStats_
|
||||
if (r is CR.GroupMemberInfo) return Pair(r.member, r.connectionStats_)
|
||||
Log.e(TAG, "apiGroupMemberInfo bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
@@ -736,9 +736,9 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long): ConnectionStats? {
|
||||
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? {
|
||||
val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))
|
||||
if (r is CR.GroupMemberSwitchStarted) return r.connectionStats
|
||||
if (r is CR.GroupMemberSwitchStarted) return Pair(r.member, r.connectionStats)
|
||||
apiErrorAlert("apiSwitchGroupMember", generalGetString(MR.strings.error_changing_address), r)
|
||||
return null
|
||||
}
|
||||
@@ -750,13 +750,27 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiAbortSwitchGroupMember(groupId: Long, groupMemberId: Long): ConnectionStats? {
|
||||
suspend fun apiAbortSwitchGroupMember(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? {
|
||||
val r = sendCmd(CC.APIAbortSwitchGroupMember(groupId, groupMemberId))
|
||||
if (r is CR.GroupMemberSwitchAborted) return r.connectionStats
|
||||
if (r is CR.GroupMemberSwitchAborted) return Pair(r.member, r.connectionStats)
|
||||
apiErrorAlert("apiAbortSwitchGroupMember", generalGetString(MR.strings.error_aborting_address_change), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSyncContactRatchet(contactId: Long, force: Boolean): ConnectionStats? {
|
||||
val r = sendCmd(CC.APISyncContactRatchet(contactId, force))
|
||||
if (r is CR.ContactRatchetSyncStarted) return r.connectionStats
|
||||
apiErrorAlert("apiSyncContactRatchet", generalGetString(MR.strings.error_synchronizing_connection), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSyncGroupMemberRatchet(groupId: Long, groupMemberId: Long, force: Boolean): Pair<GroupMember, ConnectionStats>? {
|
||||
val r = sendCmd(CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force))
|
||||
if (r is CR.GroupMemberRatchetSyncStarted) return Pair(r.member, r.connectionStats)
|
||||
apiErrorAlert("apiSyncGroupMemberRatchet", generalGetString(MR.strings.error_synchronizing_connection), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> {
|
||||
val r = sendCmd(CC.APIGetContactCode(contactId))
|
||||
if (r is CR.ContactCode) return r.contact to r.connectionCode
|
||||
@@ -1576,6 +1590,14 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactSwitch ->
|
||||
chatModel.updateContactConnectionStats(r.contact, r.switchProgress.connectionStats)
|
||||
is CR.GroupMemberSwitch ->
|
||||
chatModel.updateGroupMemberConnectionStats(r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
is CR.ContactRatchetSync ->
|
||||
chatModel.updateContactConnectionStats(r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
is CR.GroupMemberRatchetSync ->
|
||||
chatModel.updateGroupMemberConnectionStats(r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
else ->
|
||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||
}
|
||||
@@ -1796,6 +1818,8 @@ sealed class CC {
|
||||
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APIAbortSwitchContact(val contactId: Long): CC()
|
||||
class APIAbortSwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISyncContactRatchet(val contactId: Long, val force: Boolean): CC()
|
||||
class APISyncGroupMemberRatchet(val groupId: Long, val groupMemberId: Long, val force: Boolean): CC()
|
||||
class APIGetContactCode(val contactId: Long): CC()
|
||||
class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC()
|
||||
@@ -1891,6 +1915,8 @@ sealed class CC {
|
||||
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
|
||||
is APIAbortSwitchContact -> "/_abort switch @$contactId"
|
||||
is APIAbortSwitchGroupMember -> "/_abort switch #$groupId $groupMemberId"
|
||||
is APISyncContactRatchet -> if (force) "/_sync @$contactId force=on" else "/_sync @$contactId"
|
||||
is APISyncGroupMemberRatchet -> if (force) "/_sync #$groupId $groupMemberId force=on" else "/_sync #$groupId $groupMemberId"
|
||||
is APIGetContactCode -> "/_get code @$contactId"
|
||||
is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId"
|
||||
is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else ""
|
||||
@@ -1981,6 +2007,8 @@ sealed class CC {
|
||||
is APISwitchGroupMember -> "apiSwitchGroupMember"
|
||||
is APIAbortSwitchContact -> "apiAbortSwitchContact"
|
||||
is APIAbortSwitchGroupMember -> "apiAbortSwitchGroupMember"
|
||||
is APISyncContactRatchet -> "apiSyncContactRatchet"
|
||||
is APISyncGroupMemberRatchet -> "apiSyncGroupMemberRatchet"
|
||||
is APIGetContactCode -> "apiGetContactCode"
|
||||
is APIGetGroupMemberCode -> "apiGetGroupMemberCode"
|
||||
is APIVerifyContact -> "apiVerifyContact"
|
||||
@@ -3168,6 +3196,14 @@ sealed class CR {
|
||||
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("groupMemberSwitchAborted") class GroupMemberSwitchAborted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactSwitch") class ContactSwitch(val user: User, val contact: Contact, val switchProgress: SwitchProgress): CR()
|
||||
@Serializable @SerialName("groupMemberSwitch") class GroupMemberSwitch(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val switchProgress: SwitchProgress): CR()
|
||||
@Serializable @SerialName("contactRatchetSyncStarted") class ContactRatchetSyncStarted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("groupMemberRatchetSyncStarted") class GroupMemberRatchetSyncStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactRatchetSync") class ContactRatchetSync(val user: User, val contact: Contact, val ratchetSyncProgress: RatchetSyncProgress): CR()
|
||||
@Serializable @SerialName("groupMemberRatchetSync") class GroupMemberRatchetSync(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val ratchetSyncProgress: RatchetSyncProgress): CR()
|
||||
@Serializable @SerialName("contactVerificationReset") class ContactVerificationReset(val user: User, val contact: Contact): CR()
|
||||
@Serializable @SerialName("groupMemberVerificationReset") class GroupMemberVerificationReset(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("contactCode") class ContactCode(val user: User, val contact: Contact, val connectionCode: String): CR()
|
||||
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
|
||||
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: User, val verified: Boolean, val expectedCode: String): CR()
|
||||
@@ -3286,6 +3322,14 @@ sealed class CR {
|
||||
is GroupMemberSwitchStarted -> "groupMemberSwitchStarted"
|
||||
is ContactSwitchAborted -> "contactSwitchAborted"
|
||||
is GroupMemberSwitchAborted -> "groupMemberSwitchAborted"
|
||||
is ContactSwitch -> "contactSwitch"
|
||||
is GroupMemberSwitch -> "groupMemberSwitch"
|
||||
is ContactRatchetSyncStarted -> "contactRatchetSyncStarted"
|
||||
is GroupMemberRatchetSyncStarted -> "groupMemberRatchetSyncStarted"
|
||||
is ContactRatchetSync -> "contactRatchetSync"
|
||||
is GroupMemberRatchetSync -> "groupMemberRatchetSync"
|
||||
is ContactVerificationReset -> "contactVerificationReset"
|
||||
is GroupMemberVerificationReset -> "groupMemberVerificationReset"
|
||||
is ContactCode -> "contactCode"
|
||||
is GroupMemberCode -> "groupMemberCode"
|
||||
is ConnectionVerified -> "connectionVerified"
|
||||
@@ -3401,6 +3445,14 @@ sealed class CR {
|
||||
is GroupMemberSwitchStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
|
||||
is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
|
||||
is GroupMemberSwitchAborted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
|
||||
is ContactSwitch -> withUser(user, "contact: ${json.encodeToString(contact)}\nswitchProgress: ${json.encodeToString(switchProgress)}")
|
||||
is GroupMemberSwitch -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nswitchProgress: ${json.encodeToString(switchProgress)}")
|
||||
is ContactRatchetSyncStarted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
|
||||
is GroupMemberRatchetSyncStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
|
||||
is ContactRatchetSync -> withUser(user, "contact: ${json.encodeToString(contact)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}")
|
||||
is GroupMemberRatchetSync -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}")
|
||||
is ContactVerificationReset -> withUser(user, "contact: ${json.encodeToString(contact)}")
|
||||
is GroupMemberVerificationReset -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}")
|
||||
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
||||
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||
@@ -3532,7 +3584,19 @@ abstract class TerminalItem {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ConnectionStats(val rcvQueuesInfo: List<RcvQueueInfo>, val sndQueuesInfo: List<SndQueueInfo>)
|
||||
class ConnectionStats(
|
||||
val connAgentVersion: Int,
|
||||
val rcvQueuesInfo: List<RcvQueueInfo>,
|
||||
val sndQueuesInfo: List<SndQueueInfo>,
|
||||
val ratchetSyncState: RatchetSyncState,
|
||||
val ratchetSyncSupported: Boolean
|
||||
) {
|
||||
val ratchetSyncAllowed: Boolean get() =
|
||||
ratchetSyncSupported && listOf(RatchetSyncState.Allowed, RatchetSyncState.Required).contains(ratchetSyncState)
|
||||
|
||||
val ratchetSyncSendProhibited: Boolean get() =
|
||||
listOf(RatchetSyncState.Required, RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class RcvQueueInfo(
|
||||
@@ -3561,6 +3625,34 @@ enum class SndSwitchStatus {
|
||||
@SerialName("sending_qtest") SendingQTEST
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class QueueDirection {
|
||||
@SerialName("rcv") Rcv,
|
||||
@SerialName("snd") Snd
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SwitchProgress(
|
||||
val queueDirection: QueueDirection,
|
||||
val switchPhase: SwitchPhase,
|
||||
val connectionStats: ConnectionStats
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class RatchetSyncProgress(
|
||||
val ratchetSyncStatus: RatchetSyncState,
|
||||
val connectionStats: ConnectionStats
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class RatchetSyncState {
|
||||
@SerialName("ok") Ok,
|
||||
@SerialName("allowed") Allowed,
|
||||
@SerialName("required") Required,
|
||||
@SerialName("started") Started,
|
||||
@SerialName("agreed") Agreed
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) {
|
||||
val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
|
||||
|
||||
@@ -33,6 +33,8 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.ContactPreferencesView
|
||||
import chat.simplex.common.views.chat.VerifyCodeView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -81,14 +83,45 @@ fun ChatInfoView(
|
||||
switchContactAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
connStats.value = chatModel.controller.apiSwitchContact(contact.contactId)
|
||||
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
})
|
||||
},
|
||||
abortSwitchContactAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
connStats.value = chatModel.controller.apiAbortSwitchContact(contact.contactId)
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncContactConnection = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
},
|
||||
syncContactConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -176,6 +209,8 @@ fun ChatInfoLayout(
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
abortSwitchContactAddress: () -> Unit,
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
@@ -205,6 +240,11 @@ fun ChatInfoLayout(
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncContactConnection)
|
||||
} else if (developerTools) {
|
||||
SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
@@ -228,12 +268,12 @@ fun ChatInfoLayout(
|
||||
}
|
||||
if (cStats != null) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchContactAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchContactAddress
|
||||
)
|
||||
}
|
||||
@@ -419,6 +459,28 @@ fun AbortSwitchAddressButton(disabled: Boolean, abortSwitchAddress: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SynchronizeConnectionButton(syncConnection: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_sync_problem),
|
||||
stringResource(MR.strings.fix_connection),
|
||||
click = syncConnection,
|
||||
textColor = WarningOrange,
|
||||
iconColor = WarningOrange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_warning),
|
||||
stringResource(MR.strings.renegotiate_encryption),
|
||||
click = syncConnectionForce,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -496,6 +558,16 @@ fun showAbortSwitchAddressAlert(abortSwitchAddress: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.sync_connection_force_question),
|
||||
text = generalGetString(MR.strings.sync_connection_force_desc),
|
||||
confirmText = generalGetString(MR.strings.sync_connection_force_confirm),
|
||||
onConfirm = syncConnectionForce,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
@@ -518,6 +590,8 @@ fun PreviewChatInfoLayout() {
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
abortSwitchContactAddress = {},
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,7 +161,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
@@ -254,6 +255,47 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
chatModel.updateContactConnectionStats(contact, r.first)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
findModelChat = { chatId ->
|
||||
chatModel.getChat(chatId)
|
||||
},
|
||||
findModelMember = { memberId ->
|
||||
chatModel.groupMembers.find { it.id == memberId }
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
@@ -338,6 +380,12 @@ fun ChatLayout(
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
|
||||
findModelChat: (String) -> Chat?,
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
addMembers: (GroupInfo) -> Unit,
|
||||
@@ -382,7 +430,9 @@ fun ChatLayout(
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -555,6 +605,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
|
||||
findModelChat: (String) -> Chat?,
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
@@ -670,11 +726,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@@ -685,7 +741,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,6 +1163,12 @@ fun PreviewChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
@@ -1169,6 +1231,12 @@ fun PreviewGroupChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
|
||||
@@ -60,7 +60,8 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
},
|
||||
showMemberInfo = { member ->
|
||||
withApi {
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
|
||||
@@ -100,14 +100,46 @@ fun GroupMemberInfoView(
|
||||
switchMemberAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
connStats.value = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
abortSwitchMemberAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
connStats.value = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncMemberConnection = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -174,6 +206,8 @@ fun GroupMemberInfoLayout(
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
abortSwitchMemberAddress: () -> Unit,
|
||||
syncMemberConnection: () -> Unit,
|
||||
syncMemberConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
@@ -210,6 +244,11 @@ fun GroupMemberInfoLayout(
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncMemberConnection)
|
||||
} else if (developerTools) {
|
||||
SynchronizeConnectionButtonForce(syncMemberConnectionForce)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
@@ -252,12 +291,12 @@ fun GroupMemberInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchMemberAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchMemberAddress
|
||||
)
|
||||
}
|
||||
@@ -412,6 +451,8 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
abortSwitchMemberAddress = {},
|
||||
syncMemberConnection = {},
|
||||
syncMemberConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,232 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.MsgDecryptError
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
|
||||
CIMsgError(ci, timedMessagesTTL, showMember) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.decryption_error),
|
||||
text = when (msgDecryptError) {
|
||||
MsgDecryptError.RatchetHeader -> String.format(generalGetString(MR.strings.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_permanent_error_reconnect)
|
||||
MsgDecryptError.TooManySkipped -> String.format(generalGetString(MR.strings.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_permanent_error_reconnect)
|
||||
fun CIRcvDecryptionError(
|
||||
msgDecryptError: MsgDecryptError,
|
||||
msgCount: UInt,
|
||||
cInfo: ChatInfo,
|
||||
ci: ChatItem,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
|
||||
findModelChat: (String) -> Chat?,
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
showMember: Boolean
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
updateContactStats(cInfo.contact)
|
||||
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
|
||||
updateMemberStats(cInfo.groupInfo, ci.chatDir.groupMember)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BasicDecryptionErrorItem() {
|
||||
DecryptionErrorItem(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.decryption_error),
|
||||
text = alertMessage(msgDecryptError, msgCount)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val modelCInfo = findModelChat(cInfo.id)?.chatInfo
|
||||
if (modelCInfo is ChatInfo.Direct) {
|
||||
val modelContactStats = modelCInfo.contact.activeConn.connectionStats
|
||||
if (modelContactStats != null) {
|
||||
if (modelContactStats.ratchetSyncAllowed) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.fix_connection_question),
|
||||
text = alertMessage(msgDecryptError, msgCount),
|
||||
confirmText = generalGetString(MR.strings.fix_connection_confirm),
|
||||
onConfirm = { syncContactConnection(cInfo.contact) },
|
||||
)
|
||||
},
|
||||
syncSupported = true
|
||||
)
|
||||
} else if (!modelContactStats.ratchetSyncSupported) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.fix_connection_not_supported_by_contact),
|
||||
text = alertMessage(msgDecryptError, msgCount)
|
||||
)
|
||||
},
|
||||
syncSupported = false
|
||||
)
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
|
||||
val modelMember = findModelMember(ci.chatDir.groupMember.id)
|
||||
val modelMemberStats = modelMember?.activeConn?.connectionStats
|
||||
if (modelMemberStats != null) {
|
||||
if (modelMemberStats.ratchetSyncAllowed) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.fix_connection_question),
|
||||
text = alertMessage(msgDecryptError, msgCount),
|
||||
confirmText = generalGetString(MR.strings.fix_connection_confirm),
|
||||
onConfirm = { syncMemberConnection(cInfo.groupInfo, modelMember) },
|
||||
)
|
||||
},
|
||||
syncSupported = true
|
||||
)
|
||||
} else if (!modelMemberStats.ratchetSyncSupported) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member),
|
||||
text = alertMessage(msgDecryptError, msgCount)
|
||||
)
|
||||
},
|
||||
syncSupported = false
|
||||
)
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
} else {
|
||||
BasicDecryptionErrorItem()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DecryptionErrorItemFixButton(
|
||||
ci: ChatItem,
|
||||
showMember: Boolean,
|
||||
onClick: () -> Unit,
|
||||
syncSupported: Boolean
|
||||
) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = receivedColor,
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
Row {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_sync_problem),
|
||||
stringResource(MR.strings.fix_connection),
|
||||
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(2.dp))
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.fix_connection))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
|
||||
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
||||
},
|
||||
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DecryptionErrorItem(
|
||||
ci: ChatItem,
|
||||
showMember: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = receivedColor,
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alertMessage(msgDecryptError: MsgDecryptError, msgCount: UInt): String {
|
||||
return when (msgDecryptError) {
|
||||
MsgDecryptError.RatchetHeader -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
|
||||
|
||||
MsgDecryptError.TooManySkipped -> String.format(generalGetString(MR.strings.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
|
||||
|
||||
MsgDecryptError.RatchetEarlier -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
|
||||
|
||||
MsgDecryptError.Other -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ fun ChatItemView(
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
|
||||
findModelChat: (String) -> Chat?,
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
) {
|
||||
@@ -285,7 +291,7 @@ fun ChatItemView(
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
@@ -508,6 +514,12 @@ fun PreviewChatItemView() {
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
)
|
||||
@@ -531,6 +543,12 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
)
|
||||
|
||||
@@ -135,8 +135,19 @@ suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: St
|
||||
|
||||
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
|
||||
val currentMembers = chatModel.groupMembers
|
||||
val newMembers = groupMembers.map { newMember ->
|
||||
val currentMember = currentMembers.find { it.id == newMember.id }
|
||||
val currentMemberStats = currentMember?.activeConn?.connectionStats
|
||||
val newMemberConn = newMember.activeConn
|
||||
if (currentMemberStats != null && newMemberConn != null && newMemberConn.connectionStats == null) {
|
||||
newMember.copy(activeConn = newMemberConn.copy(connectionStats = currentMemberStats))
|
||||
} else {
|
||||
newMember
|
||||
}
|
||||
}
|
||||
chatModel.groupMembers.clear()
|
||||
chatModel.groupMembers.addAll(groupMembers)
|
||||
chatModel.groupMembers.addAll(newMembers)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
<string name="error_aborting_address_change">Error aborting address change</string>
|
||||
<string name="error_synchronizing_connection">Error synchronizing connection</string>
|
||||
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
|
||||
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
|
||||
<string name="error_xftp_test_server_auth">Server requires authorization to upload, check password</string>
|
||||
@@ -339,6 +340,9 @@
|
||||
<string name="abort_switch_receiving_address_question">Abort changing address?</string>
|
||||
<string name="abort_switch_receiving_address_desc">Address change will be aborted. Old receiving address will be used.</string>
|
||||
<string name="abort_switch_receiving_address_confirm">Abort</string>
|
||||
<string name="sync_connection_force_question">Renegotiate encryption?</string>
|
||||
<string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string>
|
||||
<string name="sync_connection_force_confirm">Renegotiate</string>
|
||||
<string name="view_security_code">View security code</string>
|
||||
<string name="verify_security_code">Verify security code</string>
|
||||
|
||||
@@ -816,10 +820,9 @@
|
||||
<string name="alert_text_msg_bad_hash">The hash of the previous message is different."</string>
|
||||
<string name="alert_title_msg_bad_id">Bad message ID</string>
|
||||
<string name="alert_text_msg_bad_id">The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messages failed to decrypt.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messages failed to decrypt.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d messages skipped.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">It can happen when you or your connection used the old database backup.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">This error is permanent for this connection, please re-connect.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Please report it to the developers.</string>
|
||||
|
||||
<!-- Privacy settings -->
|
||||
@@ -1065,6 +1068,17 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changing address for %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">you changed address</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
<string name="conn_event_ratchet_sync_ok">encryption ok</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">encryption re-negotiation allowed</string>
|
||||
<string name="conn_event_ratchet_sync_required">encryption re-negotiation required</string>
|
||||
<string name="conn_event_ratchet_sync_started">agreeing encryption…</string>
|
||||
<string name="conn_event_ratchet_sync_agreed">encryption agreed</string>
|
||||
<string name="snd_conn_event_ratchet_sync_ok">encryption ok for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">encryption re-negotiation allowed for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_required">encryption re-negotiation required for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
|
||||
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_observer">observer</string>
|
||||
@@ -1184,6 +1198,12 @@
|
||||
<string name="network_status">Network status</string>
|
||||
<string name="switch_receiving_address">Change receiving address</string>
|
||||
<string name="abort_switch_receiving_address">Abort changing address</string>
|
||||
<string name="fix_connection">Fix connection</string>
|
||||
<string name="fix_connection_question">Fix connection?</string>
|
||||
<string name="fix_connection_confirm">Fix</string>
|
||||
<string name="fix_connection_not_supported_by_contact">Fix not supported by contact</string>
|
||||
<string name="fix_connection_not_supported_by_group_member">Fix not supported by group member</string>
|
||||
<string name="renegotiate_encryption">Renegotiate encryption</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Create secret group</string>
|
||||
|
||||
@@ -1096,7 +1096,7 @@
|
||||
<string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string>
|
||||
<string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí).
|
||||
\nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d zprávy se nepodařilo dešifrovat.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d zprávy se nepodařilo dešifrovat.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d zprývy přeskočeny.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Tato chyba je pro toto připojení trvalá, připojte se znovu.</string>
|
||||
|
||||
@@ -1173,7 +1173,7 @@
|
||||
<string name="xftp_servers">XFTP-Server</string>
|
||||
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).
|
||||
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
<string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht unterscheidet sich.</string>
|
||||
<string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string>
|
||||
|
||||
@@ -1101,7 +1101,7 @@
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string>
|
||||
<string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">El error es permanente para esta conexión, por favor vuelve a conectarte.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d mensajes no pudieron ser descifrados.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mensajes no pudieron ser descifrados.</string>
|
||||
<string name="no_spaces">¡Sin espacios!</string>
|
||||
<string name="stop_file__action">Detener archivo</string>
|
||||
<string name="revoke_file__message">El archivo será eliminado de los servidores.</string>
|
||||
|
||||
@@ -1244,7 +1244,7 @@
|
||||
<string name="alert_text_decryption_error_too_many_skipped"> %1$d viestit ohitettu.</string>
|
||||
<string name="alert_text_msg_bad_id">Seuraavan viestin tunnus on väärä (pienempi tai yhtä suuri kuin edellisen).
|
||||
\nTämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d viestien salauksen purku epäonnistui.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d viestien salauksen purku epäonnistui.</string>
|
||||
<string name="user_unhide">Näytä</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen.</string>
|
||||
<string name="update_database_passphrase">Päivitä tietokannan tunnuslause</string>
|
||||
|
||||
@@ -1119,7 +1119,7 @@
|
||||
<string name="stop_rcv_file__title">Arrêter de recevoir le fichier \?</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Cette erreur est persistante pour cette connexion, veuillez vous reconnecter.</string>
|
||||
<string name="la_could_not_be_verified">Vous n\'avez pas pu être vérifié·e ; veuillez réessayer.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messages n\'ont pas pu être déchiffrés.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messages n\'ont pas pu être déchiffrés.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d messages sautés.</string>
|
||||
<string name="you_can_turn_on_lock">Vous pouvez activer SimpleX Lock dans les Paramètres.</string>
|
||||
<string name="v5_0_polish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-284.5q-11.5 0-20-8.75t-8.5-20.25q0-11 8.5-19.75t20-8.75q11.5 0 20.25 8.75T509-313.5q0 11.5-8.75 20.25T480-284.5Zm2.325-145.5q-12.325 0-20.575-8.375T453.5-459v-189q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T511-648v189q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375ZM190.5-477q0 65.183 28 118.591 28 53.409 90 93.409v-96q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T366-361v168.5q0 12.25-8.375 20.625T337.5-163.5H169q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T169-221h101.5q-64.5-48-101-108T133-477q0-88 46-165.75T311-762.5q10-4.5 20.25-.25T346-748q4 11 .25 21.75T332.5-711q-62.5 32-102.25 94.881Q190.5-553.237 190.5-477Zm589.5-6.5q0-64.683-28.25-118.091Q723.5-655 661.5-695v95.5q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375-12.325 0-20.575-8.375T604-599.5V-768q0-11.75 8.375-20.125T633-796.5h168.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T801.5-739H700q64 48 100.75 108t36.75 147.5q0 88.5-46.25 166.75T659-197q-10 5-20.25.25T624.5-212q-4.5-10.5-.75-21.5t14.25-16q62-31.5 102-94.381 40-62.882 40-139.619Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M96.63-124.5q-8.63 0-15.282-4.125Q74.696-132.75 71.25-139q-3.917-6.1-4.333-13.55Q66.5-160 71.5-168l383.936-661.885Q460-837.5 466.25-841t13.75-3.5q7.5 0 13.75 3.5T505-830l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.098 10.375Q872-124.5 863.37-124.5H96.63ZM146.5-182h667L480-758 146.5-182Zm337.728-57.5q12.272 0 20.522-8.478 8.25-8.478 8.25-20.75t-8.478-20.522q-8.478-8.25-20.75-8.25t-20.522 8.478q-8.25 8.478-8.25 20.75t8.478 20.522q8.478 8.25 20.75 8.25Zm.197-108.5q12.075 0 20.325-8.375T513-377v-165q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM480-470Z"/></svg>
|
||||
|
After Width: | Height: | Size: 774 B |
@@ -1097,7 +1097,7 @@
|
||||
<string name="alert_text_msg_bad_id">L\'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).
|
||||
\nPuò accadere a causa di qualche bug o quando la connessione è compromessa.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">L\'errore è permanente per questa connessione, riconnettiti.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messaggi non decifrati.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messaggi non decifrati.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash del messaggio errato</string>
|
||||
<string name="alert_text_msg_bad_hash">L\'hash del messaggio precedente è diverso.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Si prega di segnalarlo agli sviluppatori.</string>
|
||||
|
||||
@@ -1197,7 +1197,7 @@
|
||||
<string name="call_connection_via_relay">דרך ממסר</string>
|
||||
<string name="icon_descr_video_off">וידאו כבוי</string>
|
||||
<string name="icon_descr_video_on">וידאו פעיל</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d הודעות לא הצליחו לעבור פענוח.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d הודעות לא הצליחו לעבור פענוח.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">עליכם להשתמש בגרסה העדכנית ביותר של מסד הנתונים שלכם במכשיר אחד בלבד, אחרת אתם עלולים להפסיק לקבל הודעות מאנשי קשר מסוימים.</string>
|
||||
<string name="wrong_passphrase_title">סיסמה שגויה!</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">הוזמנתם לקבוצה. הצטרפו כדי ליצור קשר עם חברי הקבוצה.</string>
|
||||
|
||||
@@ -1072,7 +1072,7 @@
|
||||
<string name="alert_text_fragment_please_report_to_developers">開発者に報告してください。</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">このエラーはこの接続では永続的なものです。再接続してください。</string>
|
||||
<string name="v5_0_large_files_support">1GBまでのビデオとファイル</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d メッセージの復号化に失敗しました。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d メッセージの復号化に失敗しました。</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d メッセージをスキップしました</string>
|
||||
<string name="error_loading_smp_servers">SMP サーバーのロード中にエラーが発生しました</string>
|
||||
<string name="error_saving_user_password">ユーザーパスワード保存エラー</string>
|
||||
|
||||
@@ -1099,7 +1099,7 @@
|
||||
<string name="alert_title_msg_bad_id">Onjuiste bericht ID</string>
|
||||
<string name="alert_text_msg_bad_id">De ID van het volgende bericht is onjuist (minder of gelijk aan het vorige).
|
||||
\nHet kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d-berichten konden niet worden ontsleuteld.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d-berichten konden niet worden ontsleuteld.</string>
|
||||
<string name="no_spaces">Geen spaties!</string>
|
||||
<string name="stop_rcv_file__message">Het ontvangen van het bestand wordt gestopt.</string>
|
||||
<string name="revoke_file__confirm">Intrekken</string>
|
||||
|
||||
@@ -1096,7 +1096,7 @@
|
||||
<string name="alert_text_msg_bad_hash">Hash poprzedniej wiadomości jest inny.</string>
|
||||
<string name="alert_text_msg_bad_id">Identyfikator następnej wiadomości jest nieprawidłowy (mniejszy lub równy poprzedniej).
|
||||
\nMoże się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skompromitowane.</string>
|
||||
<string name="alert_text_decryption_error_header">Nie udało się odszyfrować %1$d wiadomości.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nie udało się odszyfrować %1$d wiadomości.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Ten błąd jest trwały dla tego połączenia, proszę o ponowne połączenie.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d pominiętych wiadomości.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Może się to zdarzyć, gdy Ty lub Twoje połączenie użyło starej kopii zapasowej bazy danych.</string>
|
||||
|
||||
@@ -1088,7 +1088,7 @@
|
||||
<string name="you_can_turn_on_lock">Você pode ativar o bloqueio SimpleX via Configurações.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash de mensagem incorreta</string>
|
||||
<string name="alert_text_msg_bad_hash">O hash da mensagem anterior é diferente.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d descriptografia das mensagens falhou</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d descriptografia das mensagens falhou</string>
|
||||
<string name="alert_title_msg_bad_id">ID de mensagem incorreta</string>
|
||||
<string name="alert_text_msg_bad_id">A ID da próxima mensagem está incorreta (menor ou igual à anterior).
|
||||
\nIsso pode acontecer por causa de algum bug ou quando a conexão está comprometida.</string>
|
||||
|
||||
@@ -1156,7 +1156,7 @@
|
||||
<string name="authentication_cancelled">Аутентификация отменена</string>
|
||||
<string name="la_mode_system">Системная</string>
|
||||
<string name="la_seconds">%d секунд</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d сообщений не удалось расшифровать.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d сообщений не удалось расшифровать.</string>
|
||||
<string name="decryption_error">Ошибка расшифровки</string>
|
||||
<string name="lock_not_enabled">Блокировка SimpleX не включена!</string>
|
||||
<string name="alert_title_msg_bad_hash">Ошибка хэш сообщения</string>
|
||||
|
||||
@@ -1052,7 +1052,7 @@
|
||||
<string name="v4_4_verify_connection_security">ตรวจสอบความปลอดภัยในการเชื่อมต่อ</string>
|
||||
<string name="incognito_info_share">เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s ต้องการเชื่อมต่อกับคุณผ่านทาง</string>
|
||||
<string name="alert_text_decryption_error_header">ข้อความ %1$d ไม่สามารถ decrypt ได้</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">ข้อความ %1$d ไม่สามารถ decrypt ได้</string>
|
||||
<string name="group_info_section_title_num_members">%1$s สมาชิก</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[คุณสามารถ <font color="#0088ff">เชื่อมต่อกับ SimpleX Chat นักพัฒนาแอปเพื่อถามคำถามและรับการอัปเดต</font>]]></string>
|
||||
<string name="you_can_share_your_address">คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้</string>
|
||||
|
||||
@@ -874,7 +874,7 @@
|
||||
<string name="show_call_on_lock_screen">Показати</string>
|
||||
<string name="no_call_on_lock_screen">Вимкнути</string>
|
||||
<string name="relay_server_if_necessary">Сервер ретрансляції використовується лише за необхідності. Інша сторона може спостерігати за вашою IP-адресою.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d повідомлення не вдалося розшифрувати.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d повідомлення не вдалося розшифрувати.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d повідомлення пропущені.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Будь ласка, повідомте про це розробникам.</string>
|
||||
<string name="send_link_previews">Надіслати попередній перегляд за посиланням</string>
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
<string name="la_immediately">立即</string>
|
||||
<string name="alert_title_msg_bad_hash">错误消息散列</string>
|
||||
<string name="alert_title_msg_bad_id">错误消息 ID</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d 消息解密失败。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 消息解密失败。</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d 已跳过消息。</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">此错误对于此连接是永久性的,请重新连接。</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">当您或您的连接使用旧数据库备份时,可能会发生这种情况。</string>
|
||||
|
||||
@@ -1120,7 +1120,7 @@
|
||||
<string name="la_mode_system">系統</string>
|
||||
<string name="alert_text_msg_bad_id">此ID的下一則訊息是錯誤(小於或等於上一則的)。
|
||||
\n當一些錯誤出現或你的連結被破壞時會發生。</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d 訊息解密失敗。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 訊息解密失敗。</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">使用SOCKS 代理伺服器</string>
|
||||
<string name="your_XFTP_servers">你的 XFTP 伺服器</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">這個連結錯誤是永久性的,請重新連接。</string>
|
||||
|
||||
@@ -25,8 +25,8 @@ android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.version_name=5.2-beta.0
|
||||
android.version_code=129
|
||||
android.version_name=5.2-beta.1
|
||||
android.version_code=131
|
||||
|
||||
desktop.version_name=1.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user