ios: ratchet synchronization (#2663)

* types

* info buttons

* prohibit send

* interactive item

* wip

* terminology

* item design

* comment

* rework

* design

* design

* move button

* update texts

* update texts

* sync not supported alert

* fix

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
spaced4ndy
2023-07-10 19:01:22 +04:00
committed by GitHub
parent 416ae400eb
commit a6a87cb7de
9 changed files with 526 additions and 38 deletions

View File

@@ -135,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))
}
@@ -523,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

View File

@@ -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)")
}

View File

@@ -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(

View File

@@ -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 {

View File

@@ -75,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()
@@ -195,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 {
@@ -626,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
)

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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?

View File

@@ -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)
}
}
}