From 2fda0454e3aa7fd13b29d05663a67e9032fd8d44 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 8 Mar 2023 01:27:28 +0300 Subject: [PATCH] android: group link role, add observer role (#1981) * android: group link role, add observer role * padding * disabled tint for buttons * proper layout for long display name --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 14 +++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 41 ++++++++--- .../java/chat/simplex/app/ui/theme/Theme.kt | 1 + .../chat/simplex/app/views/TerminalView.kt | 1 + .../chat/simplex/app/views/call/CallView.kt | 3 +- .../chat/simplex/app/views/chat/ChatView.kt | 9 ++- .../simplex/app/views/chat/ComposeView.kt | 16 ++++- .../simplex/app/views/chat/SendMsgView.kt | 41 +++++++---- .../app/views/chat/group/GroupChatInfoView.kt | 7 +- .../app/views/chat/group/GroupLinkView.kt | 72 ++++++++++++++++--- .../app/views/database/DatabaseView.kt | 2 +- .../chat/simplex/app/views/helpers/Section.kt | 3 +- .../app/src/main/res/values/strings.xml | 6 ++ 13 files changed, 171 insertions(+), 45 deletions(-) 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 990bd1a0d..dde24729d 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 @@ -486,6 +486,16 @@ data class Chat ( val chatItems: List, val chatStats: ChatStats = ChatStats(), ) { + val userCanSend: Boolean + get() = when (chatInfo) { + is ChatInfo.Direct -> true + is ChatInfo.Group -> { + val m = chatInfo.groupInfo.membership + m.memberActive && m.memberRole >= GroupMemberRole.Member + } + else -> false + } + val id: String get() = chatInfo.id @Serializable @@ -963,11 +973,13 @@ class GroupMemberRef( @Serializable enum class GroupMemberRole(val memberRole: String) { - @SerialName("member") Member("member"), // order matters in comparisons + @SerialName("observer") Observer("observer"), // order matters in comparisons + @SerialName("member") Member("member"), @SerialName("admin") Admin("admin"), @SerialName("owner") Owner("owner"); val text: String get() = when (this) { + Observer -> generalGetString(R.string.group_member_role_observer) Member -> generalGetString(R.string.group_member_role_member) Admin -> generalGetString(R.string.group_member_role_admin) Owner -> generalGetString(R.string.group_member_role_owner) 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 c1571262f..5e6868496 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 @@ -1060,9 +1060,9 @@ 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 + suspend fun apiCreateGroupLink(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + return when (val r = sendCmd(CC.APICreateGroupLink(groupId, memberRole))) { + is CR.GroupLinkCreated -> r.connReqContact to r.memberRole else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r) @@ -1072,6 +1072,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + suspend fun apiGroupLinkMemberRole(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + return when (val r = sendCmd(CC.APIGroupLinkMemberRole(groupId, memberRole))) { + is CR.GroupLink -> r.connReqContact to r.memberRole + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiGroupLinkMemberRole", generalGetString(R.string.error_updating_link_for_group), r) + } + null + } + } + } + suspend fun apiDeleteGroupLink(groupId: Long): Boolean { return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) { is CR.GroupLinkDeleted -> true @@ -1084,9 +1096,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } - suspend fun apiGetGroupLink(groupId: Long): String? { + suspend fun apiGetGroupLink(groupId: Long): Pair? { return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) { - is CR.GroupLink -> r.connReqContact + is CR.GroupLink -> r.connReqContact to r.memberRole else -> { Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") null @@ -1343,6 +1355,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a if (active(r.user)) { chatModel.updateGroup(r.toGroup) } + is CR.MemberRole -> + if (active(r.user)) { + chatModel.updateGroup(r.groupInfo) + } is CR.RcvFileStart -> chatItemSimpleUpdate(r.user, r.chatItem) is CR.RcvFileComplete -> @@ -1752,7 +1768,8 @@ 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 APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() + class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() class APIGetUserSMPServers(val userId: Long): CC() @@ -1827,7 +1844,8 @@ 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 APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}" + is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" is APIGetUserSMPServers -> "/_smp $userId" @@ -1904,6 +1922,7 @@ sealed class CC { is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" is APICreateGroupLink -> "apiCreateGroupLink" + is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" is APIGetUserSMPServers -> "apiGetUserSMPServers" @@ -2925,8 +2944,8 @@ sealed class CR { @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR() - @Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR() @@ -3129,8 +3148,8 @@ sealed class CR { is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) - is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") - is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") + is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") + is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index 873d3f313..7590ecdcf 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -17,6 +17,7 @@ enum class DefaultTheme { val DEFAULT_PADDING = 16.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 +val DEFAULT_BOTTOM_PADDING = 48.dp val DarkColorPalette = darkColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 424bb978a..2222d50a8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -83,6 +83,7 @@ fun TerminalLayout( liveMessageAlertShown = SharedPreference(get = { false }, set = {}), needToAllowVoiceToContact = false, allowedVoiceByPrefs = false, + userCanSend = true, allowVoiceToContact = {}, sendMessage = sendCommand, sendLiveMessage = null, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index 2e3f1a7e1..1952cd80c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -37,6 +37,7 @@ import androidx.webkit.WebViewClientCompat import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.DEFAULT_BOTTOM_PADDING import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.withApi @@ -240,7 +241,7 @@ private fun ActiveCallOverlayLayout( CallInfoView(call, alignment = Alignment.CenterHorizontally) } Spacer(Modifier.fillMaxHeight().weight(1f)) - Box(Modifier.fillMaxWidth().padding(bottom = 48.dp), contentAlignment = Alignment.CenterStart) { + Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { IconButton(onClick = dismiss) { Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index c9cb6d6f4..07b3792a2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -152,9 +152,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } } else if (chat.chatInfo is ChatInfo.Group) { setGroupMembers(chat.chatInfo.groupInfo, chatModel) - var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + var groupLink = link?.first + var groupLinkMemberRole = link?.second ModalManager.shared.showModalCloseable(true) { close -> - GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close) + GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, { + groupLink = it.first; + groupLinkMemberRole = it.second + }, close) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index 8e5a135e4..b4ea1ec0f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -17,6 +17,7 @@ import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContract +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import chat.simplex.app.* @@ -645,6 +647,8 @@ fun ComposeView( chatModel.sharedContent.value = null } + val userCanSend = rememberUpdatedState(chat.userCanSend) + Column { contextItemView() when { @@ -656,11 +660,11 @@ fun ComposeView( modifier = Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom, ) { - IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) { + IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) { Icon( Icons.Filled.AttachFile, contentDescription = stringResource(R.string.attach), - tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight, + tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else HighOrLowlight, modifier = Modifier .size(28.dp) .clip(CircleShape) @@ -698,6 +702,13 @@ fun ComposeView( } } + LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) { + if (!chat.userCanSend) { + clearCurrentDraft() + clearState() + } + } + val activity = LocalContext.current as Activity DisposableEffect(Unit) { val orientation = activity.resources.configuration.orientation @@ -733,6 +744,7 @@ fun ComposeView( needToAllowVoiceToContact, allowedVoiceByPrefs, allowVoiceToContact = ::allowVoiceToContact, + userCanSend = userCanSend.value, sendMessage = { sendMessage() resetLinkPreview() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index d7d555ad4..01bebb0ad 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -60,6 +60,7 @@ fun SendMsgView( liveMessageAlertShown: SharedPreference, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, + userCanSend: Boolean, allowVoiceToContact: () -> Unit, sendMessage: () -> Unit, sendLiveMessage: (suspend () -> Unit)? = null, @@ -74,10 +75,18 @@ fun SendMsgView( val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } - NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange) + NativeKeyboard(composeState, textStyle, showDeleteTextButton, userCanSend, onMessageChange) // Disable clicks on text field - if (cs.preview is ComposePreview.VoicePreview) { - Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { })) + if (cs.preview is ComposePreview.VoicePreview || !userCanSend) { + Box(Modifier + .matchParentSize() + .clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.observer_cant_send_message_title), + text = generalGetString(R.string.observer_cant_send_message_desc) + ) + }) + ) } if (showDeleteTextButton.value) { DeleteTextButton(composeState) @@ -99,11 +108,11 @@ fun SendMsgView( Row(verticalAlignment = Alignment.CenterVertically) { val stopRecOnNextClick = remember { mutableStateOf(false) } when { - needToAllowVoiceToContact || !allowedVoiceByPrefs -> { - DisallowedVoiceButton { + needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> { + DisallowedVoiceButton(userCanSend) { if (needToAllowVoiceToContact) { showNeedToAllowVoiceAlert(allowVoiceToContact) - } else { + } else if (!allowedVoiceByPrefs) { showDisabledVoiceAlert(isDirectChat) } } @@ -118,7 +127,7 @@ fun SendMsgView( && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && cs.contextItem is ComposeContextItem.NoContextItem) { Spacer(Modifier.width(10.dp)) - StartLiveMessageButton { + StartLiveMessageButton(userCanSend) { if (composeState.value.preview is ComposePreview.NoPreview) { startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) } @@ -173,6 +182,7 @@ private fun NativeKeyboard( composeState: MutableState, textStyle: MutableState, showDeleteTextButton: MutableState, + userCanSend: Boolean, onMessageChange: (String) -> Unit ) { val cs = composeState.value @@ -252,9 +262,9 @@ private fun NativeKeyboard( } showDeleteTextButton.value = it.lineCount >= 4 } - if (composeState.value.preview is ComposePreview.VoicePreview) { + if (composeState.value.preview is ComposePreview.VoicePreview || !userCanSend) { Text( - generalGetString(R.string.voice_message_send_text), + if (composeState.value.preview is ComposePreview.VoicePreview) generalGetString(R.string.voice_message_send_text) else generalGetString(R.string.you_are_observer), Modifier.padding(padding), color = HighOrLowlight, style = textStyle.value.copy(fontStyle = FontStyle.Italic) @@ -322,8 +332,8 @@ private fun RecordVoiceView(recState: MutableState, stopRecOnNex } @Composable -private fun DisallowedVoiceButton(onClick: () -> Unit) { - IconButton(onClick, Modifier.size(36.dp)) { +private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { + IconButton(onClick, Modifier.size(36.dp), enabled = enabled) { Icon( Icons.Outlined.KeyboardVoice, stringResource(R.string.icon_descr_record_voice_message), @@ -454,13 +464,13 @@ private fun SendMsgButton( } @Composable -private fun StartLiveMessageButton(onClick: () -> Unit) { +private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier.requiredSize(36.dp) .clickable( onClick = onClick, - enabled = true, + enabled = enabled, role = Role.Button, interactionSource = interactionSource, indication = rememberRipple(bounded = false, radius = 24.dp) @@ -470,7 +480,7 @@ private fun StartLiveMessageButton(onClick: () -> Unit) { Icon( Icons.Filled.Bolt, stringResource(R.string.icon_descr_send_message), - tint = MaterialTheme.colors.primary, + tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight, modifier = Modifier .size(36.dp) .padding(4.dp) @@ -571,6 +581,7 @@ fun PreviewSendMsgView() { liveMessageAlertShown = SharedPreference(get = { true }, set = { }), needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, + userCanSend = true, allowVoiceToContact = {}, sendMessage = {}, onMessageChange = { _ -> }, @@ -599,6 +610,7 @@ fun PreviewSendMsgViewEditing() { liveMessageAlertShown = SharedPreference(get = { true }, set = { }), needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, + userCanSend = true, allowVoiceToContact = {}, sendMessage = {}, onMessageChange = { _ -> }, @@ -627,6 +639,7 @@ fun PreviewSendMsgViewInProgress() { liveMessageAlertShown = SharedPreference(get = { true }, set = { }), needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, + userCanSend = true, allowVoiceToContact = {}, sendMessage = {}, onMessageChange = { _ -> }, 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 24b2b9ab3..8e072367c 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 @@ -34,7 +34,7 @@ import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.* @Composable -fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair) -> Unit, close: () -> Unit) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val developerTools = chatModel.controller.appPrefs.developerTools.get() @@ -95,9 +95,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, manageGroupLink = { - withApi { - ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) } - } + ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } } ) } @@ -300,6 +298,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) { verticalAlignment = Alignment.CenterVertically ) { Row( + Modifier.weight(1f).padding(end = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { 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 index 8dd10d4c8..ab8e35a9c 100644 --- 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 @@ -1,6 +1,9 @@ package chat.simplex.app.views.chat.group +import SectionItemView import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -15,22 +18,26 @@ 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.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.QRCode @Composable -fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) { +fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair) -> Unit) { var groupLink by rememberSaveable { mutableStateOf(connReqContact) } + val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } val cxt = LocalContext.current fun createLink() { creatingLink = true withApi { - groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId) - onGroupLinkUpdated(groupLink) + val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId) + if (link != null) { + groupLink = link.first + groupLinkMemberRole.value = link.second + onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + } creatingLink = false } } @@ -41,9 +48,24 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St } GroupLinkLayout( groupLink = groupLink, + groupInfo, + groupLinkMemberRole, creatingLink, createLink = ::createLink, share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) }, + updateLink = { + val role = groupLinkMemberRole.value + if (role != null) { + withBGApi { + val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role) + if (link != null) { + groupLink = link.first + groupLinkMemberRole.value = link.second + onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + } + } + } + }, deleteLink = { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.delete_link_question), @@ -54,7 +76,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) if (r) { groupLink = null - onGroupLinkUpdated(null) + onGroupLinkUpdated(null to null) } } } @@ -69,13 +91,18 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St @Composable fun GroupLinkLayout( groupLink: String?, + groupInfo: GroupInfo, + groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, share: () -> Unit, + updateLink: () -> Unit, deleteLink: () -> Unit ) { Column( - Modifier.padding(horizontal = DEFAULT_PADDING), + Modifier + .verticalScroll(rememberScrollState()) + .padding(start = DEFAULT_PADDING, bottom = DEFAULT_BOTTOM_PADDING, end = DEFAULT_PADDING), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top ) { @@ -93,7 +120,17 @@ fun GroupLinkLayout( if (groupLink == null) { SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink) } else { - QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f)) + SectionItemView(padding = PaddingValues(bottom = DEFAULT_PADDING)) { + RoleSelectionRow(groupInfo, groupLinkMemberRole) + } + var initialLaunch by remember { mutableStateOf(true) } + LaunchedEffect(groupLinkMemberRole.value) { + if (!initialLaunch) { + updateLink() + } + initialLaunch = false + } + QRCode(groupLink, Modifier.aspectRatio(1f)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -116,6 +153,25 @@ fun GroupLinkLayout( } } +@Composable +private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text } + ExposedDropDownSettingRow( + generalGetString(R.string.initial_member_role), + values, + selectedRole, + icon = null, + enabled = rememberUpdatedState(enabled), + onSelected = { selectedRole.value = it } + ) + } +} + @Composable fun ProgressIndicator() { Box( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index 22ea4c2db..b0e2524d1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -154,7 +154,7 @@ fun DatabaseLayout( val operationsDisabled = !stopped || progressIndicator Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp), + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = DEFAULT_BOTTOM_PADDING), horizontalAlignment = Alignment.Start, ) { AppBarTitle(stringResource(R.string.your_chat_database)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt index 3d7a4e3c1..ce8d52a31 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt @@ -85,13 +85,14 @@ fun SectionItemView( click: (() -> Unit)? = null, minHeight: Dp = 46.dp, disabled: Boolean = false, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier .fillMaxWidth() .sizeIn(minHeight = minHeight) Row( - if (click == null || disabled) modifier.padding(horizontal = DEFAULT_PADDING) else modifier.clickable(onClick = click).padding(horizontal = DEFAULT_PADDING), + if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding), verticalAlignment = Alignment.CenterVertically ) { content() diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 4028dc75a..57dd21a77 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -222,6 +222,9 @@ Only 10 images can be sent at the same time Decoding error The image cannot be decoded. Please, try a different image or contact developers. + you are observer + You can\'t send messages! + Please contact group admin. Image @@ -868,6 +871,7 @@ changing address… + observer member admin owner @@ -890,6 +894,7 @@ No contacts to add New member role + Initial role Expand role selection Invite to group Skip inviting members @@ -919,6 +924,7 @@ You can share a link or a QR code - anybody will be able to join the group. You won\'t lose members of the group if you later delete it. All group members will remain connected. Error creating group link + Error updating group link Error deleting group link Only group owners can change group preferences.