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 4557cf8bc..ee9a40158 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 @@ -839,6 +839,40 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + suspend fun apiCreateGroupLink(groupId: Long): String? { + return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) { + is CR.GroupLinkCreated -> r.connReqContact + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r) + } + null + } + } + } + + suspend fun apiDeleteGroupLink(groupId: Long): Boolean { + return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) { + is CR.GroupLinkDeleted -> true + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiDeleteGroupLink", generalGetString(R.string.error_deleting_link_for_group), r) + } + false + } + } + } + + suspend fun apiGetGroupLink(groupId: Long): String? { + return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) { + is CR.GroupLink -> r.connReqContact + else -> { + Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") + null + } + } + } + private fun networkErrorAlert(r: CR): Boolean { return when { r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent @@ -1366,6 +1400,9 @@ sealed class CC { class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() + class APICreateGroupLink(val groupId: Long): CC() + class APIDeleteGroupLink(val groupId: Long): CC() + class APIGetGroupLink(val groupId: Long): CC() class GetUserSMPServers: CC() class SetUserSMPServers(val smpServers: List): CC() class APISetChatItemTTL(val seconds: Long?): CC() @@ -1424,6 +1461,9 @@ sealed class CC { is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" + is APICreateGroupLink -> "/_create link #$groupId" + is APIDeleteGroupLink -> "/_delete link #$groupId" + is APIGetGroupLink -> "/_get link #$groupId" is GetUserSMPServers -> "/smp_servers" is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is APISetChatItemTTL -> "/_ttl ${chatItemTTLStr(seconds)}" @@ -1483,6 +1523,9 @@ sealed class CC { is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" + is APICreateGroupLink -> "apiCreateGroupLink" + is APIDeleteGroupLink -> "apiDeleteGroupLink" + is APIGetGroupLink -> "apiGetGroupLink" is GetUserSMPServers -> "getUserSMPServers" is SetUserSMPServers -> "setUserSMPServers" is APISetChatItemTTL -> "apiSetChatItemTTL" @@ -1744,6 +1787,9 @@ sealed class CR { @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val groupInfo: GroupInfo, val connReqContact: String): CR() + @Serializable @SerialName("groupLink") class GroupLink(val groupInfo: GroupInfo, val connReqContact: String): CR() + @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val groupInfo: GroupInfo): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val rcvFileTransfer: RcvFileTransfer): CR() @@ -1835,6 +1881,9 @@ sealed class CR { is ConnectedToGroupMember -> "connectedToGroupMember" is GroupRemoved -> "groupRemoved" is GroupUpdated -> "groupUpdated" + is GroupLinkCreated -> "groupLinkCreated" + is GroupLink -> "groupLink" + is GroupLinkDeleted -> "groupLinkDeleted" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" is RcvFileAccepted -> "rcvFileAccepted" is RcvFileStart -> "rcvFileStart" @@ -1925,6 +1974,9 @@ sealed class CR { is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member" is GroupRemoved -> json.encodeToString(groupInfo) is GroupUpdated -> json.encodeToString(toGroup) + is GroupLinkCreated -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact" + is GroupLink -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact" + is GroupLinkDeleted -> json.encodeToString(groupInfo) is RcvFileAcceptedSndCancelled -> noDetails() is RcvFileAccepted -> json.encodeToString(chatItem) is RcvFileStart -> json.encodeToString(chatItem) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index fafb43cb1..b88c5eab0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -65,6 +65,12 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, + manageGroupLink = { + withApi { + val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId) + ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) } + } + } ) } } @@ -117,6 +123,7 @@ fun GroupChatInfoLayout( deleteGroup: () -> Unit, clearChat: () -> Unit, leaveGroup: () -> Unit, + manageGroupLink: () -> Unit, ) { Column( Modifier @@ -134,6 +141,8 @@ fun GroupChatInfoLayout( SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) { if (groupInfo.canAddMembers) { + SectionItemView(manageGroupLink) { GroupLinkButton() } + SectionDivider() val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers SectionItemView(onAddMembersClick) { val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary @@ -150,7 +159,6 @@ fun GroupChatInfoLayout( MembersList(members, showMemberInfo) } SectionSpacer() - SectionView { if (groupInfo.canEdit) { SectionItemView(editGroupProfile) { EditGroupProfileButton() } @@ -268,6 +276,23 @@ fun MemberRow(member: GroupMember, user: Boolean = false) { } } +@Composable +fun GroupLinkButton() { + Row( + Modifier + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Outlined.Link, + stringResource(R.string.group_link), + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.group_link), color = MaterialTheme.colors.primary) + } +} + @Composable fun EditGroupProfileButton() { Row( @@ -330,7 +355,7 @@ fun PreviewGroupChatInfoLayout() { groupInfo = GroupInfo.sampleData, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt new file mode 100644 index 000000000..e5a42a7ad --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt @@ -0,0 +1,111 @@ +package chat.simplex.app.views.chat.group + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.GroupInfo +import chat.simplex.app.ui.theme.DEFAULT_PADDING +import chat.simplex.app.ui.theme.SimpleButton +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.newchat.QRCode + +@Composable +fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?) { + var groupLink by remember { mutableStateOf(connReqContact) } + val cxt = LocalContext.current + GroupLinkLayout( + groupLink = groupLink, + createLink = { + withApi { + groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId) + } + }, + share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) }, + deleteLink = { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.delete_link_question), + text = generalGetString(R.string.all_group_members_will_remain_connected), + confirmText = generalGetString(R.string.delete_verb), + onConfirm = { + withApi { + val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) + if (r) { + groupLink = null + } + } + } + ) + } + ) +} + +@Composable +fun GroupLinkLayout( + groupLink: String?, + createLink: () -> Unit, + share: () -> Unit, + deleteLink: () -> Unit +) { + Column( + Modifier.padding(horizontal = DEFAULT_PADDING), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + AppBarTitle(stringResource(R.string.group_link), false) + Text( + stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect), + Modifier.padding(bottom = 12.dp), + lineHeight = 22.sp + ) + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + if (groupLink == null) { + Text( + stringResource(R.string.if_you_later_delete_link_you_wont_lose_members), + Modifier.padding(bottom = 12.dp), + lineHeight = 22.sp + ) + SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink) + } else { + Text( + stringResource(R.string.if_you_delete_group_link_you_wont_lose_members), + Modifier.padding(bottom = 12.dp), + lineHeight = 22.sp + ) + QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f)) + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 10.dp) + ) { + SimpleButton( + stringResource(R.string.share_link), + icon = Icons.Outlined.Share, + click = share + ) + SimpleButton( + stringResource(R.string.delete_link), + icon = Icons.Outlined.Delete, + color = Color.Red, + click = deleteLink + ) + } + } + } + } +} + 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 e9641c89a..85ca6b7ce 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -760,6 +760,16 @@ Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden! Gruppe verlassen Gruppenprofil bearbeiten + ***Group link + ***Create link + ***Delete link? + ***Delete link + ***You can share a link or a QR code - anybody will be able to join the group. + ***If you later delete it - you won\'t lose members of the group connected via the link. + ***If you delete it - you won\'t lose members of the group connected via this link. + ***All group members will remain connected. + ***Error creating a group link + ***Error deleting the group link FÜR KONSOLE 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 87b9340c2..27c260630 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -760,6 +760,16 @@ Группа будет удалена для вас - это действие нельзя отменить! Выйти из группы Редактировать профиль группы + Ссылка группы + Создать ссылку + Удалить ссылку? + Удалить ссылку + Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. + Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. + Вы можете удалить ссылку, сохранив членов группы, которые через нее соединились. + Все члены группы, которые соединились через эту ссылку, останутся в группе. + Ошибка при создании ссылки группы + Ошибка при удалении ссылки группы ДЛЯ КОНСОЛИ diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index d8ae7f6ea..08479dd43 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -760,6 +760,16 @@ Group will be deleted for you - this cannot be undone! Leave group Edit group profile + Group link + Create link + Delete link? + Delete link + You can share a link or a QR code - anybody will be able to join the group. + If you later delete it - you won\'t lose members of the group connected via the link. + If you delete it - you won\'t lose members of the group connected via this link. + All group members will remain connected. + Error creating a group link + Error deleting the group link FOR CONSOLE