ios, android: abort switching connection (#2584)

This commit is contained in:
spaced4ndy
2023-06-19 14:46:08 +04:00
committed by GitHub
parent ddf81d28f1
commit 22f20a9c5f
10 changed files with 292 additions and 73 deletions

View File

@@ -2352,6 +2352,7 @@ sealed class SndConnEvent {
enum class SwitchPhase {
@SerialName("started") Started,
@SerialName("confirmed") Confirmed,
@SerialName("secured") Secured,
@SerialName("completed") Completed
}

View File

@@ -740,7 +740,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return when (val r = sendCmd(CC.APISwitchContact(contactId))) {
is CR.CmdOk -> {}
else -> {
apiErrorAlert("apiSwitchContact", generalGetString(R.string.connection_error), r)
apiErrorAlert("apiSwitchContact", generalGetString(R.string.error_changing_address), r)
}
}
}
@@ -754,6 +754,20 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiAbortSwitchContact(contactId: Long): ConnectionStats? {
val r = sendCmd(CC.APIAbortSwitchContact(contactId))
if (r is CR.ContactSwitchAborted) return r.connectionStats
apiErrorAlert("apiAbortSwitchContact", generalGetString(R.string.error_aborting_address_change), r)
return null
}
suspend fun apiAbortSwitchGroupMember(groupId: Long, groupMemberId: Long): ConnectionStats? {
val r = sendCmd(CC.APIAbortSwitchGroupMember(groupId, groupMemberId))
if (r is CR.GroupMemberSwitchAborted) return r.connectionStats
apiErrorAlert("apiAbortSwitchGroupMember", generalGetString(R.string.error_aborting_address_change), r)
return null
}
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> {
val r = sendCmd(CC.APIGetContactCode(contactId))
if (r is CR.ContactCode) return r.contact to r.connectionCode
@@ -1939,6 +1953,8 @@ sealed class CC {
class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC()
class APISwitchContact(val contactId: Long): CC()
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
class APIAbortSwitchContact(val contactId: Long): CC()
class APIAbortSwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
class APIGetContactCode(val contactId: Long): CC()
class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC()
class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC()
@@ -2032,6 +2048,8 @@ sealed class CC {
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
is APISwitchContact -> "/_switch @$contactId"
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
is APIAbortSwitchContact -> "/_abort switch @$contactId"
is APIAbortSwitchGroupMember -> "/_abort switch #$groupId $groupMemberId"
is APIGetContactCode -> "/_get code @$contactId"
is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId"
is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else ""
@@ -2120,6 +2138,8 @@ sealed class CC {
is APIGroupMemberInfo -> "apiGroupMemberInfo"
is APISwitchContact -> "apiSwitchContact"
is APISwitchGroupMember -> "apiSwitchGroupMember"
is APIAbortSwitchContact -> "apiAbortSwitchContact"
is APIAbortSwitchGroupMember -> "apiAbortSwitchGroupMember"
is APIGetContactCode -> "apiGetContactCode"
is APIGetGroupMemberCode -> "apiGetGroupMemberCode"
is APIVerifyContact -> "apiVerifyContact"
@@ -3277,7 +3297,9 @@ sealed class CR {
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR()
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
@Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR()
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats?): CR()
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR()
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("groupMemberSwitchAborted") class GroupMemberSwitchAborted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactCode") class ContactCode(val user: User, val contact: Contact, val connectionCode: String): CR()
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: User, val verified: Boolean, val expectedCode: String): CR()
@@ -3392,6 +3414,8 @@ sealed class CR {
is NetworkConfig -> "networkConfig"
is ContactInfo -> "contactInfo"
is GroupMemberInfo -> "groupMemberInfo"
is ContactSwitchAborted -> "contactSwitchAborted"
is GroupMemberSwitchAborted -> "groupMemberSwitchAborted"
is ContactCode -> "contactCode"
is GroupMemberCode -> "groupMemberCode"
is ConnectionVerified -> "connectionVerified"
@@ -3503,6 +3527,8 @@ sealed class CR {
is NetworkConfig -> json.encodeToString(networkConfig)
is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is GroupMemberInfo -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}")
is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is GroupMemberSwitchAborted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
@@ -3634,7 +3660,34 @@ abstract class TerminalItem {
}
@Serializable
class ConnectionStats(val rcvServers: List<String>?, val sndServers: List<String>?)
class ConnectionStats(val rcvQueuesInfo: List<RcvQueueInfo>, val sndQueuesInfo: List<SndQueueInfo>)
@Serializable
class RcvQueueInfo(
val rcvServer: String,
val rcvSwitchStatus: RcvSwitchStatus?,
var canAbortSwitch: Boolean
)
@Serializable
enum class RcvSwitchStatus {
@SerialName("switch_started") SwitchStarted,
@SerialName("sending_qadd") SendingQADD,
@SerialName("sending_quse") SendingQUSE,
@SerialName("received_message") ReceivedMessage
}
@Serializable
class SndQueueInfo(
val sndServer: String,
val sndSwitchStatus: SndSwitchStatus?
)
@Serializable
enum class SndSwitchStatus {
@SerialName("sending_qkey") SendingQKEY,
@SerialName("sending_qtest") SendingQTEST
}
@Serializable
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) {

View File

@@ -5,11 +5,9 @@ import InfoRowEllipsis
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionItemViewWithIcon
import SectionSpacer
import SectionTextFooter
import SectionView
import TextIconSpaced
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
@@ -46,7 +44,7 @@ import kotlinx.datetime.Clock
fun ChatInfoView(
chatModel: ChatModel,
contact: Contact,
connStats: ConnectionStats?,
connectionStats: ConnectionStats?,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
@@ -54,6 +52,7 @@ fun ChatInfoView(
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
@@ -62,7 +61,7 @@ fun ChatInfoView(
ChatInfoLayout(
chat,
contact,
connStats,
connStats = connStats,
contactNetworkStatus.value,
customUserProfile,
localAlias,
@@ -82,7 +81,18 @@ fun ChatInfoView(
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
switchContactAddress = {
showSwitchContactAddressAlert(chatModel, contact.contactId)
showSwitchAddressAlert(switchAddress = {
withApi {
chatModel.controller.apiSwitchContact(contact.contactId)
}
})
},
abortSwitchContactAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
connStats.value = chatModel.controller.apiAbortSwitchContact(contact.contactId)
}
})
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
@@ -156,7 +166,7 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
fun ChatInfoLayout(
chat: Chat,
contact: Contact,
connStats: ConnectionStats?,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
@@ -167,8 +177,10 @@ fun ChatInfoLayout(
deleteContact: () -> Unit,
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
abortSwitchContactAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
Column(
Modifier
.fillMaxWidth()
@@ -209,21 +221,30 @@ fun ChatInfoLayout(
}
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchContactAddress)
if (connStats != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status),
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(contactNetworkStatus)
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status),
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(contactNetworkStatus)
}
if (cStats != null) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
switchAddress = switchContactAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
abortSwitchAddress = abortSwitchContactAddress
)
}
val rcvServers = connStats.rcvServers
if (rcvServers != null && rcvServers.isNotEmpty()) {
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
}
val sndServers = connStats.sndServers
if (sndServers != null && sndServers.isNotEmpty()) {
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
@@ -360,7 +381,7 @@ private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is NetworkStatus.Connected ->
Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = Color.Green)
is NetworkStatus.Disconnected ->
Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
is NetworkStatus.Error ->
@@ -381,9 +402,22 @@ fun SimplexServers(text: String, servers: List<String>) {
}
@Composable
fun SwitchAddressButton(onClick: () -> Unit) {
SectionItemView(onClick) {
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
fun SwitchAddressButton(disabled: Boolean, switchAddress: () -> Unit) {
SectionItemView(switchAddress) {
Text(
stringResource(R.string.switch_receiving_address),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
@Composable
fun AbortSwitchAddressButton(disabled: Boolean, abortSwitchAddress: () -> Unit) {
SectionItemView(abortSwitchAddress) {
Text(
stringResource(R.string.abort_switch_receiving_address),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
@@ -445,20 +479,23 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C
}
}
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
fun showSwitchAddressAlert(switchAddress: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.switch_receiving_address_question),
text = generalGetString(R.string.switch_receiving_address_desc),
confirmText = generalGetString(R.string.switch_verb),
onConfirm = {
switchContactAddress(m, contactId)
},
destructive = true,
confirmText = generalGetString(R.string.change_verb),
onConfirm = switchAddress
)
}
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
m.controller.apiSwitchContact(contactId)
fun showAbortSwitchAddressAlert(abortSwitchAddress: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.abort_switch_receiving_address_question),
text = generalGetString(R.string.abort_switch_receiving_address_desc),
confirmText = generalGetString(R.string.abort_switch_receiving_address_confirm),
onConfirm = abortSwitchAddress,
destructive = true,
)
}
@Preview
@@ -474,7 +511,7 @@ fun PreviewChatInfoLayout() {
localAlias = "",
connectionCode = "123",
developerTools = false,
connStats = null,
connStats = remember { mutableStateOf(null) },
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {},
customUserProfile = null,
@@ -482,6 +519,7 @@ fun PreviewChatInfoLayout() {
deleteContact = {},
clearChat = {},
switchContactAddress = {},
abortSwitchContactAddress = {},
verifyClicked = {},
)
}

View File

@@ -37,7 +37,7 @@ import kotlinx.datetime.Clock
fun GroupMemberInfoView(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
connectionStats: ConnectionStats?,
connectionCode: String?,
chatModel: ChatModel,
close: () -> Unit,
@@ -45,6 +45,7 @@ fun GroupMemberInfoView(
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) }
@@ -98,7 +99,18 @@ fun GroupMemberInfoView(
}
},
switchMemberAddress = {
switchMemberAddress(chatModel, groupInfo, member)
showSwitchAddressAlert(switchAddress = {
withApi {
chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
})
},
abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
connStats.value = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
})
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
@@ -152,7 +164,7 @@ fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: Cha
fun GroupMemberInfoLayout(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
connStats: MutableState<ConnectionStats?>,
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
connectionCode: String?,
@@ -162,8 +174,10 @@ fun GroupMemberInfoLayout(
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
abortSwitchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
fun knownDirectChat(contactId: Long): Chat? {
val chat = getContactChat(contactId)
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
@@ -235,21 +249,26 @@ fun GroupMemberInfoLayout(
InfoRow(stringResource(R.string.info_row_connection), connLevelDesc)
}
}
if (connStats != null) {
if (cStats != null) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchMemberAddress)
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
if (rcvServers != null && rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
if (sndServers != null && sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
} else if (sndServers != null && sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
switchAddress = switchMemberAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
abortSwitchAddress = abortSwitchMemberAddress
)
}
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
}
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
}
@@ -376,10 +395,6 @@ private fun updateMemberRoleDialog(
)
}
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
@Preview
@Composable
fun PreviewGroupMemberInfoLayout() {
@@ -387,7 +402,7 @@ fun PreviewGroupMemberInfoLayout() {
GroupMemberInfoLayout(
groupInfo = GroupInfo.sampleData,
member = GroupMember.sampleData,
connStats = null,
connStats = remember { mutableStateOf(null) },
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
connectionCode = "123",
@@ -397,6 +412,7 @@ fun PreviewGroupMemberInfoLayout() {
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
abortSwitchMemberAddress = {},
verifyClicked = {},
)
}

View File

@@ -103,6 +103,7 @@
<string name="error_deleting_contact_request">Error deleting contact request</string>
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
<string name="error_changing_address">Error changing address</string>
<string name="error_aborting_address_change">Error aborting address change</string>
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
<string name="error_xftp_test_server_auth">Server requires authorization to upload, check password</string>
@@ -329,8 +330,11 @@
<string name="icon_descr_server_status_disconnected">Disconnected</string>
<string name="icon_descr_server_status_error">Error</string>
<string name="icon_descr_server_status_pending">Pending</string>
<string name="switch_receiving_address_question">Switch receiving address?</string>
<string name="switch_receiving_address_desc">This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).</string>
<string name="switch_receiving_address_question">Change receiving address?</string>
<string name="switch_receiving_address_desc">Receiving address will be changed to a different server. Address change will complete after sender comes online.</string>
<string name="abort_switch_receiving_address_question">Abort changing address?</string>
<string name="abort_switch_receiving_address_desc">Address change will be aborted. Old receiving address will be used.</string>
<string name="abort_switch_receiving_address_confirm">Abort</string>
<string name="view_security_code">View security code</string>
<string name="verify_security_code">Verify security code</string>
@@ -1170,7 +1174,8 @@
<string name="receiving_via">Receiving via</string>
<string name="sending_via">Sending via</string>
<string name="network_status">Network status</string>
<string name="switch_receiving_address">Switch receiving address</string>
<string name="switch_receiving_address">Change receiving address</string>
<string name="abort_switch_receiving_address">Abort changing address</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Create secret group</string>

View File

@@ -488,6 +488,18 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
}
func apiAbortSwitchContact(_ contactId: Int64) throws -> ConnectionStats {
let r = chatSendCmdSync(.apiAbortSwitchContact(contactId: contactId))
if case let .contactSwitchAborted(_, _, connectionStats) = r { return connectionStats }
throw r
}
func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws -> ConnectionStats {
let r = chatSendCmdSync(.apiAbortSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberSwitchAborted(_, _, _, connectionStats) = r { return 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) }

View File

@@ -36,9 +36,8 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
}
}
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View {
if let servers = servers,
servers.count > 0 {
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]) -> some View {
if servers.count > 0 {
HStack {
Text(title).frame(width: 120, alignment: .leading)
Button(serverHost(servers[0])) {
@@ -76,6 +75,7 @@ struct ChatInfoView: View {
case clearChatAlert
case networkStatusAlert
case switchAddressAlert
case abortSwitchAddressAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
@@ -84,6 +84,7 @@ struct ChatInfoView: View {
case .clearChatAlert: return "clearChatAlert"
case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case let .error(title, _): return "error \(title)"
}
}
@@ -136,12 +137,19 @@ struct ChatInfoView: View {
.onTapGesture {
alert = .networkStatusAlert
}
Button("Change receiving address") {
alert = .switchAddressAlert
}
if let connStats = connectionStats {
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
}
}
@@ -166,6 +174,7 @@ struct ChatInfoView: View {
case .clearChatAlert: return clearChatAlert()
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
@@ -369,13 +378,37 @@ struct ChatInfoView: View {
}
}
}
private func abortSwitchContactAddress() {
Task {
do {
let stats = try apiAbortSwitchContact(contact.apiId)
connectionStats = stats
} catch let error {
logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
Alert(
title: Text("Change receiving address?"),
message: Text("This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member)."),
primaryButton: .destructive(Text("Change"), action: switchAddress),
message: Text("Receiving address will be changed to a different server. Address change will complete after sender comes online."),
primaryButton: .default(Text("Change"), action: switchAddress),
secondaryButton: .cancel()
)
}
func abortSwitchAddressAlert(_ abortSwitchAddress: @escaping () -> Void) -> Alert {
Alert(
title: Text("Abort changing address?"),
message: Text("Address change will be aborted. Old receiving address will be used."),
primaryButton: .destructive(Text("Abort"), action: abortSwitchAddress),
secondaryButton: .cancel()
)
}

View File

@@ -26,6 +26,7 @@ struct GroupMemberInfoView: View {
case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
case abortSwitchAddressAlert
case connRequestSentAlert(type: ConnReqType)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
case other(alert: Alert)
@@ -35,6 +36,7 @@ struct GroupMemberInfoView: View {
case .removeMemberAlert: return "removeMemberAlert"
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .connRequestSentAlert: return "connRequestSentAlert"
case let .error(title, _): return "error \(title)"
case let .other(alert): return "other \(alert)"
@@ -127,8 +129,15 @@ struct GroupMemberInfoView: View {
Button("Change receiving address") {
alert = .switchAddressAlert
}
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
}
}
@@ -175,6 +184,7 @@ struct GroupMemberInfoView: View {
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
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
@@ -356,6 +366,21 @@ struct GroupMemberInfoView: View {
}
}
}
private func abortSwitchMemberAddress() {
Task {
do {
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats
} catch let error {
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider {

View File

@@ -71,6 +71,8 @@ public enum ChatCommand {
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
case apiSwitchContact(contactId: Int64)
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiAbortSwitchContact(contactId: Int64)
case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiGetContactCode(contactId: Int64)
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
case apiVerifyContact(contactId: Int64, connectionCode: String?)
@@ -179,6 +181,8 @@ public enum ChatCommand {
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
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 .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)"
@@ -285,6 +289,8 @@ public enum ChatCommand {
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
case .apiSwitchContact: return "apiSwitchContact"
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
case .apiAbortSwitchContact: return "apiAbortSwitchContact"
case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember"
case .apiGetContactCode: return "apiGetContactCode"
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
case .apiVerifyContact: return "apiVerifyContact"
@@ -397,6 +403,8 @@ public enum ChatResponse: Decodable, Error {
case networkConfig(networkConfig: NetCfg)
case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
case groupMemberInfo(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 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)
@@ -516,6 +524,8 @@ public enum ChatResponse: Decodable, Error {
case .networkConfig: return "networkConfig"
case .contactInfo: return "contactInfo"
case .groupMemberInfo: return "groupMemberInfo"
case .contactSwitchAborted: return "contactSwitchAborted"
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted"
case .contactCode: return "contactCode"
case .groupMemberCode: return "groupMemberCode"
case .connectionVerified: return "connectionVerified"
@@ -633,7 +643,9 @@ public enum ChatResponse: Decodable, Error {
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
case let .networkConfig(networkConfig): return String(describing: networkConfig)
case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")
case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))")
case let .groupMemberInfo(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 .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)")
@@ -1083,8 +1095,31 @@ public struct ChatSettings: Codable {
}
public struct ConnectionStats: Codable {
public var rcvServers: [String]?
public var sndServers: [String]?
public var rcvQueuesInfo: [RcvQueueInfo]
public var sndQueuesInfo: [SndQueueInfo]
}
public struct RcvQueueInfo: Codable {
public var rcvServer: String
public var rcvSwitchStatus: RcvSwitchStatus?
public var canAbortSwitch: Bool
}
public enum RcvSwitchStatus: String, Codable {
case switchStarted = "switch_started"
case sendingQADD = "sending_qadd"
case sendingQUSE = "sending_quse"
case receivedMessage = "received_message"
}
public struct SndQueueInfo: Codable {
public var sndServer: String
public var sndSwitchStatus: SndSwitchStatus?
}
public enum SndSwitchStatus: String, Codable {
case sendingQKEY = "sending_qkey"
case sendingQTEST = "sending_qtest"
}
public struct UserContactLink: Decodable {

View File

@@ -3067,6 +3067,7 @@ public enum SndConnEvent: Decodable {
public enum SwitchPhase: String, Decodable {
case started
case confirmed
case secured
case completed
}