kotlin: refactor chat contexts 2 (null secondary context, pass context instead of content tag, straighten chat state code) (#5830)

This commit is contained in:
spaced4ndy
2025-04-15 13:50:06 +00:00
committed by GitHub
parent f52d06af3a
commit 0f3e546e36
24 changed files with 462 additions and 551 deletions
@@ -80,9 +80,6 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
chatModel.chatId.value = null
chatModel.chatsContext.chatItems.clearAndNotify()
}
withContext(Dispatchers.Main) {
chatModel.chatsContext.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, contentTag = null, onComposed = onComposed)
ChatView(chatsCtx = chatModel.chatsContext, currentChatId, onComposed = onComposed)
}
}
}
@@ -393,7 +393,7 @@ fun CenterPartOfScreen() {
ModalManager.center.showInView()
}
}
else -> ChatView(currentChatId, contentTag = null) {}
else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {}
}
}
@@ -67,7 +67,7 @@ object ChatModel {
val chatId = mutableStateOf<String?>(null)
val openAroundItemId: MutableState<Long?> = mutableStateOf(null)
val chatsContext = ChatsContext(null)
val secondaryChatsContext = ChatsContext(MsgContentTag.Report)
val secondaryChatsContext = mutableStateOf<ChatsContext?>(null)
// 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
@@ -294,16 +294,15 @@ object ChatModel {
}
}
class ChatsContext(private val contentTag: MsgContentTag?) {
class ChatsContext(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 you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [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].
* If you use api call to get the items, use just [add] instead of [addToChatItems].
* 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
@@ -395,6 +394,26 @@ object ChatModel {
addChat(chat)
}
}
fun addToChatItems(index: Int, elem: ChatItem) {
chatItems.value = SnapshotStateList<ChatItem>().apply { addAll(chatItems.value); add(index, elem); chatState.itemAdded(elem.id to elem.isRcvNew) }
}
fun addToChatItems(elem: ChatItem) {
chatItems.value = SnapshotStateList<ChatItem>().apply { addAll(chatItems.value); add(elem); chatState.itemAdded(elem.id to elem.isRcvNew) }
}
fun removeLastChatItems() {
val removed: Triple<Long, Int, Boolean>
chatItems.value = SnapshotStateList<ChatItem>().apply {
addAll(chatItems.value)
val remIndex = lastIndex
val rem = removeLast()
removed = Triple(rem.id, remIndex, rem.isRcvNew)
}
chatState.itemsRemoved(listOf(removed), chatItems.value)
}
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
// mark chat non deleted
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
@@ -448,9 +467,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, contentTag)
addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
} else {
chatItems.addAndNotify(cItem, contentTag)
addToChatItems(cItem)
}
}
}
@@ -495,7 +514,7 @@ object ChatModel {
} else {
cItem
}
chatItems.addAndNotify(ci, contentTag)
addToChatItems(ci)
true
}
} else {
@@ -602,9 +621,10 @@ object ChatModel {
}
}
val popChatCollector = PopChatCollector(contentTag)
val popChatCollector = PopChatCollector(this)
class PopChatCollector(contentTag: MsgContentTag?) {
// TODO [contexts] no reason for this to be nested?
class PopChatCollector(chatsCtx: ChatsContext) {
private val subject = MutableSharedFlow<Unit>()
private var remoteHostId: Long? = null
private val chatsToPop = mutableMapOf<ChatId, Instant>()
@@ -615,7 +635,6 @@ object ChatModel {
.throttleLatest(2000)
.collect {
withContext(Dispatchers.Main) {
val chatsCtx = if (contentTag == null) chatsContext else secondaryChatsContext
chatsCtx.chats.replaceAll(popCollectedChats())
}
}
@@ -704,7 +723,7 @@ object ChatModel {
}
i--
}
chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
chatState.itemsRead(if (itemIds != null) markedReadIds else null, items)
}
return markedRead to mentionsMarkedRead
}
@@ -917,7 +936,7 @@ object ChatModel {
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withContext(Dispatchers.Main) {
chatsContext.chatItems.addAndNotify(cItem, contentTag = null)
chatsContext.addToChatItems(cItem)
}
return cItem
}
@@ -926,7 +945,7 @@ object ChatModel {
if (chatsContext.chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
withApi {
withContext(Dispatchers.Main) {
chatsContext.chatItems.removeLastAndNotify(contentTag = null)
chatsContext.removeLastChatItems()
}
}
}
@@ -1000,8 +1019,6 @@ object ChatModel {
withApi {
withContext(Dispatchers.Main) {
showingInvitation.value = null
// TODO [contexts] - why does clearAndNotify operates with listeners for both contexts?
// TODO - should it be called for both contexts here instead?
chatsContext.chatItems.clearAndNotify()
chatModel.chatId.value = withId
}
@@ -1015,7 +1032,6 @@ object ChatModel {
if (id == showingInvitation.value?.connId) {
withContext(Dispatchers.Main) {
showingInvitation.value = null
// TODO [contexts] see replaceConnReqView
chatsContext.chatItems.clearAndNotify()
chatModel.chatId.value = null
}
@@ -1067,15 +1083,6 @@ object ChatModel {
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
}
interface ChatItemsChangesListener {
// pass null itemIds if the whole chat now read
fun read(itemIds: Set<Long>?, newItems: List<ChatItem>)
fun added(item: Pair<Long, Boolean>, index: Int)
// itemId, index in old chatModel.chatItems (before the update), isRcvNew (is item unread or not)
fun removed(itemIds: List<Triple<Long, Int, Boolean>>, newItems: List<ChatItem>)
fun cleared()
}
data class ShowingInvitation(
val connId: String,
val connLink: CreatedConnLink,
@@ -2697,11 +2704,6 @@ 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, contentTag: MsgContentTag?) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatsCtx.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
}
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
value = SnapshotStateList<Chat>().apply { addAll(value); add(elem) }
}
@@ -2709,12 +2711,6 @@ fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
// For some reason, Kotlin version crashes if the list is empty
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, contentTag: MsgContentTag?) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatsCtx.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
}
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
value = SnapshotStateList<T>().apply { addAll(value); addAll(index, elems) }
}
@@ -2727,6 +2723,7 @@ fun MutableState<SnapshotStateList<Chat>>.removeAll(block: (Chat) -> Boolean) {
value = SnapshotStateList<Chat>().apply { addAll(value); removeAll(block) }
}
// TODO [contexts] operates with both contexts?
// Removes item(s) from chatItems and notifies a listener about removed item(s)
fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatItem) -> Boolean) {
val toRemove = ArrayList<Triple<Long, Int, Boolean>>()
@@ -2741,8 +2738,8 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatIte
}
}
if (toRemove.isNotEmpty()) {
chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value)
chatModel.secondaryChatsContext.chatItemsChangesListener?.removed(toRemove, value)
chatModel.chatsContext.chatState.itemsRemoved(toRemove, value)
chatModel.secondaryChatsContext.value?.chatState?.itemsRemoved(toRemove, value)
}
}
@@ -2754,18 +2751,6 @@ fun MutableState<SnapshotStateList<Chat>>.removeAt(index: Int): Chat {
return res
}
fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify(contentTag: MsgContentTag?) {
val removed: Triple<Long, Int, Boolean>
value = SnapshotStateList<ChatItem>().apply {
addAll(value)
val remIndex = lastIndex
val rem = removeLast()
removed = Triple(rem.id, remIndex, rem.isRcvNew)
}
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatsCtx.chatItemsChangesListener?.removed(listOf(removed), value)
}
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
value = SnapshotStateList<T>().apply { addAll(elems) }
}
@@ -2774,11 +2759,12 @@ fun MutableState<SnapshotStateList<Chat>>.clear() {
value = SnapshotStateList()
}
// TODO [contexts] operates with both contexts?
// Removes all chatItems and notifies a listener about it
fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() {
value = SnapshotStateList()
chatModel.chatsContext.chatItemsChangesListener?.cleared()
chatModel.secondaryChatsContext.chatItemsChangesListener?.cleared()
chatModel.chatsContext.chatState.clear()
chatModel.secondaryChatsContext.value?.chatState?.clear()
}
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
@@ -1553,7 +1553,7 @@ object ChatController {
chatModel.chatsContext.clearChat(chat.remoteHostId, updatedChatInfo)
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.clearChat(chat.remoteHostId, updatedChatInfo)
chatModel.secondaryChatsContext.value?.clearChat(chat.remoteHostId, updatedChatInfo)
}
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
close?.invoke()
@@ -2493,9 +2493,7 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
}
}
@@ -2550,10 +2548,8 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.addChatItem(rhId, cInfo, cItem)
}
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
}
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
@@ -2583,10 +2579,8 @@ object ChatController {
chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
}
}
@@ -2599,10 +2593,8 @@ object ChatController {
chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (r.reaction.chatReaction.chatItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
if (r.reaction.chatReaction.chatItem.isReport) {
chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
}
}
@@ -2646,13 +2638,11 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.isReport) {
if (toChatItem == null) {
chatModel.secondaryChatsContext.removeChatItem(rhId, cInfo, cItem)
} else {
chatModel.secondaryChatsContext.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
if (cItem.isReport) {
if (toChatItem == null) {
chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
} else {
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
}
}
@@ -2722,10 +2712,8 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (r.withMessages) {
chatModel.secondaryChatsContext.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo)
}
if (r.withMessages) {
chatModel.secondaryChatsContext.value?.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo)
}
}
}
@@ -2738,11 +2726,9 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
if (r.withMessages) {
chatModel.secondaryChatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
if (r.withMessages) {
chatModel.secondaryChatsContext.value?.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo)
}
}
}
@@ -2752,9 +2738,7 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
is CR.MemberRole ->
@@ -2763,9 +2747,7 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
is CR.MembersRoleUser ->
@@ -2776,10 +2758,8 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
r.members.forEach { member ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, member)
}
r.members.forEach { member ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, member)
}
}
}
@@ -2789,9 +2769,7 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
is CR.GroupDeleted -> // TODO update user member
@@ -3167,10 +3145,8 @@ object ChatController {
val cItem = aChatItem.chatItem
withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.upsertChatItem(rh, cInfo, cItem)
}
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
}
}
@@ -3212,8 +3188,8 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
val chatsCtx = chatModel.secondaryChatsContext
val chatsCtx = chatModel.secondaryChatsContext.value
if (chatsCtx != null) {
r.chatItemIDs.forEach { itemId ->
val cItem = chatsCtx.chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
if (chatModel.chatId.value != null) {
@@ -3240,10 +3216,8 @@ object ChatController {
} else {
val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.content.msgContent is MsgContent.MCReport) {
chatModel.secondaryChatsContext.upsertChatItem(rh, cInfo, cItem)
}
if (cItem.content.msgContent is MsgContent.MCReport) {
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
}
if (createdChat) {
@@ -3296,11 +3270,9 @@ object ChatController {
chatModel.chatsContext.popChatCollector.clear()
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.chatItems.clearAndNotify()
chatModel.secondaryChatsContext.chats.clear()
chatModel.secondaryChatsContext.popChatCollector.clear()
}
chatModel.secondaryChatsContext.value?.chatItems?.clearAndNotify()
chatModel.secondaryChatsContext.value?.chats?.clear()
chatModel.secondaryChatsContext.value?.popChatCollector?.clear()
}
}
val statuses = apiGetNetworkStatuses(rhId)
@@ -73,7 +73,7 @@ abstract class NtfManager {
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo)
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(secondaryChatsCtx = null, rhId = null, cInfo)
}
}
@@ -55,6 +55,7 @@ import java.io.File
@Composable
fun ChatInfoView(
chatsCtx: ChatModel.ChatsContext,
chatModel: ChatModel,
contact: Contact,
connectionStats: ConnectionStats?,
@@ -97,7 +98,7 @@ fun ChatInfoView(
val previousChatTTL = chatItemTTL.value
chatItemTTL.value = it
setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
connStats = connStats,
contactNetworkStatus.value,
@@ -1332,6 +1333,7 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
}
fun setChatTTLAlert(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatInfo: ChatInfo,
selectedChatTTL: MutableState<ChatItemTTL?>,
@@ -1351,7 +1353,7 @@ fun setChatTTLAlert(
} else MR.strings.enable_automatic_deletion_question),
text = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion_message else MR.strings.change_automatic_chat_deletion_message),
confirmText = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion else MR.strings.delete_messages),
onConfirm = { setChatTTL(rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) },
onConfirm = { setChatTTL(chatsCtx, rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) },
onDismiss = { selectedChatTTL.value = previousChatTTL },
onDismissRequest = { selectedChatTTL.value = previousChatTTL },
destructive = true,
@@ -1359,6 +1361,7 @@ fun setChatTTLAlert(
}
private fun setChatTTL(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatInfo: ChatInfo,
chatTTL: MutableState<ChatItemTTL?>,
@@ -1369,16 +1372,16 @@ private fun setChatTTL(
withBGApi {
try {
chatModel.controller.setChatTTL(rhId, chatInfo.chatType, chatInfo.apiId, chatTTL.value)
afterSetChatTTL(rhId, chatInfo, progressIndicator)
afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator)
} catch (e: Exception) {
chatTTL.value = previousChatTTL
afterSetChatTTL(rhId, chatInfo, progressIndicator)
afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_message_deletion), e.stackTraceToString())
}
}
}
private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) {
private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) {
try {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
@@ -1393,9 +1396,9 @@ private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressInd
}
if (chat.remoteHostId != chatModel.remoteHostId() || chat.id != chatModel.chatId.value) return
processLoadedChat(
chatsCtx,
chat,
navInfo,
contentTag = null,
pagination = pagination,
openAroundItemId = null
)
@@ -209,7 +209,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionItemView(
click = {
withBGApi {
openChat(chatRh, forwardedFromItem.chatInfo)
openChat(secondaryChatsCtx = null, chatRh, forwardedFromItem.chatInfo)
ModalManager.end.closeModals()
}
},
@@ -11,43 +11,42 @@ import kotlin.math.min
const val TRIM_KEEP_COUNT = 200
suspend fun apiLoadSingleMessage(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatType: ChatType,
apiId: Long,
itemId: Long,
contentTag: MsgContentTag?,
itemId: Long
): ChatItem? = coroutineScope {
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
chat.chatItems.firstOrNull()
}
suspend fun apiLoadMessages(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatType: ChatType,
apiId: Long,
contentTag: MsgContentTag?,
pagination: ChatPagination,
search: String = "",
openAroundItemId: Long? = null,
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) = coroutineScope {
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, pagination, search) ?: return@coroutineScope
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.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
/** When [openAroundItemId] is provided, chatId can be different too */
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
|| !isActive) return@coroutineScope
processLoadedChat(chat, navInfo, contentTag, pagination, openAroundItemId, visibleItemIndexesNonReversed)
processLoadedChat(chatsCtx, chat, navInfo, pagination, openAroundItemId, visibleItemIndexesNonReversed)
}
suspend fun processLoadedChat(
chatsCtx: ChatModel.ChatsContext,
chat: Chat,
navInfo: NavigationInfo,
contentTag: MsgContentTag?,
pagination: ChatPagination,
openAroundItemId: Long?,
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val chatState = chatsCtx.chatState
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
val oldItems = chatsCtx.chatItems.value
@@ -55,7 +54,7 @@ suspend fun processLoadedChat(
when (pagination) {
is ChatPagination.Initial -> {
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
if (contentTag == null) {
if (chatsCtx.contentTag == null) {
// update main chats, not content tagged
withContext(Dispatchers.Main) {
val oldChat = chatModel.chatsContext.getChat(chat.id)
@@ -237,24 +237,8 @@ data class ActiveChatState (
unreadAfter.value = 0
unreadAfterNewestLoaded.value = 0
}
}
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 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
return range
}
fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItemsChangesListener {
override fun read(itemIds: Set<Long>?, newItems: List<ChatItem>) {
val (_, unreadAfterItemId, _, unreadTotal, unreadAfter) = chatState
fun itemsRead(itemIds: Set<Long>?, newItems: List<ChatItem>) {
if (itemIds == null) {
// special case when the whole chat became read
unreadTotal.value = 0
@@ -287,14 +271,15 @@ fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItem
unreadTotal.value = newUnreadTotal
unreadAfter.value = newUnreadAfter
}
override fun added(item: Pair<Long, Boolean>, index: Int) {
fun itemAdded(item: Pair<Long, Boolean>) {
if (item.second) {
chatState.unreadAfter.value++
chatState.unreadTotal.value++
unreadAfter.value++
unreadTotal.value++
}
}
override fun removed(itemIds: List<Triple<Long, Int, Boolean>>, newItems: List<ChatItem>) {
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter) = chatState
fun itemsRemoved(itemIds: List<Triple<Long, Int, Boolean>>, newItems: List<ChatItem>) {
val newSplits = ArrayList<Long>()
for (split in splits.value) {
val index = itemIds.indexOfFirst { it.first == split }
@@ -343,7 +328,19 @@ fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItem
totalAfter.value -= itemIds.size
}
}
override fun cleared() { chatState.clear() }
}
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 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
return range
}
/** Helps in debugging */
@@ -57,8 +57,8 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat
// 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(
chatsCtx: ChatModel.ChatsContext,
staleChatId: State<String?>,
contentTag: MsgContentTag?,
scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) },
onComposed: suspend (chatId: String) -> Unit
) {
@@ -99,7 +99,7 @@ fun ChatView(
.distinctUntilChanged()
.filterNotNull()
.collect { chatId ->
if (contentTag == null) {
if (chatsCtx.contentTag == null) {
markUnreadChatAsRead(chatId)
}
showSearch.value = false
@@ -112,7 +112,6 @@ fun ChatView(
val chatRh = remoteHostId.value
// We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags)
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val unreadCount = remember {
derivedStateOf {
chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0
@@ -121,7 +120,6 @@ fun ChatView(
val clipboard = LocalClipboardManager.current
CompositionLocalProvider(
LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false),
LocalContentTag provides contentTag
) {
when (chatInfo) {
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
@@ -135,15 +133,16 @@ fun ChatView(
val sameText = searchText.value == value
// showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it
// (required on Android to have this check to prevent call to search with old text)
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && contentTag == null
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.contentTag == null
val c = chatModel.getChat(chatInfo.id)
if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
withBGApi {
apiFindMessages(c, value, contentTag)
apiFindMessages(chatsCtx, c, value)
searchText.value = value
}
}
ChatLayout(
chatsCtx = chatsCtx,
remoteHostId = remoteHostId,
chatInfo = activeChatInfo,
unreadCount,
@@ -175,7 +174,7 @@ fun ChatView(
}
} else {
SelectedItemsButtonsToolbar(
contentTag = contentTag,
chatsCtx = chatsCtx,
selectedChatItems = selectedChatItems,
chatInfo = chatInfo,
deleteItems = { canDeleteForAll ->
@@ -287,7 +286,7 @@ fun ChatView(
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
preloadedCode = code
}
ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) {
ChatInfoView(chatsCtx, chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) {
showSearch.value = true
}
} else if (chatInfo is ChatInfo.Group) {
@@ -297,7 +296,7 @@ fun ChatView(
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, {
GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, {
link = it
preloadedLink = it
}, close, { showSearch.value = true })
@@ -344,7 +343,7 @@ fun ChatView(
setGroupMembers(chatRh, groupInfo, chatModel)
if (!isActive) return@launch
if (contentTag == null) {
if (chatsCtx.contentTag == null) {
ModalManager.end.closeModals()
}
ModalManager.end.showModalCloseable(true) { close ->
@@ -358,12 +357,12 @@ fun ChatView(
val c = chatModel.getChat(chatId)
if (chatModel.chatId.value != chatId) return@ChatLayout
if (c != null) {
apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, contentTag, pagination, searchText.value, null, visibleItemIndexes)
apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes)
}
},
deleteMessage = { itemId, mode ->
withBGApi {
val toDeleteItem = reversedChatItemsStatic(contentTag).lastOrNull { it.id == itemId }
val toDeleteItem = reversedChatItemsStatic(chatsCtx).lastOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
val groupInfo = toModerate?.first
val groupMember = toModerate?.second
@@ -400,13 +399,11 @@ fun ChatView(
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (deletedChatItem.isReport) {
if (toChatItem != null) {
chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, deletedChatItem)
}
if (deletedChatItem.isReport) {
if (toChatItem != null) {
chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem)
}
}
}
@@ -524,10 +521,8 @@ fun ChatView(
chatModel.chatsContext.updateChatItem(cInfo, updatedCI)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(cInfo, updatedCI)
}
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI)
}
}
}
@@ -592,9 +587,7 @@ fun ChatView(
)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
}
chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
}
}
},
@@ -610,9 +603,7 @@ fun ChatView(
)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chatRh, chatInfo.id)
}
chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id)
}
}
},
@@ -676,6 +667,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
@Composable
fun ChatLayout(
chatsCtx: ChatModel.ChatsContext,
remoteHostId: State<Long?>,
chatInfo: State<ChatInfo?>,
unreadCount: State<Int>,
@@ -753,8 +745,7 @@ fun ChatLayout(
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
val composeViewHeight = remember { mutableStateOf(0.dp) }
val contentTag = LocalContentTag.current
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, contentTag == null)) {
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.contentTag == null)) {
val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
val oneHandUI = remember { appPrefs.oneHandUI.state }
@@ -768,7 +759,7 @@ fun ChatLayout(
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
chatsCtx, remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
@@ -791,7 +782,7 @@ fun ChatLayout(
}
}
}
if (contentTag == MsgContentTag.Report) {
if (chatsCtx.contentTag == MsgContentTag.Report) {
Column(
Modifier
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
@@ -802,7 +793,7 @@ fun ChatLayout(
AnimatedVisibility(selectedChatItems.value != null) {
if (chatInfo != null) {
SelectedItemsButtonsToolbar(
contentTag = contentTag,
chatsCtx = chatsCtx,
selectedChatItems = selectedChatItems,
chatInfo = chatInfo,
deleteItems = { _ ->
@@ -842,7 +833,7 @@ fun ChatLayout(
}
val reportsCount = reportsCount(chatInfo?.id)
if (oneHandUI.value && chatBottomBar.value) {
if (contentTag == null && reportsCount > 0) {
if (chatsCtx.contentTag == null && reportsCount > 0) {
ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports)
} else {
StatusBarBackground()
@@ -850,14 +841,14 @@ fun ChatLayout(
} else {
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
}
if (contentTag == MsgContentTag.Report) {
if (chatsCtx.contentTag == MsgContentTag.Report) {
if (oneHandUI.value) {
StatusBarBackground()
}
Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
Box {
if (selectedChatItems.value == null) {
GroupReportsAppBar(contentTag, { ModalManager.end.closeModal() }, onSearchValueChanged)
GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged)
} else {
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value)
}
@@ -868,13 +859,13 @@ fun ChatLayout(
Box {
if (selectedChatItems.value == null) {
if (chatInfo != null) {
ChatInfoToolbar(chatInfo, contentTag, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
}
} else {
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
}
}
if (contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
if (chatsCtx.contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports)
}
}
@@ -886,8 +877,8 @@ fun ChatLayout(
@Composable
fun BoxScope.ChatInfoToolbar(
chatsCtx: ChatModel.ChatsContext,
chatInfo: ChatInfo,
contentTag: MsgContentTag?,
back: () -> Unit,
info: () -> Unit,
startCall: (CallMediaType) -> Unit,
@@ -909,7 +900,7 @@ fun BoxScope.ChatInfoToolbar(
showSearch.value = false
}
}
if (appPlatform.isAndroid && contentTag == null) {
if (appPlatform.isAndroid && chatsCtx.contentTag == null) {
BackHandler(onBack = onBackClicked)
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
@@ -1148,6 +1139,7 @@ private var reportsListState: LazyListState? = null
@Composable
fun BoxScope.ChatItemsList(
chatsCtx: ChatModel.ChatsContext,
remoteHostId: Long?,
chatInfo: ChatInfo,
unreadCount: State<Int>,
@@ -1205,107 +1197,107 @@ fun BoxScope.ChatItemsList(
val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } }
val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } }
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
val contentTag = LocalContentTag.current
// not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val mergedItems = remember {
derivedStateOf {
MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState)
if (chatsCtx != null) {
val mergedItems = remember {
derivedStateOf {
MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState)
}
}
}
val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
val reportsCount = reportsCount(chatInfo.id)
val topPaddingToContent = topPaddingToContent(chatView = contentTag == null, contentTag == null && reportsCount > 0)
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
val numberOfBottomAppBars = numberOfBottomAppBars()
/** 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
* */
val maxHeightForList = rememberUpdatedState(
with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() }
)
val resetListState = remember { mutableStateOf(false) }
remember(chatModel.openAroundItemId.value) {
if (chatModel.openAroundItemId.value != null) {
closeSearch()
resetListState.value = !resetListState.value
val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
val reportsCount = reportsCount(chatInfo.id)
val topPaddingToContent = topPaddingToContent(
chatView = chatsCtx.contentTag == null,
additionalTopBar = chatsCtx.contentTag == null && reportsCount > 0
)
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
val numberOfBottomAppBars = numberOfBottomAppBars()
/** 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
* */
val maxHeightForList = rememberUpdatedState(
with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() }
)
val resetListState = remember { mutableStateOf(false) }
remember(chatModel.openAroundItemId.value) {
if (chatModel.openAroundItemId.value != null) {
closeSearch()
resetListState.value = !resetListState.value
}
}
}
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
val hoveredItemId = remember { mutableStateOf(null as Long?) }
val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) {
val openAroundItemId = chatModel.openAroundItemId.value
val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() }
val reportsState = reportsListState
if (openAroundItemId != null) {
highlightedItems.value += openAroundItemId
chatModel.openAroundItemId.value = null
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
val hoveredItemId = remember { mutableStateOf(null as Long?) }
val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) {
val openAroundItemId = chatModel.openAroundItemId.value
val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() }
val reportsState = reportsListState
if (openAroundItemId != null) {
highlightedItems.value += openAroundItemId
chatModel.openAroundItemId.value = null
}
hoveredItemId.value = null
if (reportsState != null) {
reportsListState = null
reportsState
} else if (index <= 0 || !searchValueIsEmpty.value) {
LazyListState(0, 0)
} else {
LazyListState(index + 1, -maxHeightForList.value)
}
})
SaveReportsStateOnDispose(chatsCtx, listState)
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val loadingMoreItems = remember { mutableStateOf(false) }
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) {
if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT)
ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect)
}
hoveredItemId.value = null
if (reportsState != null) {
reportsListState = null
reportsState
} else if (index <= 0 || !searchValueIsEmpty.value) {
LazyListState(0, 0)
} else {
LazyListState(index + 1, -maxHeightForList.value)
}
})
SaveReportsStateOnDispose(listState)
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val loadingMoreItems = remember { mutableStateOf(false) }
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) {
if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT)
ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect)
}
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, contentTag, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false
loadingMoreItems.value = true
withContext(NonCancellable) {
try {
loadMessages(chatId, pagination) {
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false
loadingMoreItems.value = true
withContext(NonCancellable) {
try {
loadMessages(chatId, pagination) {
visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
}
} finally {
loadingMoreItems.value = false
}
}
true
}
val remoteHostIdUpdated = rememberUpdatedState(remoteHostId)
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val scope = rememberCoroutineScope()
val scrollToItem: (Long) -> Unit = remember {
// In group reports just set the itemId to scroll to so the main ChatView will handle scrolling
if (chatsCtx.contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it }
scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages)
}
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
if (chatsCtx.contentTag == null) {
LaunchedEffect(Unit) {
snapshotFlow { scrollToItemId.value }.filterNotNull().collect {
if (appPlatform.isAndroid) {
ModalManager.end.closeModals()
}
scrollToItem(it)
scrollToItemId.value = null
}
} finally {
loadingMoreItems.value = false
}
}
true
}
SmallScrollOnNewMessage(listState, reversedChatItems)
val finishedInitialComposition = remember { mutableStateOf(false) }
NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
val remoteHostIdUpdated = rememberUpdatedState(remoteHostId)
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val scope = rememberCoroutineScope()
val scrollToItem: (Long) -> Unit = remember {
// In group reports just set the itemId to scroll to so the main ChatView will handle scrolling
if (contentTag == MsgContentTag.Report) 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, contentTag) }
if (contentTag == null) {
LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect {
if (appPlatform.isAndroid) {
ModalManager.end.closeModals()
DisposableEffectOnGone(
whenGone = {
VideoPlayerHolder.releaseAll()
}
scrollToItem(it)
scrollToItemId.value = null }
}
}
SmallScrollOnNewMessage(listState, reversedChatItems)
val finishedInitialComposition = remember { mutableStateOf(false) }
NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
DisposableEffectOnGone(
always = {
chatsCtx.chatItemsChangesListener = recalculateChatStatePositions(chatsCtx.chatState)
},
whenGone = {
VideoPlayerHolder.releaseAll()
chatsCtx.chatItemsChangesListener = recalculateChatStatePositions(chatsCtx.chatState)
}
)
)
@Composable
fun ChatViewListItem(
@@ -1350,7 +1342,7 @@ fun BoxScope.ChatItemsList(
highlightedItems.value = setOf()
}
}
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, 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(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, 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)
}
}
@@ -1487,7 +1479,7 @@ fun BoxScope.ChatItemsList(
}
} else {
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Row(
@@ -1502,7 +1494,7 @@ fun BoxScope.ChatItemsList(
}
} else {
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Box(
@@ -1517,7 +1509,7 @@ fun BoxScope.ChatItemsList(
}
} else { // direct message
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
@@ -1547,110 +1539,110 @@ fun BoxScope.ChatItemsList(
ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap)
}
}
LazyColumnWithScrollBar(
Modifier.align(Alignment.BottomCenter),
state = listState.value,
contentPadding = PaddingValues(
top = topPaddingToContent,
bottom = composeViewHeight.value
),
reverseLayout = true,
additionalBarOffset = composeViewHeight,
additionalTopBar = rememberUpdatedState(contentTag == null && reportsCount > 0),
chatBottomBar = remember { appPrefs.chatBottomBar.state }
) {
val mergedItemsValue = mergedItems.value
itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged ->
val isLastItem = index == mergedItemsValue.items.lastIndex
val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
val listItem = merged.newest()
val item = listItem.item
val range = if (merged is MergedItem.Grouped) {
merged.rangeInReversed.value
} else {
null
}
val showAvatar = shouldShowAvatar(item, listItem.nextItem)
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
val itemSeparation: ItemSeparation
val prevItemSeparationLargeGap: Boolean
if (merged is MergedItem.Single || isRevealed.value) {
val prev = listItem.prevItem
itemSeparation = getItemSeparation(item, prev)
val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
} else {
itemSeparation = getItemSeparation(item, null)
prevItemSeparationLargeGap = false
}
ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
}
if (last != null) {
// no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
DateSeparator(last.meta.itemTs)
}
if (item.isRcvNew) {
val itemIds = when (merged) {
is MergedItem.Single -> listOf(merged.item.item.id)
is MergedItem.Grouped -> merged.items.map { it.item.id }
LazyColumnWithScrollBar(
Modifier.align(Alignment.BottomCenter),
state = listState.value,
contentPadding = PaddingValues(
top = topPaddingToContent,
bottom = composeViewHeight.value
),
reverseLayout = true,
additionalBarOffset = composeViewHeight,
additionalTopBar = rememberUpdatedState(chatsCtx.contentTag == null && reportsCount > 0),
chatBottomBar = remember { appPrefs.chatBottomBar.state }
) {
val mergedItemsValue = mergedItems.value
itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged ->
val isLastItem = index == mergedItemsValue.items.lastIndex
val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
val listItem = merged.newest()
val item = listItem.item
val range = if (merged is MergedItem.Grouped) {
merged.rangeInReversed.value
} else {
null
}
val showAvatar = shouldShowAvatar(item, listItem.nextItem)
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
val itemSeparation: ItemSeparation
val prevItemSeparationLargeGap: Boolean
if (merged is MergedItem.Single || isRevealed.value) {
val prev = listItem.prevItem
itemSeparation = getItemSeparation(item, prev)
val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
} else {
itemSeparation = getItemSeparation(item, null)
prevItemSeparationLargeGap = false
}
ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
}
if (last != null) {
// no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
DateSeparator(last.meta.itemTs)
}
if (item.isRcvNew) {
val itemIds = when (merged) {
is MergedItem.Single -> listOf(merged.item.item.id)
is MergedItem.Grouped -> merged.items.map { it.item.id }
}
MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
}
MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
}
}
}
FloatingButtons(
reversedChatItems,
chatInfoUpdated,
topPaddingToContent,
topPaddingToContentPx,
contentTag,
loadingMoreItems,
loadingTopItems,
loadingBottomItems,
animatedScrollingInProgress,
mergedItems,
unreadCount,
maxHeight,
composeViewHeight,
searchValue,
markChatRead,
listState,
loadMessages
)
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState)
FloatingButtons(
chatsCtx,
reversedChatItems,
chatInfoUpdated,
topPaddingToContent,
topPaddingToContentPx,
loadingMoreItems,
loadingTopItems,
loadingBottomItems,
animatedScrollingInProgress,
mergedItems,
unreadCount,
maxHeight,
composeViewHeight,
searchValue,
markChatRead,
listState,
loadMessages
)
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState)
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.collect {
chatViewScrollState.value = it
}
}
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.filter { !it }
.collect {
if (animatedScrollingInProgress.value) {
animatedScrollingInProgress.value = false
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.collect {
chatViewScrollState.value = it
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.filter { !it }
.collect {
if (animatedScrollingInProgress.value) {
animatedScrollingInProgress.value = false
}
}
}
}
}
private suspend fun loadLastItems(chatId: State<ChatId>, contentTag: MsgContentTag?, listState: State<LazyListState>, loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>) {
private suspend fun loadLastItems(chatsCtx: ChatModel.ChatsContext, chatId: State<ChatId>, listState: State<LazyListState>, loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>) {
val lastVisible = listState.value.layoutInfo.visibleItemsInfo.lastOrNull()
val itemsCanCoverScreen = lastVisible != null && listState.value.layoutInfo.viewportEndOffset - listState.value.layoutInfo.afterContentPadding <= lastVisible.offset + lastVisible.size
if (!itemsCanCoverScreen) return
if (lastItemsLoaded(contentTag)) return
if (lastItemsLoaded(chatsCtx)) return
delay(500)
loadItems.value(chatId.value, ChatPagination.Last(ChatPagination.INITIAL_COUNT))
}
private fun lastItemsLoaded(contentTag: MsgContentTag?): Boolean {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
private fun lastItemsLoaded(chatsCtx: ChatModel.ChatsContext): Boolean {
val chatState = chatsCtx.chatState
return chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatsCtx.chatItems.value.lastOrNull()?.id
}
@@ -1715,11 +1707,11 @@ private fun NotifyChatListOnFinishingComposition(
@Composable
fun BoxScope.FloatingButtons(
chatsCtx: ChatModel.ChatsContext,
reversedChatItems: State<List<ChatItem>>,
chatInfo: State<ChatInfo>,
topPaddingToContent: Dp,
topPaddingToContentPx: State<Int>,
contentTag: MsgContentTag?,
loadingMoreItems: MutableState<Boolean>,
loadingTopItems: MutableState<Boolean>,
loadingBottomItems: MutableState<Boolean>,
@@ -1743,7 +1735,6 @@ fun BoxScope.FloatingButtons(
fun scrollToTopUnread() {
scope.launch {
tryBlockAndSetLoadingMore(loadingMoreItems) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
if (chatsCtx.chatState.splits.value.isNotEmpty()) {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
val oldSize = reversedChatItems.value.size
@@ -1806,7 +1797,7 @@ fun BoxScope.FloatingButtons(
animatedScrollingInProgress,
composeViewHeight,
onClick = {
if (loadingBottomItems.value || !lastItemsLoaded(contentTag)) {
if (loadingBottomItems.value || !lastItemsLoaded(chatsCtx)) {
requestedTopScroll.value = false
requestedBottomScroll.value = true
} else {
@@ -1881,11 +1872,11 @@ fun BoxScope.FloatingButtons(
@Composable
fun PreloadItems(
chatsCtx: ChatModel.ChatsContext,
chatId: String,
ignoreLoadingRequests: MutableSet<Long>,
loadingMoreItems: State<Boolean>,
resetListState: State<Boolean>,
contentTag: MsgContentTag?,
mergedItems: State<MergedItems>,
listState: State<LazyListState>,
remaining: Int,
@@ -1911,20 +1902,20 @@ fun PreloadItems(
snapshotFlow { listState.value.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { firstVisibleIndex ->
if (!preloadItemsBefore(firstVisibleIndex, chatId, ignoreLoadingRequests, contentTag, mergedItems, listState, remaining, loadItems)) {
preloadItemsAfter(firstVisibleIndex, chatId, contentTag, mergedItems, remaining, loadItems)
if (!preloadItemsBefore(chatsCtx, firstVisibleIndex, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems)) {
preloadItemsAfter(chatsCtx, firstVisibleIndex, chatId, mergedItems, remaining, loadItems)
}
loadLastItems(chatId, contentTag, listState, loadItems)
loadLastItems(chatsCtx, chatId, listState, loadItems)
}
}
}
}
private suspend fun preloadItemsBefore(
chatsCtx: ChatModel.ChatsContext,
firstVisibleIndex: Int,
chatId: State<String>,
ignoreLoadingRequests: State<MutableSet<Long>>,
contentTag: MsgContentTag?,
mergedItems: State<MergedItems>,
listState: State<LazyListState>,
remaining: Int,
@@ -1933,18 +1924,18 @@ private suspend 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 = reversedChatItemsStatic(contentTag)
val items = reversedChatItemsStatic(chatsCtx)
if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining) {
lastIndexToLoadFrom = items.lastIndex
}
if (lastIndexToLoadFrom != null) {
val loadFromItemId = items.getOrNull(lastIndexToLoadFrom)?.id ?: return false
if (!ignoreLoadingRequests.value.contains(loadFromItemId)) {
val items = reversedChatItemsStatic(contentTag)
val items = reversedChatItemsStatic(chatsCtx)
val sizeWas = items.size
val oldestItemIdWas = items.lastOrNull()?.id
val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT))
val itemsUpdated = reversedChatItemsStatic(contentTag)
val itemsUpdated = reversedChatItemsStatic(chatsCtx)
if (triedToLoad && sizeWas == itemsUpdated.size && oldestItemIdWas == itemsUpdated.lastOrNull()?.id) {
ignoreLoadingRequests.value.add(loadFromItemId)
return false
@@ -1956,14 +1947,14 @@ private suspend fun preloadItemsBefore(
}
private suspend fun preloadItemsAfter(
chatsCtx: ChatModel.ChatsContext,
firstVisibleIndex: Int,
chatId: State<String>,
contentTag: MsgContentTag?,
mergedItems: State<MergedItems>,
remaining: Int,
loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>,
) {
val items = reversedChatItemsStatic(contentTag)
val items = reversedChatItemsStatic(chatsCtx)
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)
@@ -2129,11 +2120,10 @@ private fun FloatingDate(
}
@Composable
private fun SaveReportsStateOnDispose(listState: State<LazyListState>) {
val contentTag = LocalContentTag.current
private fun SaveReportsStateOnDispose(chatsCtx: ChatModel.ChatsContext, listState: State<LazyListState>) {
DisposableEffect(Unit) {
onDispose {
reportsListState = if (contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) listState.value else null
reportsListState = if (chatsCtx.contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) listState.value else null
}
}
}
@@ -2244,10 +2234,8 @@ fun reportsCount(staleChatId: String?): Int {
}
}
private fun reversedChatItemsStatic(contentTag: MsgContentTag?): List<ChatItem> {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
return chatsCtx.chatItems.value.asReversed()
}
private fun reversedChatItemsStatic(chatsCtx: ChatModel.ChatsContext): List<ChatItem> =
chatsCtx.chatItems.value.asReversed()
private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State<Int>, mergedItems: State<MergedItems>, listState: State<LazyListState>): ListItem? {
val lastFullyVisibleOffset = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value
@@ -2323,22 +2311,20 @@ private fun scrollToItem(
}
private fun findQuotedItemFromItem(
chatsCtx: ChatModel.ChatsContext,
rhId: State<Long?>,
chatInfo: State<ChatInfo>,
scope: CoroutineScope,
scrollToItem: (Long) -> Unit,
contentTag: MsgContentTag?
scrollToItem: (Long) -> Unit
): (Long) -> Unit = { itemId: Long ->
scope.launch(Dispatchers.Default) {
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag)
val item = apiLoadSingleMessage(chatsCtx, rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId)
if (item != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateChatItem(chatInfo.value, item)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateChatItem(chatInfo.value, item)
}
chatModel.secondaryChatsContext.value?.updateChatItem(chatInfo.value, item)
}
if (item.quotedItem?.itemId != null) {
scrollToItem(item.quotedItem.itemId)
@@ -2533,15 +2519,13 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
for (di in deleted) {
if (di.deletedChatItem.chatItem.isReport) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
for (di in deleted) {
if (di.deletedChatItem.chatItem.isReport) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
@@ -2577,15 +2561,13 @@ private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
for (di in deleted) {
if (di.deletedChatItem.chatItem.isReport) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
for (di in deleted) {
if (di.deletedChatItem.chatItem.isReport) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
@@ -2938,6 +2920,7 @@ fun PreviewChatLayout() {
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
val searchValue = remember { mutableStateOf("") }
ChatLayout(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
remoteHostId = remember { mutableStateOf(null) },
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
unreadCount = unreadCount,
@@ -3015,6 +2998,7 @@ fun PreviewGroupChatLayout() {
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
val searchValue = remember { mutableStateOf("") }
ChatLayout(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
remoteHostId = remember { mutableStateOf(null) },
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
unreadCount = unreadCount,
@@ -15,7 +15,6 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.group.LocalContentTag
import chat.simplex.common.views.helpers.*
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.res.MR
@@ -60,8 +59,8 @@ private fun SelectAllButton(onClick: () -> Unit) {
@Composable
fun SelectedItemsButtonsToolbar(
chatsCtx: ChatModel.ChatsContext,
chatInfo: ChatInfo,
contentTag: MsgContentTag?,
selectedChatItems: MutableState<Set<Long>?>,
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
archiveItems: () -> Unit,
@@ -122,7 +121,6 @@ fun SelectedItemsButtonsToolbar(
}
Divider(Modifier.align(Alignment.TopStart))
}
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val chatItems = remember { derivedStateOf { chatsCtx.chatItems.value } }
LaunchedEffect(chatInfo, chatItems.value, selectedChatItems.value) {
recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
@@ -65,9 +65,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, member)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, member)
}
} else {
break
@@ -51,6 +51,7 @@ val MEMBER_ROW_VERTICAL_PADDING = 8.dp
@Composable
fun ModalData.GroupChatInfoView(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatId: String,
groupLink: CreatedConnLink?,
@@ -92,7 +93,7 @@ fun ModalData.GroupChatInfoView(
val previousChatTTL = chatItemTTL.value
chatItemTTL.value = it
setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
activeSortedMembers = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
@@ -971,10 +972,8 @@ fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSu
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, updatedMember)
}
updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember)
}
}
onSuccess()
@@ -66,9 +66,7 @@ fun GroupMemberInfoView(
chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
@@ -88,7 +86,7 @@ fun GroupMemberInfoView(
getContactChat = { chatModel.getContactChat(it) },
openDirectChat = {
withBGApi {
apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
apiLoadMessages(chatModel.chatsContext, rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
if (chatModel.getContactChat(it) != null) {
closeAll()
}
@@ -154,9 +152,7 @@ fun GroupMemberInfoView(
chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
@@ -173,9 +169,7 @@ fun GroupMemberInfoView(
chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
@@ -195,9 +189,7 @@ fun GroupMemberInfoView(
chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
@@ -223,9 +215,7 @@ fun GroupMemberInfoView(
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, copy)
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, copy)
}
r
}
@@ -262,10 +252,8 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
removedMembers.forEach { removedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, removedMember)
}
removedMembers.forEach { removedMember ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember)
}
}
}
@@ -714,10 +702,8 @@ fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInf
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
members.forEach { member ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, member)
}
members.forEach { member ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, member)
}
}
onSuccess()
@@ -815,9 +801,7 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
chatModel.chatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
}
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
}
}
}
@@ -878,10 +862,8 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, bloc
}
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, gInfo, updatedMember)
}
updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, updatedMember)
}
}
onSuccess()
@@ -14,16 +14,14 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.*
val LocalContentTag: ProvidableCompositionLocal<MsgContentTag?> = staticCompositionLocalOf { null }
@Composable
private fun GroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
ChatView(staleChatId, contentTag = MsgContentTag.Report, scrollToItemId, onComposed = {})
private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {})
}
@Composable
fun GroupReportsAppBar(
contentTag: MsgContentTag?,
chatsCtx: ChatModel.ChatsContext,
close: () -> Unit,
onSearchValueChanged: (String) -> Unit
) {
@@ -51,11 +49,11 @@ fun GroupReportsAppBar(
}
}
)
ItemsReload(contentTag)
ItemsReload(chatsCtx)
}
@Composable
private fun ItemsReload(contentTag: MsgContentTag?) {
private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
LaunchedEffect(Unit) {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
@@ -65,18 +63,19 @@ private fun ItemsReload(contentTag: MsgContentTag?) {
.filterNotNull()
.filter { it.chatInfo is ChatInfo.Group }
.collect { chat ->
reloadItems(chat, contentTag)
reloadItems(chatsCtx, chat)
}
}
}
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
openChat(chatModel.remoteHostId(), chatInfo, MsgContentTag.Report)
val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report)
openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo)
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { 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)
GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId)
} else {
LaunchedEffect(Unit) {
close()
@@ -86,6 +85,6 @@ suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: Mu
}
}
private suspend fun reloadItems(chat: Chat, contentTag: MsgContentTag?) {
apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentTag, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
private suspend fun reloadItems(chatsCtx: ChatModel.ChatsContext, chat: Chat) {
apiLoadMessages(chatsCtx, chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
}
@@ -12,12 +12,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.onRightClick
import chat.simplex.common.views.chat.group.LocalContentTag
@Composable
fun CIChatFeatureView(
chatsCtx: ChatModel.ChatsContext,
chatInfo: ChatInfo,
chatItem: ChatItem,
feature: Feature,
@@ -26,7 +25,7 @@ fun CIChatFeatureView(
revealed: State<Boolean>,
showMenu: MutableState<Boolean>,
) {
val merged = if (!revealed.value) mergedFeatures(chatItem, chatInfo) else emptyList()
val merged = if (!revealed.value) mergedFeatures(chatsCtx, chatItem, chatInfo) else emptyList()
Box(
Modifier
.combinedClickable(
@@ -73,11 +72,9 @@ private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): Feat
)
@Composable
private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<FeatureInfo>? {
val m = ChatModel
private fun mergedFeatures(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo): List<FeatureInfo>? {
val fs: ArrayList<FeatureInfo> = arrayListOf()
val icons: MutableSet<PainterBox> = mutableSetOf()
val chatsCtx = if (LocalContentTag.current == null) m.chatsContext else m.secondaryChatsContext
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
if (i != null) {
@@ -29,7 +29,6 @@ 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.chatlist.openChat
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -63,6 +62,7 @@ data class ChatItemReactionMenuItem (
@Composable
fun ChatItemView(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
cInfo: ChatInfo,
cItem: ChatItem,
@@ -277,7 +277,7 @@ fun ChatItemView(
if (searchIsNotBlank.value) {
GoToItemInnerButton(alignStart, MR.images.ic_search, 17.dp, parentActivated) {
withBGApi {
openChat(rhId, cInfo.chatType, cInfo.apiId, null, cItem.id)
openChat(secondaryChatsCtx = null, rhId, cInfo.chatType, cInfo.apiId, cItem.id)
closeReportsIfNeeded()
}
}
@@ -285,7 +285,7 @@ fun ChatItemView(
GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, 22.dp, parentActivated) {
val (chatType, apiId, msgId) = chatTypeApiIdMsgId
withBGApi {
openChat(rhId, chatType, apiId, null, msgId)
openChat(secondaryChatsCtx = null, rhId, chatType, apiId, msgId)
closeReportsIfNeeded()
}
}
@@ -364,7 +364,7 @@ fun ChatItemView(
@Composable
fun DeleteItemMenu() {
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -382,7 +382,7 @@ fun ChatItemView(
if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports)
}
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report))
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report))
Divider()
SelectItemAction(showMenu, selectChatItem)
}
@@ -472,7 +472,7 @@ fun ChatItemView(
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
}
if (!(live && cItem.meta.isLive) && !preview) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
}
if (cItem.chatDir !is CIDirection.GroupSnd) {
val groupInfo = cItem.memberToModerate(cInfo)?.first
@@ -498,7 +498,7 @@ fun ChatItemView(
ExpandItemAction(revealed, showMenu, reveal)
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -508,7 +508,7 @@ fun ChatItemView(
cItem.isDeletedContent -> {
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -522,7 +522,7 @@ fun ChatItemView(
} else {
ExpandItemAction(revealed, showMenu, reveal)
}
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -531,7 +531,7 @@ fun ChatItemView(
}
else -> {
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (selectedChatItems.value == null) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -548,7 +548,7 @@ fun ChatItemView(
RevealItemAction(revealed, showMenu, reveal)
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -560,7 +560,7 @@ fun ChatItemView(
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
MarkedDeletedItemView(chatsCtx, 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) {
@@ -582,7 +582,7 @@ fun ChatItemView(
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -631,14 +631,13 @@ fun ChatItemView(
}
@Composable fun EventItemView() {
val chatsCtx = if (LocalContentTag.current == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
CIEventView(eventItemViewText(reversedChatItems))
}
@Composable
fun DeletedItem() {
MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) {
if (revealed.value) {
HideItemAction(revealed, showMenu, reveal)
@@ -648,7 +647,7 @@ fun ChatItemView(
ExpandItemAction(revealed, showMenu, reveal)
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
@@ -729,11 +728,11 @@ fun ChatItemView(
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeature -> {
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndChatFeature -> {
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatPreference -> {
@@ -742,23 +741,23 @@ fun ChatItemView(
DeleteItemMenu()
}
is CIContent.SndChatPreference -> {
CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
CIChatFeatureView(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeatureRejected -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndModerated -> DeletedItem()
@@ -830,6 +829,7 @@ fun ItemInfoAction(
@Composable
fun DeleteItemAction(
chatsCtx: ChatModel.ChatsContext,
cItem: ChatItem,
revealed: State<Boolean>,
showMenu: MutableState<Boolean>,
@@ -838,8 +838,6 @@ fun DeleteItemAction(
deleteMessages: (List<Long>) -> Unit,
buttonText: String = stringResource(MR.strings.delete_verb),
) {
val contentTag = LocalContentTag.current
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
ItemAction(
buttonText,
painterResource(MR.images.ic_delete),
@@ -1424,6 +1422,7 @@ fun PreviewChatItemView(
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
) {
ChatItemView(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
rhId = null,
ChatInfo.Direct.sampleData,
chatItem,
@@ -1473,6 +1472,7 @@ fun PreviewChatItemView(
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
@@ -12,17 +12,15 @@ 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, chatInfo: ChatInfo, timedMessagesTTL: Int?, revealed: State<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
fun MarkedDeletedItemView(chatsCtx: ChatModel.ChatsContext, ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: Int?, revealed: State<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = MaterialTheme.appColors.receivedMessage
Surface(
@@ -35,7 +33,7 @@ fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: In
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.weight(1f, false)) {
MergedMarkedDeletedText(ci, chatInfo, revealed)
MergedMarkedDeletedText(chatsCtx, ci, chatInfo, revealed)
}
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
@@ -43,8 +41,7 @@ fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: In
}
@Composable
private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) {
val chatsCtx = if (LocalContentTag.current == null) chatModel.chatsContext else chatModel.secondaryChatsContext
private fun MergedMarkedDeletedText(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) {
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
val ciCategory = chatItem.mergeCategory
@@ -189,7 +189,7 @@ fun ErrorChatListItem() {
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
else -> openChat(rhId, ChatInfo.Direct(contact))
else -> openDirectChat(rhId, contact.contactId)
}
}
@@ -197,30 +197,33 @@ suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId)
else -> openChat(rhId, ChatInfo.Group(groupInfo))
else -> openGroupChat(rhId, groupInfo.groupId)
}
}
suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(rhId, ChatInfo.Local(noteFolder))
suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(secondaryChatsCtx = null, rhId, ChatInfo.Local(noteFolder))
suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId)
suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Direct, contactId)
suspend fun openGroupChat(rhId: Long?, groupId: Long, contentTag: MsgContentTag? = null) = openChat(rhId, ChatType.Group, groupId, contentTag)
suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Group, groupId)
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentTag: MsgContentTag? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentTag)
suspend fun openChat(secondaryChatsCtx: ChatModel.ChatsContext?, rhId: Long?, chatInfo: ChatInfo) = openChat(secondaryChatsCtx, rhId, chatInfo.chatType, chatInfo.apiId)
suspend fun openChat(
secondaryChatsCtx: ChatModel.ChatsContext?,
rhId: Long?,
chatType: ChatType,
apiId: Long,
contentTag: MsgContentTag? = null,
openAroundItemId: Long? = null
) =
) {
if (secondaryChatsCtx != null) {
chatModel.secondaryChatsContext.value = secondaryChatsCtx
}
apiLoadMessages(
chatsCtx = secondaryChatsCtx ?: chatModel.chatsContext,
rhId,
chatType,
apiId,
contentTag,
if (openAroundItemId != null) {
ChatPagination.Around(openAroundItemId, ChatPagination.INITIAL_COUNT)
} else {
@@ -229,23 +232,22 @@ suspend fun openChat(
"",
openAroundItemId
)
}
suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) {
suspend fun openLoadedChat(chat: Chat) {
withContext(Dispatchers.Main) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatsCtx.chatItemStatuses.clear()
chatsCtx.chatItems.replaceAll(chat.chatItems)
chatModel.chatsContext.chatItemStatuses.clear()
chatModel.chatsContext.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
chatsCtx.chatState.clear()
chatModel.chatsContext.chatState.clear()
}
}
suspend fun apiFindMessages(ch: Chat, search: String, contentTag: MsgContentTag?) {
suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) {
withContext(Dispatchers.Main) {
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatsCtx.chatItems.clearAndNotify()
}
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentTag, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search)
apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search)
}
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
@@ -608,9 +610,7 @@ fun markChatRead(c: Chat) {
chatModel.chatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
}
chatModel.secondaryChatsContext.value?.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
}
chatModel.controller.apiChatRead(
chat.remoteHostId,
@@ -55,13 +55,13 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
when (contactType) {
ContactType.RECENT -> {
withApi {
openChat(rhId, chat.chatInfo)
openChat(secondaryChatsCtx = null, rhId, chat.chatInfo)
ModalManager.start.closeModals()
}
}
ContactType.CHAT_DELETED -> {
withApi {
openChat(rhId, chat.chatInfo)
openChat(secondaryChatsCtx = null, rhId, chat.chatInfo)
ModalManager.start.closeModals()
}
}
@@ -543,11 +543,9 @@ fun deleteChatDatabaseFilesAndState() {
chatModel.chatsContext.popChatCollector.clear()
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.chatItems.clearAndNotify()
chatModel.secondaryChatsContext.chats.clear()
chatModel.secondaryChatsContext.popChatCollector.clear()
}
chatModel.secondaryChatsContext.value?.chatItems?.clearAndNotify()
chatModel.secondaryChatsContext.value?.chats?.clear()
chatModel.secondaryChatsContext.value?.popChatCollector?.clear()
}
}
chatModel.users.clear()
@@ -161,13 +161,20 @@ class ModalManager(private val placement: ModalPlacement? = null) {
fun closeModal() {
if (modalViews.isNotEmpty()) {
if (modalViews.lastOrNull()?.animated == false) modalViews.removeAt(modalViews.lastIndex)
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
val lastModal = modalViews.lastOrNull()
if (lastModal != null) {
if (lastModal.id == ModalViewId.SECONDARY_CHAT) chatModel.secondaryChatsContext.value = null
if (!lastModal.animated)
modalViews.removeAt(modalViews.lastIndex)
else
runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
}
}
_modalCount.value = modalViews.size - toRemove.size
}
fun closeModals() {
chatModel.secondaryChatsContext.value = null
modalViews.clear()
toRemove.clear()
_modalCount.value = 0
@@ -61,9 +61,7 @@ fun showApp() {
chatModel.chatsContext.chatItems.clearAndNotify()
}
withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.chatItems.clearAndNotify()
}
chatModel.secondaryChatsContext.value = null
}
}
}
@@ -51,7 +51,7 @@ private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState<
val chat = chatModel.getChat(call.contact.id)
if (chat != null) {
withBGApi {
openChat(chat.remoteHostId, chat.chatInfo)
openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo)
}
}
},
@@ -116,7 +116,7 @@ private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableSta
val chat = chatModel.getChat(call.contact.id)
if (chat != null) {
withBGApi {
openChat(chat.remoteHostId, chat.chatInfo)
openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo)
}
}
},