android: Disappearing messages (#1619)
* android: Disappearing messages * remove unused func * remove paren * outlined timer in meta * reserving space for meta takes into account ttl text Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
372d7ffaa9
commit
0b046315ac
@@ -2,8 +2,13 @@ package chat.simplex.app.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
@@ -21,8 +26,7 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlin.time.*
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
@@ -278,7 +282,13 @@ class ChatModel(val controller: ChatController) {
|
||||
while (i < chatItems.count()) {
|
||||
val item = chatItems[i]
|
||||
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
||||
chatItems[i] = item.withStatus(CIStatus.RcvRead())
|
||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||
chatItems[i] = newItem
|
||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||
)
|
||||
}
|
||||
markedRead++
|
||||
}
|
||||
i += 1
|
||||
@@ -400,6 +410,7 @@ interface SomeChat {
|
||||
val incognito: Boolean
|
||||
val voiceMessageAllowed: Boolean
|
||||
val fullDeletionAllowed: Boolean
|
||||
val timedMessagesTTL: Int?
|
||||
val createdAt: Instant
|
||||
val updatedAt: Instant
|
||||
}
|
||||
@@ -463,6 +474,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val incognito get() = contact.incognito
|
||||
override val voiceMessageAllowed get() = contact.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contact.fullDeletionAllowed
|
||||
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
|
||||
override val createdAt get() = contact.createdAt
|
||||
override val updatedAt get() = contact.updatedAt
|
||||
override val displayName get() = contact.displayName
|
||||
@@ -487,6 +499,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val incognito get() = groupInfo.incognito
|
||||
override val voiceMessageAllowed get() = groupInfo.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = groupInfo.fullDeletionAllowed
|
||||
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
|
||||
override val createdAt get() = groupInfo.createdAt
|
||||
override val updatedAt get() = groupInfo.updatedAt
|
||||
override val displayName get() = groupInfo.displayName
|
||||
@@ -511,6 +524,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val incognito get() = contactRequest.incognito
|
||||
override val voiceMessageAllowed get() = contactRequest.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contactRequest.fullDeletionAllowed
|
||||
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
|
||||
override val createdAt get() = contactRequest.createdAt
|
||||
override val updatedAt get() = contactRequest.updatedAt
|
||||
override val displayName get() = contactRequest.displayName
|
||||
@@ -535,6 +549,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val incognito get() = contactConnection.incognito
|
||||
override val voiceMessageAllowed get() = contactConnection.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contactConnection.fullDeletionAllowed
|
||||
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
|
||||
override val createdAt get() = contactConnection.createdAt
|
||||
override val updatedAt get() = contactConnection.updatedAt
|
||||
override val displayName get() = contactConnection.displayName
|
||||
@@ -572,6 +587,7 @@ data class Contact(
|
||||
override val incognito get() = contactConnIncognito
|
||||
override val voiceMessageAllowed get() = mergedPreferences.voice.enabled.forUser
|
||||
override val fullDeletionAllowed get() = mergedPreferences.fullDelete.enabled.forUser
|
||||
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
|
||||
override val displayName get() = localAlias.ifEmpty { profile.displayName }
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
@@ -706,6 +722,7 @@ data class GroupInfo (
|
||||
override val incognito get() = membership.memberIncognito
|
||||
override val voiceMessageAllowed get() = fullGroupPreferences.voice.on
|
||||
override val fullDeletionAllowed get() = fullGroupPreferences.fullDelete.on
|
||||
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
|
||||
override val displayName get() = groupProfile.displayName
|
||||
override val fullName get() = groupProfile.fullName
|
||||
override val image get() = groupProfile.image
|
||||
@@ -953,6 +970,7 @@ class UserContactRequest (
|
||||
override val incognito get() = false
|
||||
override val voiceMessageAllowed get() = false
|
||||
override val fullDeletionAllowed get() = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val displayName get() = profile.displayName
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
@@ -991,6 +1009,7 @@ class PendingContactConnection(
|
||||
override val incognito get() = customUserProfileId != null
|
||||
override val voiceMessageAllowed get() = false
|
||||
override val fullDeletionAllowed get() = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId)
|
||||
override val displayName: String get() {
|
||||
if (localAlias.isNotEmpty()) return localAlias
|
||||
@@ -1088,7 +1107,7 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
|
||||
val isRcvNew: Boolean get() = meta.isRcvNew
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
|
||||
@@ -1150,11 +1169,12 @@ data class ChatItem (
|
||||
file: CIFile? = null,
|
||||
itemDeleted: Boolean = false,
|
||||
itemEdited: Boolean = false,
|
||||
itemTimed: CITimed? = null,
|
||||
editable: Boolean = true
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, null, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
file = file
|
||||
@@ -1209,7 +1229,7 @@ data class ChatItem (
|
||||
)
|
||||
|
||||
fun getChatFeatureSample(feature: ChatFeature, enabled: FeatureEnabled): ChatItem {
|
||||
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled)
|
||||
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null)
|
||||
return ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
@@ -1233,6 +1253,7 @@ data class ChatItem (
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = false,
|
||||
editable = false
|
||||
),
|
||||
@@ -1268,17 +1289,30 @@ data class CIMeta (
|
||||
val updatedAt: Instant,
|
||||
val itemDeleted: Boolean,
|
||||
val itemEdited: Boolean,
|
||||
val itemTimed: CITimed?,
|
||||
val itemLive: Boolean?,
|
||||
val editable: Boolean
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs)
|
||||
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
|
||||
val isLive: Boolean get() = itemLive == true
|
||||
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
|
||||
|
||||
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
|
||||
|
||||
fun statusIcon(primaryColor: Color, metaColor: Color = HighOrLowlight): Pair<ImageVector, Color>? =
|
||||
when (itemStatus) {
|
||||
is CIStatus.SndSent -> Icons.Filled.Check to metaColor
|
||||
is CIStatus.SndErrorAuth -> Icons.Filled.Close to Color.Red
|
||||
is CIStatus.SndError -> Icons.Filled.WarningAmber to WarningYellow
|
||||
is CIStatus.RcvNew -> Icons.Filled.Circle to primaryColor
|
||||
else -> null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, itemLive: Boolean = false, editable: Boolean = true
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
|
||||
): CIMeta =
|
||||
CIMeta(
|
||||
itemId = id,
|
||||
@@ -1289,12 +1323,19 @@ data class CIMeta (
|
||||
updatedAt = ts,
|
||||
itemDeleted = itemDeleted,
|
||||
itemEdited = itemEdited,
|
||||
itemTimed = itemTimed,
|
||||
itemLive = itemLive,
|
||||
editable = editable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CITimed(
|
||||
val ttl: Int,
|
||||
val deleteAt: Instant?
|
||||
)
|
||||
|
||||
fun getTimestampText(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
|
||||
@@ -1342,10 +1383,10 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, 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("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 }
|
||||
|
||||
@@ -1363,13 +1404,22 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupEventContent -> sndGroupEvent.text
|
||||
is RcvConnEventContent -> rcvConnEvent.text
|
||||
is SndConnEventContent -> sndConnEvent.text
|
||||
is RcvChatFeature -> "${feature.text}: ${enabled.text}"
|
||||
is SndChatFeature -> "${feature.text}: ${enabled.text}"
|
||||
is RcvGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
|
||||
is SndGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
|
||||
is RcvChatFeature -> featureText(feature, enabled.text, param)
|
||||
is SndChatFeature -> featureText(feature, enabled.text, param)
|
||||
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun featureText(feature: Feature, value: String, param: Int?): String =
|
||||
if (feature.hasParam && param != null) {
|
||||
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
|
||||
} else {
|
||||
"${feature.text}: $value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -2016,39 +2016,101 @@ data class ChatSettings(
|
||||
|
||||
@Serializable
|
||||
data class FullChatPreferences(
|
||||
val fullDelete: ChatPreference,
|
||||
val voice: ChatPreference,
|
||||
val timedMessages: TimedMessagesPreference,
|
||||
val fullDelete: SimpleChatPreference,
|
||||
val voice: SimpleChatPreference,
|
||||
) {
|
||||
fun toPreferences(): ChatPreferences = ChatPreferences(fullDelete = fullDelete, voice = voice)
|
||||
|
||||
companion object {
|
||||
val sampleData = FullChatPreferences(fullDelete = ChatPreference(allow = FeatureAllowed.NO), voice = ChatPreference(allow = FeatureAllowed.YES))
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatPreferences(
|
||||
val timedMessages: ChatPreference? = null,
|
||||
val fullDelete: ChatPreference? = null,
|
||||
val voice: ChatPreference? = null,
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = ChatPreferences(
|
||||
timedMessages = ChatPreference(allow = FeatureAllowed.NO),
|
||||
fullDelete = ChatPreference(allow = FeatureAllowed.NO),
|
||||
voice = ChatPreference(allow = FeatureAllowed.YES)
|
||||
val sampleData = FullChatPreferences(
|
||||
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
|
||||
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
|
||||
voice = SimpleChatPreference(allow = FeatureAllowed.YES)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatPreference(
|
||||
data class ChatPreferences(
|
||||
val timedMessages: TimedMessagesPreference? = null,
|
||||
val fullDelete: SimpleChatPreference? = null,
|
||||
val voice: SimpleChatPreference? = null,
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = ChatPreferences(
|
||||
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
|
||||
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
|
||||
voice = SimpleChatPreference(allow = FeatureAllowed.YES)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatPreference {
|
||||
val allow: FeatureAllowed
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SimpleChatPreference(
|
||||
override val allow: FeatureAllowed
|
||||
): ChatPreference
|
||||
|
||||
@Serializable
|
||||
class TimedMessagesPreference(
|
||||
override val allow: FeatureAllowed,
|
||||
val ttl: Int? = null
|
||||
): ChatPreference {
|
||||
companion object {
|
||||
val ttlValues: List<Int?>
|
||||
get() = listOf(30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400)
|
||||
|
||||
fun ttlText(ttl: Int?): String {
|
||||
ttl ?: return generalGetString(R.string.feature_off)
|
||||
if (ttl == 0) return String.format(generalGetString(R.string.ttl_sec), 0)
|
||||
val (m_, s) = divMod(ttl, 60)
|
||||
val (h_, m) = divMod(m_, 60)
|
||||
val (d_, h) = divMod(h_, 24)
|
||||
val (mm, d) = divMod(d_, 30)
|
||||
return maybe(mm, if (mm == 1) String.format(generalGetString(R.string.ttl_month), 1) else String.format(generalGetString(R.string.ttl_months), mm)) +
|
||||
maybe(d, if (d == 1) String.format(generalGetString(R.string.ttl_day), 1) else if (d == 7) String.format(generalGetString(R.string.ttl_week), 1) else if (d == 14) String.format(generalGetString(R.string.ttl_weeks), 2) else String.format(generalGetString(R.string.ttl_days), d)) +
|
||||
maybe(h, if (h == 1) String.format(generalGetString(R.string.ttl_hour), 1) else String.format(generalGetString(R.string.ttl_hours), h)) +
|
||||
maybe(m, String.format(generalGetString(R.string.ttl_min), m)) +
|
||||
maybe(s, String.format(generalGetString(R.string.ttl_sec), s))
|
||||
}
|
||||
|
||||
fun shortTtlText(ttl: Int?): String {
|
||||
ttl ?: return generalGetString(R.string.feature_off)
|
||||
val m = ttl / 60
|
||||
if (m == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_s), ttl)
|
||||
}
|
||||
val h = m / 60
|
||||
if (h == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_m), m)
|
||||
}
|
||||
val d = h / 24
|
||||
if (d == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_h), h)
|
||||
}
|
||||
val mm = d / 30
|
||||
if (mm > 0) {
|
||||
return String.format(generalGetString(R.string.ttl_mth), mm)
|
||||
}
|
||||
val w = d / 7
|
||||
return if (w == 0 || d % 7 != 0) String.format(generalGetString(R.string.ttl_d), d) else String.format(generalGetString(R.string.ttl_w), w)
|
||||
}
|
||||
|
||||
fun divMod(n: Int, d: Int): Pair<Int, Int> =
|
||||
n / d to n % d
|
||||
|
||||
fun maybe(n: Int, s: String): String =
|
||||
if (n == 0) "" else s
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ContactUserPreferences(
|
||||
val timedMessages: ContactUserPreference,
|
||||
val timedMessages: ContactUserPreferenceTimed,
|
||||
val fullDelete: ContactUserPreference,
|
||||
val voice: ContactUserPreference,
|
||||
) {
|
||||
@@ -2060,30 +2122,37 @@ data class ContactUserPreferences(
|
||||
|
||||
companion object {
|
||||
val sampleData = ContactUserPreferences(
|
||||
timedMessages = ContactUserPreference(
|
||||
timedMessages = ContactUserPreferenceTimed(
|
||||
enabled = FeatureEnabled(forUser = false, forContact = false),
|
||||
userPreference = ContactUserPref.User(preference = ChatPreference(allow = FeatureAllowed.NO)),
|
||||
contactPreference = ChatPreference(allow = FeatureAllowed.NO)
|
||||
userPreference = ContactUserPrefTimed.User(preference = TimedMessagesPreference(allow = FeatureAllowed.NO)),
|
||||
contactPreference = TimedMessagesPreference(allow = FeatureAllowed.NO)
|
||||
),
|
||||
fullDelete = ContactUserPreference(
|
||||
enabled = FeatureEnabled(forUser = false, forContact = false),
|
||||
userPreference = ContactUserPref.User(preference = ChatPreference(allow = FeatureAllowed.NO)),
|
||||
contactPreference = ChatPreference(allow = FeatureAllowed.NO)
|
||||
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.NO)),
|
||||
contactPreference = SimpleChatPreference(allow = FeatureAllowed.NO)
|
||||
),
|
||||
voice = ContactUserPreference(
|
||||
enabled = FeatureEnabled(forUser = true, forContact = true),
|
||||
userPreference = ContactUserPref.User(preference = ChatPreference(allow = FeatureAllowed.YES)),
|
||||
contactPreference = ChatPreference(allow = FeatureAllowed.YES)
|
||||
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
|
||||
contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ContactUserPreference(
|
||||
data class ContactUserPreference (
|
||||
val enabled: FeatureEnabled,
|
||||
val userPreference: ContactUserPref,
|
||||
val contactPreference: ChatPreference,
|
||||
val contactPreference: SimpleChatPreference,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContactUserPreferenceTimed (
|
||||
val enabled: FeatureEnabled,
|
||||
val userPreference: ContactUserPrefTimed,
|
||||
val contactPreference: TimedMessagesPreference,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -2116,22 +2185,49 @@ data class FeatureEnabled(
|
||||
|
||||
@Serializable
|
||||
sealed class ContactUserPref {
|
||||
abstract val pref: ChatPreference
|
||||
abstract val pref: SimpleChatPreference
|
||||
|
||||
// contact override is set
|
||||
@Serializable @SerialName("contact") data class Contact(val preference: ChatPreference): ContactUserPref() {
|
||||
@Serializable @SerialName("contact") data class Contact(val preference: SimpleChatPreference): ContactUserPref() {
|
||||
override val pref get() = preference
|
||||
}
|
||||
// global user default is used
|
||||
@Serializable @SerialName("user") data class User(val preference: ChatPreference): ContactUserPref() {
|
||||
@Serializable @SerialName("user") data class User(val preference: SimpleChatPreference): ContactUserPref() {
|
||||
override val pref get() = preference
|
||||
}
|
||||
|
||||
val contactOverride: SimpleChatPreference?
|
||||
get() = when(this) {
|
||||
is Contact -> pref
|
||||
is User -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ContactUserPrefTimed {
|
||||
abstract val pref: TimedMessagesPreference
|
||||
|
||||
// contact override is set
|
||||
@Serializable @SerialName("contact") data class Contact(val preference: TimedMessagesPreference): ContactUserPrefTimed() {
|
||||
override val pref get() = preference
|
||||
}
|
||||
// global user default is used
|
||||
@Serializable @SerialName("user") data class User(val preference: TimedMessagesPreference): ContactUserPrefTimed() {
|
||||
override val pref get() = preference
|
||||
}
|
||||
|
||||
val contactOverride: TimedMessagesPreference?
|
||||
get() = when(this) {
|
||||
is Contact -> pref
|
||||
is User -> null
|
||||
}
|
||||
}
|
||||
|
||||
interface Feature {
|
||||
// val icon: ImageVector
|
||||
val text: String
|
||||
val iconFilled: ImageVector
|
||||
val hasParam: Boolean
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -2145,6 +2241,11 @@ enum class ChatFeature: Feature {
|
||||
else -> true
|
||||
}
|
||||
|
||||
override val hasParam: Boolean get() = when(this) {
|
||||
TimedMessages -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
override val text: String
|
||||
get() = when(this) {
|
||||
TimedMessages -> generalGetString(R.string.timed_messages)
|
||||
@@ -2215,6 +2316,11 @@ enum class GroupFeature: Feature {
|
||||
@SerialName("fullDelete") FullDelete,
|
||||
@SerialName("voice") Voice;
|
||||
|
||||
override val hasParam: Boolean get() = when(this) {
|
||||
TimedMessages -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
override val text: String
|
||||
get() = when(this) {
|
||||
TimedMessages -> generalGetString(R.string.timed_messages)
|
||||
@@ -2310,25 +2416,31 @@ sealed class ContactFeatureAllowed {
|
||||
|
||||
@Serializable
|
||||
data class ContactFeaturesAllowed(
|
||||
val timedMessages: ContactFeatureAllowed,
|
||||
val timedMessagesAllowed: Boolean,
|
||||
val timedMessagesTTL: Int?,
|
||||
val fullDelete: ContactFeatureAllowed,
|
||||
val voice: ContactFeatureAllowed
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = ContactFeaturesAllowed(
|
||||
timedMessages = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO),
|
||||
timedMessagesAllowed = false,
|
||||
timedMessagesTTL = null,
|
||||
fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO),
|
||||
voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPreferences): ContactFeaturesAllowed =
|
||||
ContactFeaturesAllowed(
|
||||
timedMessages = contactUserPrefToFeatureAllowed(contactUserPreferences.timedMessages),
|
||||
fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPreferences): ContactFeaturesAllowed {
|
||||
val pref = contactUserPreferences.timedMessages.userPreference
|
||||
val allow = pref.contactOverride?.allow
|
||||
return ContactFeaturesAllowed(
|
||||
timedMessagesAllowed = allow == FeatureAllowed.YES || allow == FeatureAllowed.ALWAYS,
|
||||
timedMessagesTTL = pref.pref.ttl,
|
||||
fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
|
||||
voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice)
|
||||
)
|
||||
}
|
||||
|
||||
fun contactUserPrefToFeatureAllowed(contactUserPreference: ContactUserPreference): ContactFeatureAllowed =
|
||||
when (val pref = contactUserPreference.userPreference) {
|
||||
@@ -2342,17 +2454,17 @@ fun contactUserPrefToFeatureAllowed(contactUserPreference: ContactUserPreference
|
||||
|
||||
fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed): ChatPreferences =
|
||||
ChatPreferences(
|
||||
timedMessages = contactFeatureAllowedToPref(contactFeaturesAllowed.timedMessages),
|
||||
timedMessages = TimedMessagesPreference(if (contactFeaturesAllowed.timedMessagesAllowed) FeatureAllowed.YES else FeatureAllowed.NO, contactFeaturesAllowed.timedMessagesTTL),
|
||||
fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
|
||||
voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice)
|
||||
)
|
||||
|
||||
fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): ChatPreference? =
|
||||
fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): SimpleChatPreference? =
|
||||
when(contactFeatureAllowed) {
|
||||
is ContactFeatureAllowed.UserDefault -> null
|
||||
is ContactFeatureAllowed.Always -> ChatPreference(allow = FeatureAllowed.ALWAYS)
|
||||
is ContactFeatureAllowed.Yes -> ChatPreference(allow = FeatureAllowed.YES)
|
||||
is ContactFeatureAllowed.No -> ChatPreference(allow = FeatureAllowed.NO)
|
||||
is ContactFeatureAllowed.Always -> SimpleChatPreference(allow = FeatureAllowed.ALWAYS)
|
||||
is ContactFeatureAllowed.Yes -> SimpleChatPreference(allow = FeatureAllowed.YES)
|
||||
is ContactFeatureAllowed.No -> SimpleChatPreference(allow = FeatureAllowed.NO)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -2371,7 +2483,7 @@ enum class FeatureAllowed {
|
||||
|
||||
@Serializable
|
||||
data class FullGroupPreferences(
|
||||
val timedMessages: GroupPreference,
|
||||
val timedMessages: TimedMessagesGroupPreference,
|
||||
val directMessages: GroupPreference,
|
||||
val fullDelete: GroupPreference,
|
||||
val voice: GroupPreference
|
||||
@@ -2381,7 +2493,7 @@ data class FullGroupPreferences(
|
||||
|
||||
companion object {
|
||||
val sampleData = FullGroupPreferences(
|
||||
timedMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
|
||||
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
voice = GroupPreference(GroupFeatureEnabled.ON)
|
||||
@@ -2391,14 +2503,14 @@ data class FullGroupPreferences(
|
||||
|
||||
@Serializable
|
||||
data class GroupPreferences(
|
||||
val timedMessages: GroupPreference?,
|
||||
val timedMessages: TimedMessagesGroupPreference?,
|
||||
val directMessages: GroupPreference?,
|
||||
val fullDelete: GroupPreference?,
|
||||
val voice: GroupPreference?
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = GroupPreferences(
|
||||
timedMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
|
||||
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
voice = GroupPreference(GroupFeatureEnabled.ON)
|
||||
@@ -2413,6 +2525,14 @@ data class GroupPreference(
|
||||
val on: Boolean get() = enable == GroupFeatureEnabled.ON
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TimedMessagesGroupPreference(
|
||||
val enable: GroupFeatureEnabled,
|
||||
val ttl: Int? = null
|
||||
) {
|
||||
val on: Boolean get() = enable == GroupFeatureEnabled.ON
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class GroupFeatureEnabled {
|
||||
@SerialName("on") ON,
|
||||
|
||||
@@ -41,7 +41,6 @@ import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -517,7 +516,7 @@ fun ComposeView(
|
||||
|
||||
fun allowVoiceToContact() {
|
||||
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
|
||||
val prefs = contact.mergedPreferences.toPreferences().copy(voice = ChatPreference(allow = FeatureAllowed.YES))
|
||||
val prefs = contact.mergedPreferences.toPreferences().copy(voice = SimpleChatPreference(allow = FeatureAllowed.YES))
|
||||
withApi {
|
||||
val toContact = chatModel.controller.apiSetContactPrefs(contact.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
|
||||
|
||||
@Composable
|
||||
fun ContactPreferencesView(
|
||||
@@ -85,6 +87,14 @@ private fun ContactPreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.contact_preferences))
|
||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
|
||||
}
|
||||
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
|
||||
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
|
||||
applyPrefs(featuresAllowed.copy(fullDelete = it))
|
||||
@@ -113,7 +123,7 @@ private fun FeatureSection(
|
||||
) {
|
||||
val enabled = FeatureEnabled.enabled(
|
||||
feature.asymmetric,
|
||||
user = ChatPreference(allow = allowFeature.value.allowed),
|
||||
user = SimpleChatPreference(allow = allowFeature.value.allowed),
|
||||
contact = pref.contactPreference
|
||||
)
|
||||
|
||||
@@ -141,6 +151,50 @@ private fun FeatureSection(
|
||||
SectionTextFooter(feature.enabledDescription(enabled))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimedMessagesFeatureSection(
|
||||
featuresAllowed: ContactFeaturesAllowed,
|
||||
pref: ContactUserPreferenceTimed,
|
||||
allowFeature: State<Boolean>,
|
||||
onTTLUpdated: (Int?) -> Unit,
|
||||
onSelected: (Boolean, Int?) -> Unit
|
||||
) {
|
||||
val enabled = FeatureEnabled.enabled(
|
||||
ChatFeature.TimedMessages.asymmetric,
|
||||
user = TimedMessagesPreference(allow = if (allowFeature.value) FeatureAllowed.YES else FeatureAllowed.NO),
|
||||
contact = pref.contactPreference
|
||||
)
|
||||
|
||||
SectionView(
|
||||
ChatFeature.TimedMessages.text.uppercase(),
|
||||
icon = ChatFeature.TimedMessages.iconFilled,
|
||||
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
|
||||
leadingIcon = true,
|
||||
) {
|
||||
SectionItemView {
|
||||
PreferenceToggle(
|
||||
generalGetString(R.string.chat_preferences_you_allow),
|
||||
checked = allowFeature.value,
|
||||
) { allow ->
|
||||
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
InfoRow(
|
||||
generalGetString(R.string.chat_preferences_contact_allows),
|
||||
pref.contactPreference.allow.text
|
||||
)
|
||||
SectionDivider()
|
||||
if (featuresAllowed.timedMessagesAllowed) {
|
||||
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
@@ -154,6 +208,20 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
|
||||
val ttlValues = TimedMessagesPreference.ttlValues
|
||||
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
|
||||
SectionItemView {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.delete_after),
|
||||
values.map { it to TimedMessagesPreference.ttlText(it) },
|
||||
selection,
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
|
||||
@@ -18,7 +18,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
|
||||
|
||||
@Composable
|
||||
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
@@ -74,18 +76,30 @@ private fun GroupPreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.group_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl ?: 86400)))
|
||||
}
|
||||
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
|
||||
if (enable == GroupFeatureEnabled.ON) {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
|
||||
} else {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
|
||||
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo) {
|
||||
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
|
||||
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo) {
|
||||
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo) {
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
|
||||
}
|
||||
if (groupInfo.canEdit) {
|
||||
@@ -100,21 +114,34 @@ private fun GroupPreferencesLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeatureEnabled>, groupInfo: GroupInfo, onSelected: (GroupFeatureEnabled) -> Unit) {
|
||||
private fun FeatureSection(
|
||||
feature: GroupFeature,
|
||||
enableFeature: State<GroupFeatureEnabled>,
|
||||
groupInfo: GroupInfo,
|
||||
preferences: FullGroupPreferences,
|
||||
onTTLUpdated: (Int?) -> Unit,
|
||||
onSelected: (GroupFeatureEnabled) -> Unit
|
||||
) {
|
||||
SectionView {
|
||||
val on = enableFeature.value == GroupFeatureEnabled.ON
|
||||
val icon = if (on) feature.iconFilled else feature.icon
|
||||
val iconTint = if (on) SimplexGreen else HighOrLowlight
|
||||
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
|
||||
if (groupInfo.canEdit) {
|
||||
SectionItemView {
|
||||
ExposedDropDownSettingRow(
|
||||
PreferenceToggleWithIcon(
|
||||
feature.text,
|
||||
GroupFeatureEnabled.values().map { it to it.text },
|
||||
enableFeature,
|
||||
icon = icon,
|
||||
iconTint = iconTint,
|
||||
onSelected = onSelected
|
||||
)
|
||||
icon,
|
||||
iconTint,
|
||||
enableFeature.value == GroupFeatureEnabled.ON,
|
||||
) { checked ->
|
||||
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
|
||||
}
|
||||
}
|
||||
if (timedOn) {
|
||||
SectionDivider()
|
||||
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
}
|
||||
} else {
|
||||
InfoRow(
|
||||
@@ -123,6 +150,10 @@ private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeat
|
||||
icon = icon,
|
||||
iconTint = iconTint,
|
||||
)
|
||||
if (timedOn) {
|
||||
SectionDivider()
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
|
||||
|
||||
@@ -3,62 +3,84 @@ package chat.simplex.app.views.chat.item
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Timer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.WarningYellow
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun CIMetaView(chatItem: ChatItem, metaColor: Color = HighOrLowlight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (!chatItem.isDeletedContent) {
|
||||
if (chatItem.meta.itemEdited) {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
modifier = Modifier.height(12.dp).padding(end = 1.dp),
|
||||
contentDescription = stringResource(R.string.icon_descr_edited),
|
||||
tint = metaColor,
|
||||
)
|
||||
}
|
||||
CIStatusView(chatItem.meta.itemStatus, metaColor)
|
||||
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
|
||||
}
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
if (meta.itemEdited) {
|
||||
StatusIconText(Icons.Outlined.Edit, color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
}
|
||||
if (meta.disappearing) {
|
||||
StatusIconText(Icons.Outlined.Timer, color)
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
if (ttl != chatTTL) {
|
||||
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
StatusIconText(icon, statusColor)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
} else if (!meta.disappearing) {
|
||||
StatusIconText(Icons.Filled.Circle, Color.Transparent)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
Text(meta.timestampText, color = color, fontSize = 13.sp)
|
||||
}
|
||||
|
||||
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
||||
var res = ""
|
||||
var repeats = 0
|
||||
if (meta.itemEdited) repeats++
|
||||
if (meta.itemTimed != null) {
|
||||
repeats++
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||
}
|
||||
}
|
||||
if (meta.itemStatus !is CIStatus.RcvRead && meta.itemStatus !is CIStatus.RcvNew) repeats++
|
||||
repeat(repeats) {
|
||||
res += " "
|
||||
}
|
||||
return res + meta.timestampText
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) {
|
||||
when (status) {
|
||||
is CIStatus.SndSent -> {
|
||||
Icon(Icons.Filled.Check, stringResource(R.string.icon_descr_sent_msg_status_sent), Modifier.height(12.dp), tint = metaColor)
|
||||
}
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
Icon(Icons.Filled.Close, stringResource(R.string.icon_descr_sent_msg_status_unauthorized_send), Modifier.height(12.dp), tint = Color.Red)
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = WarningYellow)
|
||||
}
|
||||
is CIStatus.RcvNew -> {
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
private fun StatusIconText(icon: ImageVector, color: Color) {
|
||||
Icon(icon, null, Modifier.height(12.dp), tint = color)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -67,7 +89,8 @@ fun PreviewCIMetaView() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +101,8 @@ fun PreviewCIMetaViewUnread() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.RcvNew()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,7 +113,8 @@ fun PreviewCIMetaViewSendFailed() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.SndError("CMD SYNTAX")
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,7 +124,8 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,7 +135,8 @@ fun PreviewCIMetaViewSendSent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +147,8 @@ fun PreviewCIMetaViewEdited() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,7 +160,8 @@ fun PreviewCIMetaViewEditedUnread() {
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status=CIStatus.RcvNew()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,7 +173,8 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status=CIStatus.SndSent()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -152,6 +182,7 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
@Composable
|
||||
fun PreviewCIMetaViewDeletedContent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getDeletedContentSampleData()
|
||||
chatItem = ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ fun CIVoiceView(
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
longClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
@@ -63,7 +64,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, play, pause, longClick)
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
|
||||
} else {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
|
||||
val metaReserve = if (edited)
|
||||
@@ -86,6 +87,7 @@ private fun VoiceLayout(
|
||||
brokenAudio: Boolean,
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
timedMessagesTTL: Int?,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit
|
||||
@@ -105,7 +107,7 @@ private fun VoiceLayout(
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ private fun VoiceLayout(
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@@ -174,14 +174,14 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, showMember = showMember)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem)
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
|
||||
MsgContentItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
@@ -194,7 +194,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, showMember = showMember)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
@@ -215,7 +215,7 @@ fun ChatItemView(
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
|
||||
@@ -18,7 +18,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = ReceivedColorLight,
|
||||
@@ -35,7 +35,7 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun PreviewDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getDeletedContentSampleData()
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp)
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) {
|
||||
Column(
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(chatItem.content.text)
|
||||
CIMetaView(chatItem)
|
||||
CIMetaView(chatItem, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ fun FramedItemView(
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
) {
|
||||
val sent = ci.chatDir.sent
|
||||
val chatTTL = chatInfo.timedMessagesTTL
|
||||
|
||||
fun membership(): GroupMember? {
|
||||
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
|
||||
@@ -144,7 +145,7 @@ fun FramedItemView(
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, showMember, linkMode = linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,33 +189,33 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
else -> CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
|
||||
CIMetaView(ci, metaColor)
|
||||
CIMetaView(ci, chatTTL, metaColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +224,7 @@ fun FramedItemView(
|
||||
@Composable
|
||||
fun CIMarkdownText(
|
||||
ci: ChatItem,
|
||||
chatTTL: Int?,
|
||||
showMember: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
@@ -232,7 +234,7 @@ fun CIMarkdownText(
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null,
|
||||
meta = ci.meta, linkMode = linkMode,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
Modifier.clickable(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -45,7 +45,7 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun IntegrityErrorItemViewPreview() {
|
||||
SimpleXTheme {
|
||||
IntegrityErrorItemView(
|
||||
ChatItem.getDeletedContentSampleData()
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight,
|
||||
@@ -37,7 +37,7 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = true)
|
||||
ChatItem.getSampleData(itemDeleted = true),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ fun MarkdownText (
|
||||
formattedText: List<FormattedText>? = null,
|
||||
sender: String? = null,
|
||||
meta: CIMeta? = null,
|
||||
chatTTL: Int? = null,
|
||||
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
@@ -82,10 +83,12 @@ fun MarkdownText (
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (BidiFormatter.getInstance().isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
}
|
||||
val reserve = when {
|
||||
textLayoutDirection != LocalLayoutDirection.current && meta != null -> "\n"
|
||||
meta?.itemEdited == true -> " "
|
||||
else -> " "
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
CompositionLocalProvider(
|
||||
@@ -133,7 +136,7 @@ fun MarkdownText (
|
||||
if (meta?.isLive == true) {
|
||||
append(typingIndicator(meta.recent, typingIdx))
|
||||
}
|
||||
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve + meta.timestampText) }
|
||||
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
|
||||
} else {
|
||||
@@ -165,7 +168,7 @@ fun MarkdownText (
|
||||
// With RTL language set globally links looks bad sometimes, better to add a new line to bo sure everything looks good
|
||||
/*if (metaText != null && hasLinks && LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
withStyle(reserveTimestampStyle) { append("\n" + metaText) }
|
||||
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve + meta.timestampText) }
|
||||
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
if (hasLinks && uriHandler != null) {
|
||||
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
|
||||
|
||||
@@ -69,14 +69,19 @@ private fun PreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
|
||||
TimedMessagesFeatureSection(timedMessages) {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) }
|
||||
FeatureSection(ChatFeature.FullDelete, allowFullDeletion) {
|
||||
applyPrefs(preferences.copy(fullDelete = ChatPreference(allow = it)))
|
||||
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
|
||||
FeatureSection(ChatFeature.Voice, allowVoice) {
|
||||
applyPrefs(preferences.copy(voice = ChatPreference(allow = it)))
|
||||
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
ResetSaveButtons(
|
||||
@@ -103,6 +108,22 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
|
||||
SectionTextFooter(feature.allowDescription(allowFeature.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onSelected: (Boolean) -> Unit) {
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(
|
||||
ChatFeature.TimedMessages.text,
|
||||
ChatFeature.TimedMessages.icon,
|
||||
HighOrLowlight,
|
||||
allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
|
||||
onSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
|
||||
@@ -1038,5 +1038,21 @@
|
||||
<string name="message_deletion_prohibited_in_chat">Irreversible message deletion is prohibited in this group.</string>
|
||||
<string name="group_members_can_send_voice">Group members can send voice messages.</string>
|
||||
<string name="voice_messages_are_prohibited">Voice messages are prohibited in this group.</string>
|
||||
|
||||
<string name="delete_after">Delete after</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d month</string>
|
||||
<string name="ttl_months">%d months</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_hour">%d hour</string>
|
||||
<string name="ttl_hours">%d hours</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_day">%d day</string>
|
||||
<string name="ttl_days">%d days</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="ttl_week">%d week</string>
|
||||
<string name="ttl_weeks">%d weeks</string>
|
||||
<string name="ttl_w">%dw</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user