This commit is contained in:
Avently
2025-01-07 23:17:10 +07:00
parent ec16b010f2
commit fffab507be
14 changed files with 440 additions and 165 deletions
@@ -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, reportsView = false, onComposed)
ChatView(currentChatId, reportsView = false, onComposed = onComposed)
}
}
}
@@ -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,25 +58,21 @@ 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)
val chatsContext = ChatsContext(null)
val reportsChatsContext = ChatsContext(MsgContentTag.Report)
val chats: State<List<Chat>> = chatsContext.chats
/** 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 chatItems: State<SnapshotStateList<ChatItem>> = chatsContext.chatItems
// rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
@@ -178,6 +173,24 @@ object ChatModel {
// return true if you handled the click
var centerPanelBackgroundClickHandler: (() -> Boolean)? = null
fun chatItemsForContent(contentTag: MsgContentTag?): State<List<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 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 {
@@ -250,7 +263,7 @@ object ChatModel {
}
}
fun addPresetChatTags(chatInfo: ChatInfo) {
private fun addPresetChatTags(chatInfo: ChatInfo) {
for (tag in PresetTagKind.entries) {
if (presetTagMatchesChat(tag, chatInfo)) {
presetTags[tag] = (presetTags[tag] ?: 0) + 1
@@ -340,13 +353,29 @@ 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>())
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
// set listener here that will be notified on every add/delete of a chat item
var chatItemsChangesListener: ChatItemsChangesListener? = null
val chatState = ActiveChatState()
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)
@@ -743,8 +772,6 @@ object ChatModel {
}
}
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
val current = currentUser.value ?: return
val updated = current.copy(
@@ -816,7 +843,8 @@ object ChatModel {
}
i--
}
chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
// TODO LALAL
chatsContext.chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
}
return markedRead
}
@@ -1157,7 +1185,15 @@ 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,
// actual only via getChat(.initial), otherwise, zero
val archivedReportsCount: Int = 0,
val minUnreadItemId: Long = 0,
val unreadChat: Boolean = false
)
companion object {
val sampleData = Chat(
@@ -2519,7 +2555,7 @@ fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
}
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) }
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
}
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
@@ -2531,7 +2567,7 @@ fun <T> MutableList<T>.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmp
// 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) }
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
}
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
@@ -2560,7 +2596,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 +2617,7 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
val rem = removeLast()
removed = Triple(rem.id, remIndex, rem.isRcvNew)
}
chatItemsChangesListener?.removed(listOf(removed), value)
chatModel.chatsContext.chatItemsChangesListener?.removed(listOf(removed), value)
}
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
@@ -2594,7 +2631,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()
@@ -3678,6 +3716,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
@@ -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.*
@@ -896,8 +897,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, contentFilter: ContentFilter? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
val r = sendCmd(rh, CC.ApiGetChat(type, id, contentFilter, 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) {
@@ -1500,6 +1501,9 @@ object ChatController {
withChats {
clearChat(chat.remoteHostId, updatedChatInfo)
}
withChats(MsgContentTag.Report) {
clearChat(chat.remoteHostId, updatedChatInfo)
}
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
close?.invoke()
}
@@ -2427,6 +2431,9 @@ object ChatController {
withChats {
upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
withReportsChatsIfOpen {
upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
}
}
is CR.ContactsMerged -> {
@@ -2476,6 +2483,11 @@ object ChatController {
withChats {
addChatItem(rhId, cInfo, cItem)
}
withReportsChatsIfOpen {
if (cItem.content.msgContent is MsgContent.MCReport) {
addChatItem(rhId, cInfo, cItem)
}
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
chatModel.increaseUnreadCounter(rhId, r.user)
}
@@ -2500,6 +2512,11 @@ object ChatController {
withChats {
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
withReportsChatsIfOpen {
if (cItem.content.msgContent is MsgContent.MCReport) {
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
}
}
}
is CR.ChatItemUpdated ->
@@ -2509,6 +2526,11 @@ object ChatController {
withChats {
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
withReportsChatsIfOpen {
if (r.reaction.chatReaction.chatItem.content.msgContent is MsgContent.MCReport) {
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
}
}
}
is CR.ChatItemsDeleted -> {
@@ -2544,6 +2566,15 @@ object ChatController {
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
}
withReportsChatsIfOpen {
if (cItem.content.msgContent is MsgContent.MCReport) {
if (toChatItem == null) {
removeChatItem(rhId, cInfo, cItem)
} else {
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
}
}
}
}
is CR.ReceivedGroupInvitation -> {
@@ -2603,42 +2634,63 @@ object ChatController {
withChats {
updateGroup(rhId, r.groupInfo)
}
withReportsChatsIfOpen {
updateGroup(rhId, r.groupInfo)
}
}
is CR.DeletedMember ->
if (active(r.user)) {
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)) {
withChats {
updateGroup(rhId, r.groupInfo)
}
withReportsChatsIfOpen {
updateGroup(rhId, r.groupInfo)
}
}
is CR.UserJoinedGroup ->
if (active(r.user)) {
@@ -2667,6 +2719,9 @@ object ChatController {
withChats {
updateGroup(rhId, r.toGroup)
}
withReportsChatsIfOpen {
updateGroup(rhId, r.toGroup)
}
}
is CR.NewMemberContactReceivedInv ->
if (active(r.user)) {
@@ -3005,6 +3060,11 @@ object ChatController {
val cInfo = aChatItem.chatInfo
val cItem = aChatItem.chatItem
withChats { upsertChatItem(rh, cInfo, cItem) }
withReportsChatsIfOpen {
if (cItem.content.msgContent is MsgContent.MCReport) {
upsertChatItem(rh, cInfo, cItem)
}
}
}
}
@@ -3014,10 +3074,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()
}
}
}
@@ -3062,6 +3126,11 @@ object ChatController {
chats.clear()
popChatCollector.clear()
}
withReportsChatsIfOpen {
chatItems.clearAndNotify()
chats.clear()
popChatCollector.clear()
}
}
val statuses = apiGetNetworkStatuses(rhId)
if (statuses != null) {
@@ -3204,7 +3273,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 contentFilter: ContentFilter?, 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()
@@ -3366,7 +3435,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 filter = if (contentFilter == null) {
""
} else {
" content=${contentFilter.mcTag.name.lowercase()} deleted=${onOff(contentFilter.deleted ?: true)}"
}
"/_get chat ${chatRef(type, id)}$filter ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
}
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiSendMessages -> {
val msgs = json.encodeToString(composedMessages)
@@ -3703,6 +3779,11 @@ data class NewUser(
val pastTimestamp: Boolean
)
data class ContentFilter(
val mcTag: MsgContentTag,
val deleted: Boolean?
)
sealed class ChatPagination {
class Last(val count: Int): ChatPagination()
class After(val chatItemId: Long, val count: Int): ChatPagination()
@@ -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
@@ -54,6 +55,10 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
// show "in progress"
// TODO show active remote host in chat console?
chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s))
// LALAL
// for (i in 201..210) {
// chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console("/_report #5 5965 reason=community $i"))
// }
composeState.value = ComposeState(useLinkPreviews = false)
// hide "in progress"
}
@@ -14,9 +14,11 @@ 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 contentFilter = if (contentTag != null) ContentFilter(contentTag, true) else null
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentFilter, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
chat.chatItems.firstOrNull()
}
@@ -24,28 +26,32 @@ suspend fun apiLoadMessages(
rhId: Long?,
chatType: ChatType,
apiId: Long,
contentFilter: ContentFilter?,
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, contentFilter, 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(contentFilter?.mcTag)
val contentTag = contentFilter?.mcTag
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
val oldItems = chatModel.chatItems.value
val oldItems = chatModel.chatItemsForContent(contentFilter?.mcTag).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) {
withChats(contentTag) {
if (getChat(chat.id) == null) {
addChat(chat)
} else {
updateChatInfo(chat.remoteHostId, chat.chatInfo)
}
}
withChats {
withChats(contentTag) {
chatModel.chatItemStatuses.clear()
chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
@@ -70,7 +76,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 +95,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 +110,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 +125,7 @@ suspend fun apiLoadMessages(
newItems.addAll(oldItems)
removeDuplicates(newItems, chat)
newItems.addAll(chat.chatItems)
withChats {
withChats(contentTag) {
chatItems.replaceAll(newItems)
unreadAfterNewestLoaded.value = 0
}
@@ -33,6 +33,7 @@ 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 +58,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?>, reportsView: Boolean, 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) {
@@ -70,7 +78,11 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
}
} else {
val showArchivedReports = remember { mutableStateOf(false) }
val groupReports = remember { derivedStateOf { GroupReports((activeChatInfo.value as? ChatInfo.Group)?.apiId?.toInt() ?: 0, reportsView, showArchivedReports.value) } }
val groupReports = remember { derivedStateOf {
val reportsCount = if (activeChatInfo.value is ChatInfo.Group) activeChatStats.value?.reportsCount ?: 0 else 0
GroupReports(reportsCount, reportsView, showArchivedReports.value) }
}
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()) {
@@ -126,13 +138,14 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged
if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
withBGApi {
apiFindMessages(c, value)
apiFindMessages(c, value, groupReports.value.toContentFilter())
searchText.value = value
}
}
ChatLayout(
remoteHostId = remoteHostId,
chatInfo = activeChatInfo,
reversedChatItems = reversedChatItems,
unreadCount,
composeState,
composeView = {
@@ -161,7 +174,7 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
}
} else {
SelectedItemsBottomToolbar(
chatItems = remember { chatModel.chatItems }.value,
reversedChatItems = reversedChatItems,
selectedChatItems = selectedChatItems,
chatInfo = chatInfo,
deleteItems = { canDeleteForAll ->
@@ -223,6 +236,8 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
}
},
groupReports,
showArchivedReports,
scrollToItemId,
attachmentOption,
attachmentBottomSheetState,
searchText,
@@ -291,28 +306,23 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
}
},
showGroupReports = {
val info = activeChatInfo.value ?: return@ChatLayout
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
return@ChatLayout
}
hideKeyboard(view)
ModalManager.end.showCustomModal(true) { close ->
ModalView({}, showAppBar = false) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val chatInfo = remember { activeChatInfo }.value
if (chatInfo is ChatInfo.Group) {
GroupReportsView(staleChatId)
if (oneHandUI.value) {
StatusBarBackground()
}
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
GroupReportsAppBar(groupReports, close, showArchived = { showHide ->
showArchivedReports.value = showHide
}, onSearchValueChanged)
}
} else {
LaunchedEffect(Unit) {
close()
scope.launch {
openChat(chatModel.remoteHostId(), info, ContentFilter(MsgContentTag.Report, false))
ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close ->
ModalView({}, showAppBar = false) {
val chatInfo = remember { activeChatInfo }.value
if (chatInfo is ChatInfo.Group) {
GroupReportsView(staleChatId, scrollToItemId)
} else {
LaunchedEffect(Unit) {
close()
}
}
}
}
@@ -341,16 +351,16 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
}
}
},
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.toContentFilter(), 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
@@ -382,6 +392,13 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
removeChatItem(chatRh, chatInfo, deletedChatItem)
}
}
withReportsChatsIfOpen {
if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
removeChatItem(chatRh, chatInfo, deletedChatItem)
}
}
}
}
},
@@ -556,6 +573,12 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
itemsIds
)
}
withReportsChatsIfOpen {
// 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)
}
}
}
},
markChatRead = {
@@ -572,6 +595,12 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
chatInfo.apiId
)
}
withReportsChatsIfOpen {
// 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)
}
}
}
},
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
@@ -632,10 +661,13 @@ 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>,
showArchivedReports: MutableState<Boolean>,
scrollToItemId: MutableState<Long?>,
attachmentOption: MutableState<AttachmentOption?>,
attachmentBottomSheetState: ModalBottomSheetState,
searchValue: State<String>,
@@ -646,7 +678,7 @@ fun ChatLayout(
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,
@@ -718,8 +750,8 @@ fun ChatLayout(
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, groupReports, 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,
@@ -773,6 +805,16 @@ fun ChatLayout(
GroupReportsToolbar(groupReports, withStatusBar = false, showGroupReports)
}
}
if (groupReports.value.reportsView) {
if (oneHandUI.value) {
StatusBarBackground()
}
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
GroupReportsAppBar(groupReports, { ModalManager.end.closeModal() }, showArchived = { showHide ->
showArchivedReports.value = showHide
}, onSearchValueChanged)
}
}
}
}
}
@@ -1017,7 +1059,7 @@ private fun GroupReportsToolbar(
) {
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
Spacer(Modifier.width(4.dp))
val reports = groupReports.value.activeReports
val reports = groupReports.value.reportsCount
Text(
if (reports == 1) {
stringResource(MR.strings.group_reports_active_one)
@@ -1040,6 +1082,7 @@ private fun ContactVerifiedShield() {
fun BoxScope.ChatItemsList(
remoteHostId: Long?,
chatInfo: ChatInfo,
reversedChatItems: State<List<ChatItem>>,
unreadCount: State<Int>,
composeState: MutableState<ComposeState>,
composeViewHeight: State<Dp>,
@@ -1047,10 +1090,11 @@ fun BoxScope.ChatItemsList(
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,
@@ -1075,9 +1119,8 @@ 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 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
@@ -1098,11 +1141,11 @@ fun BoxScope.ChatItemsList(
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
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) {
loadMessages(chatId, pagination) {
visibleItemIndexesNonReversed(mergedItems, listState.value)
}
} finally {
@@ -1116,21 +1159,27 @@ 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 { 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)
}
)
@@ -1152,7 +1201,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),
@@ -1177,7 +1226,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)
}
}
@@ -1363,7 +1412,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)
})
}
}
@@ -1448,14 +1497,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.toContentFilter(), ChatPagination.Last(ChatPagination.INITIAL_COUNT))
}
} finally {
loadingMoreItems.value = false
@@ -1464,20 +1513,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 {
@@ -1488,7 +1537,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
* */
@@ -1618,6 +1667,7 @@ fun BoxScope.FloatingButtons(
fun PreloadItems(
chatId: String,
ignoreLoadingRequests: MutableSet<Long>,
reversedChatItems: State<List<ChatItem>>,
mergedItems: State<MergedItems>,
listState: State<LazyListState>,
remaining: Int,
@@ -1628,8 +1678,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
@@ -1637,6 +1687,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,
@@ -1649,7 +1700,7 @@ 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 && items.size >= ChatPagination.INITIAL_COUNT) {
lastIndexToLoadFrom = items.lastIndex
}
@@ -1663,10 +1714,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)
}
}
@@ -1678,6 +1729,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,
@@ -1698,12 +1750,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
}
@@ -1984,7 +2036,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 {
@@ -1998,7 +2050,7 @@ 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) {
loadMessages(chatInfo.value.id, pagination) {
visibleItemIndexesNonReversed(mergedItems, listState.value)
}
}
@@ -2030,14 +2082,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 {
@@ -2134,7 +2190,13 @@ 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)
@@ -2143,9 +2205,9 @@ private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: Stat
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
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)
@@ -2195,6 +2257,16 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
}
}
}
withReportsChatsIfOpen {
for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
onSuccess()
}
}
@@ -2217,6 +2289,10 @@ private fun markUnreadChatAsRead(chatId: String) {
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat)
}
withReportsChatsIfOpen {
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat)
}
}
}
}
@@ -2531,10 +2607,13 @@ 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)) },
showArchivedReports = remember { mutableStateOf(false) },
scrollToItemId = remember { mutableStateOf(null) },
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
searchValue,
@@ -2545,7 +2624,7 @@ fun PreviewChatLayout() {
info = {},
showGroupReports = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
deleteMessages = { _ -> },
receiveFile = { _ -> },
@@ -2606,10 +2685,13 @@ 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)) },
showArchivedReports = remember { mutableStateOf(false) },
scrollToItemId = remember { mutableStateOf(null) },
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
searchValue,
@@ -2620,7 +2702,7 @@ fun PreviewGroupChatLayout() {
info = {},
showGroupReports = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
deleteMessages = {},
receiveFile = { _ -> },
@@ -49,7 +49,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 +108,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)
}
}
@@ -83,7 +83,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()
}
@@ -1,34 +1,40 @@
package chat.simplex.common.views.chat.group
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.ChatView
import chat.simplex.common.views.chat.apiLoadMessages
import chat.simplex.common.views.chat.item.ItemAction
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.*
import kotlinx.coroutines.launch
data class GroupReports(
val activeReports: Int,
val reportsCount: Int,
val reportsView: Boolean,
val showArchived: Boolean = false
) {
val showBar: Boolean = activeReports > 0 && !reportsView
val showBar: Boolean = reportsCount > 0 && !reportsView
fun toContentFilter(): ContentFilter? {
if (!reportsView) return null
return ContentFilter(MsgContentTag.Report, deleted = showArchived)
}
val contentTag: MsgContentTag? = if (!reportsView) null else MsgContentTag.Report
}
@Composable
fun GroupReportsView(staleChatId: State<String?>) {
ChatView(staleChatId, reportsView = true, onComposed = {})
fun GroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
ChatView(staleChatId, reportsView = true, scrollToItemId, onComposed = {})
}
@Composable
@@ -71,8 +77,8 @@ fun GroupReportsAppBar(
showSearch.value = true
})
ItemAction(
if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_hide_archived) else stringResource(MR.strings.group_reports_show_archived),
painterResource(MR.images.ic_add),
if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_show_active) else stringResource(MR.strings.group_reports_show_archived),
painterResource(if (groupReports.value.showArchived) MR.images.ic_flag else MR.images.ic_inventory_2),
onClick = {
onClosedAction.value = {
showArchived(!groupReports.value.showArchived)
@@ -84,4 +90,30 @@ fun GroupReportsAppBar(
}
}
)
ItemsReload(groupReports)
}
@Composable
private fun ItemsReload(groupReports: State<GroupReports>) {
LaunchedEffect(Unit) {
snapshotFlow { groupReports.value.showArchived to chatModel.chatId.value }
.distinctUntilChanged()
.drop(1)
.map { it.second }
.filterNotNull()
.map { chatModel.getChat(it) }
.filterNotNull()
.filter { it.chatInfo is ChatInfo.Group }
.collect { chat ->
reloadItems(chat, groupReports)
}
}
}
private suspend fun reloadItems(chat: Chat, groupReports: State<GroupReports>) {
val contentFilter = groupReports.value.toContentFilter()
if (chat.chatStats.reportsCount > 0 || contentFilter?.deleted == true) {
apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
} else {
openLoadedChat(chat.copy(chatItems = emptyList()), contentTag = contentFilter?.mcTag)
}
}
@@ -10,17 +10,11 @@ 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
@@ -209,27 +203,30 @@ 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, contentFilter: ContentFilter? = null) = openChat(rhId, ChatType.Group, groupId, contentFilter)
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.chatType, chatInfo.apiId)
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentFilter: ContentFilter? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentFilter)
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, contentFilter: ContentFilter? = null) =
apiLoadMessages(rhId, chatType, apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
suspend fun openLoadedChat(chat: Chat) {
withChats {
chatModel.chatItemStatuses.clear()
suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) {
withChats(contentTag) {
// LALAL TODO MOVE item statuses to chats context
if (contentTag == null) {
chatModel.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, contentFilter: ContentFilter?) {
withChats(contentFilter?.mcTag) {
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, contentFilter, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search = search)
}
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
@@ -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()
@@ -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,21 @@ 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 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 +124,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 +152,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 +174,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 +192,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
@@ -429,7 +429,7 @@
<string name="group_reports_active">%d reports</string>
<string name="group_reports_member_reports">Member reports</string>
<string name="group_reports_show_archived">Show archived</string>
<string name="group_reports_hide_archived">Hide archived</string>
<string name="group_reports_show_active">Show active</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
@@ -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 {