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:
Stanislav Dmitrenko
2022-12-22 01:07:37 +03:00
committed by GitHub
parent 372d7ffaa9
commit 0b046315ac
16 changed files with 505 additions and 159 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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))

View File

@@ -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
)
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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
)
}
}

View File

@@ -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)
}
}

View File

@@ -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
)
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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>