mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 16:55:27 +00:00
android, desktop: reports dashboard (#5471)
* android, desktop: reports dashboard * changes * changes * unneeded updates and fixes * changes * api change * item moderated/deleted * a lot of changes * changes * reports tag and icon in ChatList * archived by * increasing counter when new report arrives * refactor * groupInfo button and closing when needed * fix archived by * reorder * simplify * rename * filled flag * Revert "filled flag" This reverts commit8b5da85101. * removed support of archived page and counter * fix closing modal * show search button in bar without menu * removed content filter * no icon * Revert "no icon" This reverts commit86c725b53e. * fix tags * unlogs * unlogs * chat item statuses * background color * refactor * refactor --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
committed by
GitHub
parent
c8c6a832dd
commit
94815bf644
+2
@@ -29,6 +29,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
additionalTopBar: State<Boolean>,
|
||||
chatBottomBar: State<Boolean>,
|
||||
fillMaxSize: Boolean,
|
||||
content: LazyListScope.() -> Unit
|
||||
@@ -92,6 +93,7 @@ actual fun LazyColumnWithScrollBarNoAppBar(
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
additionalTopBar: State<Boolean>,
|
||||
chatBottomBar: State<Boolean>,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
|
||||
+3
@@ -81,6 +81,9 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
|
||||
chatModel.chatId.value = null
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
withChats {
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ChatList, nothing to do. Maybe to show other view except ChatList
|
||||
|
||||
@@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
.graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
|
||||
) Box2@{
|
||||
currentChatId.value?.let {
|
||||
ChatView(currentChatId, onComposed)
|
||||
ChatView(currentChatId, reportsView = false, onComposed = onComposed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,7 +393,7 @@ fun CenterPartOfScreen() {
|
||||
ModalManager.center.showInView()
|
||||
}
|
||||
}
|
||||
else -> ChatView(currentChatId) {}
|
||||
else -> ChatView(currentChatId, reportsView = false) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+253
-149
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.common.model.ChatModel.chatItemsChangesListener
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
@@ -59,28 +58,18 @@ object ChatModel {
|
||||
val ctrlInitInProgress = mutableStateOf(false)
|
||||
val dbMigrationInProgress = mutableStateOf(false)
|
||||
val incompleteInitializedDbRemoved = mutableStateOf(false)
|
||||
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
val chats: State<List<Chat>> = _chats
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
val switchingUsersAndHosts = mutableStateOf(false)
|
||||
|
||||
// current chat
|
||||
val chatId = mutableStateOf<String?>(null)
|
||||
/** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on.
|
||||
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
|
||||
* If you use api call to get the items, use just [add] instead of [addAndNotify].
|
||||
* Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */
|
||||
private val _chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
val chatItems: State<SnapshotStateList<ChatItem>> = _chatItems
|
||||
// declaration of chatsContext should be after any other variable that is directly attached to ChatsContext class, otherwise, strange crash with NullPointerException for "this" parameter in random functions
|
||||
private val chatsContext = ChatsContext()
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
var chatItemsChangesListener: ChatItemsChangesListener? = null
|
||||
val chatState = ActiveChatState()
|
||||
val chatsContext = ChatsContext(null)
|
||||
val reportsChatsContext = ChatsContext(MsgContentTag.Report)
|
||||
// declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions
|
||||
val chats: State<List<Chat>> = chatsContext.chats
|
||||
// rhId, chatId
|
||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||
val groupMembers = mutableStateOf<List<GroupMember>>(emptyList())
|
||||
val groupMembersIndexes = mutableStateOf<Map<Long, Int>>(emptyMap())
|
||||
|
||||
@@ -178,6 +167,36 @@ object ChatModel {
|
||||
// return true if you handled the click
|
||||
var centerPanelBackgroundClickHandler: (() -> Boolean)? = null
|
||||
|
||||
fun chatsForContent(contentTag: MsgContentTag?): State<SnapshotStateList<Chat>> = when(contentTag) {
|
||||
null -> chatsContext.chats
|
||||
MsgContentTag.Report -> reportsChatsContext.chats
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun chatItemsForContent(contentTag: MsgContentTag?): State<SnapshotStateList<ChatItem>> = when(contentTag) {
|
||||
null -> chatsContext.chatItems
|
||||
MsgContentTag.Report -> reportsChatsContext.chatItems
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun chatStateForContent(contentTag: MsgContentTag?): ActiveChatState = when(contentTag) {
|
||||
null -> chatsContext.chatState
|
||||
MsgContentTag.Report -> reportsChatsContext.chatState
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun chatItemsChangesListenerForContent(contentTag: MsgContentTag?): ChatItemsChangesListener? = when(contentTag) {
|
||||
null -> chatsContext.chatItemsChangesListener
|
||||
MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun setChatItemsChangeListenerForContent(listener: ChatItemsChangesListener?, contentTag: MsgContentTag?) = when(contentTag) {
|
||||
null -> chatsContext.chatItemsChangesListener = listener
|
||||
MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener = listener
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||
currentUser.value
|
||||
} else {
|
||||
@@ -210,7 +229,7 @@ object ChatModel {
|
||||
|
||||
for (chat in chats.value.filter { it.remoteHostId == rhId }) {
|
||||
for (tag in PresetTagKind.entries) {
|
||||
if (presetTagMatchesChat(tag, chat.chatInfo)) {
|
||||
if (presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats)) {
|
||||
newPresetTags[tag] = (newPresetTags[tag] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
@@ -250,17 +269,17 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresetChatTags(chatInfo: ChatInfo) {
|
||||
private fun addPresetChatTags(chatInfo: ChatInfo, chatStats: Chat.ChatStats) {
|
||||
for (tag in PresetTagKind.entries) {
|
||||
if (presetTagMatchesChat(tag, chatInfo)) {
|
||||
if (presetTagMatchesChat(tag, chatInfo, chatStats)) {
|
||||
presetTags[tag] = (presetTags[tag] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removePresetChatTags(chatInfo: ChatInfo) {
|
||||
fun removePresetChatTags(chatInfo: ChatInfo, chatStats: Chat.ChatStats) {
|
||||
for (tag in PresetTagKind.entries) {
|
||||
if (presetTagMatchesChat(tag, chatInfo)) {
|
||||
if (presetTagMatchesChat(tag, chatInfo, chatStats)) {
|
||||
val count = presetTags[tag]
|
||||
if (count != null) {
|
||||
presetTags[tag] = maxOf(0, count - 1)
|
||||
@@ -269,27 +288,6 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatTagRead(chat: Chat) {
|
||||
if (chat.unreadTag) {
|
||||
chat.chatInfo.chatTags?.let { tags ->
|
||||
markChatTagRead_(chat, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatTagRead(chat: Chat, wasUnread: Boolean) {
|
||||
val tags = chat.chatInfo.chatTags ?: return
|
||||
val nowUnread = chat.unreadTag
|
||||
|
||||
if (nowUnread && !wasUnread) {
|
||||
tags.forEach { tag ->
|
||||
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
|
||||
}
|
||||
} else if (!nowUnread && wasUnread) {
|
||||
markChatTagRead_(chat, tags)
|
||||
}
|
||||
}
|
||||
|
||||
fun moveChatTagUnread(chat: Chat, oldTags: List<Long>?, newTags: List<Long>) {
|
||||
if (chat.unreadTag) {
|
||||
oldTags?.forEach { t ->
|
||||
@@ -304,18 +302,6 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markChatTagRead_(chat: Chat, tags: List<Long>) {
|
||||
for (tag in tags) {
|
||||
val count = unreadTags[tag]
|
||||
if (count != null) {
|
||||
unreadTags[tag] = maxOf(0, count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
// TODO pass rhId?
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
@@ -340,13 +326,35 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread
|
||||
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
|
||||
chatsContext.action()
|
||||
suspend fun <T> withChats(contentTag: MsgContentTag? = null, action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
|
||||
when {
|
||||
contentTag == null -> chatsContext.action()
|
||||
contentTag == MsgContentTag.Report -> reportsChatsContext.action()
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
class ChatsContext {
|
||||
val chats = _chats
|
||||
val chatItems = _chatItems
|
||||
suspend fun <T> withReportsChatsIfOpen(action: suspend ChatsContext.() -> T) = withContext(Dispatchers.Main) {
|
||||
if (ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) {
|
||||
reportsChatsContext.action()
|
||||
}
|
||||
}
|
||||
|
||||
class ChatsContext(private val contentTag: MsgContentTag?) {
|
||||
val chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
/** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on.
|
||||
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
|
||||
* If you use api call to get the items, use just [add] instead of [addAndNotify].
|
||||
* Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */
|
||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
var chatItemsChangesListener: ChatItemsChangesListener? = null
|
||||
val chatState = ActiveChatState()
|
||||
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
suspend fun addChat(chat: Chat) {
|
||||
chats.add(index = 0, chat)
|
||||
@@ -385,6 +393,13 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatStats(rhId: Long?, chatId: ChatId, chatStats: Chat.ChatStats) {
|
||||
val i = getChatIndex(rhId, chatId)
|
||||
if (i >= 0) {
|
||||
chats[i] = chats[i].copy(chatStats = chatStats)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
suspend fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
@@ -402,7 +417,7 @@ object ChatModel {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
addPresetChatTags(cInfo)
|
||||
addPresetChatTags(cInfo, Chat.ChatStats())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +478,7 @@ object ChatModel {
|
||||
else
|
||||
chat.chatStats
|
||||
)
|
||||
updateChatTagRead(chats[i], wasUnread)
|
||||
updateChatTagReadNoContentTag(chats[i], wasUnread)
|
||||
|
||||
if (appPlatform.isDesktop && cItem.chatDir.sent) {
|
||||
reorderChat(chats[i], 0)
|
||||
@@ -479,9 +494,9 @@ object ChatModel {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||
chatItems.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem, contentTag)
|
||||
} else {
|
||||
chatItems.addAndNotify(cItem)
|
||||
chatItems.addAndNotify(cItem, contentTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,7 +515,7 @@ object ChatModel {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
res = false
|
||||
@@ -526,7 +541,7 @@ object ChatModel {
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
chatItems.addAndNotify(ci)
|
||||
chatItems.addAndNotify(ci, contentTag)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@@ -551,7 +566,7 @@ object ChatModel {
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
@@ -590,9 +605,9 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
val popChatCollector = PopChatCollector()
|
||||
val popChatCollector = PopChatCollector(contentTag)
|
||||
|
||||
class PopChatCollector {
|
||||
class PopChatCollector(contentTag: MsgContentTag?) {
|
||||
private val subject = MutableSharedFlow<Unit>()
|
||||
private var remoteHostId: Long? = null
|
||||
private val chatsToPop = mutableMapOf<ChatId, Instant>()
|
||||
@@ -602,7 +617,7 @@ object ChatModel {
|
||||
subject
|
||||
.throttleLatest(2000)
|
||||
.collect {
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chats.replaceAll(popCollectedChats())
|
||||
}
|
||||
}
|
||||
@@ -640,11 +655,10 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, itemIds: List<Long>? = null) {
|
||||
val cInfo = chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chatInfo, itemIds)
|
||||
fun markChatItemsRead(remoteHostId: Long?, id: ChatId, itemIds: List<Long>? = null) {
|
||||
val markedRead = markItemsReadInCurrentChat(id, itemIds)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(remoteHostId, cInfo.id)
|
||||
val chatIdx = getChatIndex(remoteHostId, id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
@@ -655,12 +669,47 @@ object ChatModel {
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(unreadCount = unreadCount)
|
||||
)
|
||||
updateChatTagRead(chats[chatIdx], wasUnread)
|
||||
updateChatTagReadNoContentTag(chats[chatIdx], wasUnread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
private fun markItemsReadInCurrentChat(id: ChatId, itemIds: List<Long>? = null): Int {
|
||||
var markedRead = 0
|
||||
if (chatId.value == id) {
|
||||
val items = chatItems.value
|
||||
var i = items.lastIndex
|
||||
val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf()
|
||||
val markedReadIds = mutableSetOf<Long>()
|
||||
while (i >= 0) {
|
||||
val item = items[i]
|
||||
if (item.meta.itemStatus is CIStatus.RcvNew && (itemIds == null || itemIdsFromRange.contains(item.id))) {
|
||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||
items[i] = newItem
|
||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||
)
|
||||
}
|
||||
markedReadIds.add(item.id)
|
||||
markedRead++
|
||||
if (itemIds != null) {
|
||||
itemIdsFromRange.remove(item.id)
|
||||
// already set all needed items as read, can finish the loop
|
||||
if (itemIdsFromRange.isEmpty()) break
|
||||
}
|
||||
}
|
||||
i--
|
||||
}
|
||||
chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
|
||||
}
|
||||
return markedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (contentTag != null) return
|
||||
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
@@ -673,21 +722,21 @@ object ChatModel {
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
updateChatTagRead(chats[chatIndex], wasUnread)
|
||||
updateChatTagReadNoContentTag(chats[chatIndex], wasUnread)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
var removed: ChatInfo? = null
|
||||
var removed: Chat? = null
|
||||
chats.removeAll {
|
||||
val found = it.id == id && it.remoteHostId == rhId
|
||||
if (found) {
|
||||
removed = it.chatInfo
|
||||
removed = it
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
removed?.let {
|
||||
removePresetChatTags(it)
|
||||
removePresetChatTags(it.chatInfo, it.chatStats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,9 +790,92 @@ object ChatModel {
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounterNoContentTag(rhId, user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounterNoContentTag(rhId, user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (contentTag != null) return
|
||||
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
users[i] = users[i].copy(unreadCount = users[i].unreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (contentTag != null) return
|
||||
|
||||
val tags = chat.chatInfo.chatTags ?: return
|
||||
val nowUnread = chat.unreadTag
|
||||
|
||||
if (nowUnread && !wasUnread) {
|
||||
tags.forEach { tag ->
|
||||
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
|
||||
}
|
||||
} else if (!nowUnread && wasUnread) {
|
||||
markChatTagReadNoContentTag_(chat, tags)
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatTagRead(chat: Chat) {
|
||||
if (chat.unreadTag) {
|
||||
chat.chatInfo.chatTags?.let { tags ->
|
||||
markChatTagReadNoContentTag_(chat, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markChatTagReadNoContentTag_(chat: Chat, tags: List<Long>) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (contentTag != null) return
|
||||
|
||||
for (tag in tags) {
|
||||
val count = unreadTags[tag]
|
||||
if (count != null) {
|
||||
unreadTags[tag] = maxOf(0, count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun increaseGroupReportsCounter(rhId: Long?, chatId: ChatId) {
|
||||
changeGroupReportsCounter(rhId, chatId, 1)
|
||||
}
|
||||
|
||||
fun decreaseGroupReportsCounter(rhId: Long?, chatId: ChatId) {
|
||||
changeGroupReportsCounter(rhId, chatId, -1)
|
||||
}
|
||||
|
||||
private fun changeGroupReportsCounter(rhId: Long?, chatId: ChatId, by: Int = 0) {
|
||||
if (by == 0) return
|
||||
|
||||
val i = getChatIndex(rhId, chatId)
|
||||
if (i >= 0) {
|
||||
val chat = chats.value[i]
|
||||
chats[i] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
reportsCount = (chat.chatStats.reportsCount + by).coerceAtLeast(0),
|
||||
)
|
||||
)
|
||||
val wasReportsCount = chat.chatStats.reportsCount
|
||||
val nowReportsCount = chats[i].chatStats.reportsCount
|
||||
val by = if (wasReportsCount == 0 && nowReportsCount > 0) 1 else if (wasReportsCount > 0 && nowReportsCount == 0) -1 else 0
|
||||
changeGroupReportsTagNoContentTag(by)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeGroupReportsTagNoContentTag(by: Int = 0) {
|
||||
if (by == 0 || contentTag != null) return
|
||||
presetTags[PresetTagKind.GROUP_REPORTS] = (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
@@ -773,82 +905,32 @@ object ChatModel {
|
||||
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||
withChats {
|
||||
chatItems.addAndNotify(cItem)
|
||||
chatItems.addAndNotify(cItem, contentTag = null)
|
||||
}
|
||||
return cItem
|
||||
}
|
||||
|
||||
fun removeLiveDummy() {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
if (chatItemsForContent(null).value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
withApi {
|
||||
withChats {
|
||||
chatItems.removeLastAndNotify()
|
||||
chatItems.removeLastAndNotify(contentTag = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, itemIds: List<Long>? = null): Int {
|
||||
val cInfo = chatInfo
|
||||
var markedRead = 0
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
var i = items.lastIndex
|
||||
val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf()
|
||||
val markedReadIds = mutableSetOf<Long>()
|
||||
while (i >= 0) {
|
||||
val item = items[i]
|
||||
if (item.meta.itemStatus is CIStatus.RcvNew && (itemIds == null || itemIdsFromRange.contains(item.id))) {
|
||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||
items[i] = newItem
|
||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||
)
|
||||
}
|
||||
markedReadIds.add(item.id)
|
||||
markedRead++
|
||||
if (itemIds != null) {
|
||||
itemIdsFromRange.remove(item.id)
|
||||
// already set all needed items as read, can finish the loop
|
||||
if (itemIdsFromRange.isEmpty()) break
|
||||
}
|
||||
}
|
||||
i--
|
||||
}
|
||||
chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
|
||||
}
|
||||
return markedRead
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounter(rhId, user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounter(rhId, user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) {
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
users[i] = users[i].copy(unreadCount = users[i].unreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
fun getChatItemIndexOrNull(cItem: ChatItem): Int? {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
fun getChatItemIndexOrNull(cItem: ChatItem, reversedChatItems: List<ChatItem>): Int? {
|
||||
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>> {
|
||||
fun getConnectedMemberNames(cItem: ChatItem, reversedChatItems: List<ChatItem>): Pair<Int, List<String>> {
|
||||
var count = 0
|
||||
val ns = mutableListOf<String>()
|
||||
var idx = getChatItemIndexOrNull(cItem)
|
||||
var idx = getChatItemIndexOrNull(cItem, reversedChatItems)
|
||||
if (cItem.mergeCategory != null && idx != null) {
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
while (idx < reversedChatItems.size) {
|
||||
val ci = reversedChatItems[idx]
|
||||
if (ci.mergeCategory != cItem.mergeCategory) break
|
||||
@@ -865,9 +947,8 @@ object ChatModel {
|
||||
|
||||
// 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?> {
|
||||
fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?, reversedChatItems: List<ChatItem>): Pair<Int?, ChatItem?> {
|
||||
var i = ciIndex ?: return null to null
|
||||
val reversedChatItems = chatItems.asReversed()
|
||||
val fst = reversedChatItems.lastIndex
|
||||
while (i < fst) {
|
||||
i++
|
||||
@@ -880,8 +961,7 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// 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()
|
||||
fun getPrevHiddenMember(member: GroupMember, range: IntRange, reversedChatItems: List<ChatItem>): Pair<GroupMember?, Int> {
|
||||
var prevMember: GroupMember? = null
|
||||
val names: MutableSet<Long> = mutableSetOf()
|
||||
for (i in range) {
|
||||
@@ -1157,7 +1237,13 @@ data class Chat(
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
|
||||
data class ChatStats(
|
||||
val unreadCount: Int = 0,
|
||||
// actual only via getChats() and getChat(.initial), otherwise, zero
|
||||
val reportsCount: Int = 0,
|
||||
val minUnreadItemId: Long = 0,
|
||||
val unreadChat: Boolean = false
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sampleData = Chat(
|
||||
@@ -1677,6 +1763,9 @@ data class GroupInfo (
|
||||
val canAddMembers: Boolean
|
||||
get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive
|
||||
|
||||
val canModerate: Boolean
|
||||
get() = membership.memberRole >= GroupMemberRole.Moderator && membership.memberActive
|
||||
|
||||
companion object {
|
||||
val sampleData = GroupInfo(
|
||||
groupId = 1,
|
||||
@@ -2310,6 +2399,8 @@ data class ChatItem (
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isActiveReport: Boolean get() = isReport && !isDeletedContent && meta.itemDeleted == null
|
||||
|
||||
val canBeDeletedForSelf: Boolean
|
||||
get() = (content.msgContent != null && !meta.isLive) || meta.itemDeleted != null || isDeletedContent || mergeCategory != null || showLocalDelete
|
||||
|
||||
@@ -2526,8 +2617,8 @@ fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
|
||||
value = SnapshotStateList<Chat>().apply { addAll(value); add(index, elem) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem, contentTag: MsgContentTag?) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, index) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
|
||||
@@ -2538,8 +2629,8 @@ fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
|
||||
fun <T> MutableList<T>.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate)
|
||||
|
||||
// Adds item to chatItems and notifies a listener about newly added item
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem, contentTag: MsgContentTag?) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, lastIndex) }
|
||||
}
|
||||
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
|
||||
@@ -2568,7 +2659,8 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatIte
|
||||
}
|
||||
}
|
||||
if (toRemove.isNotEmpty()) {
|
||||
chatItemsChangesListener?.removed(toRemove, value)
|
||||
chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value)
|
||||
chatModel.reportsChatsContext.chatItemsChangesListener?.removed(toRemove, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2580,7 +2672,7 @@ fun MutableState<SnapshotStateList<Chat>>.removeAt(index: Int): Chat {
|
||||
return res
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify(contentTag: MsgContentTag?) {
|
||||
val removed: Triple<Long, Int, Boolean>
|
||||
value = SnapshotStateList<ChatItem>().apply {
|
||||
addAll(value)
|
||||
@@ -2588,7 +2680,7 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
|
||||
val rem = removeLast()
|
||||
removed = Triple(rem.id, remIndex, rem.isRcvNew)
|
||||
}
|
||||
chatItemsChangesListener?.removed(listOf(removed), value)
|
||||
chatModel.chatItemsChangesListenerForContent(contentTag)?.removed(listOf(removed), value)
|
||||
}
|
||||
|
||||
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
|
||||
@@ -2602,7 +2694,8 @@ fun MutableState<SnapshotStateList<Chat>>.clear() {
|
||||
// Removes all chatItems and notifies a listener about it
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() {
|
||||
value = SnapshotStateList()
|
||||
chatItemsChangesListener?.cleared()
|
||||
chatModel.chatsContext.chatItemsChangesListener?.cleared()
|
||||
chatModel.reportsChatsContext.chatItemsChangesListener?.cleared()
|
||||
}
|
||||
|
||||
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
|
||||
@@ -3688,6 +3781,17 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MsgContentTag {
|
||||
@SerialName("text") Text,
|
||||
@SerialName("link") Link,
|
||||
@SerialName("image") Image,
|
||||
@SerialName("video") Video,
|
||||
@SerialName("voice") Voice,
|
||||
@SerialName("file") File,
|
||||
@SerialName("report") Report,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class FormattedText(val text: String, val format: Format? = null) {
|
||||
// TODO make it dependent on simplexLinkMode preference
|
||||
|
||||
+143
-14
@@ -18,6 +18,7 @@ import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -893,8 +894,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search))
|
||||
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
|
||||
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||
if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) {
|
||||
@@ -1497,6 +1498,9 @@ object ChatController {
|
||||
withChats {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
withChats(MsgContentTag.Report) {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -2402,7 +2406,7 @@ object ChatController {
|
||||
val cInfo = ChatInfo.ContactRequest(contactRequest)
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
if (chatModel.hasChat(rhId, contactRequest.id)) {
|
||||
if (hasChat(rhId, contactRequest.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
|
||||
@@ -2412,7 +2416,7 @@ object ChatController {
|
||||
ntfManager.notifyContactRequestReceived(r.user, cInfo)
|
||||
}
|
||||
is CR.ContactUpdated -> {
|
||||
if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) {
|
||||
if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) {
|
||||
val cInfo = ChatInfo.Direct(r.toContact)
|
||||
withChats {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
@@ -2424,10 +2428,13 @@ object ChatController {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
if (active(r.user) && chatModel.hasChat(rhId, r.mergedContact.id)) {
|
||||
if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.mergedContact.id)) {
|
||||
if (chatModel.chatId.value == r.mergedContact.id) {
|
||||
chatModel.chatId.value = r.intoContact.id
|
||||
}
|
||||
@@ -2472,9 +2479,19 @@ object ChatController {
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
if (cItem.isActiveReport) {
|
||||
increaseGroupReportsCounter(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
withChats {
|
||||
increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
}
|
||||
val file = cItem.file
|
||||
val mc = cItem.content.msgContent
|
||||
@@ -2497,6 +2514,11 @@ object ChatController {
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
@@ -2506,13 +2528,20 @@ object ChatController {
|
||||
withChats {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (r.reaction.chatReaction.chatItem.isReport) {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemsDeleted -> {
|
||||
if (!active(r.user)) {
|
||||
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
|
||||
if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) {
|
||||
chatModel.decreaseUnreadCounter(rhId, r.user)
|
||||
withChats {
|
||||
decreaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -2541,6 +2570,67 @@ object ChatController {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
if (toChatItem == null) {
|
||||
removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.GroupChatItemsDeleted -> {
|
||||
if (!active(r.user)) {
|
||||
val users = chatController.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
return
|
||||
}
|
||||
val cInfo = ChatInfo.Group(r.groupInfo)
|
||||
withChats {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
val cItem = chatItems.value.firstOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val isLastChatItem = getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
|
||||
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
|
||||
ntfManager.cancelNotificationsForChat(cInfo.id)
|
||||
ntfManager.displayNotification(
|
||||
r.user,
|
||||
cInfo.id,
|
||||
cInfo.displayName,
|
||||
generalGetString(MR.strings.marked_deleted_description)
|
||||
)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember != r.member_) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
if (cItem.isActiveReport) {
|
||||
decreaseGroupReportsCounter(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
val cItem = chatItems.value.firstOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember != r.member_) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
@@ -2606,30 +2696,45 @@ object ChatController {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
}
|
||||
is CR.LeftMember ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRole ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRoleUser ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberBlockedForAll ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.GroupDeleted -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
@@ -3002,6 +3107,11 @@ object ChatController {
|
||||
val cInfo = aChatItem.chatInfo
|
||||
val cItem = aChatItem.chatItem
|
||||
withChats { upsertChatItem(rh, cInfo, cItem) }
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3011,10 +3121,14 @@ object ChatController {
|
||||
val notify = { ntfManager.notifyMessageReceived(rh, user, cInfo, cItem) }
|
||||
if (!activeUser(rh, user)) {
|
||||
notify()
|
||||
} else if (withChats { upsertChatItem(rh, cInfo, cItem) }) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
} else {
|
||||
val createdChat = withChats { upsertChatItem(rh, cInfo, cItem) }
|
||||
withReportsChatsIfOpen { if (cItem.content.msgContent is MsgContent.MCReport) { upsertChatItem(rh, cInfo, cItem) } }
|
||||
if (createdChat) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3059,6 +3173,11 @@ object ChatController {
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
}
|
||||
val statuses = apiGetNetworkStatuses(rhId)
|
||||
if (statuses != null) {
|
||||
@@ -3205,7 +3324,7 @@ sealed class CC {
|
||||
class ApiGetSettings(val settings: AppSettings): CC()
|
||||
class ApiGetChatTags(val userId: Long): CC()
|
||||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||
@@ -3367,7 +3486,14 @@ sealed class CC {
|
||||
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
||||
is ApiGetChatTags -> "/_get tags $userId"
|
||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiGetChat -> {
|
||||
val tag = if (contentTag == null) {
|
||||
""
|
||||
} else {
|
||||
" content=${contentTag.name.lowercase()}"
|
||||
}
|
||||
"/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
}
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
@@ -5540,6 +5666,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR()
|
||||
@Serializable @SerialName("reactionMembers") class ReactionMembers(val user: UserRef, val memberReactions: List<MemberReaction>): CR()
|
||||
@Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR()
|
||||
@Serializable @SerialName("groupChatItemsDeleted") class GroupChatItemsDeleted(val user: UserRef, val groupInfo: GroupInfo, val chatItemIDs: List<Long>, val byUser: Boolean, val member_: GroupMember?): CR()
|
||||
@Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR()
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@@ -5724,6 +5851,7 @@ sealed class CR {
|
||||
is ChatItemReaction -> "chatItemReaction"
|
||||
is ReactionMembers -> "reactionMembers"
|
||||
is ChatItemsDeleted -> "chatItemsDeleted"
|
||||
is GroupChatItemsDeleted -> "groupChatItemsDeleted"
|
||||
is ForwardPlan -> "forwardPlan"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
@@ -5826,7 +5954,7 @@ sealed class CR {
|
||||
is ChatRunning -> noDetails()
|
||||
is ChatStopped -> noDetails()
|
||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||
is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}")
|
||||
is ApiChat -> withUser(user, "remoteHostId: ${chat.remoteHostId}\nchatInfo: ${chat.chatInfo}\nchatStats: ${chat.chatStats}\nnavInfo: ${navInfo}\nchatItems: ${chat.chatItems}")
|
||||
is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}")
|
||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||
@@ -5900,6 +6028,7 @@ sealed class CR {
|
||||
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
|
||||
is ReactionMembers -> withUser(user, "memberReactions: ${json.encodeToString(memberReactions)}")
|
||||
is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser")
|
||||
is GroupChatItemsDeleted -> withUser(user, "chatItemIDs: $chatItemIDs\nbyUser: $byUser\nmember_: $member_")
|
||||
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
|
||||
+2
@@ -23,6 +23,7 @@ expect fun LazyColumnWithScrollBar(
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
additionalBarOffset: State<Dp>? = null,
|
||||
additionalTopBar: State<Boolean> = remember { mutableStateOf(false) },
|
||||
chatBottomBar: State<Boolean> = remember { mutableStateOf(true) },
|
||||
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
|
||||
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
|
||||
@@ -42,6 +43,7 @@ expect fun LazyColumnWithScrollBarNoAppBar(
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
additionalBarOffset: State<Dp>? = null,
|
||||
additionalTopBar: State<Boolean> = remember { mutableStateOf(false) },
|
||||
chatBottomBar: State<Boolean> = remember { mutableStateOf(true) },
|
||||
content: LazyListScope.() -> Unit
|
||||
)
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
|
||||
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
|
||||
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -154,12 +155,12 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar (
|
||||
reverseLayout = true,
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(false),
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
additionalBarOffset = composeViewHeight
|
||||
) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
|
||||
+22
-14
@@ -14,9 +14,10 @@ suspend fun apiLoadSingleMessage(
|
||||
rhId: Long?,
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
itemId: Long
|
||||
itemId: Long,
|
||||
contentTag: MsgContentTag?,
|
||||
): ChatItem? = coroutineScope {
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
chat.chatItems.firstOrNull()
|
||||
}
|
||||
|
||||
@@ -24,29 +25,36 @@ suspend fun apiLoadMessages(
|
||||
rhId: Long?,
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
contentTag: MsgContentTag?,
|
||||
pagination: ChatPagination,
|
||||
chatState: ActiveChatState,
|
||||
search: String = "",
|
||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||
) = coroutineScope {
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, pagination, search) ?: return@coroutineScope
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, pagination, search) ?: return@coroutineScope
|
||||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last)
|
||||
|| !isActive) return@coroutineScope
|
||||
|
||||
val chatState = chatModel.chatStateForContent(contentTag)
|
||||
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
|
||||
val oldItems = chatModel.chatItems.value
|
||||
val oldItems = chatModel.chatItemsForContent(contentTag).value
|
||||
val newItems = SnapshotStateList<ChatItem>()
|
||||
when (pagination) {
|
||||
is ChatPagination.Initial -> {
|
||||
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
|
||||
withChats {
|
||||
if (chatModel.getChat(chat.id) == null) {
|
||||
addChat(chat)
|
||||
if (contentTag == null) {
|
||||
// update main chats, not content tagged
|
||||
withChats {
|
||||
if (getChat(chat.id) == null) {
|
||||
addChat(chat)
|
||||
} else {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo)
|
||||
updateChatStats(chat.remoteHostId, chat.id, chat.chatStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
withChats {
|
||||
chatModel.chatItemStatuses.clear()
|
||||
withChats(contentTag) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
splits.value = newSplits
|
||||
@@ -70,7 +78,7 @@ suspend fun apiLoadMessages(
|
||||
)
|
||||
val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0)
|
||||
newItems.addAll(insertAt, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = newSplits
|
||||
chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems)
|
||||
@@ -89,7 +97,7 @@ suspend fun apiLoadMessages(
|
||||
val indexToAdd = min(indexInCurrentItems + 1, newItems.size)
|
||||
val indexToAddIsLast = indexToAdd == newItems.size
|
||||
newItems.addAll(indexToAdd, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = newSplits
|
||||
chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems)
|
||||
@@ -104,7 +112,7 @@ suspend fun apiLoadMessages(
|
||||
val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed)
|
||||
// currently, items will always be added on top, which is index 0
|
||||
newItems.addAll(0, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = listOf(chat.chatItems.last().id) + newSplits
|
||||
unreadAfterItemId.value = chat.chatItems.last().id
|
||||
@@ -119,7 +127,7 @@ suspend fun apiLoadMessages(
|
||||
newItems.addAll(oldItems)
|
||||
removeDuplicates(newItems, chat)
|
||||
newItems.addAll(chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
unreadAfterNewestLoaded.value = 0
|
||||
}
|
||||
|
||||
+2
-4
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -240,14 +239,13 @@ data class ActiveChatState (
|
||||
}
|
||||
}
|
||||
|
||||
fun visibleItemIndexesNonReversed(mergedItems: State<MergedItems>, listState: LazyListState): IntRange {
|
||||
fun visibleItemIndexesNonReversed(mergedItems: State<MergedItems>, reversedItemsSize: Int, listState: LazyListState): IntRange {
|
||||
val zero = 0 .. 0
|
||||
if (listState.layoutInfo.totalItemsCount == 0) return zero
|
||||
val newest = mergedItems.value.items.getOrNull(listState.firstVisibleItemIndex)?.startIndexInReversedItems
|
||||
val oldest = mergedItems.value.items.getOrNull(listState.layoutInfo.visibleItemsInfo.last().index)?.lastIndexInReversed()
|
||||
if (newest == null || oldest == null) return zero
|
||||
val size = chatModel.chatItems.value.size
|
||||
val range = size - oldest .. size - newest
|
||||
val range = reversedItemsSize - oldest .. reversedItemsSize - newest
|
||||
if (range.first < 0 || range.last < 0) return zero
|
||||
|
||||
// visible items mapped to their underlying data structure which is chatModel.chatItems
|
||||
|
||||
+348
-113
@@ -31,8 +31,8 @@ import chat.simplex.common.model.CIDirection.GroupRcv
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.activeCall
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.markChatTagRead
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
@@ -57,10 +57,17 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat
|
||||
@Composable
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
fun ChatView(
|
||||
staleChatId: State<String?>,
|
||||
reportsView: Boolean,
|
||||
scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) },
|
||||
onComposed: suspend (chatId: String) -> Unit
|
||||
) {
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
// They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..."
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } }
|
||||
val activeChatStats = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats } }
|
||||
val user = chatModel.currentUser.value
|
||||
val chatInfo = activeChatInfo.value
|
||||
if (chatInfo == null || user == null) {
|
||||
@@ -69,6 +76,11 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
} else {
|
||||
val groupReports = remember { derivedStateOf {
|
||||
val reportsCount = if (activeChatInfo.value is ChatInfo.Group) activeChatStats.value?.reportsCount ?: 0 else 0
|
||||
GroupReports(reportsCount, reportsView) }
|
||||
}
|
||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(groupReports.value.contentTag).value.asReversed() } }
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
@@ -94,7 +106,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
if (!groupReports.value.reportsView) {
|
||||
markUnreadChatAsRead(chatId)
|
||||
}
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
selectedChatItems.value = null
|
||||
@@ -107,11 +121,14 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
val unreadCount = remember {
|
||||
derivedStateOf {
|
||||
chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == activeChatInfo.value?.id }?.chatStats?.unreadCount ?: 0
|
||||
chatModel.chatsForContent(if (reportsView) MsgContentTag.Report else null).value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false),
|
||||
LocalContentTag provides groupReports.value.contentTag
|
||||
) {
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||
var groupMembersJob: Job = remember { Job() }
|
||||
@@ -119,9 +136,19 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
|
||||
val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
|
||||
val onSearchValueChanged: (String) -> Unit = onSearchValueChanged@{ value ->
|
||||
if (searchText.value == value) return@onSearchValueChanged
|
||||
val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged
|
||||
if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
withBGApi {
|
||||
apiFindMessages(c, value, groupReports.value.toContentTag())
|
||||
searchText.value = value
|
||||
}
|
||||
}
|
||||
ChatLayout(
|
||||
remoteHostId = remoteHostId,
|
||||
chatInfo = activeChatInfo,
|
||||
reversedChatItems = reversedChatItems,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
@@ -150,7 +177,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
chatItems = remember { chatModel.chatItems }.value,
|
||||
reversedChatItems = reversedChatItems,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
deleteItems = { canDeleteForAll ->
|
||||
@@ -211,6 +238,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
)
|
||||
}
|
||||
},
|
||||
groupReports,
|
||||
scrollToItemId,
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
searchText,
|
||||
@@ -266,7 +295,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, {
|
||||
GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, scrollToItemId, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close, { showSearch.value = true })
|
||||
@@ -278,6 +307,17 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
},
|
||||
showGroupReports = {
|
||||
val info = activeChatInfo.value ?: return@ChatLayout
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
scope.launch {
|
||||
showGroupReportsView(staleChatId, scrollToItemId, info)
|
||||
}
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
groupMembersJob.cancel()
|
||||
@@ -293,7 +333,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
if (!isActive) return@launch
|
||||
|
||||
ModalManager.end.closeModals()
|
||||
if (!groupReports.value.reportsView) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
@@ -301,16 +343,16 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
},
|
||||
loadMessages = { chatId, pagination, chatState, visibleItemIndexes ->
|
||||
loadMessages = { chatId, pagination, visibleItemIndexes ->
|
||||
val c = chatModel.getChat(chatId)
|
||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||
if (c != null) {
|
||||
apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, chatState, searchText.value, visibleItemIndexes)
|
||||
apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, groupReports.value.toContentTag(), pagination, searchText.value, visibleItemIndexes)
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
@@ -341,6 +383,19 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
val deletedItem = deleted.deletedChatItem.chatItem
|
||||
if (deletedItem.isActiveReport) {
|
||||
decreaseGroupReportsCounter(chatRh, chatInfo.id)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (deletedChatItem.isReport) {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +509,11 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
withChats {
|
||||
updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -471,7 +531,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
groupMembersJob.cancel()
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
var initialCiInfo = loadChatItemInfo() ?: return@launch
|
||||
ModalManager.end.closeModals()
|
||||
if (!ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
@@ -506,7 +568,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
withChats {
|
||||
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace
|
||||
withContext(Dispatchers.Main) {
|
||||
markChatItemsRead(chatRh, chatInfo, itemsIds)
|
||||
markChatItemsRead(chatRh, chatInfo.id, itemsIds)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
chatModel.controller.apiChatItemsRead(
|
||||
@@ -516,6 +578,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
itemsIds
|
||||
)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
markChatItemsRead(chatRh, chatInfo.id, itemsIds)
|
||||
}
|
||||
}
|
||||
},
|
||||
markChatRead = {
|
||||
@@ -523,7 +588,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
withChats {
|
||||
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace
|
||||
withContext(Dispatchers.Main) {
|
||||
markChatItemsRead(chatRh, chatInfo)
|
||||
markChatItemsRead(chatRh, chatInfo.id)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
chatModel.controller.apiChatRead(
|
||||
@@ -532,18 +597,13 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
chatInfo.apiId
|
||||
)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
markChatItemsRead(chatRh, chatInfo.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
val c = chatModel.getChat(chatInfo.id) ?: return@ChatLayout
|
||||
if (chatModel.chatId.value != chatInfo.id) return@ChatLayout
|
||||
withBGApi {
|
||||
apiFindMessages(c, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
@@ -600,9 +660,12 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
fun ChatLayout(
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo?>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeView: (@Composable () -> Unit),
|
||||
groupReports: State<GroupReports>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
attachmentOption: MutableState<AttachmentOption?>,
|
||||
attachmentBottomSheetState: ModalBottomSheetState,
|
||||
searchValue: State<String>,
|
||||
@@ -611,8 +674,9 @@ fun ChatLayout(
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
showGroupReports: () -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -671,7 +735,7 @@ fun ChatLayout(
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, !groupReports.value.reportsView)) {
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
@@ -684,8 +748,8 @@ fun ChatLayout(
|
||||
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
|
||||
}) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
remoteHostId, chatInfo, reversedChatItems, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, groupReports, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
@@ -693,29 +757,90 @@ fun ChatLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
.then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
|
||||
) {
|
||||
composeView()
|
||||
if (groupReports.value.reportsView) {
|
||||
Column(
|
||||
Modifier
|
||||
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
AnimatedVisibility(selectedChatItems.value != null) {
|
||||
if (chatInfo != null) {
|
||||
SelectedItemsBottomToolbar(
|
||||
reversedChatItems = reversedChatItems,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
deleteItems = { _ ->
|
||||
val itemIds = selectedChatItems.value
|
||||
val questionText = generalGetString(MR.strings.delete_messages_cannot_be_undone_warning)
|
||||
if (itemIds != null) {
|
||||
deleteMessagesAlertDialog(itemIds.sorted(), questionText = questionText, forAll = false, deleteMessages = { ids, _ ->
|
||||
deleteMessages(remoteHostId, chatInfo, ids, false, moderate = false) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
moderateItems = {},
|
||||
forwardItems = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
// That's placeholder to take some space for bottom app bar in oneHandUI
|
||||
Box(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
.then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
|
||||
) {
|
||||
composeView()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value && chatBottomBar.value) {
|
||||
StatusBarBackground()
|
||||
if (groupReports.value.showBar) {
|
||||
ReportedCountToolbar(groupReports, withStatusBar = true, showGroupReports)
|
||||
} else {
|
||||
StatusBarBackground()
|
||||
}
|
||||
} else {
|
||||
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
|
||||
}
|
||||
Box(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
if (selectedChatItems.value == null) {
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
if (groupReports.value.reportsView) {
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
GroupReportsAppBar(groupReports, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatInfo, groupReports, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||
}
|
||||
}
|
||||
if (groupReports.value.showBar && (!oneHandUI.value || !chatBottomBar.value)) {
|
||||
ReportedCountToolbar(groupReports, withStatusBar = false, showGroupReports)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,6 +851,7 @@ fun ChatLayout(
|
||||
@Composable
|
||||
fun BoxScope.ChatInfoToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
groupReports: State<GroupReports>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
@@ -747,7 +873,7 @@ fun BoxScope.ChatInfoToolbar(
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
if (appPlatform.isAndroid && !groupReports.value.reportsView) {
|
||||
BackHandler(onBack = onBackClicked)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
@@ -941,25 +1067,65 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReportedCountToolbar(
|
||||
groupReports: State<GroupReports>,
|
||||
withStatusBar: Boolean,
|
||||
showGroupReports: () -> Unit
|
||||
) {
|
||||
Box {
|
||||
val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.clickable(onClick = showGroupReports)
|
||||
.padding(top = statusBarPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
val reports = groupReports.value.reportsCount
|
||||
Text(
|
||||
if (reports == 1) {
|
||||
stringResource(MR.strings.group_reports_active_one)
|
||||
} else {
|
||||
stringResource(MR.strings.group_reports_active).format(reports)
|
||||
},
|
||||
style = MaterialTheme.typography.button
|
||||
)
|
||||
}
|
||||
Divider(Modifier.align(Alignment.BottomStart))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactVerifiedShield() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
/** Saves current scroll position when [GroupReports] are open and user opens [ChatItemInfoView], for example, and goes back */
|
||||
private var reportsListState: LazyListState? = null
|
||||
|
||||
@Composable
|
||||
fun BoxScope.ChatItemsList(
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeViewHeight: State<Dp>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
groupReports: State<GroupReports>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
showChatInfo: () -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -984,10 +1150,9 @@ fun BoxScope.ChatItemsList(
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } }
|
||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
|
||||
val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } }
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() })
|
||||
val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatStateForContent(groupReports.value.contentTag)) } }
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() })
|
||||
/** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of
|
||||
* [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears
|
||||
* */
|
||||
@@ -996,12 +1161,17 @@ fun BoxScope.ChatItemsList(
|
||||
)
|
||||
val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, saver = LazyListState.Saver) {
|
||||
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
|
||||
if (index <= 0) {
|
||||
val reportsState = reportsListState
|
||||
if (reportsState != null) {
|
||||
reportsListState = null
|
||||
reportsState
|
||||
} else if (index <= 0) {
|
||||
LazyListState(0, 0)
|
||||
} else {
|
||||
LazyListState(index + 1, -maxHeightForList.value)
|
||||
}
|
||||
})
|
||||
SaveReportsStateOnDispose(groupReports, listState)
|
||||
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
|
||||
val loadingMoreItems = remember { mutableStateOf(false) }
|
||||
val animatedScrollingInProgress = remember { mutableStateOf(false) }
|
||||
@@ -1011,12 +1181,12 @@ fun BoxScope.ChatItemsList(
|
||||
ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect)
|
||||
}
|
||||
if (!loadingMoreItems.value) {
|
||||
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
|
||||
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), reversedChatItems, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
|
||||
if (loadingMoreItems.value) return@PreloadItems false
|
||||
try {
|
||||
loadingMoreItems.value = true
|
||||
loadMessages(chatId, pagination, chatModel.chatState) {
|
||||
visibleItemIndexesNonReversed(mergedItems, listState.value)
|
||||
loadMessages(chatId, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
|
||||
}
|
||||
} finally {
|
||||
loadingMoreItems.value = false
|
||||
@@ -1029,21 +1199,33 @@ fun BoxScope.ChatItemsList(
|
||||
val chatInfoUpdated = rememberUpdatedState(chatInfo)
|
||||
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
|
||||
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
|
||||
|
||||
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo)
|
||||
SmallScrollOnNewMessage(listState, chatModel.chatItems)
|
||||
val scrollToItem: (Long) -> Unit = remember {
|
||||
// In group reports just set the itemId to scroll to so the main ChatView will handle scrolling
|
||||
if (groupReports.value.reportsView) return@remember { scrollToItemId.value = it }
|
||||
scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages)
|
||||
}
|
||||
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, groupReports.value.contentTag) }
|
||||
if (!groupReports.value.reportsView) {
|
||||
LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect {
|
||||
if (appPlatform.isAndroid) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
scrollToItem(it)
|
||||
scrollToItemId.value = null }
|
||||
}
|
||||
}
|
||||
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo, groupReports)
|
||||
SmallScrollOnNewMessage(listState, reversedChatItems)
|
||||
val finishedInitialComposition = remember { mutableStateOf(false) }
|
||||
NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
|
||||
|
||||
DisposableEffectOnGone(
|
||||
always = {
|
||||
chatModel.chatItemsChangesListener = recalculateChatStatePositions(chatModel.chatState)
|
||||
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag)
|
||||
},
|
||||
whenGone = {
|
||||
VideoPlayerHolder.releaseAll()
|
||||
chatModel.chatItemsChangesListener = null
|
||||
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1065,7 +1247,7 @@ fun BoxScope.ChatItemsList(
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val provider = {
|
||||
providerForGallery(chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed ->
|
||||
itemScope.launch {
|
||||
listState.value.scrollToItem(
|
||||
min(reversedChatItems.value.lastIndex, indexInReversed + 1),
|
||||
@@ -1090,7 +1272,7 @@ fun BoxScope.ChatItemsList(
|
||||
highlightedItems.value = setOf()
|
||||
}
|
||||
}
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1168,7 +1350,7 @@ fun BoxScope.ChatItemsList(
|
||||
val rangeValue = range.value
|
||||
val (prevMember, memCount) =
|
||||
if (rangeValue != null) {
|
||||
chatModel.getPrevHiddenMember(member, rangeValue)
|
||||
chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value)
|
||||
} else {
|
||||
null to 1
|
||||
}
|
||||
@@ -1276,7 +1458,7 @@ fun BoxScope.ChatItemsList(
|
||||
if (selectionVisible) {
|
||||
Box(Modifier.matchParentSize().clickable {
|
||||
val checked = selectedChatItems.value?.contains(cItem.id) == true
|
||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
|
||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1290,12 +1472,13 @@ fun BoxScope.ChatItemsList(
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.align(Alignment.BottomCenter),
|
||||
state = listState.value,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(true),
|
||||
top = topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar),
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
reverseLayout = true,
|
||||
additionalBarOffset = composeViewHeight,
|
||||
additionalTopBar = remember { derivedStateOf { groupReports.value.showBar } },
|
||||
chatBottomBar = remember { appPrefs.chatBottomBar.state }
|
||||
) {
|
||||
val mergedItemsValue = mergedItems.value
|
||||
@@ -1339,8 +1522,8 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
|
||||
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState)
|
||||
FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, groupReports, markChatRead, listState)
|
||||
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopCenter), mergedItems, listState, groupReports)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.value.isScrollInProgress }
|
||||
@@ -1360,14 +1543,14 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId: Long?, chatInfo: ChatInfo) {
|
||||
private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId: Long?, chatInfo: ChatInfo, groupReports: State<GroupReports>) {
|
||||
LaunchedEffect(remoteHostId, chatInfo.id) {
|
||||
try {
|
||||
loadingMoreItems.value = true
|
||||
if (chatModel.chatState.totalAfter.value <= 0) return@LaunchedEffect
|
||||
if (chatModel.chatStateForContent(groupReports.value.contentTag).totalAfter.value <= 0) return@LaunchedEffect
|
||||
delay(500)
|
||||
withContext(Dispatchers.Default) {
|
||||
apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, groupReports.value.toContentTag(), ChatPagination.Last(ChatPagination.INITIAL_COUNT))
|
||||
}
|
||||
} finally {
|
||||
loadingMoreItems.value = false
|
||||
@@ -1376,20 +1559,20 @@ private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId:
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems: State<List<ChatItem>>) {
|
||||
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, reversedChatItems: State<List<ChatItem>>) {
|
||||
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
|
||||
LaunchedEffect(Unit) {
|
||||
var lastTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
var lastItemId = chatItems.value.lastOrNull()?.id
|
||||
var prevTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
var newestItemId = reversedChatItems.value.firstOrNull()?.id
|
||||
snapshotFlow { listState.value.layoutInfo.totalItemsCount }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.collect {
|
||||
val diff = listState.value.layoutInfo.totalItemsCount - lastTotalItems
|
||||
val sameLastItem = lastItemId == chatItems.value.lastOrNull()?.id
|
||||
lastTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
lastItemId = chatItems.value.lastOrNull()?.id
|
||||
if (diff < 1 || diff > 2 || sameLastItem) {
|
||||
val diff = listState.value.layoutInfo.totalItemsCount - prevTotalItems
|
||||
val sameNewestItem = newestItemId == reversedChatItems.value.firstOrNull()?.id
|
||||
prevTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
newestItemId = reversedChatItems.value.firstOrNull()?.id
|
||||
if (diff < 1 || diff > 2 || sameNewestItem) {
|
||||
return@collect
|
||||
}
|
||||
try {
|
||||
@@ -1400,7 +1583,7 @@ private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems:
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
/**
|
||||
* When you tap and hold a finger on a lazy column with chatItems, and then you receive a message,
|
||||
* When you tap and hold a finger on a lazy column with reversedChatItems, and then you receive a message,
|
||||
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
|
||||
* Which breaks auto-scrolling to bottom. So just ignoring the exception
|
||||
* */
|
||||
@@ -1440,11 +1623,12 @@ fun BoxScope.FloatingButtons(
|
||||
maxHeight: State<Int>,
|
||||
composeViewHeight: State<Dp>,
|
||||
searchValue: State<String>,
|
||||
groupReports: State<GroupReports>,
|
||||
markChatRead: () -> Unit,
|
||||
listState: State<LazyListState>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() })
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() })
|
||||
val bottomUnreadCount = remember {
|
||||
derivedStateOf {
|
||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||
@@ -1490,7 +1674,7 @@ fun BoxScope.FloatingButtons(
|
||||
val showDropDown = remember { mutableStateOf(false) }
|
||||
|
||||
TopEndFloatingButton(
|
||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd),
|
||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopEnd),
|
||||
topUnreadCount,
|
||||
animatedScrollingInProgress,
|
||||
onClick = {
|
||||
@@ -1512,7 +1696,7 @@ fun BoxScope.FloatingButtons(
|
||||
DefaultDropdownMenu(
|
||||
showDropDown,
|
||||
modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } },
|
||||
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent(true))
|
||||
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar))
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
@@ -1529,6 +1713,7 @@ fun BoxScope.FloatingButtons(
|
||||
fun PreloadItems(
|
||||
chatId: String,
|
||||
ignoreLoadingRequests: MutableSet<Long>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1539,8 +1724,8 @@ fun PreloadItems(
|
||||
val chatId = rememberUpdatedState(chatId)
|
||||
val loadItems = rememberUpdatedState(loadItems)
|
||||
val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests)
|
||||
PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsAfter(allowLoad, chatId, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, reversedChatItems, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsAfter(allowLoad, chatId, reversedChatItems, mergedItems, listState, remaining, loadItems)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1548,6 +1733,7 @@ private fun PreloadItemsBefore(
|
||||
allowLoad: State<Boolean>,
|
||||
chatId: State<String>,
|
||||
ignoreLoadingRequests: State<MutableSet<Long>>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1560,9 +1746,9 @@ private fun PreloadItemsBefore(
|
||||
val splits = mergedItems.value.splits
|
||||
val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
|
||||
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
|
||||
val items = chatModel.chatItems.value
|
||||
val items = reversedChatItems.value
|
||||
if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining) {
|
||||
lastIndexToLoadFrom = items.lastIndex
|
||||
lastIndexToLoadFrom = 0
|
||||
}
|
||||
if (allowLoad.value && lastIndexToLoadFrom != null) {
|
||||
items.getOrNull(items.lastIndex - lastIndexToLoadFrom)?.id
|
||||
@@ -1574,10 +1760,10 @@ private fun PreloadItemsBefore(
|
||||
.filter { !ignoreLoadingRequests.value.contains(it) }
|
||||
.collect { loadFromItemId ->
|
||||
withBGApi {
|
||||
val sizeWas = chatModel.chatItems.value.size
|
||||
val firstItemIdWas = chatModel.chatItems.value.firstOrNull()?.id
|
||||
val sizeWas = reversedChatItems.value.size
|
||||
val oldestItemIdWas = reversedChatItems.value.lastOrNull()?.id
|
||||
val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT))
|
||||
if (triedToLoad && sizeWas == chatModel.chatItems.value.size && firstItemIdWas == chatModel.chatItems.value.firstOrNull()?.id) {
|
||||
if (triedToLoad && sizeWas == reversedChatItems.value.size && oldestItemIdWas == reversedChatItems.value.lastOrNull()?.id) {
|
||||
ignoreLoadingRequests.value.add(loadFromItemId)
|
||||
}
|
||||
}
|
||||
@@ -1589,6 +1775,7 @@ private fun PreloadItemsBefore(
|
||||
private fun PreloadItemsAfter(
|
||||
allowLoad: MutableState<Boolean>,
|
||||
chatId: State<String>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1609,12 +1796,12 @@ private fun PreloadItemsAfter(
|
||||
snapshotFlow { listState.value.firstVisibleItemIndex }
|
||||
.distinctUntilChanged()
|
||||
.map { firstVisibleIndex ->
|
||||
val items = chatModel.chatItems.value
|
||||
val items = reversedChatItems.value
|
||||
val splits = mergedItems.value.splits
|
||||
val split = splits.lastOrNull { it.indexRangeInParentItems.contains(firstVisibleIndex) }
|
||||
// we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom)
|
||||
if (split != null && split.indexRangeInParentItems.first + remaining > firstVisibleIndex) {
|
||||
items.getOrNull(items.lastIndex - split.indexRangeInReversed.first)?.id
|
||||
items.getOrNull(split.indexRangeInReversed.first)?.id
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1663,13 +1850,14 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun topPaddingToContent(chatView: Boolean): Dp {
|
||||
fun topPaddingToContent(chatView: Boolean, additionalTopBar: Boolean = false): Dp {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val chatBottomBar = remember { appPrefs.chatBottomBar.state }
|
||||
val reportsPadding = if (additionalTopBar) AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
return if (oneHandUI.value && (!chatView || chatBottomBar.value)) {
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding
|
||||
} else {
|
||||
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1678,12 +1866,13 @@ private fun FloatingDate(
|
||||
modifier: Modifier,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
groupReports: State<GroupReports>
|
||||
) {
|
||||
val isNearBottom = remember(chatModel.chatId) { mutableStateOf(listState.value.firstVisibleItemIndex == 0) }
|
||||
val nearBottomIndex = remember(chatModel.chatId) { mutableStateOf(if (isNearBottom.value) -1 else 0) }
|
||||
val showDate = remember(chatModel.chatId) { mutableStateOf(false) }
|
||||
val density = LocalDensity.current.density
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() })
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() })
|
||||
val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier
|
||||
val lastVisibleItemDate = remember {
|
||||
derivedStateOf {
|
||||
@@ -1767,6 +1956,15 @@ private fun FloatingDate(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveReportsStateOnDispose(groupReports: State<GroupReports>, listState: State<LazyListState>) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
reportsListState = if (groupReports.value.reportsView && ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) listState.value else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadFilesButton(
|
||||
forwardConfirmation: ForwardConfirmation.FilesNotAccepted,
|
||||
@@ -1893,7 +2091,7 @@ private fun scrollToItem(
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
): (Long) -> Unit = { itemId: Long ->
|
||||
withApi {
|
||||
try {
|
||||
@@ -1907,8 +2105,8 @@ private fun scrollToItem(
|
||||
val pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2)
|
||||
val oldSize = reversedChatItems.value.size
|
||||
withContext(Dispatchers.Default) {
|
||||
loadMessages(chatInfo.value.id, pagination, chatModel.chatState) {
|
||||
visibleItemIndexesNonReversed(mergedItems, listState.value)
|
||||
loadMessages(chatInfo.value.id, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
|
||||
}
|
||||
}
|
||||
var repeatsLeft = 50
|
||||
@@ -1939,14 +2137,18 @@ private fun findQuotedItemFromItem(
|
||||
rhId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
scope: CoroutineScope,
|
||||
scrollToItem: (Long) -> Unit
|
||||
scrollToItem: (Long) -> Unit,
|
||||
contentTag: MsgContentTag?
|
||||
): (Long) -> Unit = { itemId: Long ->
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId)
|
||||
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag)
|
||||
if (item != null) {
|
||||
withChats {
|
||||
updateChatItem(chatInfo.value, item)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateChatItem(chatInfo.value, item)
|
||||
}
|
||||
if (item.quotedItem?.itemId != null) {
|
||||
scrollToItem(item.quotedItem.itemId)
|
||||
} else {
|
||||
@@ -2043,18 +2245,24 @@ private fun SelectedChatItem(
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State<Boolean>, selectedChatItems: MutableState<Set<Long>?>) {
|
||||
private fun selectUnselectChatItem(
|
||||
select: Boolean,
|
||||
ci: ChatItem,
|
||||
revealed: State<Boolean>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
reversedChatItems: State<List<ChatItem>>
|
||||
) {
|
||||
val itemIds = mutableSetOf<Long>()
|
||||
if (!revealed.value) {
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(ci)
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(ci, reversedChatItems.value)
|
||||
val ciCategory = ci.mergeCategory
|
||||
if (currIndex != null && ciCategory != null) {
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory, reversedChatItems.value)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (range != null) {
|
||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
||||
val reversed = reversedChatItems.value
|
||||
for (i in range) {
|
||||
itemIds.add(reversedChatItems[i].id)
|
||||
itemIds.add(reversed[i].id)
|
||||
}
|
||||
} else {
|
||||
itemIds.add(ci.id)
|
||||
@@ -2098,10 +2306,28 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
|
||||
for (di in deleted) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
if (toChatItem.isReport) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
}
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
val deletedItem = di.deletedChatItem.chatItem
|
||||
if (deletedItem.isActiveReport) {
|
||||
decreaseGroupReportsCounter(chatRh, chatInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
for (di in deleted) {
|
||||
if (di.deletedChatItem.chatItem.isReport) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
@@ -2149,15 +2375,16 @@ fun Modifier.chatViewBackgroundModifier(
|
||||
colors: Colors,
|
||||
wallpaper: AppWallpaper,
|
||||
backgroundGraphicsLayerSize: MutableState<IntSize>?,
|
||||
backgroundGraphicsLayer: GraphicsLayer?
|
||||
backgroundGraphicsLayer: GraphicsLayer?,
|
||||
drawWallpaper: Boolean
|
||||
): Modifier {
|
||||
val wallpaperImage = wallpaper.type.image
|
||||
val wallpaperType = wallpaper.type
|
||||
val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background)
|
||||
val backgroundColor = if (drawWallpaper) wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background) else colors.background
|
||||
val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||
|
||||
return this
|
||||
.then(if (wallpaperImage != null)
|
||||
.then(if (wallpaperImage != null && drawWallpaper)
|
||||
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) }
|
||||
else
|
||||
Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } }
|
||||
@@ -2303,7 +2530,7 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
|
||||
private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) {
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(
|
||||
chatModel.chatItems.value.filter { chatItemsIds.contains(it.id) },
|
||||
chatModel.chatItemsForContent(null).value.filter { chatItemsIds.contains(it.id) },
|
||||
chatInfo
|
||||
)
|
||||
}
|
||||
@@ -2440,9 +2667,12 @@ fun PreviewChatLayout() {
|
||||
ChatLayout(
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
reversedChatItems = remember { mutableStateOf(emptyList()) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
groupReports = remember { mutableStateOf(GroupReports(0, false)) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||
searchValue,
|
||||
@@ -2451,8 +2681,9 @@ fun PreviewChatLayout() {
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
back = {},
|
||||
info = {},
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
@@ -2513,9 +2744,12 @@ fun PreviewGroupChatLayout() {
|
||||
ChatLayout(
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
reversedChatItems = remember { mutableStateOf(emptyList()) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
groupReports = remember { mutableStateOf(GroupReports(0, false)) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||
searchValue,
|
||||
@@ -2524,8 +2758,9 @@ fun PreviewGroupChatLayout() {
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
back = {},
|
||||
info = {},
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = {},
|
||||
receiveFile = { _ -> },
|
||||
|
||||
+1
-1
@@ -836,7 +836,7 @@ fun ComposeView(
|
||||
|
||||
fun editPrevMessage() {
|
||||
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
||||
val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable }
|
||||
val lastEditable = chatModel.chatItemsForContent(null).value.findLast { it.meta.editable }
|
||||
if (lastEditable != null) {
|
||||
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
||||
}
|
||||
|
||||
+5
-6
@@ -21,11 +21,10 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>, onTop: Boolean) {
|
||||
val onBackClicked = { selectedChatItems.value = null }
|
||||
BackHandler(onBack = onBackClicked)
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||
title = {
|
||||
@@ -41,7 +40,7 @@ fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>
|
||||
)
|
||||
},
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
onTop = onTop,
|
||||
onSearchValueChanged = {},
|
||||
)
|
||||
}
|
||||
@@ -49,7 +48,7 @@ fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>
|
||||
@Composable
|
||||
fun SelectedItemsBottomToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
chatItems: List<ChatItem>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
moderateItems: () -> Unit,
|
||||
@@ -108,8 +107,8 @@ fun SelectedItemsBottomToolbar(
|
||||
}
|
||||
Divider(Modifier.align(Alignment.TopStart))
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
LaunchedEffect(chatInfo, reversedChatItems.value, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, reversedChatItems.value.asReversed(), selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -64,6 +65,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
+26
-3
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -45,7 +46,7 @@ import kotlinx.coroutines.*
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, scrollToItemId: MutableState<Long?>, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
@@ -70,6 +71,7 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
|
||||
.sortedByDescending { it.memberRole },
|
||||
developerTools,
|
||||
groupLink,
|
||||
scrollToItemId,
|
||||
addMembers = {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
@@ -198,6 +200,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -282,6 +287,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||
members: List<GroupMember>,
|
||||
developerTools: Boolean,
|
||||
groupLink: String?,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
addMembers: () -> Unit,
|
||||
showMemberInfo: (GroupMember) -> Unit,
|
||||
editGroupProfile: () -> Unit,
|
||||
@@ -309,12 +315,12 @@ fun ModalData.GroupChatInfoLayout(
|
||||
Box {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
LazyColumnWithScrollBar(
|
||||
state = listState,
|
||||
contentPadding = if (oneHandUI.value) {
|
||||
PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
|
||||
} else {
|
||||
PaddingValues(top = topPaddingToContent(false))
|
||||
},
|
||||
state = listState
|
||||
}
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
@@ -358,6 +364,13 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
|
||||
GroupPreferencesButton(prefsTitleId, openPreferences)
|
||||
if (groupInfo.canModerate) {
|
||||
GroupReportsButton {
|
||||
scope.launch {
|
||||
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
@@ -487,6 +500,15 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupReportsButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_flag),
|
||||
stringResource(MR.strings.group_reports_member_reports),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
|
||||
val values = remember {
|
||||
@@ -737,6 +759,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
+35
-10
@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -65,6 +66,9 @@ fun GroupMemberInfoView(
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -83,7 +87,7 @@ fun GroupMemberInfoView(
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = {
|
||||
withBGApi {
|
||||
apiLoadMessages(rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
if (chatModel.getContactChat(it) != null) {
|
||||
closeAll()
|
||||
}
|
||||
@@ -142,6 +146,9 @@ fun GroupMemberInfoView(
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, mem)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, groupInfo, mem)
|
||||
}
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
@@ -157,6 +164,9 @@ fun GroupMemberInfoView(
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -171,6 +181,9 @@ fun GroupMemberInfoView(
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -188,6 +201,9 @@ fun GroupMemberInfoView(
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -203,16 +219,16 @@ fun GroupMemberInfoView(
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
withChats {
|
||||
upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
val copy = mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, copy)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, groupInfo, copy)
|
||||
}
|
||||
r
|
||||
}
|
||||
@@ -246,6 +262,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -753,6 +772,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -786,6 +808,9 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -45,6 +45,9 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
|
||||
updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
withChats {
|
||||
updateGroup(rhId, g)
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
val LocalContentTag: ProvidableCompositionLocal<MsgContentTag?> = staticCompositionLocalOf { null }
|
||||
|
||||
data class GroupReports(
|
||||
val reportsCount: Int,
|
||||
val reportsView: Boolean,
|
||||
) {
|
||||
val showBar: Boolean = reportsCount > 0 && !reportsView
|
||||
|
||||
fun toContentTag(): MsgContentTag? {
|
||||
if (!reportsView) return null
|
||||
return MsgContentTag.Report
|
||||
}
|
||||
|
||||
val contentTag: MsgContentTag? = if (!reportsView) null else MsgContentTag.Report
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
|
||||
ChatView(staleChatId, reportsView = true, scrollToItemId, onComposed = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupReportsAppBar(
|
||||
groupReports: State<GroupReports>,
|
||||
close: () -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit
|
||||
) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
close()
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = onBackClicked)
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
fixedTitleText = stringResource(MR.strings.group_reports_member_reports),
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
IconButton({ showSearch.value = true }) {
|
||||
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
ItemsReload(groupReports)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemsReload(groupReports: State<GroupReports>) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.filterNotNull()
|
||||
.map { chatModel.getChat(it) }
|
||||
.filterNotNull()
|
||||
.filter { it.chatInfo is ChatInfo.Group }
|
||||
.collect { chat ->
|
||||
reloadItems(chat, groupReports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
|
||||
openChat(chatModel.remoteHostId(), chatInfo, MsgContentTag.Report)
|
||||
ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close ->
|
||||
ModalView({}, showAppBar = false) {
|
||||
val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value
|
||||
if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) {
|
||||
GroupReportsView(staleChatId, scrollToItemId)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reloadItems(chat: Chat, groupReports: State<GroupReports>) {
|
||||
val contentFilter = groupReports.value.toContentTag()
|
||||
apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
}
|
||||
+1
@@ -27,6 +27,7 @@ import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatJsonLength
|
||||
|
||||
+3
-2
@@ -13,6 +13,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
|
||||
import chat.simplex.common.platform.onRightClick
|
||||
import chat.simplex.common.views.chat.group.LocalContentTag
|
||||
|
||||
@Composable
|
||||
fun CIChatFeatureView(
|
||||
@@ -75,9 +76,9 @@ private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<Feature
|
||||
val m = ChatModel
|
||||
val fs: ArrayList<FeatureInfo> = arrayListOf()
|
||||
val icons: MutableSet<PainterBox> = mutableSetOf()
|
||||
var i = getChatItemIndexOrNull(chatItem)
|
||||
val reversedChatItems = m.chatItemsForContent(LocalContentTag.current).value.asReversed()
|
||||
var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
|
||||
if (i != null) {
|
||||
val reversedChatItems = m.chatItems.asReversed()
|
||||
while (i < reversedChatItems.size) {
|
||||
val f = featureInfo(reversedChatItems[i], chatInfo) ?: break
|
||||
if (!icons.contains(f.icon)) {
|
||||
|
||||
+13
-10
@@ -30,6 +30,7 @@ import chat.simplex.common.model.ChatModel.currentUser
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.group.LocalContentTag
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -481,7 +482,7 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
@@ -516,8 +517,8 @@ fun ChatItemView(
|
||||
DeleteItemMenu()
|
||||
}
|
||||
|
||||
fun mergedGroupEventText(chatItem: ChatItem): String? {
|
||||
val (count, ns) = chatModel.getConnectedMemberNames(chatItem)
|
||||
fun mergedGroupEventText(chatItem: ChatItem, reversedChatItems: List<ChatItem>): String? {
|
||||
val (count, ns) = chatModel.getConnectedMemberNames(chatItem, reversedChatItems)
|
||||
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])
|
||||
@@ -536,9 +537,9 @@ fun ChatItemView(
|
||||
}
|
||||
}
|
||||
|
||||
fun eventItemViewText(): AnnotatedString {
|
||||
fun eventItemViewText(reversedChatItems: List<ChatItem>): AnnotatedString {
|
||||
val memberDisplayName = cItem.memberDisplayName
|
||||
val t = mergedGroupEventText(cItem)
|
||||
val t = mergedGroupEventText(cItem, reversedChatItems)
|
||||
return if (!revealed.value && t != null) {
|
||||
chatEventText(t, cItem.timestampText)
|
||||
} else if (memberDisplayName != null) {
|
||||
@@ -552,12 +553,13 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun EventItemView() {
|
||||
CIEventView(eventItemViewText())
|
||||
val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed()
|
||||
CIEventView(eventItemViewText(reversedChatItems))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeletedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
@@ -746,20 +748,21 @@ fun DeleteItemAction(
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
buttonText: String = stringResource(MR.strings.delete_verb),
|
||||
) {
|
||||
val contentTag = LocalContentTag.current
|
||||
ItemAction(
|
||||
buttonText,
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
if (!revealed.value) {
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(cItem)
|
||||
val reversedChatItems = chatModel.chatItemsForContent(contentTag).value.asReversed()
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(cItem, reversedChatItems)
|
||||
val ciCategory = cItem.mergeCategory
|
||||
if (currIndex != null && ciCategory != null) {
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory, reversedChatItems)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (range != null) {
|
||||
val itemIds: ArrayList<Long> = arrayListOf()
|
||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
||||
for (i in range) {
|
||||
itemIds.add(reversedChatItems[i].id)
|
||||
}
|
||||
|
||||
+57
-41
@@ -128,17 +128,6 @@ fun FramedItemView(
|
||||
Modifier
|
||||
.background(if (sent) sentColor else receivedColor)
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = {
|
||||
if (qi.itemId != null) {
|
||||
scrollToItem(qi.itemId)
|
||||
} else {
|
||||
scrollToQuotedItemFromItem(ci.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
when (qi.content) {
|
||||
is MsgContent.MCImage -> {
|
||||
@@ -216,39 +205,66 @@ fun FramedItemView(
|
||||
.padding(start = if (tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp)
|
||||
) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.isReport) {
|
||||
if (ci.meta.itemDeleted == null) {
|
||||
FramedItemHeader(
|
||||
stringResource(if (ci.chatDir.sent) MR.strings.report_item_visibility_submitter else MR.strings.report_item_visibility_moderators),
|
||||
true,
|
||||
painterResource(MR.images.ic_flag),
|
||||
iconColor = Color.Red
|
||||
)
|
||||
} else {
|
||||
FramedItemHeader(stringResource(MR.strings.report_item_archived), true, painterResource(MR.images.ic_flag))
|
||||
@Composable
|
||||
fun Header() {
|
||||
if (ci.isReport) {
|
||||
if (ci.meta.itemDeleted == null) {
|
||||
FramedItemHeader(
|
||||
stringResource(if (ci.chatDir.sent) MR.strings.report_item_visibility_submitter else MR.strings.report_item_visibility_moderators),
|
||||
true,
|
||||
painterResource(MR.images.ic_flag),
|
||||
iconColor = Color.Red
|
||||
)
|
||||
} else {
|
||||
val text = if (ci.meta.itemDeleted is CIDeleted.Moderated && ci.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as ChatInfo.Group?)?.groupInfo?.membership?.groupMemberId) {
|
||||
stringResource(MR.strings.report_item_archived_by).format(ci.meta.itemDeleted.byGroupMember.displayName)
|
||||
} else {
|
||||
stringResource(MR.strings.report_item_archived)
|
||||
}
|
||||
FramedItemHeader(text, true, painterResource(MR.images.ic_flag))
|
||||
}
|
||||
} else if (ci.meta.itemDeleted != null) {
|
||||
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))
|
||||
}
|
||||
is CIDeleted.BlockedByAdmin -> {
|
||||
FramedItemHeader(stringResource(MR.strings.blocked_by_admin_item_description), true, painterResource(MR.images.ic_back_hand))
|
||||
}
|
||||
is CIDeleted.Deleted -> {
|
||||
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete))
|
||||
}
|
||||
}
|
||||
} else if (ci.meta.isLive) {
|
||||
FramedItemHeader(stringResource(MR.strings.live), false)
|
||||
}
|
||||
} else if (ci.meta.itemDeleted != null) {
|
||||
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))
|
||||
}
|
||||
is CIDeleted.BlockedByAdmin -> {
|
||||
FramedItemHeader(stringResource(MR.strings.blocked_by_admin_item_description), true, painterResource(MR.images.ic_back_hand))
|
||||
}
|
||||
is CIDeleted.Deleted -> {
|
||||
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete))
|
||||
}
|
||||
}
|
||||
} else if (ci.meta.isLive) {
|
||||
FramedItemHeader(stringResource(MR.strings.live), false)
|
||||
}
|
||||
if (ci.quotedItem != null) {
|
||||
ciQuoteView(ci.quotedItem)
|
||||
} else if (ci.meta.itemForwarded != null) {
|
||||
FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true)
|
||||
Column(
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = {
|
||||
if (ci.quotedItem.itemId != null) {
|
||||
scrollToItem(ci.quotedItem.itemId)
|
||||
} else {
|
||||
scrollToQuotedItemFromItem(ci.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
Header()
|
||||
ciQuoteView(ci.quotedItem)
|
||||
}
|
||||
} else {
|
||||
Header()
|
||||
if (ci.meta.itemForwarded != null) {
|
||||
FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true)
|
||||
}
|
||||
}
|
||||
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
|
||||
+17
-9
@@ -12,15 +12,17 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.chatModel
|
||||
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.group.LocalContentTag
|
||||
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?, revealed: State<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: Int?, revealed: State<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
@@ -33,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: State<
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
MergedMarkedDeletedText(ci, revealed)
|
||||
MergedMarkedDeletedText(ci, chatInfo, revealed)
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
@@ -41,11 +43,11 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: State<
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: State<Boolean>) {
|
||||
var i = getChatItemIndexOrNull(chatItem)
|
||||
private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) {
|
||||
val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed()
|
||||
var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
|
||||
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 blockedByAdmin = 0
|
||||
@@ -67,7 +69,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: State<Boolean>
|
||||
}
|
||||
val total = moderated + blocked + blockedByAdmin + deleted
|
||||
if (total <= 1)
|
||||
markedDeletedText(chatItem)
|
||||
markedDeletedText(chatItem, chatInfo)
|
||||
else if (total == moderated)
|
||||
stringResource(MR.strings.moderated_items_description).format(total, moderatedBy.joinToString(", "))
|
||||
else if (total == blockedByAdmin)
|
||||
@@ -77,7 +79,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: State<Boolean>
|
||||
else
|
||||
stringResource(MR.strings.marked_deleted_items_description).format(total)
|
||||
} else {
|
||||
markedDeletedText(chatItem)
|
||||
markedDeletedText(chatItem, chatInfo)
|
||||
}
|
||||
|
||||
Text(
|
||||
@@ -91,8 +93,14 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: State<Boolean>
|
||||
)
|
||||
}
|
||||
|
||||
fun markedDeletedText(cItem: ChatItem): String =
|
||||
if (cItem.meta.itemDeleted != null && cItem.isReport) generalGetString(MR.strings.report_item_archived)
|
||||
fun markedDeletedText(cItem: ChatItem, chatInfo: ChatInfo): String =
|
||||
if (cItem.meta.itemDeleted != null && cItem.isReport) {
|
||||
if (cItem.meta.itemDeleted is CIDeleted.Moderated && cItem.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as ChatInfo.Group?)?.groupInfo?.membership?.groupMemberId) {
|
||||
generalGetString(MR.strings.report_item_archived_by).format(cItem.meta.itemDeleted.byGroupMember.displayName)
|
||||
} else {
|
||||
generalGetString(MR.strings.report_item_archived)
|
||||
}
|
||||
}
|
||||
else when (cItem.meta.itemDeleted) {
|
||||
is CIDeleted.Moderated ->
|
||||
String.format(generalGetString(MR.strings.moderated_item_description), cItem.meta.itemDeleted.byGroupMember.displayName)
|
||||
|
||||
+26
-28
@@ -10,24 +10,17 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.markChatTagRead
|
||||
import chat.simplex.common.model.ChatModel.updateChatTagRead
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
@@ -209,27 +202,27 @@ suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat
|
||||
|
||||
suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId)
|
||||
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(rhId, ChatType.Group, groupId)
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, contentTag: MsgContentTag? = null) = openChat(rhId, ChatType.Group, groupId, contentTag)
|
||||
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentTag: MsgContentTag? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentTag)
|
||||
|
||||
private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long) =
|
||||
apiLoadMessages(rhId, chatType, apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long, contentTag: MsgContentTag? = null) =
|
||||
apiLoadMessages(rhId, chatType, apiId, contentTag, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
|
||||
suspend fun openLoadedChat(chat: Chat) {
|
||||
withChats {
|
||||
chatModel.chatItemStatuses.clear()
|
||||
suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) {
|
||||
withChats(contentTag) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
chatModel.chatState.clear()
|
||||
chatModel.chatStateForContent(contentTag).clear()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiFindMessages(ch: Chat, search: String) {
|
||||
withChats {
|
||||
suspend fun apiFindMessages(ch: Chat, search: String, contentTag: MsgContentTag?) {
|
||||
withChats(contentTag) {
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState, search = search)
|
||||
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentTag, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search = search)
|
||||
}
|
||||
|
||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
|
||||
@@ -255,7 +248,7 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
|
||||
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
|
||||
if (contact.activeConn != null) {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
MarkReadChatAction(chat, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
@@ -295,7 +288,7 @@ fun GroupMenuItems(
|
||||
}
|
||||
else -> {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
MarkReadChatAction(chat, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
@@ -316,7 +309,7 @@ fun GroupMenuItems(
|
||||
@Composable
|
||||
fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
MarkReadChatAction(chat, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
@@ -324,12 +317,12 @@ fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState<Boolean>, showMarkRea
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun MarkReadChatAction(chat: Chat, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
onClick = {
|
||||
markChatRead(chat, chatModel)
|
||||
markChatRead(chat)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
showMenu.value = false
|
||||
}
|
||||
@@ -566,12 +559,15 @@ private fun InvalidDataView() {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
fun markChatRead(c: Chat) {
|
||||
var chat = c
|
||||
withApi {
|
||||
if (chat.chatStats.unreadCount > 0) {
|
||||
withChats {
|
||||
markChatItemsRead(chat.remoteHostId, chat.chatInfo)
|
||||
markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
|
||||
}
|
||||
chatModel.controller.apiChatRead(
|
||||
chat.remoteHostId,
|
||||
@@ -612,7 +608,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
if (success) {
|
||||
withChats {
|
||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
updateChatTagRead(chat, wasUnread)
|
||||
updateChatTagReadNoContentTag(chat, wasUnread)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -874,7 +870,9 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
|
||||
}
|
||||
val updatedChat = chatModel.getChat(chatInfo.id)
|
||||
if (updatedChat != null) {
|
||||
chatModel.updateChatTagRead(updatedChat, wasUnread)
|
||||
withChats {
|
||||
updateChatTagReadNoContentTag(updatedChat, wasUnread)
|
||||
}
|
||||
}
|
||||
val current = currentState?.value
|
||||
if (current != null) {
|
||||
|
||||
+20
-8
@@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES }
|
||||
enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES }
|
||||
|
||||
sealed class ActiveFilter {
|
||||
data class PresetTag(val tag: PresetTagKind) : ActiveFilter()
|
||||
@@ -152,7 +152,7 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) {
|
||||
ModalManager.end.closeModalsExceptFirst()
|
||||
}
|
||||
AudioPlayer.stop()
|
||||
@@ -928,6 +928,8 @@ private val TAG_MIN_HEIGHT = 35.dp
|
||||
private fun TagsView(searchText: MutableState<TextFieldValue>) {
|
||||
val userTags = remember { chatModel.userTags }
|
||||
val presetTags = remember { chatModel.presetTags }
|
||||
val collapsiblePresetTags = presetTags.filter { presetCanBeCollapsed(it.key) && it.value > 0 }
|
||||
val alwaysShownPresetTags = presetTags.filter { !presetCanBeCollapsed(it.key) && it.value > 0 }
|
||||
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
val unreadTags = remember { chatModel.unreadTags }
|
||||
val rhId = chatModel.remoteHostId()
|
||||
@@ -935,13 +937,16 @@ private fun TagsView(searchText: MutableState<TextFieldValue>) {
|
||||
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
|
||||
|
||||
TagsRow {
|
||||
if (presetTags.size > 1) {
|
||||
if (presetTags.size + userTags.value.size <= 3) {
|
||||
if (collapsiblePresetTags.size > 1) {
|
||||
if (collapsiblePresetTags.size + alwaysShownPresetTags.size + userTags.value.size <= 3) {
|
||||
PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag ->
|
||||
ExpandedTagFilterView(tag)
|
||||
}
|
||||
} else {
|
||||
CollapsedTagsFilterView(searchText)
|
||||
alwaysShownPresetTags.forEach { tag ->
|
||||
ExpandedTagFilterView(tag.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,7 +1111,7 @@ private fun CollapsedTagsFilterView(searchText: MutableState<TextFieldValue>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
|
||||
val selectedPresetTag = when (val af = activeFilter.value) {
|
||||
is ActiveFilter.PresetTag -> af.tag
|
||||
is ActiveFilter.PresetTag -> if (presetCanBeCollapsed(af.tag)) af.tag else null
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -1152,7 +1157,7 @@ private fun CollapsedTagsFilterView(searchText: MutableState<TextFieldValue>) {
|
||||
)
|
||||
}
|
||||
PresetTagKind.entries.forEach { tag ->
|
||||
if ((presetTags[tag] ?: 0) > 0) {
|
||||
if ((presetTags[tag] ?: 0) > 0 && presetCanBeCollapsed(tag)) {
|
||||
ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction)
|
||||
}
|
||||
}
|
||||
@@ -1214,14 +1219,15 @@ fun filteredChats(
|
||||
|
||||
private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean =
|
||||
when (activeFilter) {
|
||||
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo)
|
||||
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo, chat.chatStats)
|
||||
is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false
|
||||
is ActiveFilter.Unread -> chat.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean =
|
||||
fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat.ChatStats): Boolean =
|
||||
when (tag) {
|
||||
PresetTagKind.GROUP_REPORTS -> chatStats.reportsCount > 0
|
||||
PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true
|
||||
PresetTagKind.CONTACTS -> when (chatInfo) {
|
||||
is ChatInfo.Direct -> !(chatInfo.contact.activeConn == null && chatInfo.contact.profile.contactLink != null && chatInfo.contact.active) && !chatInfo.contact.chatDeleted
|
||||
@@ -1246,6 +1252,7 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean =
|
||||
|
||||
private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResource, StringResource> =
|
||||
when (tag) {
|
||||
PresetTagKind.GROUP_REPORTS -> (if (active) MR.images.ic_flag_filled else MR.images.ic_flag) to MR.strings.chat_list_group_reports
|
||||
PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites
|
||||
PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts
|
||||
PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups
|
||||
@@ -1253,6 +1260,11 @@ private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResou
|
||||
PresetTagKind.NOTES -> (if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed) to MR.strings.chat_list_notes
|
||||
}
|
||||
|
||||
private fun presetCanBeCollapsed(tag: PresetTagKind): Boolean = when (tag) {
|
||||
PresetTagKind.GROUP_REPORTS -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
|
||||
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
|
||||
}
|
||||
|
||||
+15
-1
@@ -175,7 +175,7 @@ fun ChatPreviewView(
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft, sp20) }
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci) to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
@@ -322,6 +322,8 @@ fun ChatPreviewView(
|
||||
} else if (cInfo is ChatInfo.Group) {
|
||||
if (progressByTimeout) {
|
||||
progressView()
|
||||
} else if (chat.chatStats.reportsCount > 0) {
|
||||
GroupReportsIcon()
|
||||
} else {
|
||||
IncognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
@@ -469,6 +471,18 @@ fun IncognitoIcon(incognito: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupReportsIcon() {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.error,
|
||||
modifier = Modifier
|
||||
.size(21.sp.toDp())
|
||||
.offset(x = 2.sp.toDp())
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String {
|
||||
return if (groupInfo.membership.memberIncognito)
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import chat.simplex.common.model.ChatController.apiDeleteChatTag
|
||||
import chat.simplex.common.model.ChatController.apiSetChatTags
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
@@ -72,11 +73,11 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode:
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
modifier = if (reorderMode) Modifier.dragContainer(dragDropState) else Modifier,
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
|
||||
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
),
|
||||
state = listState,
|
||||
verticalArrangement = if (oneHandUI.value) Arrangement.Bottom else Arrangement.Top,
|
||||
) {
|
||||
@Composable fun CreateList() {
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -534,6 +535,11 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
}
|
||||
chatModel.users.clear()
|
||||
ntfManager.cancelAllNotifications()
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ fun DefaultAppBar(
|
||||
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
||||
val modifier = if (!showSearch) {
|
||||
Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { })
|
||||
} else Modifier.imePadding()
|
||||
} else if (!onTop) Modifier.imePadding()
|
||||
else Modifier
|
||||
|
||||
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
val prefAlpha = remember { appPrefs.inAppBarsAlpha.state }
|
||||
|
||||
+28
-13
@@ -77,8 +77,19 @@ class ModalData(val keyboardCoversBar: Boolean = true) {
|
||||
val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar)
|
||||
}
|
||||
|
||||
enum class ModalViewId {
|
||||
GROUP_REPORTS
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private val modalViews = arrayListOf<Triple<Boolean, ModalData, (@Composable ModalData.(close: () -> Unit) -> Unit)>>()
|
||||
data class ModalViewHolder(
|
||||
val id: ModalViewId?,
|
||||
val animated: Boolean,
|
||||
val data: ModalData,
|
||||
val modal: @Composable ModalData.(close: () -> Unit) -> Unit
|
||||
)
|
||||
|
||||
private val modalViews = arrayListOf<ModalViewHolder>()
|
||||
private val _modalCount = mutableStateOf(0)
|
||||
val modalCount: State<Int> = _modalCount
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
@@ -88,19 +99,23 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
showCustomModal { close ->
|
||||
fun hasModalOpen(id: ModalViewId): Boolean = modalViews.any { it.id == id }
|
||||
|
||||
fun isLastModalOpen(id: ModalViewId): Boolean = modalViews.lastOrNull()?.id == id
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
showCustomModal(id = id) { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
showCustomModal { close ->
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
showCustomModal(id = id) { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showCustomModal")
|
||||
val data = ModalData(keyboardCoversBar = keyboardCoversBar)
|
||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||
@@ -111,7 +126,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
|
||||
// to prevent unneeded animation on different situations
|
||||
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
|
||||
modalViews.add(Triple(anim, data, modal))
|
||||
modalViews.add(ModalViewHolder(id, anim, data, modal))
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
|
||||
if (placement == ModalPlacement.CENTER) {
|
||||
@@ -139,7 +154,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
if (modalViews.lastOrNull()?.animated == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
|
||||
}
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
@@ -161,10 +176,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
@Composable
|
||||
fun showInView() {
|
||||
// Without animation
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.animated == false) {
|
||||
modalViews.lastOrNull()?.let {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) {
|
||||
it.modal(it.data, ::closeModal)
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -179,8 +194,8 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
}
|
||||
) {
|
||||
modalViews.getOrNull(it - 1)?.let {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) {
|
||||
it.modal(it.data, ::closeModal)
|
||||
}
|
||||
}
|
||||
// This is needed because if we delete from modalViews immediately on request, animation will be bad
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
|
||||
withChats {
|
||||
updateGroup(rhId = rhId, groupInfo)
|
||||
chatItems.clearAndNotify()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
}
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<string name="report_item_visibility_submitter">Only you and moderators see it</string>
|
||||
<string name="report_item_visibility_moderators">Only sender and moderators see it</string>
|
||||
<string name="report_item_archived">archived report</string>
|
||||
<string name="report_item_archived_by">archived report by %s</string>
|
||||
<string name="blocked_item_description">blocked</string>
|
||||
<string name="blocked_by_admin_item_description">blocked by admin</string>
|
||||
<string name="blocked_items_description">%d messages blocked</string>
|
||||
@@ -440,8 +441,13 @@
|
||||
<string name="chat_list_groups">Groups</string>
|
||||
<string name="chat_list_businesses">Businesses</string>
|
||||
<string name="chat_list_notes">Notes</string>
|
||||
<string name="chat_list_group_reports">Reports</string>
|
||||
<string name="chat_list_all">All</string>
|
||||
<string name="chat_list_add_list">Add list</string>
|
||||
<string name="group_reports_active_one">1 report</string>
|
||||
<string name="group_reports_active">%d reports</string>
|
||||
<string name="group_reports_member_reports">Member reports</string>
|
||||
<string name="group_reports_archived_member_reports">Archived member reports</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M204-123.5v-672h337l19.13 85.5H796v363H545.5l-19-84.5h-265v308H204Z"/></svg>
|
||||
|
After Width: | Height: | Size: 192 B |
@@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
@@ -61,6 +62,9 @@ fun showApp() {
|
||||
chatModel.chatId.value = null
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.activeCall.value?.let {
|
||||
|
||||
+7
-3
@@ -36,6 +36,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
additionalTopBar: State<Boolean>,
|
||||
chatBottomBar: State<Boolean>,
|
||||
fillMaxSize: Boolean,
|
||||
content: LazyListScope.() -> Unit
|
||||
@@ -93,7 +94,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier
|
||||
Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) {
|
||||
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +109,7 @@ actual fun LazyColumnWithScrollBarNoAppBar(
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
additionalTopBar: State<Boolean>,
|
||||
chatBottomBar: State<Boolean>,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
@@ -135,7 +137,7 @@ actual fun LazyColumnWithScrollBarNoAppBar(
|
||||
val scrollBarDraggingState = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +149,13 @@ private fun ScrollBar(
|
||||
scrollJob: MutableState<Job>,
|
||||
scrollBarDraggingState: MutableState<Boolean>,
|
||||
additionalBarHeight: State<Dp>?,
|
||||
additionalTopBar: State<Boolean>,
|
||||
chatBottomBar: State<Boolean>,
|
||||
) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val topBarPadding = if (additionalTopBar.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
val padding = if (additionalBarHeight != null) {
|
||||
PaddingValues(top = if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value)
|
||||
PaddingValues(top = topBarPadding + if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value)
|
||||
} else if (reverseLayout) {
|
||||
PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user