android, desktop: role-based preferences, SimpleX links preference (#4023)

* android, desktop: role-based preferences, SimpleX links preference

* disable for now

* different way of disabling send button

* mic icon filled

* icons

* braces

* colors

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-04-13 11:51:10 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent 433b3652fb
commit de81434b36
11 changed files with 258 additions and 73 deletions
@@ -805,6 +805,24 @@ data class Chat(
val id: String get() = chatInfo.id
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
if (chatInfo is ChatInfo.Group) {
val groupInfo = chatInfo.groupInfo
val p = groupInfo.fullGroupPreferences
when (feature) {
GroupFeature.TimedMessages -> p.timedMessages.on
GroupFeature.DirectMessages -> p.directMessages.on(groupInfo.membership)
GroupFeature.FullDelete -> p.fullDelete.on
GroupFeature.Reactions -> p.reactions.on
GroupFeature.Voice -> p.voice.on(groupInfo.membership)
GroupFeature.Files -> p.files.on(groupInfo.membership)
GroupFeature.SimplexLinks -> p.simplexLinks.on(groupInfo.membership)
GroupFeature.History -> p.history.on
}
} else {
true
}
@Serializable
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
@@ -1240,7 +1258,7 @@ data class GroupInfo (
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Reactions -> fullGroupPreferences.reactions.on
ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Voice -> fullGroupPreferences.voice.on(membership)
ChatFeature.Calls -> false
}
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
@@ -2319,8 +2337,8 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatPreference") class RcvChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndChatPreference") class SndChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@@ -2352,8 +2370,8 @@ sealed class CIContent: ItemContent {
is SndChatFeature -> featureText(feature, enabled.text, param)
is RcvChatPreference -> preferenceText(feature, allowed, param)
is SndChatPreference -> preferenceText(feature, allowed, param)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is SndModerated -> generalGetString(MR.strings.moderated_description)
@@ -2390,11 +2408,23 @@ sealed class CIContent: ItemContent {
private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short)
fun featureText(feature: Feature, enabled: String, param: Int?): String =
if (feature.hasParam) {
fun featureText(feature: Feature, enabled: String, param: Int?, role: GroupMemberRole? = null): String =
(if (feature.hasParam) {
"${feature.text}: ${timeText(param)}"
} else {
"${feature.text}: $enabled"
}) + (
if (feature.hasRole && role != null)
" (${roleText(role)})"
else
""
)
private fun roleText(role: GroupMemberRole?): String =
when (role) {
GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners)
GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins)
else -> generalGetString(MR.strings.feature_roles_all_members)
}
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
@@ -3430,6 +3430,7 @@ interface Feature {
@Composable
fun iconFilled(): Painter
val hasParam: Boolean
val hasRole: Boolean
}
@Serializable
@@ -3449,6 +3450,7 @@ enum class ChatFeature: Feature {
TimedMessages -> true
else -> false
}
override val hasRole: Boolean = false
override val text: String
get() = when(this) {
@@ -3549,6 +3551,7 @@ enum class GroupFeature: Feature {
@SerialName("reactions") Reactions,
@SerialName("voice") Voice,
@SerialName("files") Files,
@SerialName("simplexLinks") SimplexLinks,
@SerialName("history") History;
override val hasParam: Boolean get() = when(this) {
@@ -3556,6 +3559,18 @@ enum class GroupFeature: Feature {
else -> false
}
override val hasRole: Boolean
get() = when (this) {
TimedMessages -> false
DirectMessages -> true
FullDelete -> false
Reactions -> false
Voice -> true
Files -> true
SimplexLinks -> true
History -> false
}
override val text: String
get() = when(this) {
TimedMessages -> generalGetString(MR.strings.timed_messages)
@@ -3564,6 +3579,7 @@ enum class GroupFeature: Feature {
Reactions -> generalGetString(MR.strings.message_reactions)
Voice -> generalGetString(MR.strings.voice_messages)
Files -> generalGetString(MR.strings.files_and_media)
SimplexLinks -> generalGetString(MR.strings.simplex_links)
History -> generalGetString(MR.strings.recent_history)
}
@@ -3575,6 +3591,7 @@ enum class GroupFeature: Feature {
Reactions -> painterResource(MR.images.ic_add_reaction)
Voice -> painterResource(MR.images.ic_keyboard_voice)
Files -> painterResource(MR.images.ic_draft)
SimplexLinks -> painterResource(MR.images.ic_link)
History -> painterResource(MR.images.ic_schedule)
}
@@ -3586,6 +3603,7 @@ enum class GroupFeature: Feature {
Reactions -> painterResource(MR.images.ic_add_reaction_filled)
Voice -> painterResource(MR.images.ic_keyboard_voice_filled)
Files -> painterResource(MR.images.ic_draft_filled)
SimplexLinks -> painterResource(MR.images.ic_link)
History -> painterResource(MR.images.ic_schedule_filled)
}
@@ -3616,6 +3634,10 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_files)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_files)
}
SimplexLinks -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_simplex_links)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_simplex_links)
}
History -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.enable_sending_recent_history)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disable_sending_recent_history)
@@ -3647,6 +3669,10 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_files)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.files_are_prohibited_in_group)
}
SimplexLinks -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_simplex_links)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.simplex_links_are_prohibited_in_group)
}
History -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.recent_history_is_sent_to_new_members)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.recent_history_is_not_sent_to_new_members)
@@ -3760,11 +3786,12 @@ enum class FeatureAllowed {
@Serializable
data class FullGroupPreferences(
val timedMessages: TimedMessagesGroupPreference,
val directMessages: GroupPreference,
val directMessages: RoleGroupPreference,
val fullDelete: GroupPreference,
val reactions: GroupPreference,
val voice: GroupPreference,
val files: GroupPreference,
val voice: RoleGroupPreference,
val files: RoleGroupPreference,
val simplexLinks: RoleGroupPreference,
val history: GroupPreference,
) {
fun toGroupPreferences(): GroupPreferences =
@@ -3775,17 +3802,19 @@ data class FullGroupPreferences(
reactions = reactions,
voice = voice,
files = files,
simplexLinks = simplexLinks,
history = history
)
companion object {
val sampleData = FullGroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = GroupPreference(GroupFeatureEnabled.ON),
files = GroupPreference(GroupFeatureEnabled.ON),
voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
history = GroupPreference(GroupFeatureEnabled.ON),
)
}
@@ -3794,21 +3823,23 @@ data class FullGroupPreferences(
@Serializable
data class GroupPreferences(
val timedMessages: TimedMessagesGroupPreference? = null,
val directMessages: GroupPreference? = null,
val directMessages: RoleGroupPreference? = null,
val fullDelete: GroupPreference? = null,
val reactions: GroupPreference? = null,
val voice: GroupPreference? = null,
val files: GroupPreference? = null,
val voice: RoleGroupPreference? = null,
val files: RoleGroupPreference? = null,
val simplexLinks: RoleGroupPreference? = null,
val history: GroupPreference? = null,
) {
companion object {
val sampleData = GroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = GroupPreference(GroupFeatureEnabled.ON),
files = GroupPreference(GroupFeatureEnabled.ON),
voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
history = GroupPreference(GroupFeatureEnabled.ON),
)
}
@@ -3819,6 +3850,26 @@ data class GroupPreference(
val enable: GroupFeatureEnabled
) {
val on: Boolean get() = enable == GroupFeatureEnabled.ON
fun enabled(role: GroupMemberRole?, m: GroupMember?): GroupFeatureEnabled =
when (enable) {
GroupFeatureEnabled.OFF -> GroupFeatureEnabled.OFF
GroupFeatureEnabled.ON ->
if (role != null && m != null) {
if (m.memberRole >= role) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF
} else {
GroupFeatureEnabled.ON
}
}
}
@Serializable
data class RoleGroupPreference(
val enable: GroupFeatureEnabled,
val role: GroupMemberRole? = null,
) {
fun on(m: GroupMember): Boolean =
enable == GroupFeatureEnabled.ON && m.memberRole >= (role ?: GroupMemberRole.Observer)
}
@Serializable
@@ -85,6 +85,7 @@ fun TerminalLayout(
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
@@ -1,6 +1,7 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.common.views.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
@@ -11,6 +12,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -18,8 +21,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.filesToDelete
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.Indigo
import chat.simplex.common.ui.theme.isSystemInDarkTheme
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -115,6 +117,15 @@ data class ComposeState(
}
}
val attachmentPreview: Boolean
get() = when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
is ComposePreview.MediaPreview -> preview.content.isNotEmpty()
is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> true
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
@@ -243,10 +254,23 @@ fun ComposeView(
attachmentOption: MutableState<AttachmentOption?>,
showChooseAttachment: () -> Unit
) {
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
fun isSimplexLink(link: String): Boolean =
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
fun parseMessage(msg: String): Pair<String?, Boolean> {
if (msg.isBlank()) return null to false
val parsedMsg = parseToMarkdown(msg) ?: return null to false
val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }
val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink }
return link?.text to simplexLink
}
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
// default value parsed because of draft
val hasSimplexLink = rememberSaveable { mutableStateOf(parseMessage(composeState.value.message).second) }
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get()
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
@@ -255,15 +279,6 @@ fun ComposeView(
AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } }
fun isSimplexLink(link: String): Boolean =
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
fun parseMessage(msg: String): String? {
val parsedMsg = parseToMarkdown(msg)
val link = parsedMsg?.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }
return link?.text
}
fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
@@ -283,7 +298,9 @@ fun ComposeView(
fun showLinkPreview(s: String) {
prevLinkUrl.value = linkUrl.value
linkUrl.value = parseMessage(s)
val parsed = parseMessage(s)
linkUrl.value = parsed.first
hasSimplexLink.value = parsed.second
val url = linkUrl.value
if (url != null) {
if (url != composeState.value.linkPreview?.uri && url != pendingLinkUrl.value) {
@@ -387,7 +404,7 @@ fun ComposeView(
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(msgText)
val url = parseMessage(msgText).first
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
@@ -563,8 +580,16 @@ fun ComposeView(
} else {
textStyle.value = smallFont
if (composeState.value.linkPreviewAllowed) {
if (s.isNotEmpty()) showLinkPreview(s)
else resetLinkPreview()
if (s.isNotEmpty()) {
showLinkPreview(s)
} else {
resetLinkPreview()
hasSimplexLink.value = false
}
} else if (s.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) {
hasSimplexLink.value = parseMessage(s).second
} else {
hasSimplexLink.value = false
}
}
}
@@ -700,6 +725,16 @@ fun ComposeView(
}
}
@Composable
fun MsgNotAllowedView(reason: String, icon: Painter) {
val color = CurrentColors.collectAsState().value.appColors.receivedMessage
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
Icon(icon, null, tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Text(reason, fontStyle = FontStyle.Italic)
}
}
@Composable
fun contextItemView() {
when (val contextItem = composeState.value.contextItem) {
@@ -743,7 +778,17 @@ fun ComposeView(
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
val simplexLinkProhibited = hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
val fileProhibited = composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
if (simplexLinkProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.simplex_links_not_allowed), icon = painterResource(MR.images.ic_link))
} else if (fileProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.files_and_media_not_allowed), icon = painterResource(MR.images.ic_draft))
} else if (voiceProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.voice_messages_not_allowed), icon = painterResource(MR.images.ic_mic))
}
contextItemView()
when {
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
@@ -764,7 +809,7 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
@@ -871,6 +916,7 @@ fun ComposeView(
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited),
nextSendGrpInv = nextSendGrpInv.value,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
@@ -39,6 +39,7 @@ fun SendMsgView(
isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>,
sendMsgEnabled: Boolean,
sendButtonEnabled: Boolean,
nextSendGrpInv: Boolean,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
@@ -75,7 +76,8 @@ fun SendMsgView(
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
cs.endLiveDisabled ||
!sendButtonEnabled
PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
if (!cs.inProgress) {
sendMessage(null)
@@ -430,7 +432,7 @@ private fun SendMsgButton(
.padding(4.dp)
.alpha(alpha.value)
.clip(CircleShape)
.background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary)
.background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary.copy(alpha = 0.75f))
.padding(3.dp)
)
}
@@ -552,6 +554,7 @@ fun PreviewSendMsgView() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -586,6 +589,7 @@ fun PreviewSendMsgViewEditing() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -620,6 +624,7 @@ fun PreviewSendMsgViewInProgress() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -309,7 +309,7 @@ fun GroupMemberInfoLayout(
SectionView {
if (contactId != null && knownDirectChat(contactId) != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (contactId != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
@@ -335,7 +335,7 @@ fun GroupMemberInfoLayout(
val clipboard = LocalClipboardManager.current
ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
} else {
@@ -6,7 +6,6 @@ import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@@ -20,13 +19,20 @@ import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
null to generalGetString(MR.strings.feature_roles_all_members),
GroupMemberRole.Admin to generalGetString(MR.strings.feature_roles_admins),
GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners)
)
@Composable
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit,) {
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) {
val groupInfo = remember { derivedStateOf {
val ch = m.getChat(chatId)
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
if (g == null || ch?.remoteHostId != rhId) null else g
if (g == null || ch.remoteHostId != rhId) null else g
}}
val gInfo = groupInfo.value ?: return
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
@@ -81,7 +87,7 @@ private fun GroupPreferencesLayout(
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
}
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
FeatureSection(GroupFeature.TimedMessages, timedMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
if (enable == GroupFeatureEnabled.ON) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
} else {
@@ -90,33 +96,45 @@ private fun GroupPreferencesLayout(
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
val directMessagesRole = remember(preferences) { mutableStateOf(preferences.directMessages.role) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, directMessagesRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(directMessages = RoleGroupPreference(enable = enable, role)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = enable)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) }
FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(reactions = GroupPreference(enable = it)))
FeatureSection(GroupFeature.Reactions, allowReactions, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(reactions = GroupPreference(enable = enable)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
val voiceRole = remember(preferences) { mutableStateOf(preferences.voice.role) }
FeatureSection(GroupFeature.Voice, allowVoice, voiceRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(voice = RoleGroupPreference(enable = enable, role)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
val filesRole = remember(preferences) { mutableStateOf(preferences.files.role) }
FeatureSection(GroupFeature.Files, allowFiles, filesRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
}
// TODO enable simplexLinks preference in 5.8
// SectionDividerSpaced(true, maxBottomPadding = false)
// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
// }
SectionDividerSpaced(true, maxBottomPadding = false)
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
FeatureSection(GroupFeature.History, enableHistory, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(history = GroupPreference(enable = it)))
FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(history = GroupPreference(enable = enable)))
}
if (groupInfo.canEdit) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
@@ -134,10 +152,11 @@ private fun GroupPreferencesLayout(
private fun FeatureSection(
feature: GroupFeature,
enableFeature: State<GroupFeatureEnabled>,
enableForRole: State<GroupMemberRole?>? = null,
groupInfo: GroupInfo,
preferences: FullGroupPreferences,
onTTLUpdated: (Int?) -> Unit,
onSelected: (GroupFeatureEnabled) -> Unit
onSelected: (GroupFeatureEnabled, GroupMemberRole?) -> Unit
) {
SectionView {
val on = enableFeature.value == GroupFeatureEnabled.ON
@@ -151,7 +170,7 @@ private fun FeatureSection(
iconTint,
enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value)
}
if (timedOn) {
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
@@ -165,6 +184,18 @@ private fun FeatureSection(
onSelected = onTTLUpdated
)
}
if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.feature_enabled_for),
featureRoles,
enableForRole,
// remove in v5.8
enabled = remember { mutableStateOf(false) },
onSelected = { value ->
onSelected(enableFeature.value, value)
}
)
}
} else {
InfoRow(
feature.text,
@@ -175,6 +206,14 @@ private fun FeatureSection(
if (timedOn) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(preferences.timedMessages.ttl))
}
if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) {
InfoRow(generalGetString(MR.strings.feature_enabled_for), featureRoles.firstOrNull { it.first == enableForRole.value }?.second ?: generalGetString(MR.strings.feature_roles_all_members), textColor = MaterialTheme.colors.secondary)
}
}
}
KeyChangeEffect(enableFeature.value) {
if (enableFeature.value == GroupFeatureEnabled.OFF) {
onSelected(enableFeature.value, null)
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
@@ -16,6 +16,7 @@ import chat.simplex.common.platform.onRightClick
@Composable
fun CIChatFeatureView(
chatInfo: ChatInfo,
chatItem: ChatItem,
feature: Feature,
iconColor: Color,
@@ -23,7 +24,7 @@ fun CIChatFeatureView(
revealed: MutableState<Boolean>,
showMenu: MutableState<Boolean>,
) {
val merged = if (!revealed.value) mergedFeatures(chatItem) else emptyList()
val merged = if (!revealed.value) mergedFeatures(chatItem, chatInfo) else emptyList()
Box(
Modifier
.combinedClickable(
@@ -70,7 +71,7 @@ private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): Feat
)
@Composable
private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? {
private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<FeatureInfo>? {
val m = ChatModel
val fs: ArrayList<FeatureInfo> = arrayListOf()
val icons: MutableSet<PainterBox> = mutableSetOf()
@@ -78,7 +79,7 @@ private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? {
if (i != null) {
val reversedChatItems = m.chatItems.asReversed()
while (i < reversedChatItems.size) {
val f = featureInfo(reversedChatItems[i]) ?: break
val f = featureInfo(reversedChatItems[i], chatInfo) ?: break
if (!icons.contains(f.icon)) {
fs.add(0, f)
icons.add(f.icon)
@@ -90,12 +91,12 @@ private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? {
}
@Composable
private fun featureInfo(ci: ChatItem): FeatureInfo? =
private fun featureInfo(ci: ChatItem, chatInfo: ChatInfo): FeatureInfo? =
when (ci.content) {
is CIContent.RcvChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.SndChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
else -> null
}
@@ -442,11 +442,11 @@ fun ChatItemView(
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeature -> {
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndChatFeature -> {
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatPreference -> {
@@ -454,23 +454,23 @@ fun ChatItemView(
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> {
CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
CIChatFeatureView(cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeatureRejected -> {
CIChatFeatureView(cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndModerated -> DeletedItem()
@@ -254,12 +254,12 @@ fun TextIconSpaced(extraPadding: Boolean = false) {
}
@Composable
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null) {
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) {
SectionItemViewSpaceBetween {
Row {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary)
Text(title)
Text(title, color = textColor)
}
Text(value, color = MaterialTheme.colors.secondary)
}
@@ -348,6 +348,9 @@
<string name="files_and_media_prohibited">Files and media prohibited!</string>
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
<string name="files_and_media_not_allowed">Files and media not allowed</string>
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -1532,6 +1535,7 @@
<string name="message_reactions">Message reactions</string>
<string name="voice_messages">Voice messages</string>
<string name="files_and_media">Files and media</string>
<string name="simplex_links">SimpleX links</string>
<string name="recent_history">Visible history</string>
<string name="audio_video_calls">Audio/video calls</string>
<string name="available_in_v51">\nAvailable in v5.1</string>
@@ -1589,6 +1593,8 @@
<string name="prohibit_message_reactions_group">Prohibit messages reactions.</string>
<string name="allow_to_send_files">Allow to send files and media.</string>
<string name="prohibit_sending_files">Prohibit sending files and media.</string>
<string name="allow_to_send_simplex_links">Allow to send SimpleX links.</string>
<string name="prohibit_sending_simplex_links">Prohibit sending SimpleX links</string>
<string name="enable_sending_recent_history">Send up to 100 last messages to new members.</string>
<string name="disable_sending_recent_history">Do not send history to new members.</string>
<string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string>
@@ -1603,6 +1609,8 @@
<string name="message_reactions_are_prohibited">Message reactions are prohibited in this group.</string>
<string name="group_members_can_send_files">Group members can send files and media.</string>
<string name="files_are_prohibited_in_group">Files and media are prohibited in this group.</string>
<string name="group_members_can_send_simplex_links">Group members can send SimpleX links.</string>
<string name="simplex_links_are_prohibited_in_group">SimpleX links are prohibited in this group.</string>
<string name="recent_history_is_sent_to_new_members">Up to 100 last messages are sent to new members.</string>
<string name="recent_history_is_not_sent_to_new_members">History is not sent to new members.</string>
<string name="delete_after">Delete after</string>
@@ -1625,6 +1633,10 @@
<string name="feature_offered_item">offered %s</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="feature_cancelled_item">cancelled %s</string>
<string name="feature_roles_all_members">all members</string>
<string name="feature_roles_admins">admins</string>
<string name="feature_roles_owners">owners</string>
<string name="feature_enabled_for">Enabled for</string>
<!-- WhatsNewView.kt -->
<string name="whats_new">What\'s new</string>