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 8f52d53a4e..37906db46b 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 @@ -782,6 +782,12 @@ class GroupMember ( } } +@Serializable +class GroupMemberRef( + val groupMemberId: Long, + val profile: Profile +) + @Serializable enum class GroupMemberRole(val memberRole: String) { @SerialName("member") Member("member"), // order matters in comparisons @@ -1221,6 +1227,8 @@ sealed class CIContent: ItemContent { @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 } @Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when(this) { is SndMsgContent -> msgContent.text @@ -1234,6 +1242,8 @@ sealed class CIContent: ItemContent { is SndGroupInvitation -> groupInvitation.text is RcvGroupEventContent -> rcvGroupEvent.text is SndGroupEventContent -> sndGroupEvent.text + is RcvConnEventContent -> rcvConnEvent.text + is SndConnEventContent -> sndConnEvent.text } } @@ -1592,6 +1602,46 @@ sealed class SndGroupEvent() { } } +@Serializable +sealed class RcvConnEvent { + @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent() + + val text: String get() = when (this) { + is SwitchQueue -> when (phase) { + SwitchPhase.Completed -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_completed) + else -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_changing) + } + } +} + +@Serializable +sealed class SndConnEvent { + @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent() + + val text: String + get() = when (this) { + is SwitchQueue -> { + member?.profile?.profileViewName?.let { + return when (phase) { + SwitchPhase.Completed -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_completed_for_member), it) + else -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_changing_for_member), it) + } + } + when (phase) { + SwitchPhase.Completed -> generalGetString(R.string.snd_conn_event_switch_queue_phase_completed) + else -> generalGetString(R.string.snd_conn_event_switch_queue_phase_changing) + } + } + } +} + +@Serializable +enum class SwitchPhase { + @SerialName("started") Started, + @SerialName("confirmed") Confirmed, + @SerialName("completed") Completed +} + sealed class ChatItemTTL: Comparable { object Day: ChatItemTTL() object Week: ChatItemTTL() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 9e83e7c09d..970061df84 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -498,6 +498,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } + suspend fun apiSwitchContact(contactId: Long) { + return when (val r = sendCmd(CC.APISwitchContact(contactId))) { + is CR.CmdOk -> {} + else -> { + apiErrorAlert("apiSwitchContact", generalGetString(R.string.connection_error), r) + } + } + } + + suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long) { + return when (val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))) { + is CR.CmdOk -> {} + else -> { + apiErrorAlert("apiSwitchGroupMember", generalGetString(R.string.error_changing_address), r) + } + } + } + suspend fun apiAddContact(): String? { val r = sendCmd(CC.AddContact()) return when (r) { @@ -1443,6 +1461,8 @@ sealed class CC { class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC() class APIContactInfo(val contactId: Long): 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 AddContact: CC() class Connect(val connReq: String): CC() class ApiDeleteChat(val type: ChatType, val id: Long): CC() @@ -1507,6 +1527,8 @@ sealed class CC { is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" + is APISwitchContact -> "/_switch @$contactId" + is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId" is AddContact -> "/connect" is Connect -> "/connect $connReq" is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" @@ -1572,6 +1594,8 @@ sealed class CC { is APISetChatSettings -> "/apiSetChatSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" + is APISwitchContact -> "apiSwitchContact" + is APISwitchGroupMember -> "apiSwitchGroupMember" is AddContact -> "addContact" is Connect -> "connect" is ApiDeleteChat -> "apiDeleteChat" @@ -1620,7 +1644,7 @@ sealed class CC { companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" - fun smpServersStr(smpServers: List) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ",") + fun smpServersStr(smpServers: List) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ";") } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 388c2f5b99..0d805d9066 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -65,6 +65,9 @@ fun ChatInfoView( }, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, + switchContactAddress = { + showSwitchContactAddressAlert(chatModel, contact.contactId) + } ) } } @@ -117,6 +120,7 @@ fun ChatInfoLayout( onLocalAliasChanged: (String) -> Unit, deleteContact: () -> Unit, clearChat: () -> Unit, + switchContactAddress: () -> Unit, ) { Column( Modifier @@ -142,8 +146,12 @@ fun ChatInfoLayout( SectionSpacer() - if (connStats != null) { - SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { + SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { + if (developerTools) { + SwitchAddressButton(switchContactAddress) + SectionDivider() + } + if (connStats != null) { SectionItemView({ AlertManager.shared.showAlertMsg( generalGetString(R.string.network_status), @@ -162,8 +170,8 @@ fun ChatInfoLayout( SimplexServers(stringResource(R.string.sending_via), sndServers) } } - SectionSpacer() } + SectionSpacer() SectionView { ClearChatButton(clearChat) SectionDivider() @@ -231,7 +239,9 @@ fun LocalAliasEditor( color = HighOrLowlight ) }, - leadingIcon = if (leadingIcon) {{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }} else null, + leadingIcon = if (leadingIcon) { + { Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) } + } else null, color = HighOrLowlight, focus = focus, textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center), @@ -311,6 +321,13 @@ fun SimplexServers(text: String, servers: List) { } } +@Composable +fun SwitchAddressButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary) + } +} + @Composable fun ClearChatButton(onClick: () -> Unit) { SettingsActionItem( @@ -340,6 +357,21 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C } } +private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) { + AlertManager.shared.showAlertMsg( + 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) + } + ) +} + +private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi { + m.controller.apiSwitchContact(contactId) +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -356,7 +388,9 @@ fun PreviewChatInfoLayout() { connStats = null, onLocalAliasChanged = {}, customUserProfile = null, - deleteContact = {}, clearChat = {} + deleteContact = {}, + clearChat = {}, + switchContactAddress = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 255608eae1..e46cb3a2ca 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -24,6 +24,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.SimplexServers +import chat.simplex.app.views.chat.SwitchAddressButton import chat.simplex.app.views.chatlist.openChat import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.SettingsActionItem @@ -81,6 +82,9 @@ fun GroupMemberInfoView( } } } + }, + switchMemberAddress = { + switchMemberAddress(chatModel, groupInfo, member) } ) } @@ -113,6 +117,7 @@ fun GroupMemberInfoLayout( openDirectChat: () -> Unit, removeMember: () -> Unit, onRoleSelected: (GroupMemberRole) -> Unit, + switchMemberAddress: () -> Unit, ) { Column( Modifier @@ -154,12 +159,15 @@ fun GroupMemberInfoLayout( } } SectionSpacer() - - if (connStats != null) { - val rcvServers = connStats.rcvServers - val sndServers = connStats.sndServers - if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) { - SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { + SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { + if (developerTools) { + SwitchAddressButton(switchMemberAddress) + SectionDivider() + } + if (connStats != null) { + 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()) { @@ -170,9 +178,9 @@ fun GroupMemberInfoLayout( SimplexServers(stringResource(R.string.sending_via), sndServers) } } - SectionSpacer() } } + SectionSpacer() if (member.canBeRemoved(groupInfo)) { SectionView { @@ -280,6 +288,10 @@ private fun updateMemberRoleDialog( ) } +private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi { + m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId) +} + @Preview @Composable fun PreviewGroupMemberInfoLayout() { @@ -292,7 +304,8 @@ fun PreviewGroupMemberInfoLayout() { developerTools = false, openDirectChat = {}, removeMember = {}, - onRoleSelected = {} + onRoleSelected = {}, + switchMemberAddress = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt similarity index 94% rename from apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt index 1915c97746..08be3a421c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt @@ -17,7 +17,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme @Composable -fun CIGroupEventView(ci: ChatItem) { +fun CIEventView(ci: ChatItem) { fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) { return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) } } @@ -50,9 +50,9 @@ fun CIGroupEventView(ci: ChatItem) { name = "Dark Mode" ) @Composable -fun CIGroupEventViewPreview() { +fun CIEventViewPreview() { SimpleXTheme { - CIGroupEventView( + CIEventView( ChatItem.getGroupEventSample() ) } 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 9ce4dfe5b1..cf1d52a1af 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 @@ -1,7 +1,6 @@ package chat.simplex.app.views.chat.item import android.content.* -import android.net.Uri import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -158,8 +157,10 @@ fun ChatItemView( is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, 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 -> CIGroupEventView(cItem) - is CIContent.SndGroupEventContent -> CIGroupEventView(cItem) + is CIContent.RcvGroupEventContent -> CIEventView(cItem) + is CIContent.SndGroupEventContent -> CIEventView(cItem) + is CIContent.RcvConnEventContent -> CIEventView(cItem) + is CIContent.SndConnEventContent -> CIEventView(cItem) } } } diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index bd6cafaa22..5f2ac9dece 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -65,6 +65,7 @@ Fehler beim Löschen der Gruppe Fehler beim Löschen der Kontaktanfrage Fehler beim Löschen der anstehenden Kontaktaufnahme + *** Error changing address Sofortige Benachrichtigungen @@ -213,6 +214,8 @@ Getrennt Fehler Ausstehend + *** Switch receiving address? + *** 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). Nachricht senden @@ -729,6 +732,14 @@ hat die Gruppe verlassen Gruppenprofil aktualisiert + + *** changed address for you + *** changing address… + *** you changed address for %s + *** changing address for %s… + *** you changed address + *** changing address… + Mitglied Admin @@ -796,6 +807,7 @@ Rolle Rolle ändern Ändern + ***Switch Die Mitgliederrolle ändern? Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten. @@ -811,6 +823,7 @@ Empfangen über Senden über Netzwerkstatus + ***Switch receiving address (BETA) Geheime Gruppe erstellen diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 46d4d681a6..3578758694 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -61,10 +61,11 @@ Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью. Ошибка при принятии запроса на соединение Отправитель мог удалить запрос на соединение. - Ошибка удаления контакта + Ошибка при удалении контакта Ошибка удаления группы Ошибка удаления запроса Ошибка удаления ожидаемого соединения + Ошибка при изменении адреса Мгновенные уведомления @@ -213,6 +214,8 @@ Соединение с сервером не установлено Ошибка соединения с сервером Ожидается соединение с сервером + Переключить адрес получения? + Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы). Отправить сообщение @@ -729,6 +732,14 @@ вы покинули группу профиль группы обновлен + + поменял(а) адрес для вас + смена адреса… + вы поменяли адрес для %s + смена адреса для %s… + вы поменяли адрес + смена адреса… + член группы админ @@ -796,6 +807,7 @@ Роль Поменять роль Поменять + Переключить Поменять роль в группе? Роль будет изменена на \"%s\". Все в группе получат сообщение. Роль будет изменена на \"%s\". Будет отправлено новое приглашение. @@ -811,6 +823,7 @@ Получение через Отправка через Состояние сети + Переключить адрес получения (BETA) Создать скрытую группу diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 16729b912c..ef21cc670e 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -65,6 +65,7 @@ Error deleting group Error deleting contact request Error deleting pending contact connection + Error changing address Instant notifications @@ -213,6 +214,8 @@ Disconnected Error Pending + Switch receiving address? + 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). Send Message @@ -729,6 +732,14 @@ you left group profile updated + + changed address for you + changing address… + you changed address for %s + changing address for %s… + you changed address + changing address… + member admin @@ -796,6 +807,7 @@ Role Change role Change + Switch Change group role? The role will be changed to \"%s\". Everyone in the group will be notified. The role will be changed to \"%s\". The member will receive a new invitation. @@ -811,6 +823,7 @@ Receiving via Sending via Network status + Switch receiving address (BETA) Create secret group