diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle
index 2028d9d4a8..54e227b51e 100644
--- a/apps/android/app/build.gradle
+++ b/apps/android/app/build.gradle
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 32
- versionCode 112
- versionName "4.6.1"
+ versionCode 114
+ versionName "5.0-beta.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -77,7 +77,18 @@ android {
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
- android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "pl", "ru", "zh-rCN")
+ android.defaultConfig.resConfigs(
+ "en",
+ "cs",
+ "de",
+ "es",
+ "fr",
+ "it",
+ "nl",
+ "pl",
+ "ru",
+ "zh-rCN"
+ )
// }
if (isBundle) {
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
index 74a5667b43..285be3e472 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
@@ -1308,6 +1308,7 @@ data class ChatItem (
is CIContent.SndCall -> showNtfDir
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
is CIContent.RcvIntegrityError -> showNtfDir
+ is CIContent.RcvDecryptionError -> showNtfDir
is CIContent.RcvGroupInvitation -> showNtfDir
is CIContent.SndGroupInvitation -> showNtfDir
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
@@ -1611,6 +1612,7 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
+ @Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@@ -1637,6 +1639,7 @@ sealed class CIContent: ItemContent {
is SndCall -> status.text(duration)
is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text
+ is RcvDecryptionError -> msgDecryptError.text
is RcvGroupInvitation -> groupInvitation.text
is SndGroupInvitation -> groupInvitation.text
is RcvGroupEventContent -> rcvGroupEvent.text
@@ -1675,6 +1678,19 @@ sealed class CIContent: ItemContent {
}
}
+@Serializable
+enum class MsgDecryptError {
+ @SerialName("ratchetHeader") RatchetHeader,
+ @SerialName("earlier") Earlier,
+ @SerialName("tooManySkipped") TooManySkipped;
+
+ val text: String get() = when (this) {
+ RatchetHeader -> generalGetString(R.string.decryption_error_permanent)
+ Earlier -> generalGetString(R.string.decryption_error)
+ TooManySkipped -> generalGetString(R.string.decryption_error_permanent)
+ }
+}
+
@Serializable
class CIQuote (
val chatDir: CIDirection? = null,
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt
new file mode 100644
index 0000000000..23960060be
--- /dev/null
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt
@@ -0,0 +1,26 @@
+package chat.simplex.app.views.chat.item
+
+import androidx.compose.runtime.Composable
+import chat.simplex.app.R
+import chat.simplex.app.model.*
+import chat.simplex.app.views.helpers.AlertManager
+import chat.simplex.app.views.helpers.generalGetString
+
+@Composable
+fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
+ CIMsgError(ci, timedMessagesTTL, showMember) {
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(R.string.decryption_error),
+ text = when (msgDecryptError) {
+ MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount) + "\n" +
+ generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
+ generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
+ MsgDecryptError.Earlier -> String.format(generalGetString(R.string.alert_text_decryption_error_earlier), msgCount) + "\n" +
+ generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database)
+ MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount) + "\n" +
+ generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
+ generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
+ }
+ )
+ }
+}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
index c8daf52bd1..326c81b9f5 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
@@ -247,7 +247,8 @@ fun ChatItemView(
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
- is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
+ 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.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)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt
index 976e34f6cc..fa25e0f88a 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt
@@ -17,19 +17,41 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
+import chat.simplex.app.model.MsgErrorType
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
-fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
+fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
+ CIMsgError(ci, timedMessagesTTL, showMember) {
+ when (msgError) {
+ is MsgErrorType.MsgSkipped ->
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(R.string.alert_title_skipped_messages),
+ text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
+ )
+ is MsgErrorType.MsgBadHash ->
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(R.string.alert_title_msg_bad_hash),
+ text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
+ generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
+ generalGetString(R.string.alert_text_fragment_please_report_to_developers)
+ )
+ is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(R.string.alert_title_msg_bad_id),
+ text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
+ generalGetString(R.string.alert_text_fragment_please_report_to_developers)
+ )
+ }
+ }
+}
+
+@Composable
+fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
Surface(
- Modifier.clickable(onClick = {
- AlertManager.shared.showAlertMsg(
- title = generalGetString(R.string.alert_title_skipped_messages),
- text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
- )
- }),
+ Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
) {
@@ -59,6 +81,7 @@ fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boo
fun IntegrityErrorItemViewPreview() {
SimpleXTheme {
IntegrityErrorItemView(
+ MsgErrorType.MsgBadHash(),
ChatItem.getDeletedContentSampleData(),
null
)
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index 6aa7a83b46..cd955e7464 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -32,6 +32,8 @@
moderated
invalid chat
invalid data
+ Permanent decryption error
+ Decryption error
connection %1$d
@@ -741,7 +743,17 @@
bad message ID
duplicate message
Skipped messages
- It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages.
+ It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised.
+ Bad message hash
+ The hash of the previous message is different."
+ Bad message 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.
+ %1$d messages failed to decrypt.
+ %1$d messages failed to decrypt and won\'t be shown.
+ %1$d messages skipped.
+ It can happen when you or your connection used the old database backup.
+ This error is permanent for this connection, please re-connect.
+ Please report it to the developers.
Privacy & security
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
new file mode 100644
index 0000000000..849196cd46
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
@@ -0,0 +1,42 @@
+//
+// CIRcvDecryptionError.swift
+// SimpleX (iOS)
+//
+// Created by Evgeny on 15/04/2023.
+// Copyright © 2023 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+import SimpleXChat
+
+let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
+
+struct CIRcvDecryptionError: View {
+ var msgDecryptError: MsgDecryptError
+ var msgCount: UInt32
+ var chatItem: ChatItem
+ var showMember = false
+
+ 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 .earlier:
+ message = Text("\(msgCount) messages failed to decrypt and won't be shown.") + Text("\n") + why
+ case .tooManySkipped:
+ message = Text("\(msgCount) messages skipped.") + Text("\n") + why + Text("\n") + permanent
+ }
+ AlertManager.shared.showAlert(Alert(title: Text("Decryption error"), message: message))
+ }
+ }
+}
+
+//struct CIRcvDecryptionError_Previews: PreviewProvider {
+// static var previews: some View {
+// CIRcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 1, chatItem: ChatItem.getIntegrityErrorSample())
+// }
+//}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
index 7a4fa46822..1ab631232e 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift
@@ -10,9 +10,53 @@ import SwiftUI
import SimpleXChat
struct IntegrityErrorItemView: View {
+ var msgError: MsgErrorType
var chatItem: ChatItem
var showMember = false
+ var body: some View {
+ CIMsgError(chatItem: chatItem, showMember: showMember) {
+ switch msgError {
+ case .msgSkipped:
+ AlertManager.shared.showAlertMsg(
+ title: "Skipped messages",
+ message: """
+ It can happen when:
+ 1. The messages expired in the sending client after 2 days or on the server after 30 days.
+ 2. Message decryption failed, because you or your contact used old database backup.
+ 3. The connection was compromised.
+ """
+ )
+ case .msgBadHash:
+ AlertManager.shared.showAlert(Alert(
+ title: Text("Bad message hash"),
+ message: Text("The hash of the previous message is different.") + Text("\n") +
+ Text(decryptErrorReason) + Text("\n") +
+ Text("Please report it to the developers.")
+ ))
+ case .msgBadId: msgBadIdAlert()
+ case .msgDuplicate: msgBadIdAlert()
+ }
+ }
+ }
+
+ private func msgBadIdAlert() {
+ AlertManager.shared.showAlert(Alert(
+ title: Text("Bad message ID"),
+ message: Text("""
+ The ID of the next message is incorrect (less or equal to the previous).
+ It can happen because of some bug or when the connection is compromised.
+ """) + Text("\n") +
+ Text("Please report it to the developers.")
+ ))
+ }
+}
+
+struct CIMsgError: View {
+ var chatItem: ChatItem
+ var showMember = false
+ var onTap: () -> Void
+
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
@@ -29,26 +73,12 @@ struct IntegrityErrorItemView: View {
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
- .onTapGesture { skippedMessagesAlert() }
- }
-
- private func skippedMessagesAlert() {
- AlertManager.shared.showAlertMsg(
- title: "Skipped messages",
- message: """
- It can happen when:
- 1. The messages expire on the server if they were not received for 30 days,
- 2. The server you use to receive the messages from this contact was updated and restarted.
- 3. The connection is compromised.
- Please connect to the developers via Settings to receive the updates about the servers.
- We will be adding server redundancy to prevent lost messages.
- """
- )
+ .onTapGesture(perform: onTap)
}
}
struct IntegrityErrorItemView_Previews: PreviewProvider {
static var previews: some View {
- IntegrityErrorItemView(chatItem: ChatItem.getIntegrityErrorSample())
+ IntegrityErrorItemView(msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample())
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index 67b067f1a5..82895e3f8a 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -55,7 +55,8 @@ struct ChatItemContentView: View {
case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration)
- case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember)
+ case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem, showMember: showMember)
+ case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvGroupEvent: eventItemView()
@@ -132,6 +133,17 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
),
revealed: Binding.constant(true)
)
+ ChatItemView(
+ chatInfo: ChatInfo.sampleData.direct,
+ chatItem: ChatItem(
+ chatDir: .directRcv,
+ meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
+ content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
+ quotedItem: nil,
+ file: nil
+ ),
+ revealed: Binding.constant(true)
+ )
ChatItemView(
chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem(
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index d23ac670c5..0c1225da9b 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -78,18 +78,17 @@ struct UserProfilesView: View {
}
Section {
let users = filteredUsers()
- ForEach(users) { u in
- userView(u.user)
+ let v = ForEach(users) { u in
+ userView(u.user, allowDelete: users.count > 1)
}
- .onDelete { indexSet in
- if let i = indexSet.first {
- if m.users.count > 1 && (m.users[i].user.hidden || visibleUsersCount > 1) {
- showDeleteConfirmation = true
- userToDelete = users[i].user
- } else {
- alert = .cantDeleteLastUser
+ if #available(iOS 16, *) {
+ v.onDelete { indexSet in
+ if let i = indexSet.first {
+ confirmDeleteUser(users[i].user)
}
}
+ } else {
+ v
}
if trimmedSearchTextOrPassword == "" {
@@ -108,7 +107,11 @@ struct UserProfilesView: View {
}
}
- .toolbar { EditButton() }
+ .toolbar {
+ if #available(iOS 16, *) {
+ EditButton()
+ }
+ }
.navigationTitle("Your chat profiles")
.searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled(true)
@@ -293,8 +296,8 @@ struct UserProfilesView: View {
}
}
- private func userView(_ user: User) -> some View {
- Button {
+ @ViewBuilder private func userView(_ user: User, allowDelete: Bool) -> some View {
+ let v = Button {
Task {
do {
try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user))
@@ -323,7 +326,7 @@ struct UserProfilesView: View {
}
.disabled(user.activeUser)
.foregroundColor(.primary)
- .deleteDisabled(m.users.count <= 1)
+ .deleteDisabled(!allowDelete)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if user.hidden {
Button("Unhide") {
@@ -357,6 +360,26 @@ struct UserProfilesView: View {
.tint(.accentColor)
}
}
+ if #available(iOS 16, *) {
+ v
+ } else if !allowDelete {
+ v
+ } else {
+ v.swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ Button("Delete", role: .destructive) {
+ confirmDeleteUser(user)
+ }
+ }
+ }
+ }
+
+ private func confirmDeleteUser(_ user: User) {
+ if m.users.count > 1 && (user.hidden || visibleUsersCount > 1) {
+ showDeleteConfirmation = true
+ userToDelete = user
+ } else {
+ alert = .cantDeleteLastUser
+ }
}
private func setUserPrivacy(_ user: User, successAlert: UserProfilesAlert? = nil, _ api: @escaping () async throws -> User) {
diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift
index 10c9a5945d..5a015d9e5b 100644
--- a/apps/ios/SimpleX NSE/NotificationService.swift
+++ b/apps/ios/SimpleX NSE/NotificationService.swift
@@ -291,7 +291,8 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
}
}
- return cItem.showMutableNotification ? (aChatItem.chatId, .nse(notification: createMessageReceivedNtf(user, cInfo, cItem))) : nil
+ let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
+ return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil
case let .rcvFileSndCancelled(_, aChatItem, _):
cleanupFile(aChatItem)
return nil
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index c83afb1fd7..e866dfef3b 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -99,11 +99,6 @@
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; };
- 5CB634A229E1EE550066AD6B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349D29E1EE550066AD6B /* libffi.a */; };
- 5CB634A329E1EE550066AD6B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349E29E1EE550066AD6B /* libgmp.a */; };
- 5CB634A429E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349F29E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a */; };
- 5CB634A529E1EE550066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB634A029E1EE550066AD6B /* libgmpxx.a */; };
- 5CB634A629E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB634A129E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a */; };
5CB634A829E437960066AD6B /* PasscodeEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634A729E437960066AD6B /* PasscodeEntry.swift */; };
5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */; };
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */; };
@@ -121,6 +116,7 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
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 */; };
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
@@ -159,6 +155,11 @@
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
+ 644E729C29E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E729729E9B496003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a */; };
+ 644E729D29E9B497003534BE /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E729829E9B497003534BE /* libffi.a */; };
+ 644E729E29E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E729929E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a */; };
+ 644E729F29E9B497003534BE /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E729A29E9B497003534BE /* libgmpxx.a */; };
+ 644E72A029E9B497003534BE /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E729B29E9B497003534BE /* libgmp.a */; };
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
@@ -363,11 +364,6 @@
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; };
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; };
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = ""; };
- 5CB6349D29E1EE550066AD6B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
- 5CB6349E29E1EE550066AD6B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CB6349F29E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a"; sourceTree = ""; };
- 5CB634A029E1EE550066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
- 5CB634A129E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a"; sourceTree = ""; };
5CB634A729E437960066AD6B /* PasscodeEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeEntry.swift; sourceTree = ""; };
5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthView.swift; sourceTree = ""; };
5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAppPasscodeView.swift; sourceTree = ""; };
@@ -389,6 +385,7 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
+ 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; };
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptRequestsView.swift; sourceTree = ""; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; };
@@ -425,6 +422,11 @@
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; };
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; };
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = ""; };
+ 644E729729E9B496003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a"; sourceTree = ""; };
+ 644E729829E9B497003534BE /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 644E729929E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a"; sourceTree = ""; };
+ 644E729A29E9B497003534BE /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 644E729B29E9B497003534BE /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = ""; };
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = ""; };
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = ""; };
@@ -483,13 +485,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CB634A529E1EE550066AD6B /* libgmpxx.a in Frameworks */,
+ 644E729F29E9B497003534BE /* libgmpxx.a in Frameworks */,
+ 644E72A029E9B497003534BE /* libgmp.a in Frameworks */,
+ 644E729E29E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CB634A229E1EE550066AD6B /* libffi.a in Frameworks */,
- 5CB634A629E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a in Frameworks */,
- 5CB634A329E1EE550066AD6B /* libgmp.a in Frameworks */,
- 5CB634A429E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a in Frameworks */,
+ 644E729C29E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
+ 644E729D29E9B497003534BE /* libffi.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -549,11 +551,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CB6349D29E1EE550066AD6B /* libffi.a */,
- 5CB6349E29E1EE550066AD6B /* libgmp.a */,
- 5CB634A029E1EE550066AD6B /* libgmpxx.a */,
- 5CB634A129E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq-ghc8.10.7.a */,
- 5CB6349F29E1EE550066AD6B /* libHSsimplex-chat-4.6.1.2-C345n6sAXGM3veVcbT76Lq.a */,
+ 644E729829E9B497003534BE /* libffi.a */,
+ 644E729B29E9B497003534BE /* libgmp.a */,
+ 644E729A29E9B497003534BE /* libgmpxx.a */,
+ 644E729729E9B496003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h-ghc8.10.7.a */,
+ 644E729929E9B497003534BE /* libHSsimplex-chat-5.0.0.0-HDKKvfAWZqx9YjijiAKu4h.a */,
);
path = Libraries;
sourceTree = "";
@@ -798,6 +800,7 @@
1841511920742C6E152E469F /* AnimatedImageView.swift */,
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */,
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */,
+ 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */,
);
path = ChatItem;
sourceTree = "";
@@ -1094,6 +1097,7 @@
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */,
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
+ 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */,
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */,
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
@@ -1441,7 +1445,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 140;
+ CURRENT_PROJECT_VERSION = 141;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1462,7 +1466,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 4.6.1;
+ MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1483,7 +1487,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 140;
+ CURRENT_PROJECT_VERSION = 141;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1504,7 +1508,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 4.6.1;
+ MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1563,7 +1567,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 140;
+ CURRENT_PROJECT_VERSION = 141;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1576,7 +1580,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 4.6.1;
+ MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1595,7 +1599,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 140;
+ CURRENT_PROJECT_VERSION = 141;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1608,7 +1612,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 4.6.1;
+ MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index b05b9996c7..1b33d34b28 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -1756,6 +1756,7 @@ public struct ChatItem: Identifiable, Decodable {
case .sndCall: return showNtfDir
case .rcvCall: return false // notification is shown on .callInvitation instead
case .rcvIntegrityError: return showNtfDir
+ case .rcvDecryptionError: return showNtfDir
case .rcvGroupInvitation: return showNtfDir
case .sndGroupInvitation: return showNtfDir
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
@@ -2099,6 +2100,7 @@ public enum CIContent: Decodable, ItemContent {
case sndCall(status: CICallStatus, duration: Int)
case rcvCall(status: CICallStatus, duration: Int)
case rcvIntegrityError(msgError: MsgErrorType)
+ case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32)
case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
@@ -2127,6 +2129,7 @@ public enum CIContent: Decodable, ItemContent {
case let .sndCall(status, duration): return status.text(duration)
case let .rcvCall(status, duration): return status.text(duration)
case let .rcvIntegrityError(msgError): return msgError.text
+ case let .rcvDecryptionError(msgDecryptError, msgCount): return msgDecryptError.text
case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
@@ -2173,6 +2176,20 @@ public enum CIContent: Decodable, ItemContent {
}
}
+public enum MsgDecryptError: String, Decodable {
+ case ratchetHeader
+ case earlier
+ case tooManySkipped
+
+ var text: String {
+ switch self {
+ case .ratchetHeader: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
+ case .earlier: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
+ case .tooManySkipped: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
+ }
+ }
+}
+
public struct CIQuote: Decodable, ItemContent {
var chatDir: CIDirection?
public var itemId: Int64?
diff --git a/cabal.project b/cabal.project
index 556f2d3faf..81e856aceb 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 9f0b9a83d6dfbd926daf09883a81bf370544f48e
+ tag: 2b93e0b17d0556988885757e5b7305f6a1db65a7
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index c4109cf7de..94935cac41 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."9f0b9a83d6dfbd926daf09883a81bf370544f48e" = "1pnsk2qzb10d3j7rxjqvbwirymky5d55b13y3a6mwj7qbgzzqcy9";
+ "https://github.com/simplex-chat/simplexmq.git"."2b93e0b17d0556988885757e5b7305f6a1db65a7" = "08dvvd5fgfypdrb0x9pd8f4xm4xwawbrb59k24zn7fdmg636q8ij";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index bc1e537d81..f6e82e57ba 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -47,6 +47,7 @@ import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.System (SystemTime, systemToUTCTime)
import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime)
+import Data.Word (Word32)
import qualified Database.SQLite.Simple as DB
import Simplex.Chat.Archive
import Simplex.Chat.Call
@@ -2630,6 +2631,19 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ERR err -> do
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
+ forM_ (agentMsgDecryptError err) $ \e@(mde, n) -> do
+ ci_ <- withStore $ \db ->
+ getDirectChatItemsLast db user contactId 1 ""
+ >>= liftIO
+ . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False Nothing)
+ . (mdeUpdatedCI e <=< headMaybe)
+ case ci_ of
+ Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
+ _ -> createInternalChatItem user (CDDirectRcv ct) (CIRcvDecryptionError mde n) Nothing
+ where
+ headMaybe = \case
+ x : _ -> Just x
+ _ -> Nothing
-- TODO add debugging output
_ -> pure ()
@@ -2791,9 +2805,40 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ERR err -> do
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
+ forM_ (agentMsgDecryptError err) $ \e@(mde, n) -> do
+ ci_ <- withStore $ \db ->
+ getGroupMemberChatItemLast db user groupId (groupMemberId' m)
+ >>= liftIO
+ . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False Nothing)
+ . mdeUpdatedCI e
+ case ci_ of
+ Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci)
+ _ -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvDecryptionError mde n) Nothing
-- TODO add debugging output
_ -> pure ()
+ agentMsgDecryptError :: AgentErrorType -> Maybe (MsgDecryptError, Word32)
+ agentMsgDecryptError = \case
+ AGENT (A_CRYPTO RATCHET_HEADER) -> Just (MDERatchetHeader, 1)
+ AGENT (A_CRYPTO (RATCHET_EARLIER n)) -> Just (MDEEarlier, n + 1) -- 1 is added to account for the message that has A_DUPLICATE error
+ AGENT (A_CRYPTO (RATCHET_SKIPPED n)) -> Just (MDETooManySkipped, n)
+ -- we are not treating this as decryption error, as in many cases it happens as the result of duplicate or redundant delivery,
+ -- and we don't have a way to differentiate.
+ -- we could store the hashes of past messages in the agent, or delaying message deletion after ACK
+ -- A_DUPLICATE -> Nothing
+ _ -> Nothing
+
+ mdeUpdatedCI :: (MsgDecryptError, Word32) -> CChatItem c -> Maybe (ChatItem c 'MDRcv, CIContent 'MDRcv)
+ mdeUpdatedCI (mde', n') (CChatItem _ ci@ChatItem {content = CIRcvDecryptionError mde n})
+ | mde == mde' = case mde of
+ MDERatchetHeader -> r (n + n')
+ MDEEarlier -> r n -- the first error in a sequence has the largest number – it's the number of messages to receive to catch up, keeping it
+ MDETooManySkipped -> r n' -- the numbers are not added as sequential MDETooManySkipped will have it incremented by 1
+ | otherwise = Nothing
+ where
+ r n'' = Just (ci, CIRcvDecryptionError mde n'')
+ mdeUpdatedCI _ _ = Nothing
+
processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> m ()
processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
case agentMsg of
@@ -3469,9 +3514,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
MsgOk -> pure ()
- MsgError e -> case e of
- MsgSkipped {} -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
- _ -> toView $ CRMsgIntegrityError user e
+ MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
xInfo :: Contact -> Profile -> m ()
xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 5dd6048663..a437f8a7a9 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -74,7 +74,7 @@ updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-c
simplexmqCommitQ :: Q Exp
simplexmqCommitQ = do
- s <- either error B.unpack . A.parseOnly commitHashP <$> runIO (B.readFile "./cabal.project")
+ s <- either (const "") B.unpack . A.parseOnly commitHashP <$> runIO (B.readFile "./cabal.project")
[|fromString s|]
where
commitHashP :: A.Parser ByteString
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index ad2bf2f8cb..82dbcb695c 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -29,6 +29,7 @@ import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay)
import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime)
import Data.Type.Equality
import Data.Typeable (Typeable)
+import Data.Word (Word32)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
@@ -39,7 +40,7 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgErrorType (..), MsgMeta
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody)
-import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
+import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow, (<$?>))
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
deriving (Eq, Show, Ord, Generic)
@@ -711,6 +712,7 @@ data CIContent (d :: MsgDirection) where
CISndCall :: CICallStatus -> Int -> CIContent 'MDSnd
CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv
CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv
+ CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv
CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv
@@ -733,6 +735,19 @@ data CIContent (d :: MsgDirection) where
deriving instance Show (CIContent d)
+data MsgDecryptError
+ = MDERatchetHeader
+ | MDEEarlier
+ | MDETooManySkipped
+ deriving (Eq, Show, Generic)
+
+instance ToJSON MsgDecryptError where
+ toJSON = J.genericToJSON . enumJSON $ dropPrefix "MDE"
+ toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MDE"
+
+instance FromJSON MsgDecryptError where
+ parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MDE"
+
ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool
ciRequiresAttention content = case msgDirection @d of
SMDSnd -> True
@@ -741,6 +756,7 @@ ciRequiresAttention content = case msgDirection @d of
CIRcvDeleted _ -> True
CIRcvCall {} -> True
CIRcvIntegrityError _ -> True
+ CIRcvDecryptionError {} -> True
CIRcvGroupInvitation {} -> True
CIRcvGroupEvent rge -> case rge of
RGEMemberAdded {} -> False
@@ -905,6 +921,7 @@ ciContentToText = \case
CISndCall status duration -> "outgoing call: " <> ciCallInfoText status duration
CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration
CIRcvIntegrityError err -> msgIntegrityError err
+ CIRcvDecryptionError err n -> msgDecryptErrorText err n
CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole
CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole
CIRcvGroupEvent event -> rcvGroupEventToText event
@@ -924,13 +941,22 @@ ciContentToText = \case
msgIntegrityError :: MsgErrorType -> Text
msgIntegrityError = \case
- MsgSkipped fromId toId
- | fromId == toId -> "1 skipped message"
- | otherwise -> T.pack (show $ toId - fromId + 1) <> " skipped messages"
- MsgBadId msgId -> "unexpected message ID " <> T.pack (show msgId)
+ MsgSkipped fromId toId ->
+ "skipped message ID " <> tshow fromId
+ <> if fromId == toId then "" else ".." <> tshow toId
+ MsgBadId msgId -> "unexpected message ID " <> tshow msgId
MsgBadHash -> "incorrect message hash"
MsgDuplicate -> "duplicate message ID"
+msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text
+msgDecryptErrorText err n =
+ "decryption error, possibly due to the device change (" <> errName <> if n == 1 then ")" else ", " <> tshow n <> " messages)"
+ where
+ errName = case err of
+ MDERatchetHeader -> "header"
+ MDEEarlier -> "earlier message"
+ MDETooManySkipped -> "too many skipped messages"
+
msgDirToModeratedContent_ :: SMsgDirection d -> CIContent d
msgDirToModeratedContent_ = \case
SMDRcv -> CIRcvModerated
@@ -968,6 +994,7 @@ data JSONCIContent
| JCISndCall {status :: CICallStatus, duration :: Int} -- duration in seconds
| JCIRcvCall {status :: CICallStatus, duration :: Int}
| JCIRcvIntegrityError {msgError :: MsgErrorType}
+ | JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
| JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
| JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
| JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent}
@@ -1002,6 +1029,7 @@ jsonCIContent = \case
CISndCall status duration -> JCISndCall {status, duration}
CIRcvCall status duration -> JCIRcvCall {status, duration}
CIRcvIntegrityError err -> JCIRcvIntegrityError err
+ CIRcvDecryptionError err n -> JCIRcvDecryptionError err n
CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole}
CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole}
CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent}
@@ -1028,6 +1056,7 @@ aciContentJSON = \case
JCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration
JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err
+ JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent
@@ -1054,6 +1083,7 @@ data DBJSONCIContent
| DBJCISndCall {status :: CICallStatus, duration :: Int}
| DBJCIRcvCall {status :: CICallStatus, duration :: Int}
| DBJCIRcvIntegrityError {msgError :: DBMsgErrorType}
+ | DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
| DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
| DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent}
@@ -1088,6 +1118,7 @@ dbJsonCIContent = \case
CISndCall status duration -> DBJCISndCall {status, duration}
CIRcvCall status duration -> DBJCIRcvCall {status, duration}
CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err
+ CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n
CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole}
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge
@@ -1114,6 +1145,7 @@ aciContentDBJSON = \case
DBJCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration
DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err
+ DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 520dc7fa84..ffdf4382a8 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -226,9 +226,11 @@ module Simplex.Chat.Store
getDirectChatItem,
getDirectChatItemBySharedMsgId,
getDirectChatItemByAgentMsgId,
+ getDirectChatItemsLast,
getGroupChatItem,
getGroupChatItemBySharedMsgId,
getGroupMemberCIBySharedMsgId,
+ getGroupMemberChatItemLast,
getDirectChatItemIdByText,
getGroupChatItemIdByText,
getChatItemByFileId,
@@ -3929,35 +3931,36 @@ getDirectChat db user contactId pagination search_ = do
CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count search
getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
-getDirectChatLast_ db user@User {userId} contactId count search = do
+getDirectChatLast_ db user contactId count search = do
contact <- getContact db user contactId
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
- chatItems <- ExceptT getDirectChatItemsLast_
+ chatItems <- getDirectChatItemsLast db user contactId count search
pure $ Chat (DirectChat contact) (reverse chatItems) stats
- where
- getDirectChatItemsLast_ :: IO (Either StoreError [CChatItem 'CTDirect])
- getDirectChatItemsLast_ = do
- tz <- getCurrentTimeZone
- currentTs <- getCurrentTime
- mapM (toDirectChatItem tz currentTs)
- <$> DB.query
- db
- [sql|
- SELECT
- -- ChatItem
- i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
- -- CIFile
- f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
- -- DirectQuote
- ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
- FROM chat_items i
- LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
- LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.contact_id = i.contact_id
- WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
- ORDER BY i.chat_item_id DESC
- LIMIT ?
- |]
- (userId, contactId, search, count)
+
+-- the last items in reverse order (the last item in the conversation is the first in the returned list)
+getDirectChatItemsLast :: DB.Connection -> User -> ContactId -> Int -> String -> ExceptT StoreError IO [CChatItem 'CTDirect]
+getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do
+ tz <- getCurrentTimeZone
+ currentTs <- getCurrentTime
+ mapM (toDirectChatItem tz currentTs)
+ <$> DB.query
+ db
+ [sql|
+ SELECT
+ -- ChatItem
+ i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live,
+ -- CIFile
+ f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol,
+ -- DirectQuote
+ ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
+ FROM chat_items i
+ LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
+ LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.contact_id = i.contact_id
+ WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
+ ORDER BY i.chat_item_id DESC
+ LIMIT ?
+ |]
+ (userId, contactId, search, count)
getDirectChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatAfter_ db user@User {userId} contactId afterChatItemId count search = do
@@ -4089,6 +4092,22 @@ getGroupChatLast_ db user@User {userId} groupId count search = do
|]
(userId, groupId, search, count)
+getGroupMemberChatItemLast :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO (CChatItem 'CTGroup)
+getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do
+ chatItemId <-
+ ExceptT . firstRow fromOnly (SEChatItemNotFoundByGroupId groupId) $
+ DB.query
+ db
+ [sql|
+ SELECT chat_item_id
+ FROM chat_items
+ WHERE user_id = ? AND group_id = ? AND group_member_id = ?
+ ORDER BY item_ts DESC, chat_item_id DESC
+ LIMIT 1
+ |]
+ (userId, groupId, groupMemberId)
+ getGroupChatItem db user groupId chatItemId
+
getGroupChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatAfter_ db user@User {userId} groupId afterChatItemId count search = do
groupInfo <- getGroupInfo db user groupId
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 754c14fd2d..3bd2ee13f0 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -27,6 +27,7 @@ import Data.Text.Encoding (decodeLatin1)
import Data.Time.Clock (DiffTime, UTCTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.Time.LocalTime (ZonedTime (..), localDay, localTimeOfDay, timeOfDayToTime, utcToZonedTime)
+import Data.Word (Word32)
import GHC.Generics (Generic)
import qualified Network.HTTP.Types as Q
import Numeric (showFFloat)
@@ -365,6 +366,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
CIDirectRcv -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts meta
+ CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts meta
CIRcvGroupEvent {} -> showRcvItemProhibited from
_ -> showRcvItem from
where
@@ -381,6 +383,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
CIGroupRcv m -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts meta
+ CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts meta
CIRcvGroupInvitation {} -> showRcvItemProhibited from
_ -> showRcvItem from
where
@@ -489,20 +492,14 @@ msgPreview = msgPlain . preview . msgContentText
| T.length t <= 120 = t
| otherwise = T.take 120 t <> "..."
-viewRcvIntegrityError :: StyledString -> MsgErrorType -> CurrentTime -> CIMeta с 'MDRcv -> [StyledString]
+viewRcvIntegrityError :: StyledString -> MsgErrorType -> CurrentTime -> CIMeta c 'MDRcv -> [StyledString]
viewRcvIntegrityError from msgErr ts meta = receivedWithTime_ ts from [] meta (viewMsgIntegrityError msgErr) False
+viewRcvDecryptionError :: StyledString -> MsgDecryptError -> Word32 -> CurrentTime -> CIMeta c 'MDRcv -> [StyledString]
+viewRcvDecryptionError from err n ts meta = receivedWithTime_ ts from [] meta [ttyError $ msgDecryptErrorText err n] False
+
viewMsgIntegrityError :: MsgErrorType -> [StyledString]
-viewMsgIntegrityError err = msgError $ case err of
- MsgSkipped fromId toId ->
- "skipped message ID " <> show fromId
- <> if fromId == toId then "" else ".." <> show toId
- MsgBadId msgId -> "unexpected message ID " <> show msgId
- MsgBadHash -> "incorrect message hash"
- MsgDuplicate -> "duplicate message ID"
- where
- msgError :: String -> [StyledString]
- msgError s = [ttyError s]
+viewMsgIntegrityError err = [ttyError $ msgIntegrityError err]
viewInvalidConnReq :: [StyledString]
viewInvalidConnReq =
diff --git a/stack.yaml b/stack.yaml
index 02d0edbc8c..3c800500aa 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 9f0b9a83d6dfbd926daf09883a81bf370544f48e
+ commit: 2b93e0b17d0556988885757e5b7305f6a1db65a7
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 92dc654ccb..dac642d883 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -17,9 +17,11 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (ChatOpts (..))
+import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode)
import qualified Simplex.Messaging.Crypto as C
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
+import System.FilePath ((>))
import Test.Hspec
chatDirectTests :: SpecWith FilePath
@@ -79,6 +81,8 @@ chatDirectTests = do
sameVerificationCode "123 456 789" "12345 6789" `shouldBe` True
it "mark contact verified" testMarkContactVerified
it "mark group member verified" testMarkGroupMemberVerified
+ describe "message errors" $ do
+ it "show message decryption error and update count" testMsgDecryptError
testAddContact :: HasCallStack => SpecWith FilePath
testAddContact = versionTestMatrix2 runTestAddContact
@@ -1763,3 +1767,49 @@ testMarkGroupMemberVerified =
alice <## "member ID: 2"
alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost"
+
+testMsgDecryptError :: HasCallStack => FilePath -> IO ()
+testMsgDecryptError tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+ alice #> "@bob hi"
+ bob <# "alice> hi"
+ bob #> "@alice hey"
+ alice <# "bob> hey"
+ copyDb "bob" "bob_old"
+ withTestChat tmp "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+ alice #> "@bob hello"
+ bob <# "alice> hello"
+ bob #> "@alice hello too"
+ alice <# "bob> hello too"
+ withTestChat tmp "bob_old" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+ alice #> "@bob 1"
+ bob <# "alice> decryption error, possibly due to the device change (header)"
+ alice #> "@bob 2"
+ alice #> "@bob 3"
+ (bob )
+ bob ##> "/tail @alice 1"
+ bob <# "alice> decryption error, possibly due to the device change (header, 3 messages)"
+ bob #> "@alice 1"
+ alice <# "bob> decryption error, possibly due to the device change (header)"
+ bob #> "@alice 2"
+ bob #> "@alice 3"
+ (alice )
+ alice ##> "/tail @bob 1"
+ alice <# "bob> decryption error, possibly due to the device change (header, 3 messages)"
+ alice #> "@bob 4"
+ bob <# "alice> decryption error, possibly due to the device change (header)"
+ withTestChat tmp "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+ alice #> "@bob hello again"
+ bob <# "alice> skipped message ID 5..8"
+ bob <# "alice> hello again"
+ bob #> "@alice received!"
+ alice <# "bob> received!"
+ where
+ copyDb from to = do
+ copyFile (chatStoreFile $ tmp > from) (chatStoreFile $ tmp > to)
+ copyFile (agentStoreFile $ tmp > from) (agentStoreFile $ tmp > to)
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index a6b1802665..b808359923 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -9,7 +9,10 @@ import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import Control.Monad (when)
import qualified Data.Text as T
+import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (GroupMemberRole (..))
+import System.Directory (copyFile)
+import System.FilePath ((>))
import Test.Hspec
chatGroupTests :: SpecWith FilePath
@@ -48,6 +51,8 @@ chatGroupTests = do
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
it "group link member role" testGroupLinkMemberRole
it "leaving and deleting the group joined via link should NOT delete previously existing direct contacts" testGroupLinkLeaveDelete
+ describe "group message errors" $ do
+ it "show message decryption error and update count" testGroupMsgDecryptError
testGroup :: HasCallStack => SpecWith FilePath
testGroup = versionTestMatrix3 runTestGroup
@@ -1986,3 +1991,76 @@ testGroupLinkLeaveDelete =
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
+
+testGroupMsgDecryptError :: HasCallStack => FilePath -> IO ()
+testGroupMsgDecryptError tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ createGroup3 "team" alice bob cath
+ alice #> "#team hi"
+ [bob, cath] *<# "#team alice> hi"
+ bob #> "#team hey"
+ [alice, cath] *<# "#team bob> hey"
+ copyDb "bob" "bob_old"
+ withTestChat tmp "bob" $ \bob -> do
+ bob <## "2 contacts connected (use /cs for the list)"
+ bob <## "#team: connected to server(s)"
+ alice #> "#team hello"
+ [bob, cath] *<# "#team alice> hello"
+ bob #> "#team hello too"
+ [alice, cath] *<# "#team bob> hello too"
+ withTestChat tmp "bob_old" $ \bob -> do
+ bob <## "2 contacts connected (use /cs for the list)"
+ bob <## "#team: connected to server(s)"
+ alice #> "#team 1"
+ bob <# "#team alice> decryption error, possibly due to the device change (header)"
+ cath <# "#team alice> 1"
+ alice #> "#team 2"
+ cath <# "#team alice> 2"
+ alice #> "#team 3"
+ cath <# "#team alice> 3"
+ (bob )
+ bob ##> "/tail #team 1"
+ bob <# "#team alice> decryption error, possibly due to the device change (header, 3 messages)"
+ bob #> "#team 1"
+ alice <# "#team bob> decryption error, possibly due to the device change (header)"
+ -- cath <# "#team bob> 1"
+ bob #> "#team 2"
+ cath <# "#team bob> incorrect message hash"
+ cath <# "#team bob> 2"
+ bob #> "#team 3"
+ cath <# "#team bob> 3"
+ (alice )
+ alice ##> "/tail #team 1"
+ alice <# "#team bob> decryption error, possibly due to the device change (header, 3 messages)"
+ alice #> "#team 4"
+ (bob )
+ cath <# "#team alice> 4"
+ bob ##> "/tail #team 4"
+ bob
+ <##? [ "#team alice> decryption error, possibly due to the device change (header, 4 messages)",
+ "#team 1",
+ "#team 2",
+ "#team 3"
+ ]
+ withTestChat tmp "bob" $ \bob -> do
+ bob <## "2 contacts connected (use /cs for the list)"
+ bob <## "#team: connected to server(s)"
+ alice #> "#team hello again"
+ bob <# "#team alice> skipped message ID 8..11"
+ [bob, cath] *<# "#team alice> hello again"
+ bob #> "#team received!"
+ alice <# "#team bob> received!"
+ bob #> "#team 4"
+ alice <# "#team bob> 4"
+ bob #> "#team 5"
+ cath <# "#team bob> decryption error, possibly due to the device change (earlier message, 2 messages)"
+ cath <# "#team bob> incorrect message hash"
+ [alice, cath] *<# "#team bob> 5"
+ bob #> "#team 6"
+ [alice, cath] *<# "#team bob> 6"
+ where
+ copyDb from to = do
+ copyFile (chatStoreFile $ tmp > from) (chatStoreFile $ tmp > to)
+ copyFile (agentStoreFile $ tmp > from) (agentStoreFile $ tmp > to)
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index 31b1a8c002..ff112d8547 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -272,6 +272,9 @@ getInAnyOrder f cc ls = do
(<#) :: HasCallStack => TestCC -> String -> Expectation
cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line
+(*<#) :: HasCallStack => [TestCC] -> String -> Expectation
+ccs *<# line = concurrentlyN_ $ map (<# line) ccs
+
(?<#) :: HasCallStack => TestCC -> String -> Expectation
cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line