android, desktop: block members (#3290)
* android, desktop: block members * fixes * more fixes * fix * fix * color * color and icon --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
68873464d7
commit
4cc20a2d32
@ -137,6 +137,7 @@ object ChatModel {
|
||||
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
fun getGroupMember(groupMemberId: Long): GroupMember? = groupMembers.firstOrNull { it.groupMemberId == groupMemberId }
|
||||
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
@ -442,6 +443,78 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun getChatItemIndexOrNull(cItem: ChatItem): Int? {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
val index = reversedChatItems.indexOfFirst { it.id == cItem.id }
|
||||
return if (index != -1) index else null
|
||||
}
|
||||
|
||||
// this function analyses "connected" events and assumes that each member will be there only once
|
||||
fun getConnectedMemberNames(cItem: ChatItem): Pair<Int, List<String>> {
|
||||
var count = 0
|
||||
val ns = mutableListOf<String>()
|
||||
var idx = getChatItemIndexOrNull(cItem)
|
||||
if (cItem.mergeCategory != null && idx != null) {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
while (idx < reversedChatItems.size) {
|
||||
val ci = reversedChatItems[idx]
|
||||
if (ci.mergeCategory != cItem.mergeCategory) break
|
||||
val m = ci.memberConnected
|
||||
if (m != null) {
|
||||
ns.add(m.displayName)
|
||||
}
|
||||
count++
|
||||
idx++
|
||||
}
|
||||
}
|
||||
return count to ns
|
||||
}
|
||||
|
||||
// returns the index of the passed item and the next item (it has smaller index)
|
||||
fun getNextChatItem(ci: ChatItem): Pair<Int?, ChatItem?> {
|
||||
val i = getChatItemIndexOrNull(ci)
|
||||
return if (i != null) {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
i to if (i > 0) reversedChatItems[i - 1] else null
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
}
|
||||
|
||||
// returns the index of the first item in the same merged group (the first hidden item)
|
||||
// and the previous visible item with another merge category
|
||||
fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?): Pair<Int?, ChatItem?> {
|
||||
var i = ciIndex ?: return null to null
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
val fst = reversedChatItems.lastIndex
|
||||
while (i < fst) {
|
||||
i++
|
||||
val ci = reversedChatItems[i]
|
||||
if (ciCategory == null || ciCategory != ci.mergeCategory) {
|
||||
return i - 1 to ci
|
||||
}
|
||||
}
|
||||
return i to null
|
||||
}
|
||||
|
||||
// returns the previous member in the same merge group and the count of members in this group
|
||||
fun getPrevHiddenMember(member: GroupMember, range: IntRange): Pair<GroupMember?, Int> {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
var prevMember: GroupMember? = null
|
||||
val names: MutableSet<Long> = mutableSetOf()
|
||||
for (i in range) {
|
||||
val dir = reversedChatItems[i].chatDir
|
||||
if (dir is CIDirection.GroupRcv) {
|
||||
val m = dir.groupMember
|
||||
if (prevMember == null && m.groupMemberId != member.groupMemberId) {
|
||||
prevMember = m
|
||||
}
|
||||
names.add(m.groupMemberId)
|
||||
}
|
||||
}
|
||||
return prevMember to names.size
|
||||
}
|
||||
|
||||
// func popChat(_ id: String) {
|
||||
// if let i = getChatIndex(id) {
|
||||
// popChat_(i)
|
||||
@ -474,7 +547,7 @@ object ChatModel {
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembers.indexOfFirst { it.id == member.id }
|
||||
val memberIndex = groupMembers.indexOfFirst { it.groupMemberId == member.groupMemberId }
|
||||
if (memberIndex >= 0) {
|
||||
groupMembers[memberIndex] = member
|
||||
false
|
||||
@ -1090,11 +1163,11 @@ data class GroupMember (
|
||||
val groupMemberId: Long,
|
||||
val groupId: Long,
|
||||
val memberId: String,
|
||||
var memberRole: GroupMemberRole,
|
||||
var memberCategory: GroupMemberCategory,
|
||||
var memberStatus: GroupMemberStatus,
|
||||
var memberSettings: GroupMemberSettings,
|
||||
var invitedBy: InvitedBy,
|
||||
val memberRole: GroupMemberRole,
|
||||
val memberCategory: GroupMemberCategory,
|
||||
val memberStatus: GroupMemberStatus,
|
||||
val memberSettings: GroupMemberSettings,
|
||||
val invitedBy: InvitedBy,
|
||||
val localDisplayName: String,
|
||||
val memberProfile: LocalProfile,
|
||||
val memberContactId: Long? = null,
|
||||
@ -1467,7 +1540,7 @@ data class ChatItem (
|
||||
chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.chatViewName
|
||||
else null
|
||||
|
||||
val isDeletedContent: Boolean get() =
|
||||
@ -1491,6 +1564,29 @@ data class ChatItem (
|
||||
else -> null
|
||||
}
|
||||
|
||||
val mergeCategory: CIMergeCategory?
|
||||
get() = when (content) {
|
||||
is CIContent.RcvChatFeature,
|
||||
is CIContent.SndChatFeature,
|
||||
is CIContent.RcvGroupFeature,
|
||||
is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature
|
||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||
is RcvGroupEvent.UserRole, is RcvGroupEvent.UserDeleted, is RcvGroupEvent.GroupDeleted, is RcvGroupEvent.MemberCreatedContact -> null
|
||||
else -> CIMergeCategory.RcvGroupEvent
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) {
|
||||
is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft -> null
|
||||
else -> CIMergeCategory.SndGroupEvent
|
||||
}
|
||||
else -> {
|
||||
if (meta.itemDeleted == null) {
|
||||
null
|
||||
} else {
|
||||
if (chatDir.sent) CIMergeCategory.SndItemDeleted else CIMergeCategory.RcvItemDeleted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
|
||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
@ -1695,6 +1791,15 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
enum class CIMergeCategory {
|
||||
MemberConnected,
|
||||
RcvGroupEvent,
|
||||
SndGroupEvent,
|
||||
SndItemDeleted,
|
||||
RcvItemDeleted,
|
||||
ChatFeature,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIDirection {
|
||||
@Serializable @SerialName("directSnd") class DirectSnd: CIDirection()
|
||||
@ -1895,7 +2000,9 @@ sealed class CIContent: ItemContent {
|
||||
|
||||
@Serializable @SerialName("sndMsgContent") class SndMsgContent(override val msgContent: MsgContent): CIContent()
|
||||
@Serializable @SerialName("rcvMsgContent") class RcvMsgContent(override val msgContent: MsgContent): CIContent()
|
||||
// legacy - since v4.3.0 itemDeleted field is used
|
||||
@Serializable @SerialName("sndDeleted") class SndDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
// legacy - since v4.3.0 itemDeleted field is used
|
||||
@Serializable @SerialName("rcvDeleted") class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
@ -745,6 +745,9 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSetMemberSettings(groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings): Boolean =
|
||||
sendCommandOkResp(CC.ApiSetMemberSettings(groupId, groupMemberId, memberSettings))
|
||||
|
||||
suspend fun apiContactInfo(contactId: Long): Pair<ConnectionStats, Profile?>? {
|
||||
val r = sendCmd(CC.APIContactInfo(contactId))
|
||||
if (r is CR.ContactInfo) return r.connectionStats to r.customUserProfile
|
||||
@ -1926,6 +1929,7 @@ sealed class CC {
|
||||
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
||||
class APIGetNetworkConfig: CC()
|
||||
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
|
||||
class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC()
|
||||
class APIContactInfo(val contactId: Long): CC()
|
||||
class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISwitchContact(val contactId: Long): CC()
|
||||
@ -2036,6 +2040,7 @@ sealed class CC {
|
||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||
is APIGetNetworkConfig -> "/network"
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
|
||||
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
|
||||
is APIContactInfo -> "/_info @$contactId"
|
||||
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
|
||||
is APISwitchContact -> "/_switch @$contactId"
|
||||
@ -2139,9 +2144,10 @@ sealed class CC {
|
||||
is APITestProtoServer -> "testProtoServer"
|
||||
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
||||
is APIGetChatItemTTL -> "apiGetChatItemTTL"
|
||||
is APISetNetworkConfig -> "/apiSetNetworkConfig"
|
||||
is APIGetNetworkConfig -> "/apiGetNetworkConfig"
|
||||
is APISetChatSettings -> "/apiSetChatSettings"
|
||||
is APISetNetworkConfig -> "apiSetNetworkConfig"
|
||||
is APIGetNetworkConfig -> "apiGetNetworkConfig"
|
||||
is APISetChatSettings -> "apiSetChatSettings"
|
||||
is ApiSetMemberSettings -> "apiSetMemberSettings"
|
||||
is APIContactInfo -> "apiContactInfo"
|
||||
is APIGroupMemberInfo -> "apiGroupMemberInfo"
|
||||
is APISwitchContact -> "apiSwitchContact"
|
||||
|
@ -382,7 +382,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
|
||||
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> {
|
||||
return memberDeliveryStatuses.mapNotNull { mds ->
|
||||
chatModel.groupMembers.firstOrNull { it.groupMemberId == mds.groupMemberId }?.let { mem ->
|
||||
chatModel.getGroupMember(mds.groupMemberId)?.let { mem ->
|
||||
mem to mds.memberDeliveryStatus
|
||||
}
|
||||
}
|
||||
|
@ -152,6 +152,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
chatModel.chatId.value = null
|
||||
chatModel.groupMembers.clear()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
@ -212,7 +213,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
@ -263,6 +264,25 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatInfo, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId, encrypted ->
|
||||
withApi { chatModel.controller.receiveFile(user, fileId, encrypted) }
|
||||
},
|
||||
@ -442,6 +462,7 @@ fun ChatLayout(
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@ -517,7 +538,7 @@ fun ChatLayout(
|
||||
) {
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools,
|
||||
@ -744,6 +765,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@ -846,31 +868,27 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
val nextItem = if (i - 1 >= 0) reversedChatItems[i - 1] else null
|
||||
fun getConnectedMemberNames(): List<String> {
|
||||
val ns = mutableListOf<String>()
|
||||
var idx = i
|
||||
while (idx < reversedChatItems.size) {
|
||||
val m = reversedChatItems[idx].memberConnected
|
||||
if (m != null) {
|
||||
ns.add(m.displayName)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
idx++
|
||||
}
|
||||
return ns
|
||||
}
|
||||
if (cItem.memberConnected != null && nextItem?.memberConnected != null) {
|
||||
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
|
||||
Box(Modifier.size(0.dp)) {}
|
||||
} else {
|
||||
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
if (showMemberImage(member, prevItem)) {
|
||||
val (prevMember, memCount) =
|
||||
if (range != null) {
|
||||
chatModel.getPrevHiddenMember(member, range)
|
||||
} else {
|
||||
null to 1
|
||||
}
|
||||
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
@ -880,7 +898,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
) {
|
||||
if (cItem.content.showMemberName) {
|
||||
Text(
|
||||
member.displayName,
|
||||
memberNames(member, prevMember, memCount),
|
||||
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
@ -898,7 +916,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
) {
|
||||
MemberImage(member)
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -907,28 +925,45 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(swipeableModifier)
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
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 = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
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, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
|
||||
}
|
||||
|
||||
val (currIndex, nextItem) = chatModel.getNextChatItem(cItem)
|
||||
val ciCategory = cItem.mergeCategory
|
||||
if (ciCategory != null && ciCategory == nextItem?.mergeCategory) {
|
||||
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
|
||||
} else {
|
||||
val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (revealed.value && range != null) {
|
||||
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
||||
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1]
|
||||
ChatItemView(ci, null, prev)
|
||||
}
|
||||
} else {
|
||||
ChatItemView(cItem, range, prevItem)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1106,10 +1141,12 @@ fun PreloadItems(
|
||||
}
|
||||
}
|
||||
|
||||
fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean {
|
||||
return prevItem == null || prevItem.chatDir is CIDirection.GroupSnd ||
|
||||
(prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId)
|
||||
}
|
||||
private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean =
|
||||
when (val dir = prevItem?.chatDir) {
|
||||
is CIDirection.GroupSnd -> true
|
||||
is CIDirection.GroupRcv -> dir.groupMember.groupMemberId != member.groupMemberId
|
||||
else -> false
|
||||
}
|
||||
|
||||
val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
|
||||
@ -1206,6 +1243,29 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount: Int): String {
|
||||
val name = member.displayName
|
||||
val prevName = prevMember?.displayName
|
||||
return if (prevName != null) {
|
||||
if (memCount > 2) {
|
||||
stringResource(MR.strings.group_members_n).format(name, prevName, memCount - 2)
|
||||
} else {
|
||||
stringResource(MR.strings.group_members_2).format(name, prevName)
|
||||
}
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
|
||||
if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
|
||||
currIndex..prevHidden
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
sealed class ProviderMedia {
|
||||
data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia()
|
||||
data class Video(val uri: URI, val preview: String): ProviderMedia()
|
||||
@ -1347,6 +1407,7 @@ fun PreviewChatLayout() {
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
@ -1418,6 +1479,7 @@ fun PreviewGroupChatLayout() {
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = {},
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
|
@ -4,10 +4,12 @@ import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewLongClickable
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.*
|
||||
@ -31,6 +33,7 @@ import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
@ -82,7 +85,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
member to null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
@ -157,6 +160,23 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeMemberAlert(groupInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.button_remove_member_question),
|
||||
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val updatedMember = chatModel.controller.apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
|
||||
if (updatedMember != null) {
|
||||
chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
@ -238,8 +258,10 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
items(filteredMembers.value) { member ->
|
||||
Divider()
|
||||
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
|
||||
MemberRow(member)
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) {
|
||||
DropDownMenuForMember(member, groupInfo, showMenu)
|
||||
MemberRow(member, onClick = { showMemberInfo(member) })
|
||||
}
|
||||
}
|
||||
item {
|
||||
@ -344,7 +366,7 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -> Unit)? = null) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@ -390,6 +412,29 @@ private fun MemberVerifiedShield() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropDownMenuForMember(member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
||||
removeMemberAlert(groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (member.memberSettings.showMessages) {
|
||||
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
||||
blockMemberAlert(groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
|
||||
unblockMemberAlert(groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupLinkButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
|
@ -96,6 +96,8 @@ fun GroupMemberInfoView(
|
||||
connectViaAddress = { connReqUri ->
|
||||
connectViaMemberAddressAlert(connReqUri)
|
||||
},
|
||||
blockMember = { blockMemberAlert(groupInfo, member) },
|
||||
unblockMember = { unblockMemberAlert(groupInfo, member) },
|
||||
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
|
||||
onRoleSelected = {
|
||||
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||
@ -162,7 +164,7 @@ fun GroupMemberInfoView(
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
connectionCode,
|
||||
@ -224,6 +226,8 @@ fun GroupMemberInfoLayout(
|
||||
openDirectChat: (Long) -> Unit,
|
||||
createMemberContact: () -> Unit,
|
||||
connectViaAddress: (String) -> Unit,
|
||||
blockMember: () -> Unit,
|
||||
unblockMember: () -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
@ -338,9 +342,14 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
if (member.memberSettings.showMessages) {
|
||||
BlockMemberButton(blockMember)
|
||||
} else {
|
||||
UnblockMemberButton(unblockMember)
|
||||
}
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
RemoveMemberButton(removeMember)
|
||||
}
|
||||
}
|
||||
@ -396,6 +405,26 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BlockMemberButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_back_hand),
|
||||
stringResource(MR.strings.block_member_button),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnblockMemberButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_do_not_touch),
|
||||
stringResource(MR.strings.unblock_member_button),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@ -485,6 +514,43 @@ fun connectViaMemberAddressAlert(connReqUri: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun blockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.block_member_question),
|
||||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.block_member_confirmation),
|
||||
onConfirm = {
|
||||
toggleShowMemberMessages(gInfo, mem, false)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun unblockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unblock_member_question),
|
||||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.unblock_member_confirmation),
|
||||
onConfirm = {
|
||||
toggleShowMemberMessages(gInfo, mem, true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleShowMemberMessages(gInfo: GroupInfo, member: GroupMember, showMessages: Boolean) {
|
||||
val updatedMemberSettings = member.memberSettings.copy(showMessages = showMessages)
|
||||
updateMemberSettings(gInfo, member, updatedMemberSettings)
|
||||
}
|
||||
|
||||
fun updateMemberSettings(gInfo: GroupInfo, member: GroupMember, memberSettings: GroupMemberSettings) {
|
||||
withBGApi {
|
||||
val success = ChatController.apiSetMemberSettings(gInfo.groupId, member.groupMemberId, memberSettings)
|
||||
if (success) {
|
||||
ChatModel.upsertGroupMember(gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupMemberInfoLayout() {
|
||||
@ -500,6 +566,8 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
openDirectChat = {},
|
||||
createMemberContact = {},
|
||||
connectViaAddress = {},
|
||||
blockMember = {},
|
||||
unblockMember = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
|
@ -1,19 +1,119 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.Feature
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
|
||||
import chat.simplex.common.views.helpers.onRightClick
|
||||
|
||||
@Composable
|
||||
fun CIChatFeatureView(
|
||||
chatItem: ChatItem,
|
||||
feature: Feature,
|
||||
iconColor: Color,
|
||||
icon: Painter? = null,
|
||||
revealed: MutableState<Boolean>,
|
||||
showMenu: MutableState<Boolean>,
|
||||
) {
|
||||
val merged = if (!revealed.value) mergedFeatures(chatItem) else emptyList()
|
||||
Box(
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = {}
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
if (!revealed.value && merged != null) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
merged.forEach {
|
||||
FeatureIconView(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FullFeatureView(chatItem, feature, iconColor, icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FeatureInfo(
|
||||
val icon: PainterBox,
|
||||
val color: Color,
|
||||
val param: String?
|
||||
)
|
||||
|
||||
private class PainterBox(
|
||||
val featureName: String,
|
||||
val icon: Painter,
|
||||
) {
|
||||
override fun hashCode(): Int = featureName.hashCode()
|
||||
override fun equals(other: Any?): Boolean = other is PainterBox && featureName == other.featureName
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): FeatureInfo =
|
||||
FeatureInfo(
|
||||
icon = PainterBox(type, iconFilled()),
|
||||
color = color,
|
||||
param = if (this.hasParam && param != null) timeText(param) else null
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? {
|
||||
val m = ChatModel
|
||||
val fs: ArrayList<FeatureInfo> = arrayListOf()
|
||||
val icons: MutableSet<PainterBox> = mutableSetOf()
|
||||
var i = getChatItemIndexOrNull(chatItem)
|
||||
if (i != null) {
|
||||
val reversedChatItems = m.chatItems.asReversed()
|
||||
while (i < reversedChatItems.size) {
|
||||
val f = featureInfo(reversedChatItems[i]) ?: break
|
||||
if (!icons.contains(f.icon)) {
|
||||
fs.add(0, f)
|
||||
icons.add(f.icon)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return if (fs.size > 1) fs else null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun featureInfo(ci: ChatItem): FeatureInfo? =
|
||||
when (ci.content) {
|
||||
is CIContent.RcvChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
|
||||
is CIContent.SndChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
|
||||
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
|
||||
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureIconView(f: FeatureInfo) {
|
||||
val icon = @Composable { Icon(f.icon.icon, null, Modifier.size(20.dp), tint = f.color) }
|
||||
if (f.param != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
icon()
|
||||
Text(chatEventText(f.param, ""), maxLines = 1)
|
||||
}
|
||||
} else {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FullFeatureView(
|
||||
chatItem: ChatItem,
|
||||
feature: Feature,
|
||||
iconColor: Color,
|
||||
@ -24,7 +124,7 @@ fun CIChatFeatureView(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(icon ?: feature.iconFilled(), feature.text, Modifier.size(18.dp), tint = iconColor)
|
||||
Icon(icon ?: feature.iconFilled(), feature.text, Modifier.size(20.dp), tint = iconColor)
|
||||
Text(
|
||||
chatEventText(chatItem),
|
||||
Modifier,
|
||||
|
@ -1,11 +1,9 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -14,12 +12,7 @@ import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CIEventView(text: AnnotatedString) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
|
||||
}
|
||||
Text(text, Modifier.padding(horizontal = 6.dp, vertical = 6.dp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
|
||||
}
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
|
@ -21,9 +21,8 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.chat.ComposeContextItem
|
||||
import chat.simplex.common.views.chat.ComposeState
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@ -47,7 +46,10 @@ fun ChatItemView(
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
revealed: MutableState<Boolean>,
|
||||
range: IntRange?,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@ -63,14 +65,12 @@ fun ChatItemView(
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
getConnectedMemberNames: (() -> List<String>)? = null,
|
||||
developerTools: Boolean,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
val live = composeState.value.liveMessage != null
|
||||
@ -178,61 +178,75 @@ fun ChatItemView(
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||
MsgReactionsMenu()
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.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
|
||||
})
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
|
||||
val fileSource = getLoadedFileSource(cItem.file)
|
||||
when {
|
||||
fileSource != null -> shareFile(cItem.text, fileSource)
|
||||
else -> clipboard.shareText(cItem.content.text)
|
||||
if (cItem.content.msgContent != null) {
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||
MsgReactionsMenu()
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
|
||||
copyItemToClipboard(cItem, clipboard)
|
||||
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) && getLoadedFilePath(cItem.file) != null) {
|
||||
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.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
|
||||
})
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
|
||||
val fileSource = getLoadedFileSource(cItem.file)
|
||||
when {
|
||||
fileSource != null -> shareFile(cItem.text, fileSource)
|
||||
else -> clipboard.shareText(cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.hide_verb),
|
||||
painterResource(MR.images.ic_visibility_off),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
|
||||
copyItemToClipboard(cItem, clipboard)
|
||||
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) && getLoadedFilePath(cItem.file) != null) {
|
||||
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
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)
|
||||
})
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
if (revealed.value) {
|
||||
HideItemAction(revealed, showMenu)
|
||||
}
|
||||
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, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
|
||||
}
|
||||
} else if (cItem.meta.itemDeleted != null) {
|
||||
if (revealed.value) {
|
||||
HideItemAction(revealed, showMenu)
|
||||
} else if (!cItem.isDeletedContent) {
|
||||
RevealItemAction(revealed, showMenu)
|
||||
} else if (range != null) {
|
||||
ExpandItemAction(revealed, showMenu)
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
} else if (cItem.isDeletedContent) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
} else if (cItem.mergeCategory != null) {
|
||||
if (revealed.value) {
|
||||
ShrinkItemAction(revealed, showMenu)
|
||||
} else {
|
||||
ExpandItemAction(revealed, showMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,25 +255,18 @@ fun ChatItemView(
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.reveal_verb),
|
||||
painterResource(MR.images.ic_visibility),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
RevealItemAction(revealed, showMenu)
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
@ -281,7 +288,7 @@ fun ChatItemView(
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,9 +296,32 @@ fun ChatItemView(
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
fun mergedGroupEventText(chatItem: ChatItem): String? {
|
||||
val (count, ns) = chatModel.getConnectedMemberNames(chatItem)
|
||||
val members = when {
|
||||
ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0])
|
||||
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
|
||||
ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2])
|
||||
ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2)
|
||||
else -> ""
|
||||
}
|
||||
return if (count <= 1) {
|
||||
null
|
||||
} else if (ns.isEmpty()) {
|
||||
generalGetString(MR.strings.rcv_group_events_count).format(count)
|
||||
} else if (count > ns.size) {
|
||||
members + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size)
|
||||
} else {
|
||||
members
|
||||
}
|
||||
}
|
||||
|
||||
fun eventItemViewText(): AnnotatedString {
|
||||
val memberDisplayName = cItem.memberDisplayName
|
||||
return if (memberDisplayName != null) {
|
||||
val t = mergedGroupEventText(cItem)
|
||||
return if (!revealed.value && t != null) {
|
||||
chatEventText(t, cItem.timestampText)
|
||||
} else if (memberDisplayName != null) {
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
@ -305,35 +335,12 @@ fun ChatItemView(
|
||||
CIEventView(eventItemViewText())
|
||||
}
|
||||
|
||||
fun membersConnectedText(): String? {
|
||||
return if (getConnectedMemberNames != null) {
|
||||
val ns = getConnectedMemberNames()
|
||||
when {
|
||||
ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2)
|
||||
ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2])
|
||||
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun membersConnectedItemText(): AnnotatedString {
|
||||
val t = membersConnectedText()
|
||||
return if (t != null) {
|
||||
chatEventText(t, cItem.timestampText)
|
||||
} else {
|
||||
eventItemViewText()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModeratedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,26 +359,61 @@ fun ChatItemView(
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
|
||||
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.RcvDirectEventContent -> EventItemView()
|
||||
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
|
||||
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
||||
else -> EventItemView()
|
||||
is CIContent.RcvDirectEventContent -> {
|
||||
EventItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvGroupEventContent -> {
|
||||
when (c.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
||||
else -> EventItemView()
|
||||
}
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> {
|
||||
EventItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvConnEventContent -> {
|
||||
EventItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndConnEventContent -> {
|
||||
EventItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvChatFeature -> {
|
||||
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndChatFeature -> {
|
||||
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> EventItemView()
|
||||
is CIContent.RcvConnEventContent -> EventItemView()
|
||||
is CIContent.SndConnEventContent -> EventItemView()
|
||||
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.SndChatPreference -> {
|
||||
CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvGroupFeature -> {
|
||||
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndGroupFeature -> {
|
||||
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvChatFeatureRejected -> {
|
||||
CIChatFeatureView(cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvGroupFeatureRejected -> {
|
||||
CIChatFeatureView(cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndModerated -> ModeratedItem()
|
||||
is CIContent.RcvModerated -> ModeratedItem()
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
@ -430,16 +472,38 @@ fun ItemInfoAction(
|
||||
@Composable
|
||||
fun DeleteItemAction(
|
||||
cItem: ChatItem,
|
||||
revealed: MutableState<Boolean>,
|
||||
showMenu: MutableState<Boolean>,
|
||||
questionText: String,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_verb),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
if (!revealed.value && cItem.meta.itemDeleted != null) {
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(cItem)
|
||||
val ciCategory = cItem.mergeCategory
|
||||
if (currIndex != null && ciCategory != null) {
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (range != null) {
|
||||
val itemIds: ArrayList<Long> = arrayListOf()
|
||||
for (i in range) {
|
||||
itemIds.add(chatModel.chatItems.asReversed()[i].id)
|
||||
}
|
||||
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
||||
} else {
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
}
|
||||
} else {
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
}
|
||||
} else {
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
}
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
@ -463,6 +527,54 @@ fun ModerateItemAction(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevealItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.reveal_verb),
|
||||
painterResource(MR.images.ic_visibility),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HideItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.hide_verb),
|
||||
painterResource(MR.images.ic_visibility_off),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.expand_verb),
|
||||
painterResource(MR.images.ic_expand_all),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.hide_verb),
|
||||
painterResource(MR.images.ic_collapse_all),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: Painter, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
@ -542,6 +654,26 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size),
|
||||
text = questionText,
|
||||
buttons = {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
deleteMessages(itemIds)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_member_message__question),
|
||||
@ -575,7 +707,10 @@ fun PreviewChatItemView() {
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
@ -606,7 +741,10 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
|
@ -202,10 +202,16 @@ fun FramedItemView(
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted != null) {
|
||||
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
|
||||
FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag))
|
||||
} else {
|
||||
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete))
|
||||
when (ci.meta.itemDeleted) {
|
||||
is CIDeleted.Moderated -> {
|
||||
FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag))
|
||||
}
|
||||
is CIDeleted.Blocked -> {
|
||||
FramedItemHeader(stringResource(MR.strings.blocked_item_description), true, painterResource(MR.images.ic_back_hand))
|
||||
}
|
||||
else -> {
|
||||
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete))
|
||||
}
|
||||
}
|
||||
} else if (ci.meta.isLive) {
|
||||
FramedItemHeader(stringResource(MR.strings.live), false)
|
||||
|
@ -2,25 +2,25 @@ package chat.simplex.common.views.chat.item
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.CIDeleted
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
@ -32,11 +32,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
|
||||
MarkedDeletedText(String.format(generalGetString(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
|
||||
} else {
|
||||
MarkedDeletedText(generalGetString(MR.strings.marked_deleted_description))
|
||||
}
|
||||
MergedMarkedDeletedText(ci, revealed)
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
@ -44,7 +40,41 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkedDeletedText(text: String) {
|
||||
private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<Boolean>) {
|
||||
var i = getChatItemIndexOrNull(chatItem)
|
||||
val ciCategory = chatItem.mergeCategory
|
||||
val text = if (!revealed.value && ciCategory != null && i != null) {
|
||||
val reversedChatItems = ChatModel.chatItems.asReversed()
|
||||
var moderated = 0
|
||||
var blocked = 0
|
||||
var deleted = 0
|
||||
val moderatedBy: MutableSet<String> = mutableSetOf()
|
||||
while (i < reversedChatItems.size) {
|
||||
val ci = reversedChatItems.getOrNull(i)
|
||||
if (ci?.mergeCategory != ciCategory) break
|
||||
when (val itemDeleted = ci.meta.itemDeleted ?: break) {
|
||||
is CIDeleted.Moderated -> {
|
||||
moderated += 1
|
||||
moderatedBy.add(itemDeleted.byGroupMember.displayName)
|
||||
}
|
||||
is CIDeleted.Blocked -> blocked += 1
|
||||
is CIDeleted.Deleted -> deleted += 1
|
||||
}
|
||||
i++
|
||||
}
|
||||
val total = moderated + blocked + deleted
|
||||
if (total <= 1)
|
||||
markedDeletedText(chatItem.meta)
|
||||
else if (total == moderated)
|
||||
stringResource(MR.strings.moderated_items_description).format(total, moderatedBy.joinToString(", "))
|
||||
else if (total == blocked)
|
||||
stringResource(MR.strings.blocked_items_description).format(total)
|
||||
else
|
||||
stringResource(MR.strings.marked_deleted_items_description).format(total)
|
||||
} else {
|
||||
markedDeletedText(chatItem.meta)
|
||||
}
|
||||
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) }
|
||||
@ -56,6 +86,16 @@ private fun MarkedDeletedText(text: String) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun markedDeletedText(meta: CIMeta): String =
|
||||
when (meta.itemDeleted) {
|
||||
is CIDeleted.Moderated ->
|
||||
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
|
||||
is CIDeleted.Blocked ->
|
||||
generalGetString(MR.strings.blocked_item_description)
|
||||
else ->
|
||||
generalGetString(MR.strings.marked_deleted_description)
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
|
@ -98,6 +98,34 @@ fun SectionItemView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionItemViewLongClickable(
|
||||
click: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
minHeight: Dp = 46.dp,
|
||||
disabled: Boolean = false,
|
||||
extraPadding: Boolean = false,
|
||||
padding: PaddingValues = if (extraPadding)
|
||||
PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING)
|
||||
else
|
||||
PaddingValues(horizontal = DEFAULT_PADDING),
|
||||
content: (@Composable RowScope.() -> Unit)
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = minHeight)
|
||||
Row(
|
||||
if (disabled) {
|
||||
modifier.padding(padding)
|
||||
} else {
|
||||
modifier.combinedClickable(onClick = click, onLongClick = longClick).onRightClick(longClick).padding(padding)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionItemViewWithIcon(
|
||||
click: (() -> Unit)? = null,
|
||||
|
@ -30,7 +30,11 @@
|
||||
<!-- Item Content - ChatModel.kt -->
|
||||
<string name="deleted_description">deleted</string>
|
||||
<string name="marked_deleted_description">marked deleted</string>
|
||||
<string name="marked_deleted_items_description">%d messages marked deleted</string>
|
||||
<string name="moderated_item_description">moderated by %s</string>
|
||||
<string name="moderated_items_description">%d messages moderated by %s</string>
|
||||
<string name="blocked_item_description">blocked</string>
|
||||
<string name="blocked_items_description">%d messages blocked</string>
|
||||
<string name="sending_files_not_yet_supported">sending files is not supported yet</string>
|
||||
<string name="receiving_files_not_yet_supported">receiving files is not supported yet</string>
|
||||
<string name="sender_you_pronoun">you</string>
|
||||
@ -243,7 +247,9 @@
|
||||
<string name="hide_verb">Hide</string>
|
||||
<string name="allow_verb">Allow</string>
|
||||
<string name="moderate_verb">Moderate</string>
|
||||
<string name="expand_verb">Expand</string>
|
||||
<string name="delete_message__question">Delete message?</string>
|
||||
<string name="delete_messages__question">Delete %d messages?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
|
||||
<string name="delete_member_message__question">Delete member message?</string>
|
||||
@ -1132,9 +1138,14 @@
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
|
||||
<string name="rcv_group_event_1_member_connected">%s connected</string>
|
||||
<string name="rcv_group_event_2_members_connected">%s and %s connected</string>
|
||||
<string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string>
|
||||
<string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string>
|
||||
<string name="rcv_group_events_count">%d group events</string>
|
||||
<string name="rcv_group_and_other_events">and %d other events</string>
|
||||
<string name="group_members_2">%s and %s</string>
|
||||
<string name="group_members_n">%s, %s and %d members</string>
|
||||
|
||||
<string name="rcv_group_event_open_chat">Open</string>
|
||||
|
||||
@ -1250,10 +1261,21 @@
|
||||
<string name="recipient_colon_delivery_status">%s: %s</string>
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member_question">Remove member?</string>
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
|
||||
<string name="button_send_direct_message">Send direct message</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
|
||||
<string name="remove_member_confirmation">Remove</string>
|
||||
<string name="remove_member_button">Remove member</string>
|
||||
<string name="block_member_question">Block member?</string>
|
||||
<string name="block_member_button">Block member</string>
|
||||
<string name="block_member_confirmation">Block</string>
|
||||
<string name="block_member_desc">All new messages from %s will be hidden!</string>
|
||||
<string name="unblock_member_question">Unblock member?</string>
|
||||
<string name="unblock_member_button">Unblock member</string>
|
||||
<string name="unblock_member_confirmation">Unblock</string>
|
||||
<string name="unblock_member_desc">Messages from %s will be shown!</string>
|
||||
<string name="member_info_section_title_member">MEMBER</string>
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Change role</string>
|
||||
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M504.5-45q-92 0-169.75-46T210-216.5l-147.5-247 19-19.5q15-15 36-16.5T156-489l129 93v-410.5q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v522l-185-134L258-250q37.5 68.5 103.318 108 65.817 39.5 143.182 39.5 112.792 0 192.896-78.104Q777.5-258.708 777.5-371v-395.688q0-10.812 8.154-19.562 8.153-8.75 20.75-8.75 11.096 0 19.846 8.787Q835-777.425 835-766.5V-371q0 136-96.832 231Q641.335-45 504.5-45Zm-55-446.5v-395q0-10.925 8.654-19.713 8.653-8.787 20.25-8.787 12.096 0 20.346 8.787Q507-897.425 507-886.5v395h-57.5Zm164.5 0v-355q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v355H614ZM468-297Z"/></svg>
|
After Width: | Height: | Size: 782 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m291.5-89.5-40.5-40 229-229 229 229-40.5 40L480-278 291.5-89.5Zm188.5-513-229-229 40.5-40.5L480-683.5 668.5-872l40.5 40.5-229 229Z"/></svg>
|
After Width: | Height: | Size: 236 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M835-207.5 777.5-265v-501.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q835-778.175 835-766.5v559ZM342.5-699 285-756.5v-50q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088V-699ZM507-535.5 449.5-592v-294.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q507-898.175 507-886.5v351ZM671.5-483H614v-363.668q0-11.582 8.425-19.957 8.426-8.375 20.5-8.375 12.075 0 20.325 8.351t8.25 19.935V-483ZM751-126.5 342.5-535v250.5L165-415l199 291q7 11 17.839 16.75 10.84 5.75 24.161 5.75h280.5q17.897 0 34.948-6.25Q738.5-114 751-126.5ZM406-44q-27.049 0-51.274-12.5Q330.5-69 316-91.5L59.5-468l19-15.5q17-14.5 39-18.25t43.573 12.938L285-394.5v-198l-253-253L73.5-887 873-87l-41 41-41-40.5q-20 19.5-47.17 31T686.5-44H406Zm140.5-287.5ZM560-483Z"/></svg>
|
After Width: | Height: | Size: 911 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m480-84.5-237-237 42.5-42L480-169l195-194.5 42 42-237 237Zm-195-512-42-42 237-237 237 237-42 42L480-791 285-596.5Z"/></svg>
|
After Width: | Height: | Size: 220 B |
Loading…
Reference in New Issue
Block a user