mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
android: message reactions (#2448)
* android: message reactions * android: reactions UI * call api to add/remove reactions, UI for preferences * fix preferences * hide Reactions preferences, ios: always show React menu, update icons * fix reactions menu * improve voice message layout
This commit is contained in:
parent
b6a4f5f518
commit
a32fd5e665
23 changed files with 558 additions and 210 deletions
|
@ -4,7 +4,6 @@ This readme is currently a stub and as such is in development.
|
|||
|
||||
Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app.
|
||||
|
||||
|
||||
## Gotchas
|
||||
|
||||
#### SHA Signature for verification for app links/deep links
|
||||
|
@ -23,3 +22,15 @@ To find your SHA certificate fingerprint perform the following steps.
|
|||
More information is available [here](https://developer.android.com/training/app-links/verify-site-associations#manual-verification). If there is no response when running the `pm get-app-links` command, the intents in `AndroidManifest.xml` are likely misspecified. A verification attempt can be triggered using `adb shell pm verify-app-links --re-verify chat.simplex.app`.
|
||||
|
||||
Note that this is not an issue for the app store build of the app as this is signed with our app store credentials and thus there is a stable signature over users. Developers do not have general access to these credentials for development and testing.
|
||||
|
||||
## Adding icons
|
||||
|
||||
1. Find a [Material symbol](https://fonts.google.com/icons?icon.style=Rounded) in Rounded category.
|
||||
|
||||
2. Set weight to 400, grade to -25 and size to 48px.
|
||||
|
||||
3. Click on the icon, choose Android and download XML file.
|
||||
|
||||
4. Update the color to black (#FF000000) and the size to "24.dp", as in other icons.
|
||||
|
||||
For example, this is [add reaction icon](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:add_reaction:FILL@0;wght@300;GRAD@-25;opsz@24&icon.style=Rounded).
|
||||
|
|
|
@ -238,6 +238,17 @@ class ChatModel(val controller: ChatController) {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(cInfo.id)
|
||||
|
@ -729,6 +740,7 @@ data class Contact(
|
|||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
|
||||
ChatFeature.Reactions -> mergedPreferences.reactions.enabled.forUser
|
||||
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
|
||||
ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser
|
||||
}
|
||||
|
@ -750,12 +762,14 @@ data class Contact(
|
|||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Reactions -> mergedPreferences.reactions.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
||||
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Reactions -> mergedPreferences.reactions.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
@ -888,6 +902,7 @@ data class GroupInfo (
|
|||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
|
||||
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
|
||||
ChatFeature.Reactions -> fullGroupPreferences.reactions.on
|
||||
ChatFeature.Voice -> fullGroupPreferences.voice.on
|
||||
ChatFeature.Calls -> false
|
||||
}
|
||||
|
@ -1257,6 +1272,20 @@ class AChatItem (
|
|||
val chatItem: ChatItem
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ACIReaction(
|
||||
val chatInfo: ChatInfo,
|
||||
val chatReaction: CIReaction
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class CIReaction(
|
||||
val chatDir: CIDirection,
|
||||
val chatItem: ChatItem,
|
||||
val sentAt: Instant,
|
||||
val reaction: MsgReaction
|
||||
)
|
||||
|
||||
@Serializable @Stable
|
||||
data class ChatItem (
|
||||
val chatDir: CIDirection,
|
||||
|
@ -1264,6 +1293,7 @@ data class ChatItem (
|
|||
val content: CIContent,
|
||||
val formattedText: List<FormattedText>? = null,
|
||||
val quotedItem: CIQuote? = null,
|
||||
val reactions: List<CIReactionCount>,
|
||||
val file: CIFile? = null
|
||||
) {
|
||||
val id: Long get() = meta.itemId
|
||||
|
@ -1280,6 +1310,11 @@ data class ChatItem (
|
|||
|
||||
val isRcvNew: Boolean get() = meta.isRcvNew
|
||||
|
||||
val allowAddReaction: Boolean get() =
|
||||
meta.itemDeleted == null && !isLiveDummy && (reactions.count { it.userReacted } < 3)
|
||||
|
||||
private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
|
||||
else null
|
||||
|
@ -1369,6 +1404,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
reactions = listOf(),
|
||||
file = file
|
||||
)
|
||||
|
||||
|
@ -1384,6 +1420,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead()),
|
||||
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus)
|
||||
)
|
||||
|
||||
|
@ -1399,6 +1436,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(id, ts, text, status),
|
||||
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
|
||||
|
@ -1408,6 +1446,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead()),
|
||||
content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
|
||||
|
@ -1417,6 +1456,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead()),
|
||||
content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
|
||||
|
@ -1427,6 +1467,7 @@ data class ChatItem (
|
|||
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead()),
|
||||
content = content,
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
}
|
||||
|
@ -1452,6 +1493,7 @@ data class ChatItem (
|
|||
),
|
||||
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
|
||||
|
@ -1472,6 +1514,7 @@ data class ChatItem (
|
|||
),
|
||||
content = CIContent.SndMsgContent(MsgContent.MCText("")),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
|
||||
|
@ -1481,6 +1524,7 @@ data class ChatItem (
|
|||
meta = meta ?: CIMeta.invalidJSON(),
|
||||
content = CIContent.InvalidJSON(json),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
}
|
||||
|
@ -1729,6 +1773,93 @@ class CIQuote (
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CIReactionCount(val reaction: MsgReaction, val userReacted: Boolean, val totalReacted: Int)
|
||||
|
||||
@Serializable(with = MsgReactionSerializer::class)
|
||||
sealed class MsgReaction {
|
||||
@Serializable(with = MsgReactionSerializer::class) class Emoji(val emoji: MREmojiChar): MsgReaction()
|
||||
@Serializable(with = MsgReactionSerializer::class) class Unknown(val type: String? = null, val json: JsonElement): MsgReaction()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is Emoji -> when (emoji) {
|
||||
MREmojiChar.Heart -> "❤️"
|
||||
else -> emoji.value
|
||||
}
|
||||
is Unknown -> ""
|
||||
}
|
||||
|
||||
val cmdString: String get() = when(this) {
|
||||
is Emoji -> emoji.cmdString
|
||||
is Unknown -> ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
val values: List<MsgReaction> get() = MREmojiChar.values().map(::Emoji)
|
||||
}
|
||||
}
|
||||
|
||||
object MsgReactionSerializer : KSerializer<MsgReaction> {
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgReaction", PolymorphicKind.SEALED) {
|
||||
element("Emoji", buildClassSerialDescriptor("Emoji") {
|
||||
element<String>("emoji")
|
||||
})
|
||||
element("Unknown", buildClassSerialDescriptor("Unknown"))
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): MsgReaction {
|
||||
require(decoder is JsonDecoder)
|
||||
val json = decoder.decodeJsonElement()
|
||||
return if (json is JsonObject && "type" in json) {
|
||||
when(val t = json["type"]?.jsonPrimitive?.content ?: "") {
|
||||
"emoji" -> {
|
||||
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
|
||||
if (emoji == null) MsgReaction.Unknown(t, json) else MsgReaction.Emoji(emoji)
|
||||
}
|
||||
else -> MsgReaction.Unknown(t, json)
|
||||
}
|
||||
} else {
|
||||
MsgReaction.Unknown("", json)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: MsgReaction) {
|
||||
require(encoder is JsonEncoder)
|
||||
val json = when (value) {
|
||||
is MsgReaction.Emoji ->
|
||||
buildJsonObject {
|
||||
put("type", "emoji")
|
||||
put("emoji", json.encodeToJsonElement(value.emoji))
|
||||
}
|
||||
is MsgReaction.Unknown -> value.json
|
||||
}
|
||||
encoder.encodeJsonElement(json)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MREmojiChar(val value: String) {
|
||||
@SerialName("👍") ThumbsUp("👍"),
|
||||
@SerialName("👎") ThumbsDown("👎"),
|
||||
@SerialName("😀") Smile("😀"),
|
||||
@SerialName("🎉") Celebration("🎉"),
|
||||
@SerialName("😕") Confused("😕"),
|
||||
@SerialName("❤") Heart("❤"),
|
||||
@SerialName("🚀") Launch("🚀"),
|
||||
@SerialName("👀") Looking("👀");
|
||||
|
||||
val cmdString: String get() = when(this) {
|
||||
ThumbsUp -> "+"
|
||||
ThumbsDown -> "-"
|
||||
Smile -> ")"
|
||||
Celebration -> "!"
|
||||
Confused -> "?"
|
||||
Heart -> "*"
|
||||
Launch -> "^"
|
||||
Looking -> "%"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CIFile(
|
||||
val fileId: Long,
|
||||
|
|
|
@ -600,6 +600,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiChatItemReaction(type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
|
||||
val r = sendCmd(CC.ApiChatItemReaction(type, id, itemId, add, reaction))
|
||||
if (r is CR.ChatItemReaction) return r.reaction.chatReaction.chatItem
|
||||
Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChatItem(type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? {
|
||||
val r = sendCmd(CC.ApiDeleteChatItem(type, id, itemId, mode))
|
||||
if (r is CR.ChatItemDeleted) return r
|
||||
|
@ -1378,6 +1385,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
is CR.ChatItemReaction -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
is CR.ChatItemDeleted -> {
|
||||
if (!active(r.user)) {
|
||||
if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) {
|
||||
|
@ -1885,6 +1897,7 @@ sealed class CC {
|
|||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiNewGroup(val userId: Long, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
|
@ -1973,6 +1986,7 @@ sealed class CC {
|
|||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiNewGroup -> "/_group $userId ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
|
@ -2059,6 +2073,7 @@ sealed class CC {
|
|||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
|
||||
is ApiChatItemReaction -> "apiChatItemReaction"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
|
@ -2473,15 +2488,23 @@ data class ChatSettings(
|
|||
data class FullChatPreferences(
|
||||
val timedMessages: TimedMessagesPreference,
|
||||
val fullDelete: SimpleChatPreference,
|
||||
val reactions: SimpleChatPreference,
|
||||
val voice: SimpleChatPreference,
|
||||
val calls: SimpleChatPreference,
|
||||
) {
|
||||
fun toPreferences(): ChatPreferences = ChatPreferences(timedMessages = timedMessages, fullDelete = fullDelete, voice = voice, calls = calls)
|
||||
fun toPreferences(): ChatPreferences = ChatPreferences(
|
||||
timedMessages = timedMessages,
|
||||
fullDelete = fullDelete,
|
||||
reactions = reactions,
|
||||
voice = voice,
|
||||
calls = calls
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sampleData = FullChatPreferences(
|
||||
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
|
||||
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
|
||||
reactions = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
)
|
||||
|
@ -2492,6 +2515,7 @@ data class FullChatPreferences(
|
|||
data class ChatPreferences(
|
||||
val timedMessages: TimedMessagesPreference?,
|
||||
val fullDelete: SimpleChatPreference?,
|
||||
val reactions: SimpleChatPreference?,
|
||||
val voice: SimpleChatPreference?,
|
||||
val calls: SimpleChatPreference?,
|
||||
) {
|
||||
|
@ -2499,6 +2523,7 @@ data class ChatPreferences(
|
|||
when (feature) {
|
||||
ChatFeature.TimedMessages -> this.copy(timedMessages = TimedMessagesPreference(allow = allowed, ttl = param ?: this.timedMessages?.ttl))
|
||||
ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed))
|
||||
ChatFeature.Reactions -> this.copy(reactions = SimpleChatPreference(allow = allowed))
|
||||
ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed))
|
||||
ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed))
|
||||
}
|
||||
|
@ -2507,6 +2532,7 @@ data class ChatPreferences(
|
|||
val sampleData = ChatPreferences(
|
||||
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
|
||||
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
|
||||
reactions = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
|
||||
)
|
||||
|
@ -2579,12 +2605,14 @@ data class TimedMessagesPreference(
|
|||
data class ContactUserPreferences(
|
||||
val timedMessages: ContactUserPreferenceTimed,
|
||||
val fullDelete: ContactUserPreference,
|
||||
val reactions: ContactUserPreference,
|
||||
val voice: ContactUserPreference,
|
||||
val calls: ContactUserPreference,
|
||||
) {
|
||||
fun toPreferences(): ChatPreferences = ChatPreferences(
|
||||
timedMessages = timedMessages.userPreference.pref,
|
||||
fullDelete = fullDelete.userPreference.pref,
|
||||
reactions = reactions.userPreference.pref,
|
||||
voice = voice.userPreference.pref,
|
||||
calls = calls.userPreference.pref
|
||||
)
|
||||
|
@ -2601,6 +2629,11 @@ data class ContactUserPreferences(
|
|||
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.NO)),
|
||||
contactPreference = SimpleChatPreference(allow = FeatureAllowed.NO)
|
||||
),
|
||||
reactions = ContactUserPreference(
|
||||
enabled = FeatureEnabled(forUser = true, forContact = true),
|
||||
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
|
||||
contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES)
|
||||
),
|
||||
voice = ContactUserPreference(
|
||||
enabled = FeatureEnabled(forUser = true, forContact = true),
|
||||
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
|
||||
|
@ -2697,6 +2730,7 @@ interface Feature {
|
|||
enum class ChatFeature: Feature {
|
||||
@SerialName("timedMessages") TimedMessages,
|
||||
@SerialName("fullDelete") FullDelete,
|
||||
@SerialName("reactions") Reactions,
|
||||
@SerialName("voice") Voice,
|
||||
@SerialName("calls") Calls;
|
||||
|
||||
|
@ -2714,6 +2748,7 @@ enum class ChatFeature: Feature {
|
|||
get() = when(this) {
|
||||
TimedMessages -> generalGetString(R.string.timed_messages)
|
||||
FullDelete -> generalGetString(R.string.full_deletion)
|
||||
Reactions -> generalGetString(R.string.message_reactions)
|
||||
Voice -> generalGetString(R.string.voice_messages)
|
||||
Calls -> generalGetString(R.string.audio_video_calls)
|
||||
}
|
||||
|
@ -2722,6 +2757,7 @@ enum class ChatFeature: Feature {
|
|||
@Composable get() = when(this) {
|
||||
TimedMessages -> painterResource(R.drawable.ic_timer)
|
||||
FullDelete -> painterResource(R.drawable.ic_delete_forever)
|
||||
Reactions -> painterResource(R.drawable.ic_add_reaction)
|
||||
Voice -> painterResource(R.drawable.ic_keyboard_voice)
|
||||
Calls -> painterResource(R.drawable.ic_call)
|
||||
}
|
||||
|
@ -2730,6 +2766,7 @@ enum class ChatFeature: Feature {
|
|||
override fun iconFilled(): Painter = when(this) {
|
||||
TimedMessages -> painterResource(R.drawable.ic_timer_filled)
|
||||
FullDelete -> painterResource(R.drawable.ic_delete_forever_filled)
|
||||
Reactions -> painterResource(R.drawable.ic_add_reaction_filled)
|
||||
Voice -> painterResource(R.drawable.ic_keyboard_voice_filled)
|
||||
Calls -> painterResource(R.drawable.ic_call_filled)
|
||||
}
|
||||
|
@ -2746,7 +2783,12 @@ enum class ChatFeature: Feature {
|
|||
FeatureAllowed.YES -> generalGetString(R.string.allow_irreversible_message_deletion_only_if)
|
||||
FeatureAllowed.NO -> generalGetString(R.string.contacts_can_mark_messages_for_deletion)
|
||||
}
|
||||
Voice -> when (allowed) {
|
||||
Reactions -> when (allowed) {
|
||||
FeatureAllowed.ALWAYS -> generalGetString(R.string.allow_your_contacts_adding_message_reactions)
|
||||
FeatureAllowed.YES -> generalGetString(R.string.allow_message_reactions_only_if)
|
||||
FeatureAllowed.NO -> generalGetString(R.string.prohibit_message_reactions)
|
||||
}
|
||||
Voice -> when (allowed) {
|
||||
FeatureAllowed.ALWAYS -> generalGetString(R.string.allow_your_contacts_to_send_voice_messages)
|
||||
FeatureAllowed.YES -> generalGetString(R.string.allow_voice_messages_only_if)
|
||||
FeatureAllowed.NO -> generalGetString(R.string.prohibit_sending_voice_messages)
|
||||
|
@ -2772,6 +2814,12 @@ enum class ChatFeature: Feature {
|
|||
enabled.forContact -> generalGetString(R.string.only_your_contact_can_delete)
|
||||
else -> generalGetString(R.string.message_deletion_prohibited)
|
||||
}
|
||||
Reactions -> when {
|
||||
enabled.forUser && enabled.forContact -> generalGetString(R.string.both_you_and_your_contact_can_add_message_reactions)
|
||||
enabled.forUser -> generalGetString(R.string.only_you_can_add_message_reactions)
|
||||
enabled.forContact -> generalGetString(R.string.only_your_contact_can_add_message_reactions)
|
||||
else -> generalGetString(R.string.message_reactions_prohibited_in_this_chat)
|
||||
}
|
||||
Voice -> when {
|
||||
enabled.forUser && enabled.forContact -> generalGetString(R.string.both_you_and_your_contact_can_send_voice)
|
||||
enabled.forUser -> generalGetString(R.string.only_you_can_send_voice)
|
||||
|
@ -2792,6 +2840,7 @@ enum class GroupFeature: Feature {
|
|||
@SerialName("timedMessages") TimedMessages,
|
||||
@SerialName("directMessages") DirectMessages,
|
||||
@SerialName("fullDelete") FullDelete,
|
||||
@SerialName("reactions") Reactions,
|
||||
@SerialName("voice") Voice;
|
||||
|
||||
override val hasParam: Boolean get() = when(this) {
|
||||
|
@ -2804,6 +2853,7 @@ enum class GroupFeature: Feature {
|
|||
TimedMessages -> generalGetString(R.string.timed_messages)
|
||||
DirectMessages -> generalGetString(R.string.direct_messages)
|
||||
FullDelete -> generalGetString(R.string.full_deletion)
|
||||
Reactions -> generalGetString(R.string.message_reactions)
|
||||
Voice -> generalGetString(R.string.voice_messages)
|
||||
}
|
||||
|
||||
|
@ -2812,6 +2862,7 @@ enum class GroupFeature: Feature {
|
|||
TimedMessages -> painterResource(R.drawable.ic_timer)
|
||||
DirectMessages -> painterResource(R.drawable.ic_swap_horizontal_circle)
|
||||
FullDelete -> painterResource(R.drawable.ic_delete_forever)
|
||||
Reactions -> painterResource(R.drawable.ic_add_reaction)
|
||||
Voice -> painterResource(R.drawable.ic_keyboard_voice)
|
||||
}
|
||||
|
||||
|
@ -2820,6 +2871,7 @@ enum class GroupFeature: Feature {
|
|||
TimedMessages -> painterResource(R.drawable.ic_timer_filled)
|
||||
DirectMessages -> painterResource(R.drawable.ic_swap_horizontal_circle_filled)
|
||||
FullDelete -> painterResource(R.drawable.ic_delete_forever_filled)
|
||||
Reactions -> painterResource(R.drawable.ic_add_reaction_filled)
|
||||
Voice -> painterResource(R.drawable.ic_keyboard_voice_filled)
|
||||
}
|
||||
|
||||
|
@ -2838,6 +2890,10 @@ enum class GroupFeature: Feature {
|
|||
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_delete_messages)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_deletion)
|
||||
}
|
||||
Reactions -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_message_reactions)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_reactions_group)
|
||||
}
|
||||
Voice -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_send_voice)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_sending_voice)
|
||||
|
@ -2857,6 +2913,10 @@ enum class GroupFeature: Feature {
|
|||
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_delete)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.message_deletion_prohibited_in_chat)
|
||||
}
|
||||
Reactions -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_add_message_reactions)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.message_reactions_are_prohibited)
|
||||
}
|
||||
Voice -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_send_voice)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(R.string.voice_messages_are_prohibited)
|
||||
|
@ -2897,6 +2957,7 @@ data class ContactFeaturesAllowed(
|
|||
val timedMessagesAllowed: Boolean,
|
||||
val timedMessagesTTL: Int?,
|
||||
val fullDelete: ContactFeatureAllowed,
|
||||
val reactions: ContactFeatureAllowed,
|
||||
val voice: ContactFeatureAllowed,
|
||||
val calls: ContactFeatureAllowed,
|
||||
) {
|
||||
|
@ -2905,6 +2966,7 @@ data class ContactFeaturesAllowed(
|
|||
timedMessagesAllowed = false,
|
||||
timedMessagesTTL = null,
|
||||
fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO),
|
||||
reactions = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
|
||||
voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
|
||||
calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
|
||||
)
|
||||
|
@ -2918,6 +2980,7 @@ fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPrefere
|
|||
timedMessagesAllowed = allow == FeatureAllowed.YES || allow == FeatureAllowed.ALWAYS,
|
||||
timedMessagesTTL = pref.pref.ttl,
|
||||
fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
|
||||
reactions = contactUserPrefToFeatureAllowed(contactUserPreferences.reactions),
|
||||
voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice),
|
||||
calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls),
|
||||
)
|
||||
|
@ -2937,6 +3000,7 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed
|
|||
ChatPreferences(
|
||||
timedMessages = TimedMessagesPreference(if (contactFeaturesAllowed.timedMessagesAllowed) FeatureAllowed.YES else FeatureAllowed.NO, contactFeaturesAllowed.timedMessagesTTL),
|
||||
fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
|
||||
reactions = contactFeatureAllowedToPref(contactFeaturesAllowed.reactions),
|
||||
voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice),
|
||||
calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls),
|
||||
)
|
||||
|
@ -2968,16 +3032,24 @@ data class FullGroupPreferences(
|
|||
val timedMessages: TimedMessagesGroupPreference,
|
||||
val directMessages: GroupPreference,
|
||||
val fullDelete: GroupPreference,
|
||||
val reactions: GroupPreference,
|
||||
val voice: GroupPreference
|
||||
) {
|
||||
fun toGroupPreferences(): GroupPreferences =
|
||||
GroupPreferences(timedMessages = timedMessages, directMessages = directMessages, fullDelete = fullDelete, voice = voice)
|
||||
GroupPreferences(
|
||||
timedMessages = timedMessages,
|
||||
directMessages = directMessages,
|
||||
fullDelete = fullDelete,
|
||||
reactions = reactions,
|
||||
voice = voice
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sampleData = FullGroupPreferences(
|
||||
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
|
||||
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
reactions = GroupPreference(GroupFeatureEnabled.ON),
|
||||
voice = GroupPreference(GroupFeatureEnabled.ON)
|
||||
)
|
||||
}
|
||||
|
@ -2988,6 +3060,7 @@ data class GroupPreferences(
|
|||
val timedMessages: TimedMessagesGroupPreference?,
|
||||
val directMessages: GroupPreference?,
|
||||
val fullDelete: GroupPreference?,
|
||||
val reactions: GroupPreference?,
|
||||
val voice: GroupPreference?
|
||||
) {
|
||||
companion object {
|
||||
|
@ -2995,6 +3068,7 @@ data class GroupPreferences(
|
|||
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
|
||||
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
reactions = GroupPreference(GroupFeatureEnabled.ON),
|
||||
voice = GroupPreference(GroupFeatureEnabled.ON)
|
||||
)
|
||||
}
|
||||
|
@ -3166,6 +3240,7 @@ sealed class CR {
|
|||
@Serializable @SerialName("newChatItem") class NewChatItem(val user: User, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: User, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: User, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: User, val added: Boolean, val reaction: ACIReaction): CR()
|
||||
@Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: User, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR()
|
||||
@Serializable @SerialName("contactsList") class ContactsList(val user: User, val contacts: List<Contact>): CR()
|
||||
// group events
|
||||
|
@ -3277,6 +3352,7 @@ sealed class CR {
|
|||
is NewChatItem -> "newChatItem"
|
||||
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
|
||||
is ChatItemUpdated -> "chatItemUpdated"
|
||||
is ChatItemReaction -> "chatItemReaction"
|
||||
is ChatItemDeleted -> "chatItemDeleted"
|
||||
is ContactsList -> "contactsList"
|
||||
is GroupCreated -> "groupCreated"
|
||||
|
@ -3386,6 +3462,7 @@ sealed class CR {
|
|||
is NewChatItem -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
|
||||
is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser")
|
||||
is ContactsList -> withUser(user, json.encodeToString(contacts))
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
|
|
|
@ -258,6 +258,20 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
|||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
}
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = cItem.id,
|
||||
add = add,
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
|
@ -316,6 +330,7 @@ fun ChatLayout(
|
|||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
addMembers: (GroupInfo) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
|
@ -358,7 +373,7 @@ fun ChatLayout(
|
|||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -531,6 +546,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: () -> Unit,
|
||||
|
@ -644,11 +660,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
|
@ -659,7 +675,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1082,6 +1098,7 @@ fun PreviewChatLayout() {
|
|||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
|
@ -1142,6 +1159,7 @@ fun PreviewGroupChatLayout() {
|
|||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
|
|
|
@ -40,10 +40,10 @@ fun ComposeVoiceView(
|
|||
val audioPlaying = rememberSaveable { mutableStateOf(false) }
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.height(57.dp)
|
||||
.fillMaxWidth()
|
||||
.background(sentColor)
|
||||
.padding(top = 8.dp),
|
||||
.padding(top = 3.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
|
|
|
@ -95,6 +95,11 @@ private fun ContactPreferencesLayout(
|
|||
applyPrefs(featuresAllowed.copy(fullDelete = it))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
// val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) }
|
||||
// FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) {
|
||||
// applyPrefs(featuresAllowed.copy(reactions = it))
|
||||
// }
|
||||
// SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) }
|
||||
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
|
||||
applyPrefs(featuresAllowed.copy(voice = it))
|
||||
|
|
|
@ -95,6 +95,11 @@ private fun GroupPreferencesLayout(
|
|||
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
// val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) }
|
||||
// FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) {
|
||||
// applyPrefs(preferences.copy(reactions = GroupPreference(enable = it)))
|
||||
// }
|
||||
// SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
|
||||
|
|
|
@ -211,6 +211,7 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
|||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
|
||||
)
|
||||
private val fileChatItemWtFile = ChatItem(
|
||||
|
@ -218,6 +219,7 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
|||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
|
||||
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
override val values = listOf(
|
||||
|
|
|
@ -24,7 +24,7 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
|
|||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 14.sp,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -150,32 +150,32 @@ private fun VoiceLayout(
|
|||
}
|
||||
}
|
||||
sent -> {
|
||||
Row {
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) {
|
||||
Spacer(Modifier.height(56.dp))
|
||||
Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp))
|
||||
DurationText(text, PaddingValues(end = 12.dp))
|
||||
}
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Row {
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) {
|
||||
Spacer(Modifier.height(56.dp))
|
||||
Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp))
|
||||
DurationText(text, PaddingValues(end = 12.dp))
|
||||
}
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
}
|
||||
Box(Modifier.padding(top = 6.dp, end = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Row {
|
||||
Column {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Row {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp))
|
||||
Spacer(Modifier.height(56.dp))
|
||||
}
|
||||
}
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp))
|
||||
Spacer(Modifier.height(56.dp))
|
||||
Box(Modifier.padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ package chat.simplex.app.views.chat.item
|
|||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
|
@ -16,8 +17,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
|
@ -45,7 +48,8 @@ fun ChatItemView(
|
|||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
@ -75,186 +79,246 @@ fun ChatItemView(
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.delete_message_cannot_be_undone_warning)
|
||||
} else {
|
||||
generalGetString(R.string.delete_message_mark_deleted_warning)
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.moderate_message_will_be_deleted_warning)
|
||||
} else {
|
||||
generalGetString(R.string.moderate_message_will_be_marked_warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
|
||||
when {
|
||||
filePath != null -> shareFile(context, cItem.text, filePath)
|
||||
else -> shareText(context, cItem.content.text)
|
||||
@Composable
|
||||
fun ChatItemReactions() {
|
||||
Row {
|
||||
cItem.reactions.forEach { r ->
|
||||
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) {
|
||||
modifier = modifier.clickable {
|
||||
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
|
||||
copyText(context, cItem.content.text)
|
||||
showMenu.value = false
|
||||
})
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
|
||||
saveImage(context, cItem.file)
|
||||
} else {
|
||||
writePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
}
|
||||
Row(modifier.padding(2.dp)) {
|
||||
Text(r.reaction.text, fontSize = 12.sp)
|
||||
if (r.totalReacted > 1) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("${r.totalReacted}",
|
||||
fontSize = 11.5.sp,
|
||||
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) {
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.delete_message_cannot_be_undone_warning)
|
||||
} else {
|
||||
generalGetString(R.string.delete_message_mark_deleted_warning)
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.moderate_message_will_be_deleted_warning)
|
||||
} else {
|
||||
generalGetString(R.string.moderate_message_will_be_marked_warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgReactionsMenu() {
|
||||
val rs = MsgReaction.values.mapNotNull { r ->
|
||||
if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) {
|
||||
r
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (rs.isNotEmpty()) {
|
||||
Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState())) {
|
||||
rs.forEach() { r ->
|
||||
Box(
|
||||
Modifier.size(36.dp).clickable {
|
||||
setReaction(cInfo, cItem, true, r)
|
||||
showMenu.value = false
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(r.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||
MsgReactionsMenu()
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
|
||||
when {
|
||||
filePath != null -> shareFile(context, cItem.text, filePath)
|
||||
else -> shareText(context, cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(R.string.hide_verb),
|
||||
painterResource(R.drawable.ic_visibility_off),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
|
||||
copyText(context, cItem.content.text)
|
||||
showMenu.value = false
|
||||
})
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
|
||||
saveImage(context, cItem.file)
|
||||
} else {
|
||||
writePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(R.string.hide_verb),
|
||||
painterResource(R.drawable.ic_visibility_off),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
|
||||
}
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
painterResource(R.drawable.ic_visibility),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
painterResource(R.drawable.ic_visibility),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
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, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
MsgContentItemDropdownMenu()
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
} else {
|
||||
framedItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
when (val c = cItem.content) {
|
||||
is CIContent.SndMsgContent -> ContentItem()
|
||||
is CIContent.RcvMsgContent -> ContentItem()
|
||||
is CIContent.SndDeleted -> DeletedItem()
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, 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)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
|
||||
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
|
||||
}
|
||||
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
|
||||
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
|
||||
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
|
||||
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
when (val c = cItem.content) {
|
||||
is CIContent.SndMsgContent -> ContentItem()
|
||||
is CIContent.RcvMsgContent -> ContentItem()
|
||||
is CIContent.SndDeleted -> DeletedItem()
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, 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)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
|
||||
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
|
||||
}
|
||||
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
|
||||
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
|
||||
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
|
||||
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
if (cItem.content.msgContent != null && cItem.meta.itemDeleted == null && cItem.reactions.isNotEmpty()) {
|
||||
ChatItemReactions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -430,7 +494,8 @@ fun PreviewChatItemView() {
|
|||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
acceptFeature = { _, _, _ -> },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +516,8 @@ fun PreviewChatItemViewDeletedContent() {
|
|||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
acceptFeature = { _, _, _ -> },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,9 +202,9 @@ fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boole
|
|||
Divider(
|
||||
Modifier.padding(
|
||||
start = DEFAULT_PADDING_HALF,
|
||||
top = if (maxTopPadding) 40.dp else 30.dp,
|
||||
top = if (maxTopPadding) 37.dp else 27.dp,
|
||||
end = DEFAULT_PADDING_HALF,
|
||||
bottom = if (maxBottomPadding) 40.dp else 30.dp)
|
||||
bottom = if (maxBottomPadding) 37.dp else 27.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,11 @@ private fun PreferencesLayout(
|
|||
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
// val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.allow) }
|
||||
// FeatureSection(ChatFeature.Reactions, allowReactions) {
|
||||
// applyPrefs(preferences.copy(reactions = SimpleChatPreference(allow = it)))
|
||||
// }
|
||||
// SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
|
||||
FeatureSection(ChatFeature.Voice, allowVoice) {
|
||||
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.dp"
|
||||
android:height="24.dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M480.33,875Q398.63,875 326.7,843.74Q254.78,812.47 201.14,758.74Q147.5,705 116.25,633.21Q85,561.42 85,479.8Q85,398.09 116.36,325.93Q147.73,253.78 201.13,200.39Q254.54,147 326.79,116Q399.04,85 479.92,85Q526.76,85 570.69,95.51Q614.62,106.03 654,125Q650.5,133.5 649.25,142.08Q648,150.67 648,160Q648,167.9 648.75,174.95Q649.5,182 652,189Q614,166.5 570.93,154.5Q527.86,142.5 479.9,142.5Q339.69,142.5 241.09,240.75Q142.5,339 142.5,479.51Q142.5,620.03 241.24,718.76Q339.97,817.5 480.49,817.5Q621,817.5 719.25,718.91Q817.5,620.31 817.5,480Q817.5,441.47 809.25,404.99Q801,368.5 786,336Q796.96,343.76 810.93,347.88Q824.9,352 840,352Q843.36,352 846.75,352Q850.14,352 853.5,351.5Q864,382 869.5,413.93Q875,445.87 875,479.82Q875,560.91 843.99,633.3Q812.97,705.68 759.49,758.99Q706,812.3 633.98,843.65Q561.95,875 480.33,875ZM624.45,426.5Q647.4,426.5 662.45,411.5Q677.5,396.49 677.5,373.55Q677.5,350.6 662.5,335.55Q647.49,320.5 624.55,320.5Q601.6,320.5 586.55,335.5Q571.5,350.51 571.5,373.45Q571.5,396.4 586.5,411.45Q601.51,426.5 624.45,426.5ZM335.45,426.5Q358.4,426.5 373.45,411.5Q388.5,396.49 388.5,373.55Q388.5,350.6 373.5,335.55Q358.49,320.5 335.55,320.5Q312.6,320.5 297.55,335.5Q282.5,350.51 282.5,373.45Q282.5,396.4 297.5,411.45Q312.51,426.5 335.45,426.5ZM480,696Q544.5,696 599.25,661.5Q654,627 679,566.5L281,566.5Q307,627 361.25,661.5Q415.5,696 480,696ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM811.5,188.5L760,188.5Q747.75,188.5 739.63,180.33Q731.5,172.15 731.5,159.82Q731.5,147.5 739.63,139.25Q747.75,131 760,131L811.5,131L811.5,80Q811.5,67.75 819.67,59.38Q827.85,51 840.17,51Q852.5,51 860.75,59.38Q869,67.75 869,80L869,131L920,131Q932.25,131 940.63,139.43Q949,147.85 949,160.17Q949,172.5 940.63,180.5Q932.25,188.5 920,188.5L869,188.5L869,240Q869,252.25 860.58,260.38Q852.15,268.5 839.83,268.5Q827.5,268.5 819.5,260.38Q811.5,252.25 811.5,240L811.5,188.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.dp"
|
||||
android:height="24.dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M811.5,188.5L760,188.5Q748.5,188.5 740,180.33Q731.5,172.15 731.5,159.82Q731.5,147.5 739.63,139.25Q747.75,131 760,131L811.5,131L811.5,80Q811.5,67.75 819.67,59.38Q827.85,51 840.17,51Q852.5,51 860.75,59.38Q869,67.75 869,80L869,131L920,131Q932.25,131 940.63,139.43Q949,147.85 949,160.17Q949,172.5 940.63,180.5Q932.25,188.5 920,188.5L869,188.5L869,240Q869,251.5 860.58,260Q852.15,268.5 839.83,268.5Q827.5,268.5 819.5,260.38Q811.5,252.25 811.5,240L811.5,188.5ZM480.33,875Q398.63,875 326.7,843.74Q254.78,812.47 201.14,758.74Q147.5,705 116.25,633.21Q85,561.42 85,479.8Q85,398.09 116.36,325.93Q147.73,253.78 201.13,200.39Q254.54,147 326.79,116Q399.04,85 479.92,85Q526.76,85 570.69,95.51Q614.62,106.03 654,125Q650.5,133.5 649.25,142.08Q648,150.67 648,159.57Q648,198 671.44,228.46Q694.88,258.93 732.5,267.5Q741.57,305.17 772.03,328.58Q802.48,352 840.28,352Q843.36,352 846.75,352Q850.14,352 853.5,351.5Q864,382 869.5,413.93Q875,445.87 875,479.82Q875,560.91 843.99,633.3Q812.97,705.68 759.49,758.99Q706,812.3 633.98,843.65Q561.95,875 480.33,875ZM624.45,426.5Q647.4,426.5 662.45,411.5Q677.5,396.49 677.5,373.55Q677.5,350.6 662.5,335.55Q647.49,320.5 624.55,320.5Q601.6,320.5 586.55,335.5Q571.5,350.51 571.5,373.45Q571.5,396.4 586.5,411.45Q601.51,426.5 624.45,426.5ZM335.45,426.5Q358.4,426.5 373.45,411.5Q388.5,396.49 388.5,373.55Q388.5,350.6 373.5,335.55Q358.49,320.5 335.55,320.5Q312.6,320.5 297.55,335.5Q282.5,350.51 282.5,373.45Q282.5,396.4 297.5,411.45Q312.51,426.5 335.45,426.5ZM480,696Q544.5,696 599.25,661.5Q654,627 679,566.5L281,566.5Q307,627 361.25,661.5Q415.5,696 480,696Z"/>
|
||||
</vector>
|
|
@ -1250,6 +1250,7 @@
|
|||
<string name="timed_messages">Disappearing messages</string>
|
||||
<string name="direct_messages">Direct messages</string>
|
||||
<string name="full_deletion">Delete for everyone</string>
|
||||
<string name="message_reactions">Message reactions</string>
|
||||
<string name="voice_messages">Voice messages</string>
|
||||
<string name="audio_video_calls">Audio/video calls</string>
|
||||
<string name="available_in_v51">\nAvailable in v5.1</string>
|
||||
|
@ -1269,6 +1270,9 @@
|
|||
<string name="allow_your_contacts_to_send_voice_messages">Allow your contacts to send voice messages.</string>
|
||||
<string name="allow_voice_messages_only_if">Allow voice messages only if your contact allows them.</string>
|
||||
<string name="prohibit_sending_voice_messages">Prohibit sending voice messages.</string>
|
||||
<string name="allow_your_contacts_adding_message_reactions">Allow your contacts adding message reactions.</string>
|
||||
<string name="allow_message_reactions_only_if">Allow message reactions only if your contact allows them.</string>
|
||||
<string name="prohibit_message_reactions">Prohibit message reactions.</string>
|
||||
<string name="allow_your_contacts_to_call">Allow your contacts to call you.</string>
|
||||
<string name="allow_calls_only_if">Allow calls only if your contact allows them.</string>
|
||||
<string name="prohibit_calls">Prohibit audio/video calls.</string>
|
||||
|
@ -1284,6 +1288,10 @@
|
|||
<string name="only_you_can_send_voice">Only you can send voice messages.</string>
|
||||
<string name="only_your_contact_can_send_voice">Only your contact can send voice messages.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Voice messages are prohibited in this chat.</string>
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">Both you and your contact can add message reactions.</string>
|
||||
<string name="only_you_can_add_message_reactions">Only you can add message reactions.</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Only your contact can add message reactions.</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">Message reactions are prohibited in this chat.</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Both you and your contact can make calls.</string>
|
||||
<string name="only_you_can_make_calls">Only you can make calls.</string>
|
||||
<string name="only_your_contact_can_make_calls">Only your contact can make calls.</string>
|
||||
|
@ -1296,6 +1304,8 @@
|
|||
<string name="prohibit_message_deletion">Prohibit irreversible message deletion.</string>
|
||||
<string name="allow_to_send_voice">Allow to send voice messages.</string>
|
||||
<string name="prohibit_sending_voice">Prohibit sending voice messages.</string>
|
||||
<string name="allow_message_reactions">Allow message reactions.</string>
|
||||
<string name="prohibit_message_reactions_group">Prohibit messages reactions.</string>
|
||||
<string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Disappearing messages are prohibited in this group.</string>
|
||||
<string name="group_members_can_send_dms">Group members can send direct messages.</string>
|
||||
|
@ -1304,6 +1314,8 @@
|
|||
<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="group_members_can_add_message_reactions">Group members can add message reactions.</string>
|
||||
<string name="message_reactions_are_prohibited">Message reactions 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>
|
||||
|
|
|
@ -444,7 +444,6 @@ struct ChatView: View {
|
|||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
var ci: ChatItem
|
||||
var showMember: Bool = false
|
||||
var maxWidth: CGFloat
|
||||
|
@ -538,7 +537,7 @@ struct ChatView: View {
|
|||
private func menu(live: Bool) -> [UIMenuElement] {
|
||||
var menu: [UIMenuElement] = []
|
||||
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
|
||||
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction && developerTools,
|
||||
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
|
||||
let rm = reactionUIMenu() {
|
||||
menu.append(rm)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import SimpleXChat
|
|||
struct ContactPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var contact: Contact
|
||||
@State var featuresAllowed: ContactFeaturesAllowed
|
||||
@State var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
|
@ -25,9 +24,7 @@ struct ContactPreferencesView: View {
|
|||
List {
|
||||
timedMessagesFeatureSection()
|
||||
featureSection(.fullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, $featuresAllowed.fullDelete)
|
||||
if developerTools {
|
||||
featureSection(.reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, $featuresAllowed.reactions)
|
||||
}
|
||||
// featureSection(.reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, $featuresAllowed.reactions)
|
||||
featureSection(.voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, $featuresAllowed.voice)
|
||||
featureSection(.calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, $featuresAllowed.calls)
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import SimpleXChat
|
|||
struct GroupPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var preferences: FullGroupPreferences
|
||||
@State var currentPreferences: FullGroupPreferences
|
||||
|
@ -26,9 +25,7 @@ struct GroupPreferencesView: View {
|
|||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable)
|
||||
if developerTools {
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
}
|
||||
// featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
|
|
|
@ -11,7 +11,6 @@ import SimpleXChat
|
|||
|
||||
struct PreferencesView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State var profile: LocalProfile
|
||||
@State var preferences: FullPreferences
|
||||
@State var currentPreferences: FullPreferences
|
||||
|
@ -21,9 +20,7 @@ struct PreferencesView: View {
|
|||
List {
|
||||
timedMessagesFeatureSection($preferences.timedMessages.allow)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.allow)
|
||||
if developerTools {
|
||||
featureSection(.reactions, $preferences.reactions.allow)
|
||||
}
|
||||
// featureSection(.reactions, $preferences.reactions.allow)
|
||||
featureSection(.voice, $preferences.voice.allow)
|
||||
featureSection(.calls, $preferences.calls.allow)
|
||||
|
||||
|
|
|
@ -2474,7 +2474,11 @@ public enum MsgReaction: Hashable {
|
|||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case let .emoji(emoji): return emoji.rawValue
|
||||
case let .emoji(emoji):
|
||||
switch emoji {
|
||||
case .heart: return "❤️"
|
||||
default: return emoji.rawValue
|
||||
}
|
||||
case .unknown: return "?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# the binaries folders should be in ~/Downloads folder
|
||||
rm -rf ./apps/ios/Libraries/mac-aarch64 ./apps/ios/Libraries/mac-x86_64 ./apps/ios/Libraries/ios ./apps/ios/Libraries/sim
|
||||
mkdir -p ./apps/ios/Libraries/mac-aarch64 ./apps/ios/Libraries/mac-x86_64 ./apps/ios/Libraries/ios ./apps/ios/Libraries/sim
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# the binaries folder should be in ~/Downloads folder
|
||||
rm -rf ./apps/ios/Libraries/mac ./apps/ios/Libraries/ios ./apps/ios/Libraries/sim
|
||||
mkdir -p ./apps/ios/Libraries/mac ./apps/ios/Libraries/ios ./apps/ios/Libraries/sim
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue