mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 12:56:03 +00:00
changes
This commit is contained in:
@@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
.graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
|
||||
) Box2@{
|
||||
currentChatId.value?.let {
|
||||
ChatView(currentChatId, reportsView = false, onComposed)
|
||||
ChatView(currentChatId, reportsView = false, onComposed = onComposed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
-24
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.common.model.ChatModel.chatItemsChangesListener
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
@@ -59,25 +58,21 @@ object ChatModel {
|
||||
val ctrlInitInProgress = mutableStateOf(false)
|
||||
val dbMigrationInProgress = mutableStateOf(false)
|
||||
val incompleteInitializedDbRemoved = mutableStateOf(false)
|
||||
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
val chats: State<List<Chat>> = _chats
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
val switchingUsersAndHosts = mutableStateOf(false)
|
||||
|
||||
// current chat
|
||||
val chatId = mutableStateOf<String?>(null)
|
||||
val chatsContext = ChatsContext(null)
|
||||
val reportsChatsContext = ChatsContext(MsgContentTag.Report)
|
||||
val chats: State<List<Chat>> = chatsContext.chats
|
||||
/** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on.
|
||||
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
|
||||
* If you use api call to get the items, use just [add] instead of [addAndNotify].
|
||||
* Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */
|
||||
private val _chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
val chatItems: State<SnapshotStateList<ChatItem>> = _chatItems
|
||||
// declaration of chatsContext should be after any other variable that is directly attached to ChatsContext class, otherwise, strange crash with NullPointerException for "this" parameter in random functions
|
||||
private val chatsContext = ChatsContext()
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
var chatItemsChangesListener: ChatItemsChangesListener? = null
|
||||
val chatState = ActiveChatState()
|
||||
val chatItems: State<SnapshotStateList<ChatItem>> = chatsContext.chatItems
|
||||
// rhId, chatId
|
||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||
@@ -178,6 +173,24 @@ object ChatModel {
|
||||
// return true if you handled the click
|
||||
var centerPanelBackgroundClickHandler: (() -> Boolean)? = null
|
||||
|
||||
fun chatItemsForContent(contentTag: MsgContentTag?): State<List<ChatItem>> = when(contentTag) {
|
||||
null -> chatsContext.chatItems
|
||||
MsgContentTag.Report -> reportsChatsContext.chatItems
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun chatStateForContent(contentTag: MsgContentTag?): ActiveChatState = when(contentTag) {
|
||||
null -> chatsContext.chatState
|
||||
MsgContentTag.Report -> reportsChatsContext.chatState
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun setChatItemsChangeListenerForContent(listener: ChatItemsChangesListener?, contentTag: MsgContentTag?) = when(contentTag) {
|
||||
null -> chatsContext.chatItemsChangesListener = listener
|
||||
MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener = listener
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||
currentUser.value
|
||||
} else {
|
||||
@@ -250,7 +263,7 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresetChatTags(chatInfo: ChatInfo) {
|
||||
private fun addPresetChatTags(chatInfo: ChatInfo) {
|
||||
for (tag in PresetTagKind.entries) {
|
||||
if (presetTagMatchesChat(tag, chatInfo)) {
|
||||
presetTags[tag] = (presetTags[tag] ?: 0) + 1
|
||||
@@ -340,13 +353,29 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread
|
||||
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
|
||||
chatsContext.action()
|
||||
suspend fun <T> withChats(contentTag: MsgContentTag? = null, action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
|
||||
when {
|
||||
contentTag == null -> chatsContext.action()
|
||||
contentTag == MsgContentTag.Report -> reportsChatsContext.action()
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
class ChatsContext {
|
||||
val chats = _chats
|
||||
val chatItems = _chatItems
|
||||
suspend fun <T> withReportsChatsIfOpen(action: suspend ChatsContext.() -> T) = withContext(Dispatchers.Main) {
|
||||
if (ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) {
|
||||
reportsChatsContext.action()
|
||||
}
|
||||
}
|
||||
|
||||
class ChatsContext(private val contentTag: MsgContentTag?) {
|
||||
val chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
var chatItemsChangesListener: ChatItemsChangesListener? = null
|
||||
val chatState = ActiveChatState()
|
||||
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
suspend fun addChat(chat: Chat) {
|
||||
chats.add(index = 0, chat)
|
||||
@@ -743,8 +772,6 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
val updated = current.copy(
|
||||
@@ -816,7 +843,8 @@ object ChatModel {
|
||||
}
|
||||
i--
|
||||
}
|
||||
chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
|
||||
// TODO LALAL
|
||||
chatsContext.chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items)
|
||||
}
|
||||
return markedRead
|
||||
}
|
||||
@@ -1157,7 +1185,15 @@ data class Chat(
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
|
||||
data class ChatStats(
|
||||
val unreadCount: Int = 0,
|
||||
// actual only via getChats() and getChat(.initial), otherwise, zero
|
||||
val reportsCount: Int = 0,
|
||||
// actual only via getChat(.initial), otherwise, zero
|
||||
val archivedReportsCount: Int = 0,
|
||||
val minUnreadItemId: Long = 0,
|
||||
val unreadChat: Boolean = false
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sampleData = Chat(
|
||||
@@ -2519,7 +2555,7 @@ fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
|
||||
@@ -2531,7 +2567,7 @@ fun <T> MutableList<T>.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmp
|
||||
|
||||
// Adds item to chatItems and notifies a listener about newly added item
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
|
||||
}
|
||||
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
|
||||
@@ -2560,7 +2596,8 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatIte
|
||||
}
|
||||
}
|
||||
if (toRemove.isNotEmpty()) {
|
||||
chatItemsChangesListener?.removed(toRemove, value)
|
||||
chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value)
|
||||
chatModel.reportsChatsContext.chatItemsChangesListener?.removed(toRemove, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2580,7 +2617,7 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
|
||||
val rem = removeLast()
|
||||
removed = Triple(rem.id, remIndex, rem.isRcvNew)
|
||||
}
|
||||
chatItemsChangesListener?.removed(listOf(removed), value)
|
||||
chatModel.chatsContext.chatItemsChangesListener?.removed(listOf(removed), value)
|
||||
}
|
||||
|
||||
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
|
||||
@@ -2594,7 +2631,8 @@ fun MutableState<SnapshotStateList<Chat>>.clear() {
|
||||
// Removes all chatItems and notifies a listener about it
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() {
|
||||
value = SnapshotStateList()
|
||||
chatItemsChangesListener?.cleared()
|
||||
chatModel.chatsContext.chatItemsChangesListener?.cleared()
|
||||
chatModel.reportsChatsContext.chatItemsChangesListener?.cleared()
|
||||
}
|
||||
|
||||
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
|
||||
@@ -3678,6 +3716,17 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MsgContentTag {
|
||||
@SerialName("text") Text,
|
||||
@SerialName("link") Link,
|
||||
@SerialName("image") Image,
|
||||
@SerialName("video") Video,
|
||||
@SerialName("voice") Voice,
|
||||
@SerialName("file") File,
|
||||
@SerialName("report") Report,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class FormattedText(val text: String, val format: Format? = null) {
|
||||
// TODO make it dependent on simplexLinkMode preference
|
||||
|
||||
+89
-8
@@ -18,6 +18,7 @@ import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -896,8 +897,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentFilter: ContentFilter? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, contentFilter, pagination, search))
|
||||
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
|
||||
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||
if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) {
|
||||
@@ -1500,6 +1501,9 @@ object ChatController {
|
||||
withChats {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
withChats(MsgContentTag.Report) {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -2427,6 +2431,9 @@ object ChatController {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
@@ -2476,6 +2483,11 @@ object ChatController {
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.content.msgContent is MsgContent.MCReport) {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
@@ -2500,6 +2512,11 @@ object ChatController {
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.content.msgContent is MsgContent.MCReport) {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
@@ -2509,6 +2526,11 @@ object ChatController {
|
||||
withChats {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (r.reaction.chatReaction.chatItem.content.msgContent is MsgContent.MCReport) {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemsDeleted -> {
|
||||
@@ -2544,6 +2566,15 @@ object ChatController {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.content.msgContent is MsgContent.MCReport) {
|
||||
if (toChatItem == null) {
|
||||
removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
@@ -2603,42 +2634,63 @@ object ChatController {
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMember ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
}
|
||||
is CR.LeftMember ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRole ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRoleUser ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberBlockedForAll ->
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.GroupDeleted -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.UserJoinedGroup ->
|
||||
if (active(r.user)) {
|
||||
@@ -2667,6 +2719,9 @@ object ChatController {
|
||||
withChats {
|
||||
updateGroup(rhId, r.toGroup)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateGroup(rhId, r.toGroup)
|
||||
}
|
||||
}
|
||||
is CR.NewMemberContactReceivedInv ->
|
||||
if (active(r.user)) {
|
||||
@@ -3005,6 +3060,11 @@ object ChatController {
|
||||
val cInfo = aChatItem.chatInfo
|
||||
val cItem = aChatItem.chatItem
|
||||
withChats { upsertChatItem(rh, cInfo, cItem) }
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.content.msgContent is MsgContent.MCReport) {
|
||||
upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3014,10 +3074,14 @@ object ChatController {
|
||||
val notify = { ntfManager.notifyMessageReceived(rh, user, cInfo, cItem) }
|
||||
if (!activeUser(rh, user)) {
|
||||
notify()
|
||||
} else if (withChats { upsertChatItem(rh, cInfo, cItem) }) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
} else {
|
||||
val createdChat = withChats { upsertChatItem(rh, cInfo, cItem) }
|
||||
withReportsChatsIfOpen { if (cItem.content.msgContent is MsgContent.MCReport) { upsertChatItem(rh, cInfo, cItem) } }
|
||||
if (createdChat) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3062,6 +3126,11 @@ object ChatController {
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
}
|
||||
val statuses = apiGetNetworkStatuses(rhId)
|
||||
if (statuses != null) {
|
||||
@@ -3204,7 +3273,7 @@ sealed class CC {
|
||||
class ApiGetSettings(val settings: AppSettings): CC()
|
||||
class ApiGetChatTags(val userId: Long): CC()
|
||||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val contentFilter: ContentFilter?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||
@@ -3366,7 +3435,14 @@ sealed class CC {
|
||||
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
||||
is ApiGetChatTags -> "/_get tags $userId"
|
||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiGetChat -> {
|
||||
val filter = if (contentFilter == null) {
|
||||
""
|
||||
} else {
|
||||
" content=${contentFilter.mcTag.name.lowercase()} deleted=${onOff(contentFilter.deleted ?: true)}"
|
||||
}
|
||||
"/_get chat ${chatRef(type, id)}$filter ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
}
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
@@ -3703,6 +3779,11 @@ data class NewUser(
|
||||
val pastTimestamp: Boolean
|
||||
)
|
||||
|
||||
data class ContentFilter(
|
||||
val mcTag: MsgContentTag,
|
||||
val deleted: Boolean?
|
||||
)
|
||||
|
||||
sealed class ChatPagination {
|
||||
class Last(val count: Int): ChatPagination()
|
||||
class After(val chatItemId: Long, val count: Int): ChatPagination()
|
||||
|
||||
+5
@@ -26,6 +26,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
|
||||
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
|
||||
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -54,6 +55,10 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
// show "in progress"
|
||||
// TODO show active remote host in chat console?
|
||||
chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s))
|
||||
// LALAL
|
||||
// for (i in 201..210) {
|
||||
// chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console("/_report #5 5965 reason=community $i"))
|
||||
// }
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
// hide "in progress"
|
||||
}
|
||||
|
||||
+18
-12
@@ -14,9 +14,11 @@ suspend fun apiLoadSingleMessage(
|
||||
rhId: Long?,
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
itemId: Long
|
||||
itemId: Long,
|
||||
contentTag: MsgContentTag?,
|
||||
): ChatItem? = coroutineScope {
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
val contentFilter = if (contentTag != null) ContentFilter(contentTag, true) else null
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentFilter, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
chat.chatItems.firstOrNull()
|
||||
}
|
||||
|
||||
@@ -24,28 +26,32 @@ suspend fun apiLoadMessages(
|
||||
rhId: Long?,
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
contentFilter: ContentFilter?,
|
||||
pagination: ChatPagination,
|
||||
chatState: ActiveChatState,
|
||||
search: String = "",
|
||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||
) = coroutineScope {
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, pagination, search) ?: return@coroutineScope
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentFilter, pagination, search) ?: return@coroutineScope
|
||||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last)
|
||||
|| !isActive) return@coroutineScope
|
||||
|
||||
val chatState = chatModel.chatStateForContent(contentFilter?.mcTag)
|
||||
val contentTag = contentFilter?.mcTag
|
||||
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
|
||||
val oldItems = chatModel.chatItems.value
|
||||
val oldItems = chatModel.chatItemsForContent(contentFilter?.mcTag).value
|
||||
val newItems = SnapshotStateList<ChatItem>()
|
||||
when (pagination) {
|
||||
is ChatPagination.Initial -> {
|
||||
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
|
||||
withChats {
|
||||
if (chatModel.getChat(chat.id) == null) {
|
||||
withChats(contentTag) {
|
||||
if (getChat(chat.id) == null) {
|
||||
addChat(chat)
|
||||
} else {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo)
|
||||
}
|
||||
}
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
@@ -70,7 +76,7 @@ suspend fun apiLoadMessages(
|
||||
)
|
||||
val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0)
|
||||
newItems.addAll(insertAt, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = newSplits
|
||||
chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems)
|
||||
@@ -89,7 +95,7 @@ suspend fun apiLoadMessages(
|
||||
val indexToAdd = min(indexInCurrentItems + 1, newItems.size)
|
||||
val indexToAddIsLast = indexToAdd == newItems.size
|
||||
newItems.addAll(indexToAdd, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = newSplits
|
||||
chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems)
|
||||
@@ -104,7 +110,7 @@ suspend fun apiLoadMessages(
|
||||
val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed)
|
||||
// currently, items will always be added on top, which is index 0
|
||||
newItems.addAll(0, chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
splits.value = listOf(chat.chatItems.last().id) + newSplits
|
||||
unreadAfterItemId.value = chat.chatItems.last().id
|
||||
@@ -119,7 +125,7 @@ suspend fun apiLoadMessages(
|
||||
newItems.addAll(oldItems)
|
||||
removeDuplicates(newItems, chat)
|
||||
newItems.addAll(chat.chatItems)
|
||||
withChats {
|
||||
withChats(contentTag) {
|
||||
chatItems.replaceAll(newItems)
|
||||
unreadAfterNewestLoaded.value = 0
|
||||
}
|
||||
|
||||
+155
-73
@@ -33,6 +33,7 @@ import chat.simplex.common.model.ChatModel.activeCall
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.markChatTagRead
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
@@ -57,10 +58,17 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat
|
||||
@Composable
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
fun ChatView(
|
||||
staleChatId: State<String?>,
|
||||
reportsView: Boolean,
|
||||
scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) },
|
||||
onComposed: suspend (chatId: String) -> Unit
|
||||
) {
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
// They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..."
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } }
|
||||
val activeChatStats = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats } }
|
||||
val user = chatModel.currentUser.value
|
||||
val chatInfo = activeChatInfo.value
|
||||
if (chatInfo == null || user == null) {
|
||||
@@ -70,7 +78,11 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
}
|
||||
} else {
|
||||
val showArchivedReports = remember { mutableStateOf(false) }
|
||||
val groupReports = remember { derivedStateOf { GroupReports((activeChatInfo.value as? ChatInfo.Group)?.apiId?.toInt() ?: 0, reportsView, showArchivedReports.value) } }
|
||||
val groupReports = remember { derivedStateOf {
|
||||
val reportsCount = if (activeChatInfo.value is ChatInfo.Group) activeChatStats.value?.reportsCount ?: 0 else 0
|
||||
GroupReports(reportsCount, reportsView, showArchivedReports.value) }
|
||||
}
|
||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(groupReports.value.contentTag).value.asReversed() } }
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
@@ -126,13 +138,14 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged
|
||||
if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
withBGApi {
|
||||
apiFindMessages(c, value)
|
||||
apiFindMessages(c, value, groupReports.value.toContentFilter())
|
||||
searchText.value = value
|
||||
}
|
||||
}
|
||||
ChatLayout(
|
||||
remoteHostId = remoteHostId,
|
||||
chatInfo = activeChatInfo,
|
||||
reversedChatItems = reversedChatItems,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
@@ -161,7 +174,7 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
}
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
chatItems = remember { chatModel.chatItems }.value,
|
||||
reversedChatItems = reversedChatItems,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
deleteItems = { canDeleteForAll ->
|
||||
@@ -223,6 +236,8 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
}
|
||||
},
|
||||
groupReports,
|
||||
showArchivedReports,
|
||||
scrollToItemId,
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
searchText,
|
||||
@@ -291,28 +306,23 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
}
|
||||
},
|
||||
showGroupReports = {
|
||||
val info = activeChatInfo.value ?: return@ChatLayout
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
ModalManager.end.showCustomModal(true) { close ->
|
||||
ModalView({}, showAppBar = false) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val chatInfo = remember { activeChatInfo }.value
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
GroupReportsView(staleChatId)
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||
GroupReportsAppBar(groupReports, close, showArchived = { showHide ->
|
||||
showArchivedReports.value = showHide
|
||||
}, onSearchValueChanged)
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
scope.launch {
|
||||
openChat(chatModel.remoteHostId(), info, ContentFilter(MsgContentTag.Report, false))
|
||||
ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close ->
|
||||
ModalView({}, showAppBar = false) {
|
||||
val chatInfo = remember { activeChatInfo }.value
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
GroupReportsView(staleChatId, scrollToItemId)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,16 +351,16 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
}
|
||||
}
|
||||
},
|
||||
loadMessages = { chatId, pagination, chatState, visibleItemIndexes ->
|
||||
loadMessages = { chatId, pagination, visibleItemIndexes ->
|
||||
val c = chatModel.getChat(chatId)
|
||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||
if (c != null) {
|
||||
apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, chatState, searchText.value, visibleItemIndexes)
|
||||
apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, groupReports.value.toContentFilter(), pagination, searchText.value, visibleItemIndexes)
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
@@ -382,6 +392,13 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -556,6 +573,12 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
itemsIds
|
||||
)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace
|
||||
withContext(Dispatchers.Main) {
|
||||
markChatItemsRead(chatRh, chatInfo, itemsIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
markChatRead = {
|
||||
@@ -572,6 +595,12 @@ fun ChatView(staleChatId: State<String?>, reportsView: Boolean, onComposed: susp
|
||||
chatInfo.apiId
|
||||
)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace
|
||||
withContext(Dispatchers.Main) {
|
||||
markChatItemsRead(chatRh, chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
@@ -632,10 +661,13 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
fun ChatLayout(
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo?>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeView: (@Composable () -> Unit),
|
||||
groupReports: State<GroupReports>,
|
||||
showArchivedReports: MutableState<Boolean>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
attachmentOption: MutableState<AttachmentOption?>,
|
||||
attachmentBottomSheetState: ModalBottomSheetState,
|
||||
searchValue: State<String>,
|
||||
@@ -646,7 +678,7 @@ fun ChatLayout(
|
||||
info: () -> Unit,
|
||||
showGroupReports: () -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -718,8 +750,8 @@ fun ChatLayout(
|
||||
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
|
||||
}) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, groupReports, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
remoteHostId, chatInfo, reversedChatItems, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, groupReports, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
@@ -773,6 +805,16 @@ fun ChatLayout(
|
||||
GroupReportsToolbar(groupReports, withStatusBar = false, showGroupReports)
|
||||
}
|
||||
}
|
||||
if (groupReports.value.reportsView) {
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||
GroupReportsAppBar(groupReports, { ModalManager.end.closeModal() }, showArchived = { showHide ->
|
||||
showArchivedReports.value = showHide
|
||||
}, onSearchValueChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1059,7 @@ private fun GroupReportsToolbar(
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
val reports = groupReports.value.activeReports
|
||||
val reports = groupReports.value.reportsCount
|
||||
Text(
|
||||
if (reports == 1) {
|
||||
stringResource(MR.strings.group_reports_active_one)
|
||||
@@ -1040,6 +1082,7 @@ private fun ContactVerifiedShield() {
|
||||
fun BoxScope.ChatItemsList(
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeViewHeight: State<Dp>,
|
||||
@@ -1047,10 +1090,11 @@ fun BoxScope.ChatItemsList(
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
groupReports: State<GroupReports>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
showChatInfo: () -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -1075,9 +1119,8 @@ fun BoxScope.ChatItemsList(
|
||||
showViaProxy: Boolean
|
||||
) {
|
||||
val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } }
|
||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
|
||||
val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } }
|
||||
val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatStateForContent(groupReports.value.contentTag)) } }
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() })
|
||||
/** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of
|
||||
* [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears
|
||||
@@ -1098,11 +1141,11 @@ fun BoxScope.ChatItemsList(
|
||||
val animatedScrollingInProgress = remember { mutableStateOf(false) }
|
||||
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
|
||||
if (!loadingMoreItems.value) {
|
||||
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
|
||||
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), reversedChatItems, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
|
||||
if (loadingMoreItems.value) return@PreloadItems false
|
||||
try {
|
||||
loadingMoreItems.value = true
|
||||
loadMessages(chatId, pagination, chatModel.chatState) {
|
||||
loadMessages(chatId, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, listState.value)
|
||||
}
|
||||
} finally {
|
||||
@@ -1116,21 +1159,27 @@ fun BoxScope.ChatItemsList(
|
||||
val chatInfoUpdated = rememberUpdatedState(chatInfo)
|
||||
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
|
||||
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
|
||||
|
||||
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo)
|
||||
SmallScrollOnNewMessage(listState, chatModel.chatItems)
|
||||
val scrollToItem: (Long) -> Unit = remember {
|
||||
// In group reports just set the itemId to scroll to so the main ChatView will handle scrolling
|
||||
if (groupReports.value.reportsView) return@remember { scrollToItemId.value = it }
|
||||
scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages)
|
||||
}
|
||||
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, groupReports.value.contentTag) }
|
||||
if (!groupReports.value.reportsView) {
|
||||
LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect { scrollToItem(it); scrollToItemId.value = null } }
|
||||
}
|
||||
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo, groupReports)
|
||||
SmallScrollOnNewMessage(listState, reversedChatItems)
|
||||
val finishedInitialComposition = remember { mutableStateOf(false) }
|
||||
NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
|
||||
|
||||
DisposableEffectOnGone(
|
||||
always = {
|
||||
chatModel.chatItemsChangesListener = recalculateChatStatePositions(chatModel.chatState)
|
||||
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag)
|
||||
},
|
||||
whenGone = {
|
||||
VideoPlayerHolder.releaseAll()
|
||||
chatModel.chatItemsChangesListener = null
|
||||
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1152,7 +1201,7 @@ fun BoxScope.ChatItemsList(
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val provider = {
|
||||
providerForGallery(chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed ->
|
||||
itemScope.launch {
|
||||
listState.value.scrollToItem(
|
||||
min(reversedChatItems.value.lastIndex, indexInReversed + 1),
|
||||
@@ -1177,7 +1226,7 @@ fun BoxScope.ChatItemsList(
|
||||
highlightedItems.value = setOf()
|
||||
}
|
||||
}
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,7 +1412,7 @@ fun BoxScope.ChatItemsList(
|
||||
if (selectionVisible) {
|
||||
Box(Modifier.matchParentSize().clickable {
|
||||
val checked = selectedChatItems.value?.contains(cItem.id) == true
|
||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
|
||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1448,14 +1497,14 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId: Long?, chatInfo: ChatInfo) {
|
||||
private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId: Long?, chatInfo: ChatInfo, groupReports: State<GroupReports>) {
|
||||
LaunchedEffect(remoteHostId, chatInfo.id) {
|
||||
try {
|
||||
loadingMoreItems.value = true
|
||||
if (chatModel.chatState.totalAfter.value <= 0) return@LaunchedEffect
|
||||
if (chatModel.chatStateForContent(groupReports.value.contentTag).totalAfter.value <= 0) return@LaunchedEffect
|
||||
delay(500)
|
||||
withContext(Dispatchers.Default) {
|
||||
apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, groupReports.value.toContentFilter(), ChatPagination.Last(ChatPagination.INITIAL_COUNT))
|
||||
}
|
||||
} finally {
|
||||
loadingMoreItems.value = false
|
||||
@@ -1464,20 +1513,20 @@ private fun LoadLastItems(loadingMoreItems: MutableState<Boolean>, remoteHostId:
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems: State<List<ChatItem>>) {
|
||||
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, reversedChatItems: State<List<ChatItem>>) {
|
||||
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
|
||||
LaunchedEffect(Unit) {
|
||||
var lastTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
var lastItemId = chatItems.value.lastOrNull()?.id
|
||||
var prevTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
var newestItemId = reversedChatItems.value.firstOrNull()?.id
|
||||
snapshotFlow { listState.value.layoutInfo.totalItemsCount }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.collect {
|
||||
val diff = listState.value.layoutInfo.totalItemsCount - lastTotalItems
|
||||
val sameLastItem = lastItemId == chatItems.value.lastOrNull()?.id
|
||||
lastTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
lastItemId = chatItems.value.lastOrNull()?.id
|
||||
if (diff < 1 || diff > 2 || sameLastItem) {
|
||||
val diff = listState.value.layoutInfo.totalItemsCount - prevTotalItems
|
||||
val sameNewestItem = newestItemId == reversedChatItems.value.firstOrNull()?.id
|
||||
prevTotalItems = listState.value.layoutInfo.totalItemsCount
|
||||
newestItemId = reversedChatItems.value.firstOrNull()?.id
|
||||
if (diff < 1 || diff > 2 || sameNewestItem) {
|
||||
return@collect
|
||||
}
|
||||
try {
|
||||
@@ -1488,7 +1537,7 @@ private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems:
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
/**
|
||||
* When you tap and hold a finger on a lazy column with chatItems, and then you receive a message,
|
||||
* When you tap and hold a finger on a lazy column with reversedChatItems, and then you receive a message,
|
||||
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
|
||||
* Which breaks auto-scrolling to bottom. So just ignoring the exception
|
||||
* */
|
||||
@@ -1618,6 +1667,7 @@ fun BoxScope.FloatingButtons(
|
||||
fun PreloadItems(
|
||||
chatId: String,
|
||||
ignoreLoadingRequests: MutableSet<Long>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1628,8 +1678,8 @@ fun PreloadItems(
|
||||
val chatId = rememberUpdatedState(chatId)
|
||||
val loadItems = rememberUpdatedState(loadItems)
|
||||
val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests)
|
||||
PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsAfter(allowLoad, chatId, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, reversedChatItems, mergedItems, listState, remaining, loadItems)
|
||||
PreloadItemsAfter(allowLoad, chatId, reversedChatItems, mergedItems, listState, remaining, loadItems)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1637,6 +1687,7 @@ private fun PreloadItemsBefore(
|
||||
allowLoad: State<Boolean>,
|
||||
chatId: State<String>,
|
||||
ignoreLoadingRequests: State<MutableSet<Long>>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1649,7 +1700,7 @@ private fun PreloadItemsBefore(
|
||||
val splits = mergedItems.value.splits
|
||||
val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
|
||||
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
|
||||
val items = chatModel.chatItems.value
|
||||
val items = reversedChatItems.value
|
||||
if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining && items.size >= ChatPagination.INITIAL_COUNT) {
|
||||
lastIndexToLoadFrom = items.lastIndex
|
||||
}
|
||||
@@ -1663,10 +1714,10 @@ private fun PreloadItemsBefore(
|
||||
.filter { !ignoreLoadingRequests.value.contains(it) }
|
||||
.collect { loadFromItemId ->
|
||||
withBGApi {
|
||||
val sizeWas = chatModel.chatItems.value.size
|
||||
val firstItemIdWas = chatModel.chatItems.value.firstOrNull()?.id
|
||||
val sizeWas = reversedChatItems.value.size
|
||||
val oldestItemIdWas = reversedChatItems.value.lastOrNull()?.id
|
||||
val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT))
|
||||
if (triedToLoad && sizeWas == chatModel.chatItems.value.size && firstItemIdWas == chatModel.chatItems.value.firstOrNull()?.id) {
|
||||
if (triedToLoad && sizeWas == reversedChatItems.value.size && oldestItemIdWas == reversedChatItems.value.lastOrNull()?.id) {
|
||||
ignoreLoadingRequests.value.add(loadFromItemId)
|
||||
}
|
||||
}
|
||||
@@ -1678,6 +1729,7 @@ private fun PreloadItemsBefore(
|
||||
private fun PreloadItemsAfter(
|
||||
allowLoad: MutableState<Boolean>,
|
||||
chatId: State<String>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
remaining: Int,
|
||||
@@ -1698,12 +1750,12 @@ private fun PreloadItemsAfter(
|
||||
snapshotFlow { listState.value.firstVisibleItemIndex }
|
||||
.distinctUntilChanged()
|
||||
.map { firstVisibleIndex ->
|
||||
val items = chatModel.chatItems.value
|
||||
val items = reversedChatItems.value
|
||||
val splits = mergedItems.value.splits
|
||||
val split = splits.lastOrNull { it.indexRangeInParentItems.contains(firstVisibleIndex) }
|
||||
// we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom)
|
||||
if (split != null && split.indexRangeInParentItems.first + remaining > firstVisibleIndex) {
|
||||
items.getOrNull(items.lastIndex - split.indexRangeInReversed.first)?.id
|
||||
items.getOrNull(split.indexRangeInReversed.first)?.id
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1984,7 +2036,7 @@ private fun scrollToItem(
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
mergedItems: State<MergedItems>,
|
||||
listState: State<LazyListState>,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
): (Long) -> Unit = { itemId: Long ->
|
||||
withApi {
|
||||
try {
|
||||
@@ -1998,7 +2050,7 @@ private fun scrollToItem(
|
||||
val pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2)
|
||||
val oldSize = reversedChatItems.value.size
|
||||
withContext(Dispatchers.Default) {
|
||||
loadMessages(chatInfo.value.id, pagination, chatModel.chatState) {
|
||||
loadMessages(chatInfo.value.id, pagination) {
|
||||
visibleItemIndexesNonReversed(mergedItems, listState.value)
|
||||
}
|
||||
}
|
||||
@@ -2030,14 +2082,18 @@ private fun findQuotedItemFromItem(
|
||||
rhId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
scope: CoroutineScope,
|
||||
scrollToItem: (Long) -> Unit
|
||||
scrollToItem: (Long) -> Unit,
|
||||
contentTag: MsgContentTag?
|
||||
): (Long) -> Unit = { itemId: Long ->
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId)
|
||||
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag)
|
||||
if (item != null) {
|
||||
withChats {
|
||||
updateChatItem(chatInfo.value, item)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updateChatItem(chatInfo.value, item)
|
||||
}
|
||||
if (item.quotedItem?.itemId != null) {
|
||||
scrollToItem(item.quotedItem.itemId)
|
||||
} else {
|
||||
@@ -2134,7 +2190,13 @@ private fun SelectedChatItem(
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State<Boolean>, selectedChatItems: MutableState<Set<Long>?>) {
|
||||
private fun selectUnselectChatItem(
|
||||
select: Boolean,
|
||||
ci: ChatItem,
|
||||
revealed: State<Boolean>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
reversedChatItems: State<List<ChatItem>>
|
||||
) {
|
||||
val itemIds = mutableSetOf<Long>()
|
||||
if (!revealed.value) {
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(ci)
|
||||
@@ -2143,9 +2205,9 @@ private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: Stat
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (range != null) {
|
||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
||||
val reversed = reversedChatItems.value
|
||||
for (i in range) {
|
||||
itemIds.add(reversedChatItems[i].id)
|
||||
itemIds.add(reversed[i].id)
|
||||
}
|
||||
} else {
|
||||
itemIds.add(ci.id)
|
||||
@@ -2195,6 +2257,16 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
|
||||
}
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
for (di in deleted) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@@ -2217,6 +2289,10 @@ private fun markUnreadChatAsRead(chatId: String) {
|
||||
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
markChatTagRead(chat)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
markChatTagRead(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2531,10 +2607,13 @@ fun PreviewChatLayout() {
|
||||
ChatLayout(
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
reversedChatItems = remember { mutableStateOf(emptyList()) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
groupReports = remember { mutableStateOf(GroupReports(0, false)) },
|
||||
showArchivedReports = remember { mutableStateOf(false) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||
searchValue,
|
||||
@@ -2545,7 +2624,7 @@ fun PreviewChatLayout() {
|
||||
info = {},
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
@@ -2606,10 +2685,13 @@ fun PreviewGroupChatLayout() {
|
||||
ChatLayout(
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
reversedChatItems = remember { mutableStateOf(emptyList()) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
groupReports = remember { mutableStateOf(GroupReports(0, false)) },
|
||||
showArchivedReports = remember { mutableStateOf(false) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||
searchValue,
|
||||
@@ -2620,7 +2702,7 @@ fun PreviewGroupChatLayout() {
|
||||
info = {},
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = {},
|
||||
receiveFile = { _ -> },
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>
|
||||
@Composable
|
||||
fun SelectedItemsBottomToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
chatItems: List<ChatItem>,
|
||||
reversedChatItems: State<List<ChatItem>>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
moderateItems: () -> Unit,
|
||||
@@ -108,8 +108,8 @@ fun SelectedItemsBottomToolbar(
|
||||
}
|
||||
Divider(Modifier.align(Alignment.TopStart))
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
LaunchedEffect(chatInfo, reversedChatItems.value, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, reversedChatItems.value.asReversed(), selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ fun GroupMemberInfoView(
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = {
|
||||
withBGApi {
|
||||
apiLoadMessages(rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
if (chatModel.getContactChat(it) != null) {
|
||||
closeAll()
|
||||
}
|
||||
|
||||
+44
-12
@@ -1,34 +1,40 @@
|
||||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.ChatView
|
||||
import chat.simplex.common.views.chat.apiLoadMessages
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class GroupReports(
|
||||
val activeReports: Int,
|
||||
val reportsCount: Int,
|
||||
val reportsView: Boolean,
|
||||
val showArchived: Boolean = false
|
||||
) {
|
||||
val showBar: Boolean = activeReports > 0 && !reportsView
|
||||
val showBar: Boolean = reportsCount > 0 && !reportsView
|
||||
|
||||
fun toContentFilter(): ContentFilter? {
|
||||
if (!reportsView) return null
|
||||
return ContentFilter(MsgContentTag.Report, deleted = showArchived)
|
||||
}
|
||||
|
||||
val contentTag: MsgContentTag? = if (!reportsView) null else MsgContentTag.Report
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupReportsView(staleChatId: State<String?>) {
|
||||
ChatView(staleChatId, reportsView = true, onComposed = {})
|
||||
fun GroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
|
||||
ChatView(staleChatId, reportsView = true, scrollToItemId, onComposed = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -71,8 +77,8 @@ fun GroupReportsAppBar(
|
||||
showSearch.value = true
|
||||
})
|
||||
ItemAction(
|
||||
if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_hide_archived) else stringResource(MR.strings.group_reports_show_archived),
|
||||
painterResource(MR.images.ic_add),
|
||||
if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_show_active) else stringResource(MR.strings.group_reports_show_archived),
|
||||
painterResource(if (groupReports.value.showArchived) MR.images.ic_flag else MR.images.ic_inventory_2),
|
||||
onClick = {
|
||||
onClosedAction.value = {
|
||||
showArchived(!groupReports.value.showArchived)
|
||||
@@ -84,4 +90,30 @@ fun GroupReportsAppBar(
|
||||
}
|
||||
}
|
||||
)
|
||||
ItemsReload(groupReports)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemsReload(groupReports: State<GroupReports>) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { groupReports.value.showArchived to chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.map { it.second }
|
||||
.filterNotNull()
|
||||
.map { chatModel.getChat(it) }
|
||||
.filterNotNull()
|
||||
.filter { it.chatInfo is ChatInfo.Group }
|
||||
.collect { chat ->
|
||||
reloadItems(chat, groupReports)
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun reloadItems(chat: Chat, groupReports: State<GroupReports>) {
|
||||
val contentFilter = groupReports.value.toContentFilter()
|
||||
if (chat.chatStats.reportsCount > 0 || contentFilter?.deleted == true) {
|
||||
apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
} else {
|
||||
openLoadedChat(chat.copy(chatItems = emptyList()), contentTag = contentFilter?.mcTag)
|
||||
}
|
||||
}
|
||||
+14
-17
@@ -10,17 +10,11 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -209,27 +203,30 @@ suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat
|
||||
|
||||
suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId)
|
||||
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(rhId, ChatType.Group, groupId)
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, contentFilter: ContentFilter? = null) = openChat(rhId, ChatType.Group, groupId, contentFilter)
|
||||
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentFilter: ContentFilter? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentFilter)
|
||||
|
||||
private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long) =
|
||||
apiLoadMessages(rhId, chatType, apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState)
|
||||
private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long, contentFilter: ContentFilter? = null) =
|
||||
apiLoadMessages(rhId, chatType, apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||
|
||||
suspend fun openLoadedChat(chat: Chat) {
|
||||
withChats {
|
||||
chatModel.chatItemStatuses.clear()
|
||||
suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) {
|
||||
withChats(contentTag) {
|
||||
// LALAL TODO MOVE item statuses to chats context
|
||||
if (contentTag == null) {
|
||||
chatModel.chatItemStatuses.clear()
|
||||
}
|
||||
chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
chatModel.chatState.clear()
|
||||
chatModel.chatStateForContent(contentTag).clear()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiFindMessages(ch: Chat, search: String) {
|
||||
withChats {
|
||||
suspend fun apiFindMessages(ch: Chat, search: String, contentFilter: ContentFilter?) {
|
||||
withChats(contentFilter?.mcTag) {
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState, search = search)
|
||||
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentFilter, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search = search)
|
||||
}
|
||||
|
||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -534,6 +535,11 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
chats.clear()
|
||||
popChatCollector.clear()
|
||||
}
|
||||
}
|
||||
chatModel.users.clear()
|
||||
ntfManager.cancelAllNotifications()
|
||||
|
||||
+26
-13
@@ -77,8 +77,19 @@ class ModalData(val keyboardCoversBar: Boolean = true) {
|
||||
val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar)
|
||||
}
|
||||
|
||||
enum class ModalViewId {
|
||||
GROUP_REPORTS
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private val modalViews = arrayListOf<Triple<Boolean, ModalData, (@Composable ModalData.(close: () -> Unit) -> Unit)>>()
|
||||
data class ModalViewHolder(
|
||||
val id: ModalViewId?,
|
||||
val animated: Boolean,
|
||||
val data: ModalData,
|
||||
val modal: @Composable ModalData.(close: () -> Unit) -> Unit
|
||||
)
|
||||
|
||||
private val modalViews = arrayListOf<ModalViewHolder>()
|
||||
private val _modalCount = mutableStateOf(0)
|
||||
val modalCount: State<Int> = _modalCount
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
@@ -88,19 +99,21 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
showCustomModal { close ->
|
||||
fun hasModalOpen(id: ModalViewId): Boolean = modalViews.any { it.id == id }
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
showCustomModal(id = id) { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
showCustomModal { close ->
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
showCustomModal(id = id) { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showCustomModal")
|
||||
val data = ModalData(keyboardCoversBar = keyboardCoversBar)
|
||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||
@@ -111,7 +124,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
|
||||
// to prevent unneeded animation on different situations
|
||||
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
|
||||
modalViews.add(Triple(anim, data, modal))
|
||||
modalViews.add(ModalViewHolder(id, anim, data, modal))
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
|
||||
if (placement == ModalPlacement.CENTER) {
|
||||
@@ -139,7 +152,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
if (modalViews.lastOrNull()?.animated == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
|
||||
}
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
@@ -161,10 +174,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
@Composable
|
||||
fun showInView() {
|
||||
// Without animation
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.animated == false) {
|
||||
modalViews.lastOrNull()?.let {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) {
|
||||
it.modal(it.data, ::closeModal)
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -179,8 +192,8 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
}
|
||||
) {
|
||||
modalViews.getOrNull(it - 1)?.let {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) {
|
||||
it.modal(it.data, ::closeModal)
|
||||
}
|
||||
}
|
||||
// This is needed because if we delete from modalViews immediately on request, animation will be bad
|
||||
|
||||
@@ -429,7 +429,7 @@
|
||||
<string name="group_reports_active">%d reports</string>
|
||||
<string name="group_reports_member_reports">Member reports</string>
|
||||
<string name="group_reports_show_archived">Show archived</string>
|
||||
<string name="group_reports_hide_archived">Hide archived</string>
|
||||
<string name="group_reports_show_active">Show active</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
@@ -61,6 +62,9 @@ fun showApp() {
|
||||
chatModel.chatId.value = null
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.activeCall.value?.let {
|
||||
|
||||
Reference in New Issue
Block a user