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>
This commit is contained in:
Stanislav Dmitrenko 2023-03-08 01:27:28 +03:00 committed by Evgeny Poberezkin
parent be19af62d9
commit 2fda0454e3
13 changed files with 171 additions and 45 deletions

View File

@ -486,6 +486,16 @@ data class Chat (
val chatItems: List<ChatItem>, val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats(), 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 val id: String get() = chatInfo.id
@Serializable @Serializable
@ -963,11 +973,13 @@ class GroupMemberRef(
@Serializable @Serializable
enum class GroupMemberRole(val memberRole: String) { 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("admin") Admin("admin"),
@SerialName("owner") Owner("owner"); @SerialName("owner") Owner("owner");
val text: String get() = when (this) { val text: String get() = when (this) {
Observer -> generalGetString(R.string.group_member_role_observer)
Member -> generalGetString(R.string.group_member_role_member) Member -> generalGetString(R.string.group_member_role_member)
Admin -> generalGetString(R.string.group_member_role_admin) Admin -> generalGetString(R.string.group_member_role_admin)
Owner -> generalGetString(R.string.group_member_role_owner) Owner -> generalGetString(R.string.group_member_role_owner)

View File

@ -1060,9 +1060,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
} }
} }
suspend fun apiCreateGroupLink(groupId: Long): String? { suspend fun apiCreateGroupLink(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) { return when (val r = sendCmd(CC.APICreateGroupLink(groupId, memberRole))) {
is CR.GroupLinkCreated -> r.connReqContact is CR.GroupLinkCreated -> r.connReqContact to r.memberRole
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), 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<String, GroupMemberRole>? {
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 { suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) { return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
is CR.GroupLinkDeleted -> true 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<String, GroupMemberRole>? {
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) { return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
is CR.GroupLink -> r.connReqContact is CR.GroupLink -> r.connReqContact to r.memberRole
else -> { else -> {
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
null null
@ -1343,6 +1355,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (active(r.user)) { if (active(r.user)) {
chatModel.updateGroup(r.toGroup) chatModel.updateGroup(r.toGroup)
} }
is CR.MemberRole ->
if (active(r.user)) {
chatModel.updateGroup(r.groupInfo)
}
is CR.RcvFileStart -> is CR.RcvFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete -> is CR.RcvFileComplete ->
@ -1752,7 +1768,8 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC() class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): 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 APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC()
class APIGetUserSMPServers(val userId: Long): CC() class APIGetUserSMPServers(val userId: Long): CC()
@ -1827,7 +1844,8 @@ sealed class CC {
is ApiLeaveGroup -> "/_leave #$groupId" is ApiLeaveGroup -> "/_leave #$groupId"
is ApiListMembers -> "/_members #$groupId" is ApiListMembers -> "/_members #$groupId"
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" 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 APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId" is APIGetGroupLink -> "/_get link #$groupId"
is APIGetUserSMPServers -> "/_smp $userId" is APIGetUserSMPServers -> "/_smp $userId"
@ -1904,6 +1922,7 @@ sealed class CC {
is ApiListMembers -> "apiListMembers" is ApiListMembers -> "apiListMembers"
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
is APICreateGroupLink -> "apiCreateGroupLink" is APICreateGroupLink -> "apiCreateGroupLink"
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink" is APIGetGroupLink -> "apiGetGroupLink"
is APIGetUserSMPServers -> "apiGetUserSMPServers" 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("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("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("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("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): 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() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR()
// receiving file events // receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR() @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 ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))

View File

@ -17,6 +17,7 @@ enum class DefaultTheme {
val DEFAULT_PADDING = 16.dp val DEFAULT_PADDING = 16.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
val DEFAULT_BOTTOM_PADDING = 48.dp
val DarkColorPalette = darkColors( val DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files

View File

@ -83,6 +83,7 @@ fun TerminalLayout(
liveMessageAlertShown = SharedPreference(get = { false }, set = {}), liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false, allowedVoiceByPrefs = false,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = sendCommand, sendMessage = sendCommand,
sendLiveMessage = null, sendLiveMessage = null,

View File

@ -37,6 +37,7 @@ import androidx.webkit.WebViewClientCompat
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* 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.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.helpers.withApi
@ -240,7 +241,7 @@ private fun ActiveCallOverlayLayout(
CallInfoView(call, alignment = Alignment.CenterHorizontally) CallInfoView(call, alignment = Alignment.CenterHorizontally)
} }
Spacer(Modifier.fillMaxHeight().weight(1f)) 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) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss) { IconButton(onClick = dismiss) {
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))

View File

@ -152,9 +152,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
} }
} else if (chat.chatInfo is ChatInfo.Group) { } else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel) 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 -> ModalManager.shared.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close) GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
groupLink = it.first;
groupLinkMemberRole = it.second
}, close)
} }
} }
} }

View File

@ -17,6 +17,7 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import chat.simplex.app.* import chat.simplex.app.*
@ -645,6 +647,8 @@ fun ComposeView(
chatModel.sharedContent.value = null chatModel.sharedContent.value = null
} }
val userCanSend = rememberUpdatedState(chat.userCanSend)
Column { Column {
contextItemView() contextItemView()
when { when {
@ -656,11 +660,11 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) { IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
Icon( Icon(
Icons.Filled.AttachFile, Icons.Filled.AttachFile,
contentDescription = stringResource(R.string.attach), 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 modifier = Modifier
.size(28.dp) .size(28.dp)
.clip(CircleShape) .clip(CircleShape)
@ -698,6 +702,13 @@ fun ComposeView(
} }
} }
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
if (!chat.userCanSend) {
clearCurrentDraft()
clearState()
}
}
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
DisposableEffect(Unit) { DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation val orientation = activity.resources.configuration.orientation
@ -733,6 +744,7 @@ fun ComposeView(
needToAllowVoiceToContact, needToAllowVoiceToContact,
allowedVoiceByPrefs, allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact, allowVoiceToContact = ::allowVoiceToContact,
userCanSend = userCanSend.value,
sendMessage = { sendMessage = {
sendMessage() sendMessage()
resetLinkPreview() resetLinkPreview()

View File

@ -60,6 +60,7 @@ fun SendMsgView(
liveMessageAlertShown: SharedPreference<Boolean>, liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean, needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean, allowedVoiceByPrefs: Boolean,
userCanSend: Boolean,
allowVoiceToContact: () -> Unit, allowVoiceToContact: () -> Unit,
sendMessage: () -> Unit, sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null, sendLiveMessage: (suspend () -> Unit)? = null,
@ -74,10 +75,18 @@ fun SendMsgView(
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange) NativeKeyboard(composeState, textStyle, showDeleteTextButton, userCanSend, onMessageChange)
// Disable clicks on text field // Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview) { if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { })) 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) { if (showDeleteTextButton.value) {
DeleteTextButton(composeState) DeleteTextButton(composeState)
@ -99,11 +108,11 @@ fun SendMsgView(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) } val stopRecOnNextClick = remember { mutableStateOf(false) }
when { when {
needToAllowVoiceToContact || !allowedVoiceByPrefs -> { needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
DisallowedVoiceButton { DisallowedVoiceButton(userCanSend) {
if (needToAllowVoiceToContact) { if (needToAllowVoiceToContact) {
showNeedToAllowVoiceAlert(allowVoiceToContact) showNeedToAllowVoiceAlert(allowVoiceToContact)
} else { } else if (!allowedVoiceByPrefs) {
showDisabledVoiceAlert(isDirectChat) showDisabledVoiceAlert(isDirectChat)
} }
} }
@ -118,7 +127,7 @@ fun SendMsgView(
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) { && cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
StartLiveMessageButton { StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) { if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
} }
@ -173,6 +182,7 @@ private fun NativeKeyboard(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>, showDeleteTextButton: MutableState<Boolean>,
userCanSend: Boolean,
onMessageChange: (String) -> Unit onMessageChange: (String) -> Unit
) { ) {
val cs = composeState.value val cs = composeState.value
@ -252,9 +262,9 @@ private fun NativeKeyboard(
} }
showDeleteTextButton.value = it.lineCount >= 4 showDeleteTextButton.value = it.lineCount >= 4
} }
if (composeState.value.preview is ComposePreview.VoicePreview) { if (composeState.value.preview is ComposePreview.VoicePreview || !userCanSend) {
Text( 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), Modifier.padding(padding),
color = HighOrLowlight, color = HighOrLowlight,
style = textStyle.value.copy(fontStyle = FontStyle.Italic) style = textStyle.value.copy(fontStyle = FontStyle.Italic)
@ -322,8 +332,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
} }
@Composable @Composable
private fun DisallowedVoiceButton(onClick: () -> Unit) { private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
Icon( Icon(
Icons.Outlined.KeyboardVoice, Icons.Outlined.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
@ -454,13 +464,13 @@ private fun SendMsgButton(
} }
@Composable @Composable
private fun StartLiveMessageButton(onClick: () -> Unit) { private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.clickable( .clickable(
onClick = onClick, onClick = onClick,
enabled = true, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = rememberRipple(bounded = false, radius = 24.dp)
@ -470,7 +480,7 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
Icon( Icon(
Icons.Filled.Bolt, Icons.Filled.Bolt,
stringResource(R.string.icon_descr_send_message), stringResource(R.string.icon_descr_send_message),
tint = MaterialTheme.colors.primary, tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.padding(4.dp) .padding(4.dp)
@ -571,6 +581,7 @@ fun PreviewSendMsgView() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
@ -599,6 +610,7 @@ fun PreviewSendMsgViewEditing() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
@ -627,6 +639,7 @@ fun PreviewSendMsgViewInProgress() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },

View File

@ -34,7 +34,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
@Composable @Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) { fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
BackHandler(onBack = close) BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
@ -95,9 +95,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = { manageGroupLink = {
withApi { ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
}
} }
) )
} }
@ -300,6 +298,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {

View File

@ -1,6 +1,9 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import SectionItemView
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.*
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode import chat.simplex.app.views.newchat.QRCode
@Composable @Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) { fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) } var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) } var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current val cxt = LocalContext.current
fun createLink() { fun createLink() {
creatingLink = true creatingLink = true
withApi { withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId) val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
onGroupLinkUpdated(groupLink) if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
}
creatingLink = false creatingLink = false
} }
} }
@ -41,9 +48,24 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
} }
GroupLinkLayout( GroupLinkLayout(
groupLink = groupLink, groupLink = groupLink,
groupInfo,
groupLinkMemberRole,
creatingLink, creatingLink,
createLink = ::createLink, createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) }, 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 = { deleteLink = {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.delete_link_question), 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) val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) { if (r) {
groupLink = null groupLink = null
onGroupLinkUpdated(null) onGroupLinkUpdated(null to null)
} }
} }
} }
@ -69,13 +91,18 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
@Composable @Composable
fun GroupLinkLayout( fun GroupLinkLayout(
groupLink: String?, groupLink: String?,
groupInfo: GroupInfo,
groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean, creatingLink: Boolean,
createLink: () -> Unit, createLink: () -> Unit,
share: () -> Unit, share: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit deleteLink: () -> Unit
) { ) {
Column( Column(
Modifier.padding(horizontal = DEFAULT_PADDING), Modifier
.verticalScroll(rememberScrollState())
.padding(start = DEFAULT_PADDING, bottom = DEFAULT_BOTTOM_PADDING, end = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
@ -93,7 +120,17 @@ fun GroupLinkLayout(
if (groupLink == null) { if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink) SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
} else { } 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( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -116,6 +153,25 @@ fun GroupLinkLayout(
} }
} }
@Composable
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole?>, 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 @Composable
fun ProgressIndicator() { fun ProgressIndicator() {
Box( Box(

View File

@ -154,7 +154,7 @@ fun DatabaseLayout(
val operationsDisabled = !stopped || progressIndicator val operationsDisabled = !stopped || progressIndicator
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = DEFAULT_BOTTOM_PADDING),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.your_chat_database)) AppBarTitle(stringResource(R.string.your_chat_database))

View File

@ -85,13 +85,14 @@ fun SectionItemView(
click: (() -> Unit)? = null, click: (() -> Unit)? = null,
minHeight: Dp = 46.dp, minHeight: Dp = 46.dp,
disabled: Boolean = false, disabled: Boolean = false,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
content: (@Composable RowScope.() -> Unit) content: (@Composable RowScope.() -> Unit)
) { ) {
val modifier = Modifier val modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.sizeIn(minHeight = minHeight) .sizeIn(minHeight = minHeight)
Row( 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 verticalAlignment = Alignment.CenterVertically
) { ) {
content() content()

View File

@ -222,6 +222,9 @@
<string name="images_limit_desc">Only 10 images can be sent at the same time</string> <string name="images_limit_desc">Only 10 images can be sent at the same time</string>
<string name="image_decoding_exception_title">Decoding error</string> <string name="image_decoding_exception_title">Decoding error</string>
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string> <string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
<string name="you_are_observer">you are observer</string>
<string name="observer_cant_send_message_title">You can\'t send messages!</string>
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt --> <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string> <string name="image_descr">Image</string>
@ -868,6 +871,7 @@
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string> <string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
<!-- GroupMemberRole --> <!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string>
<string name="group_member_role_member">member</string> <string name="group_member_role_member">member</string>
<string name="group_member_role_admin">admin</string> <string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">owner</string> <string name="group_member_role_owner">owner</string>
@ -890,6 +894,7 @@
<!-- AddGroupMembersView.kt --> <!-- AddGroupMembersView.kt -->
<string name="no_contacts_to_add">No contacts to add</string> <string name="no_contacts_to_add">No contacts to add</string>
<string name="new_member_role">New member role</string> <string name="new_member_role">New member role</string>
<string name="initial_member_role">Initial role</string>
<string name="icon_descr_expand_role">Expand role selection</string> <string name="icon_descr_expand_role">Expand role selection</string>
<string name="invite_to_group_button">Invite to group</string> <string name="invite_to_group_button">Invite to group</string>
<string name="skip_inviting_button">Skip inviting members</string> <string name="skip_inviting_button">Skip inviting members</string>
@ -919,6 +924,7 @@
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string> <string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
<string name="error_creating_link_for_group">Error creating group link</string> <string name="error_creating_link_for_group">Error creating group link</string>
<string name="error_updating_link_for_group">Error updating group link</string>
<string name="error_deleting_link_for_group">Error deleting group link</string> <string name="error_deleting_link_for_group">Error deleting group link</string>
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string> <string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>