Skip to content

Commit

Permalink
ios: ratchet synchronization (#2663)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
spaced4ndy and epoberezkin committed Jul 10, 2023
1 parent 416ae40 commit a6a87cb
Show file tree
Hide file tree
Showing 9 changed files with 527 additions and 39 deletions.
18 changes: 18 additions & 0 deletions apps/ios/Shared/Model/ChatModel.swift
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions apps/ios/Shared/Model/SimpleXAPI.swift
Expand Up @@ -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
}

Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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)")
}
Expand Down
72 changes: 70 additions & 2 deletions apps/ios/Shared/Views/Chat/ChatInfoView.swift
Expand Up @@ -76,6 +76,7 @@ struct ChatInfoView: View {
case networkStatusAlert
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")

var id: String {
Expand All @@ -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)"
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 })
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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(
Expand Down

0 comments on commit a6a87cb

Please sign in to comment.