mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a22706d01 | |||
| bfac8bd58a | |||
| 34c10fc12f | |||
| 22fc2e5151 | |||
| 2396531414 | |||
| a3906cf88f | |||
| d03fc53151 | |||
| 69138dd99f | |||
| f9d01f6f8e | |||
| b90f7a932f | |||
| 40db6686c1 | |||
| 21f9c7fd50 | |||
| d15560a29f | |||
| f197c4174c | |||
| 99719088e5 | |||
| 01611fd719 | |||
| 0ce7d16a59 | |||
| bf20b73893 | |||
| c9ae77e9f3 | |||
| d875fc99f4 | |||
| 1c4c48f168 | |||
| 122d5948af | |||
| fd5e5d295f | |||
| 3a2e59a6e8 | |||
| 494724de01 | |||
| 089e5f6ec1 | |||
| 74c6a425b3 | |||
| 0698f4acfc | |||
| 779a91f5cb | |||
| 20c4c02f63 | |||
| d2a473c908 | |||
| 01d0c4ef96 | |||
| bab2341406 | |||
| c98e8e774f | |||
| cf32d1d950 | |||
| 065cc170da | |||
| a84b56d39d | |||
| 937e66b3b8 | |||
| 638b237b8c | |||
| 37a5c353ea | |||
| 31eca1687c | |||
| 7e9ab7cd85 | |||
| 8ff2f66bb9 | |||
| e2de454426 | |||
| f87a4a43c9 | |||
| 83ce1f5808 | |||
| 4bbee43602 | |||
| 0ee2769e15 | |||
| 82e6b2eefc | |||
| 97894c7f53 | |||
| ff42ca69de | |||
| 5ccf00f70e | |||
| 22be251862 | |||
| 84bd23a99d | |||
| 9737f6baa6 | |||
| 39e3a8cb70 | |||
| 649561fbc9 | |||
| c681c89657 | |||
| 7ad0a2dd87 | |||
| 71d891acc2 | |||
| 7faa5b4f6a | |||
| c93ed87228 | |||
| 2b170830df | |||
| 19755e359d | |||
| 53c77d310a | |||
| 796d3916bd | |||
| b93abb9555 | |||
| c31560238a | |||
| 2b7ad80dfd | |||
| d8f5b6e813 | |||
| b6359559c1 | |||
| 5826d14866 | |||
| a9ca467a80 | |||
| 185e307e70 | |||
| f738d2731c | |||
| ea61fd7e2b |
+11
@@ -11,6 +11,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
|
import chat.simplex.common.views.chat.ChatSectionArea
|
||||||
import chat.simplex.common.views.chat.ComposeState
|
import chat.simplex.common.views.chat.ComposeState
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.migration.MigrationToDeviceState
|
import chat.simplex.common.views.migration.MigrationToDeviceState
|
||||||
@@ -65,6 +66,8 @@ object ChatModel {
|
|||||||
// current chat
|
// current chat
|
||||||
val chatId = mutableStateOf<String?>(null)
|
val chatId = mutableStateOf<String?>(null)
|
||||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||||
|
// chatItemId, SectionArea
|
||||||
|
val chatItemsSectionArea = mutableMapOf<Long, ChatSectionArea>()
|
||||||
// rhId, chatId
|
// rhId, chatId
|
||||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||||
@@ -329,6 +332,7 @@ object ChatModel {
|
|||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
// Prevent situation when chat item already in the list received from backend
|
// Prevent situation when chat item already in the list received from backend
|
||||||
if (chatItems.value.none { it.id == cItem.id }) {
|
if (chatItems.value.none { it.id == cItem.id }) {
|
||||||
|
chatItemsSectionArea[cItem.id] = ChatSectionArea.Bottom
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||||
} else {
|
} else {
|
||||||
@@ -377,6 +381,7 @@ object ChatModel {
|
|||||||
} else {
|
} else {
|
||||||
cItem
|
cItem
|
||||||
}
|
}
|
||||||
|
chatItemsSectionArea[ci.id] = ChatSectionArea.Bottom
|
||||||
chatItems.add(ci)
|
chatItems.add(ci)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -608,6 +613,7 @@ object ChatModel {
|
|||||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chatItems.add(cItem)
|
chatItems.add(cItem)
|
||||||
|
chatItemsSectionArea[cItem.id] = ChatSectionArea.Bottom
|
||||||
}
|
}
|
||||||
return cItem
|
return cItem
|
||||||
}
|
}
|
||||||
@@ -615,6 +621,7 @@ object ChatModel {
|
|||||||
fun removeLiveDummy() {
|
fun removeLiveDummy() {
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.removeLast()
|
chatItems.removeLast()
|
||||||
|
chatItemsSectionArea.remove(ChatItem.TEMP_LIVE_CHAT_ITEM_ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2307,6 +2314,10 @@ fun <T> MutableState<SnapshotStateList<T>>.removeAt(index: Int): T {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> MutableState<SnapshotStateList<T>>.removeRange(fromIndex: Int, toIndex: Int) {
|
||||||
|
value = SnapshotStateList<T>().apply { addAll(value); removeRange(fromIndex, toIndex) }
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> MutableState<SnapshotStateList<T>>.removeLast() {
|
fun <T> MutableState<SnapshotStateList<T>>.removeLast() {
|
||||||
value = SnapshotStateList<T>().apply { addAll(value); removeLast() }
|
value = SnapshotStateList<T>().apply { addAll(value); removeLast() }
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-7
@@ -865,11 +865,15 @@ object ChatController {
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? {
|
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Pair<Chat, ChatLandingSection>? {
|
||||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
||||||
if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh)
|
if (r is CR.ApiChat) return if (rh == null) Pair(r.chat, r.section) else Pair(r.chat.copy(remoteHostId = rh), r.section)
|
||||||
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_get_chat_item_not_found_title), generalGetString(MR.strings.failed_to_get_chat_item_not_found_description))
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers))
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3468,11 +3472,15 @@ sealed class ChatPagination {
|
|||||||
class Last(val count: Int): ChatPagination()
|
class Last(val count: Int): ChatPagination()
|
||||||
class After(val chatItemId: Long, val count: Int): ChatPagination()
|
class After(val chatItemId: Long, val count: Int): ChatPagination()
|
||||||
class Before(val chatItemId: Long, val count: Int): ChatPagination()
|
class Before(val chatItemId: Long, val count: Int): ChatPagination()
|
||||||
|
class Around(val chatItemId: Long, val count: Int): ChatPagination()
|
||||||
|
class Initial(val count: Int): ChatPagination()
|
||||||
|
|
||||||
val cmdString: String get() = when (this) {
|
val cmdString: String get() = when (this) {
|
||||||
is Last -> "count=${this.count}"
|
is Last -> "count=${this.count}"
|
||||||
is After -> "after=${this.chatItemId} count=${this.count}"
|
is After -> "after=${this.chatItemId} count=${this.count}"
|
||||||
is Before -> "before=${this.chatItemId} count=${this.count}"
|
is Before -> "before=${this.chatItemId} count=${this.count}"
|
||||||
|
is Around -> "around=${this.chatItemId} count=${this.count}"
|
||||||
|
is Initial -> "initial=${this.count}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -4857,7 +4865,10 @@ class APIResponse(val resp: CR, val remoteHostId: Long?, val corr: String? = nul
|
|||||||
} else if (type == "apiChat") {
|
} else if (type == "apiChat") {
|
||||||
val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject)
|
val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject)
|
||||||
val chat = parseChatData(resp["chat"]!!)
|
val chat = parseChatData(resp["chat"]!!)
|
||||||
return APIResponse(CR.ApiChat(user, chat), remoteHostId, corr)
|
val section = resp["section"]?.let {
|
||||||
|
json.decodeFromJsonElement<ChatLandingSection>(it)
|
||||||
|
} ?: ChatLandingSection.Latest
|
||||||
|
return APIResponse(CR.ApiChat(user, chat, section), remoteHostId, corr)
|
||||||
} else if (type == "chatCmdError") {
|
} else if (type == "chatCmdError") {
|
||||||
val userObject = resp["user_"]?.jsonObject
|
val userObject = resp["user_"]?.jsonObject
|
||||||
val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull()
|
val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull()
|
||||||
@@ -4914,7 +4925,7 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("chatRunning") class ChatRunning: CR()
|
@Serializable @SerialName("chatRunning") class ChatRunning: CR()
|
||||||
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
|
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
|
||||||
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
|
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
|
||||||
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat): CR()
|
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val section: ChatLandingSection): CR()
|
||||||
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
|
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
|
||||||
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR()
|
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR()
|
||||||
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
||||||
@@ -5264,7 +5275,7 @@ sealed class CR {
|
|||||||
is ChatRunning -> noDetails()
|
is ChatRunning -> noDetails()
|
||||||
is ChatStopped -> noDetails()
|
is ChatStopped -> noDetails()
|
||||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||||
is ApiChat -> withUser(user, json.encodeToString(chat))
|
is ApiChat -> withUser(user, "section: ${json.encodeToString(section)}\n${json.encodeToString(chat)}")
|
||||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||||
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
||||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||||
@@ -5504,6 +5515,12 @@ sealed class GroupLinkPlan {
|
|||||||
@Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan()
|
@Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class ChatLandingSection {
|
||||||
|
@SerialName("latest") Latest,
|
||||||
|
@SerialName("unread") Unread,
|
||||||
|
}
|
||||||
|
|
||||||
abstract class TerminalItem {
|
abstract class TerminalItem {
|
||||||
abstract val id: Long
|
abstract val id: Long
|
||||||
abstract val remoteHostId: Long?
|
abstract val remoteHostId: Long?
|
||||||
|
|||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
package chat.simplex.common.views.chat
|
||||||
|
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.chatController
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
const val MAX_SECTION_SIZE = 500
|
||||||
|
|
||||||
|
enum class ChatSectionArea {
|
||||||
|
Bottom,
|
||||||
|
Current,
|
||||||
|
Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChatSectionAreaBoundary (
|
||||||
|
var minIndex: Int,
|
||||||
|
var maxIndex: Int,
|
||||||
|
val area: ChatSectionArea
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ChatSection (
|
||||||
|
val items: MutableList<SectionItems>,
|
||||||
|
val boundary: ChatSectionAreaBoundary,
|
||||||
|
// chatItemId, index in rendered LazyColumn
|
||||||
|
val itemPositions: MutableMap<Long, Int>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SectionItems (
|
||||||
|
val mergeCategory: CIMergeCategory?,
|
||||||
|
val items: MutableList<ChatItem>,
|
||||||
|
val revealed: Boolean,
|
||||||
|
val showAvatar: MutableSet<Long>,
|
||||||
|
var originalItemsRange: IntRange
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ChatSectionLoader (
|
||||||
|
val position: Int,
|
||||||
|
val sectionArea: ChatSectionArea
|
||||||
|
) {
|
||||||
|
fun prepareItems(items: List<ChatItem>): List<ChatItem> {
|
||||||
|
val chatItemsSectionArea = chatModel.chatItemsSectionArea
|
||||||
|
val itemsToAdd = mutableListOf<ChatItem>()
|
||||||
|
val sectionsToMerge = mutableMapOf<ChatSectionArea, ChatSectionArea>()
|
||||||
|
val itemsThatCouldRequireMerge = mutableListOf<ChatItem>()
|
||||||
|
for (cItem in items) {
|
||||||
|
val itemSectionArea = chatItemsSectionArea[cItem.id]
|
||||||
|
if (itemSectionArea == null) {
|
||||||
|
itemsToAdd.add(cItem)
|
||||||
|
val targetSection = sectionsToMerge[sectionArea] ?: this.sectionArea
|
||||||
|
chatItemsSectionArea[cItem.id] = targetSection
|
||||||
|
|
||||||
|
if (targetSection == this.sectionArea) {
|
||||||
|
itemsThatCouldRequireMerge.add(cItem)
|
||||||
|
}
|
||||||
|
} else if (itemSectionArea != this.sectionArea) {
|
||||||
|
val (targetSection, sectionToDrop) = when (itemSectionArea) {
|
||||||
|
ChatSectionArea.Bottom -> ChatSectionArea.Bottom to this.sectionArea
|
||||||
|
ChatSectionArea.Current -> if (this.sectionArea == ChatSectionArea.Bottom) ChatSectionArea.Bottom to itemSectionArea else itemSectionArea to this.sectionArea
|
||||||
|
ChatSectionArea.Destination -> if (this.sectionArea == ChatSectionArea.Bottom) ChatSectionArea.Bottom to itemSectionArea else itemSectionArea to this.sectionArea
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetSection != sectionToDrop) {
|
||||||
|
sectionsToMerge[sectionToDrop] = targetSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionsToMerge.isNotEmpty()) {
|
||||||
|
chatModel.chatItems.value.forEach {
|
||||||
|
val currentSection = chatItemsSectionArea[it.id]
|
||||||
|
val newSection = sectionsToMerge[currentSection]
|
||||||
|
if (newSection != null) {
|
||||||
|
chatItemsSectionArea[it.id] = newSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsThatCouldRequireMerge.forEach {
|
||||||
|
val targetSection = sectionsToMerge[sectionArea] ?: sectionArea
|
||||||
|
chatItemsSectionArea[it.id] = targetSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsToAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChatSection.getPreviousShownItem(sectionIndex: Int, itemIndex: Int): ChatItem? {
|
||||||
|
val section = items.getOrNull(sectionIndex) ?: return null
|
||||||
|
|
||||||
|
return if (section.mergeCategory == null) {
|
||||||
|
section.items.getOrNull(itemIndex + 1) ?: items.getOrNull(sectionIndex + 1)?.items?.firstOrNull()
|
||||||
|
} else {
|
||||||
|
items.getOrNull(sectionIndex + 1)?.items?.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChatSection.getNextShownItem(sectionIndex: Int, itemIndex: Int): ChatItem? {
|
||||||
|
val section = items.getOrNull(sectionIndex) ?: return null
|
||||||
|
|
||||||
|
return if (section.mergeCategory == null) {
|
||||||
|
section.items.getOrNull(itemIndex - 1) ?: items.getOrNull(sectionIndex - 1)?.items?.lastOrNull()
|
||||||
|
} else {
|
||||||
|
items.getOrNull(sectionIndex - 1)?.items?.lastOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<ChatSection> {
|
||||||
|
if (isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val chatItemsSectionArea = chatModel.chatItemsSectionArea
|
||||||
|
val sections = mutableListOf<ChatSection>()
|
||||||
|
val first = this[0]
|
||||||
|
|
||||||
|
val showAvatar = mutableSetOf<Long>()
|
||||||
|
if (first.chatDir is CIDirection.GroupRcv) {
|
||||||
|
val second = getOrNull(1)
|
||||||
|
if (second != null) {
|
||||||
|
if (second.chatDir !is CIDirection.GroupRcv || second.chatDir.groupMember.memberId != first.chatDir.groupMember.memberId) {
|
||||||
|
showAvatar.add(first.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showAvatar.add(first.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var recent = SectionItems(
|
||||||
|
mergeCategory = first.mergeCategory,
|
||||||
|
items = mutableListOf(first),
|
||||||
|
revealed = first.mergeCategory == null || revealedItems.contains(first.id),
|
||||||
|
showAvatar = showAvatar,
|
||||||
|
originalItemsRange = 0..0
|
||||||
|
)
|
||||||
|
|
||||||
|
val area = chatItemsSectionArea[recent.items[0].id] ?: ChatSectionArea.Bottom
|
||||||
|
|
||||||
|
sections.add(
|
||||||
|
ChatSection(
|
||||||
|
items = mutableListOf(recent),
|
||||||
|
boundary = ChatSectionAreaBoundary(minIndex = 0, maxIndex = 0, area = area),
|
||||||
|
itemPositions = mutableMapOf(recent.items[0].id to 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var prev = this[0]
|
||||||
|
var index = 0
|
||||||
|
var positionInList = 0;
|
||||||
|
while (index < size) {
|
||||||
|
if (index == 0) {
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val item = this[index]
|
||||||
|
val itemArea = chatItemsSectionArea[item.id] ?: ChatSectionArea.Bottom
|
||||||
|
val existingSection = sections.find { it.boundary.area == itemArea }
|
||||||
|
|
||||||
|
if (existingSection == null) {
|
||||||
|
positionInList++
|
||||||
|
val newSection = SectionItems(
|
||||||
|
mergeCategory = item.mergeCategory,
|
||||||
|
items = mutableListOf(item),
|
||||||
|
revealed = item.mergeCategory == null || revealedItems.contains(item.id),
|
||||||
|
showAvatar = mutableSetOf(item.id),
|
||||||
|
originalItemsRange = index..index
|
||||||
|
)
|
||||||
|
sections.add(
|
||||||
|
ChatSection(
|
||||||
|
items = mutableListOf(newSection),
|
||||||
|
boundary = ChatSectionAreaBoundary(minIndex = index, maxIndex = index, area = itemArea),
|
||||||
|
itemPositions = mutableMapOf(item.id to positionInList)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
recent = existingSection.items.last()
|
||||||
|
val category = item.mergeCategory
|
||||||
|
if (recent.mergeCategory == category) {
|
||||||
|
if (category == null || recent.revealed || revealedItems.contains(item.id)) {
|
||||||
|
positionInList++
|
||||||
|
}
|
||||||
|
if (item.chatDir is CIDirection.GroupRcv && prev.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember.memberId != (prev.chatDir as CIDirection.GroupRcv).groupMember.memberId) {
|
||||||
|
recent.showAvatar.add(item.id)
|
||||||
|
}
|
||||||
|
recent.items.add(item)
|
||||||
|
recent.originalItemsRange = recent.originalItemsRange.first..index
|
||||||
|
existingSection.itemPositions[item.id] = positionInList
|
||||||
|
} else {
|
||||||
|
positionInList++
|
||||||
|
val newSectionItems = SectionItems(
|
||||||
|
mergeCategory = item.mergeCategory,
|
||||||
|
items = mutableListOf(item),
|
||||||
|
revealed = item.mergeCategory == null || revealedItems.contains(item.id),
|
||||||
|
showAvatar = if (item.chatDir is CIDirection.GroupRcv && (prev.chatDir !is CIDirection.GroupRcv || (prev.chatDir as CIDirection.GroupRcv).groupMember.memberId != item.chatDir.groupMember.memberId)) {
|
||||||
|
mutableSetOf(item.id)
|
||||||
|
} else {
|
||||||
|
mutableSetOf()
|
||||||
|
},
|
||||||
|
originalItemsRange = index..index
|
||||||
|
)
|
||||||
|
existingSection.itemPositions[item.id] = positionInList
|
||||||
|
existingSection.items.add(newSectionItems)
|
||||||
|
}
|
||||||
|
existingSection.boundary.maxIndex = index
|
||||||
|
}
|
||||||
|
prev = item
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ChatSection>.chatItemPosition(chatItemId: Long): Int? {
|
||||||
|
for (section in this) {
|
||||||
|
val position = section.itemPositions[chatItemId]
|
||||||
|
if (position != null) {
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ChatSection>.revealedItemCount(): Int {
|
||||||
|
var count = 0
|
||||||
|
for (section in this) {
|
||||||
|
var i = 0;
|
||||||
|
while (i < section.items.size) {
|
||||||
|
val item = section.items[i]
|
||||||
|
if (item.revealed) {
|
||||||
|
count += item.items.size
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ChatSection>.dropTemporarySections() {
|
||||||
|
val bottomSection = this.find { it.boundary.area == ChatSectionArea.Bottom }
|
||||||
|
if (bottomSection != null) {
|
||||||
|
val itemsOutsideOfSection = chatModel.chatItems.value.lastIndex - bottomSection.boundary.maxIndex
|
||||||
|
chatModel.chatItems.removeRange(fromIndex = 0, toIndex = itemsOutsideOfSection + bottomSection.excessItemCount())
|
||||||
|
chatModel.chatItemsSectionArea.clear()
|
||||||
|
chatModel.chatItems.value.associateTo(chatModel.chatItemsSectionArea) { it.id to ChatSectionArea.Bottom }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ChatSection.excessItemCount(): Int {
|
||||||
|
return max(boundary.maxIndex - boundary.minIndex + 1 - MAX_SECTION_SIZE, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun landingSectionToArea(chatLandingSection: ChatLandingSection) = when (chatLandingSection) {
|
||||||
|
ChatLandingSection.Latest -> ChatSectionArea.Bottom
|
||||||
|
ChatLandingSection.Unread -> ChatSectionArea.Current
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiLoadBottomSection(chatInfo: ChatInfo, rhId: Long?) {
|
||||||
|
val chat = chatController.apiGetChat(rh = rhId, type = chatInfo.chatType, id = chatInfo.apiId)
|
||||||
|
if (chatModel.chatId.value != chatInfo.id || chat == null) return
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val updatedItems = chatModel.chatItems.value.toMutableStateList()
|
||||||
|
var insertIndex = updatedItems.size
|
||||||
|
var needsMerge = false
|
||||||
|
|
||||||
|
for (cItem in chat.first.chatItems.asReversed()) {
|
||||||
|
if (chatModel.chatItemsSectionArea[cItem.id] == null) {
|
||||||
|
updatedItems.add(insertIndex, cItem)
|
||||||
|
chatModel.chatItemsSectionArea[cItem.id] = ChatSectionArea.Bottom
|
||||||
|
} else {
|
||||||
|
needsMerge = true
|
||||||
|
chatModel.chatItemsSectionArea[cItem.id] = ChatSectionArea.Bottom
|
||||||
|
insertIndex = max(0, insertIndex - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsMerge) {
|
||||||
|
updatedItems.associateTo(chatModel.chatItemsSectionArea) { it.id to ChatSectionArea.Bottom }
|
||||||
|
}
|
||||||
|
|
||||||
|
chatModel.chatItems.replaceAll(updatedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
+428
-239
@@ -47,8 +47,7 @@ import kotlinx.coroutines.flow.*
|
|||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.math.abs
|
import kotlin.math.*
|
||||||
import kotlin.math.sign
|
|
||||||
|
|
||||||
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
|
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
|
||||||
|
|
||||||
@@ -292,13 +291,35 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadPrevMessages = { chatId ->
|
loadMessages = { chatId, scrollDirection, (itemId, idx, area) ->
|
||||||
val c = chatModel.getChat(chatId)
|
val c = chatModel.getChat(chatId) ?: return@ChatLayout
|
||||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
withBGApi {
|
||||||
if (c != null && firstId != null) {
|
when (scrollDirection) {
|
||||||
withBGApi {
|
ScrollDirection.Up -> {
|
||||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
val chatSectionLoader = ChatSectionLoader(idx, area)
|
||||||
|
apiLoadMessages(
|
||||||
|
rhId = c.remoteHostId,
|
||||||
|
chatInfo = c.chatInfo,
|
||||||
|
chatModel = chatModel,
|
||||||
|
itemId = itemId,
|
||||||
|
search = "",
|
||||||
|
chatSectionLoader = chatSectionLoader,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ScrollDirection.Down -> {
|
||||||
|
val chatSectionLoader = ChatSectionLoader(idx + 1, area)
|
||||||
|
apiLoadMessages(
|
||||||
|
rhId = c.remoteHostId,
|
||||||
|
chatInfo = c.chatInfo,
|
||||||
|
chatModel = chatModel,
|
||||||
|
itemId = itemId,
|
||||||
|
search = "",
|
||||||
|
chatSectionLoader = chatSectionLoader,
|
||||||
|
pagination = ChatPagination.After(itemId, ChatPagination.PRELOAD_COUNT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -583,7 +604,7 @@ fun ChatLayout(
|
|||||||
back: () -> Unit,
|
back: () -> Unit,
|
||||||
info: () -> Unit,
|
info: () -> Unit,
|
||||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||||
loadPrevMessages: (ChatId) -> Unit,
|
loadMessages: (ChatId, ScrollDirection, Triple<Long, Int, ChatSectionArea>) -> Unit,
|
||||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||||
deleteMessages: (List<Long>) -> Unit,
|
deleteMessages: (List<Long>) -> Unit,
|
||||||
receiveFile: (Long) -> Unit,
|
receiveFile: (Long) -> Unit,
|
||||||
@@ -611,7 +632,7 @@ fun ChatLayout(
|
|||||||
onComposed: suspend (chatId: String) -> Unit,
|
onComposed: suspend (chatId: String) -> Unit,
|
||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
showViaProxy: Boolean,
|
showViaProxy: Boolean,
|
||||||
showSearch: MutableState<Boolean>
|
showSearch: MutableState<Boolean>,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
||||||
@@ -649,10 +670,10 @@ fun ChatLayout(
|
|||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
|
||||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||||
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -922,7 +943,7 @@ fun BoxScope.ChatItemsList(
|
|||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
selectedChatItems: MutableState<Set<Long>?>,
|
selectedChatItems: MutableState<Set<Long>?>,
|
||||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||||
loadPrevMessages: (ChatId) -> Unit,
|
loadMessages: (ChatId, ScrollDirection, Triple<Long, Int, ChatSectionArea>) -> Unit,
|
||||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||||
deleteMessages: (List<Long>) -> Unit,
|
deleteMessages: (List<Long>) -> Unit,
|
||||||
receiveFile: (Long) -> Unit,
|
receiveFile: (Long) -> Unit,
|
||||||
@@ -943,11 +964,12 @@ fun BoxScope.ChatItemsList(
|
|||||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||||
onComposed: suspend (chatId: String) -> Unit,
|
onComposed: suspend (chatId: String) -> Unit,
|
||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
showViaProxy: Boolean
|
showViaProxy: Boolean,
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ScrollToBottom(chatInfo.id, listState, chatModel.chatItems)
|
val scrollAdjustmentEnabled = remember { mutableStateOf(true) }
|
||||||
|
ScrollToBottom(chatInfo.id, listState, chatModel.chatItems, scrollAdjustmentEnabled)
|
||||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||||
// Scroll to bottom when search value changes from something to nothing and back
|
// Scroll to bottom when search value changes from something to nothing and back
|
||||||
LaunchedEffect(searchValue.value.isEmpty()) {
|
LaunchedEffect(searchValue.value.isEmpty()) {
|
||||||
@@ -961,21 +983,106 @@ fun BoxScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||||
|
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
|
||||||
|
val sections = remember { derivedStateOf { reversedChatItems.value.putIntoSections(revealedItems.value) } }
|
||||||
|
val preloadItemsEnabled = remember { mutableStateOf(true) }
|
||||||
|
val boundaries = remember { derivedStateOf { sections.value.map { it.boundary } } }
|
||||||
|
val scrollPosition: State<(Int) -> Int> = remember { mutableStateOf({ idx -> min(sections.value.revealedItemCount() - 1, idx + 1 ) }) }
|
||||||
|
|
||||||
|
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, preloadItemsEnabled, boundaries, loadMessages)
|
||||||
|
|
||||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
|
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
|
||||||
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
|
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
|
||||||
|
val chatInfoUpdated = rememberUpdatedState(chatInfo)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch {
|
||||||
|
snapshotFlow { chatInfoUpdated.value.id }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect {
|
||||||
|
if (revealedItems.value.isNotEmpty()) {
|
||||||
|
revealedItems.value = setOf()
|
||||||
|
}
|
||||||
|
val firstUnreadItem = reversedChatItems.value.findLast { it.isRcvNew }
|
||||||
|
if (firstUnreadItem != null) {
|
||||||
|
val firstUnreadItemIndexIdx = sections.value.chatItemPosition(firstUnreadItem.id)
|
||||||
|
if (firstUnreadItemIndexIdx != null) {
|
||||||
|
scrollAdjustmentEnabled.value = false
|
||||||
|
listState.scrollToItem(scrollPosition.value(firstUnreadItemIndexIdx), -maxHeight.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatModel.chatItemsSectionArea[firstUnreadItem.id] != ChatSectionArea.Bottom) {
|
||||||
|
withBGApi {
|
||||||
|
scrollAdjustmentEnabled.value = false
|
||||||
|
try {
|
||||||
|
apiLoadBottomSection(chatInfoUpdated.value, remoteHostId)
|
||||||
|
} finally {
|
||||||
|
delay(600)
|
||||||
|
scrollAdjustmentEnabled.value = true
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollAdjustmentEnabled.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val scrollToItem: State<(Long) -> Unit> = remember {
|
val scrollToItem: State<(Long) -> Unit> = remember {
|
||||||
mutableStateOf(
|
mutableStateOf({ itemId: Long ->
|
||||||
{ itemId: Long ->
|
val index = sections.value.chatItemPosition(itemId)
|
||||||
val index = reversedChatItems.value.indexOfFirst { it.id == itemId }
|
preloadItemsEnabled.value = false
|
||||||
if (index != -1) {
|
|
||||||
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) }
|
if (index != null) {
|
||||||
|
scope.launch {
|
||||||
|
listState.animateScrollToItem(scrollPosition.value(index), -maxHeight.value)
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
val destinationSection = sections.value.find { it.boundary.area == ChatSectionArea.Destination }
|
||||||
|
val itemsToDrop = destinationSection?.items?.flatMap { it.items }?.toList()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
itemsToDrop?.forEach {
|
||||||
|
chatModel.chatItemsSectionArea[it.id] = ChatSectionArea.Current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chatSectionLoader = ChatSectionLoader(0, ChatSectionArea.Destination)
|
||||||
|
apiLoadMessages(
|
||||||
|
rhId = remoteHostId,
|
||||||
|
chatInfo = chatInfoUpdated.value,
|
||||||
|
chatModel = chatModel,
|
||||||
|
itemId = itemId,
|
||||||
|
search = "",
|
||||||
|
chatSectionLoader = chatSectionLoader,
|
||||||
|
pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2)
|
||||||
|
)
|
||||||
|
val idx = sections.value.chatItemPosition(itemId)
|
||||||
|
scope.launch {
|
||||||
|
if (idx != null) {
|
||||||
|
listState.animateScrollToItem(scrollPosition.value(idx), -maxHeight.value)
|
||||||
|
if (!itemsToDrop.isNullOrEmpty()) {
|
||||||
|
itemsToDrop.forEach {
|
||||||
|
chatModel.chatItemsSectionArea.remove(it.id)
|
||||||
|
}
|
||||||
|
chatModel.chatItems.removeAll { chatModel.chatItemsSectionArea[it.id] == null }
|
||||||
|
val newIdx = reversedChatItems.value.indexOfFirst { it.id == itemId }
|
||||||
|
listState.scrollToItem(scrollPosition.value(newIdx), -maxHeight.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
||||||
LaunchedEffect(chatInfo.id) {
|
LaunchedEffect(chatInfo.id) {
|
||||||
@@ -993,23 +1100,14 @@ fun BoxScope.ChatItemsList(
|
|||||||
VideoPlayerHolder.releaseAll()
|
VideoPlayerHolder.releaseAll()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
LazyColumnWithScrollBar(
|
@Composable
|
||||||
Modifier.align(Alignment.BottomCenter),
|
fun ChatViewListItem(i: Int, range: IntRange?, showAvatar: Boolean, cItem: ChatItem, prevItem: ChatItem?, nextItem: ChatItem?) {
|
||||||
state = listState,
|
|
||||||
reverseLayout = true,
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
top = topPaddingToContent(),
|
|
||||||
bottom = composeViewHeight.value
|
|
||||||
),
|
|
||||||
additionalBarOffset = composeViewHeight
|
|
||||||
) {
|
|
||||||
itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
|
||||||
val itemScope = rememberCoroutineScope()
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||||
) {
|
) {
|
||||||
|
val itemScope = rememberCoroutineScope()
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||||
itemScope.launch {
|
itemScope.launch {
|
||||||
@@ -1021,246 +1119,293 @@ fun BoxScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val revealed = remember { mutableStateOf(false) }
|
val revealed = remember { mutableStateOf(revealedItems.value.contains(cItem.id)) }
|
||||||
|
|
||||||
|
KeyChangeEffect(revealed.value) {
|
||||||
|
val revealIds = if (range == null) setOf(cItem.id) else reversedChatItems.value.subList(range.first, range.last + 1).map { it.id }.toSet()
|
||||||
|
|
||||||
|
if (revealIds.isNotEmpty()) {
|
||||||
|
if (revealed.value) {
|
||||||
|
revealedItems.value = revealedItems.value.toMutableSet().apply { addAll(revealIds) }
|
||||||
|
} else {
|
||||||
|
revealedItems.value = revealedItems.value.toMutableSet().apply { removeAll(revealIds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) {
|
||||||
|
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||||
|
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||||
|
}) {
|
||||||
|
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, 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.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
||||||
|
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||||
|
if (it == DismissValue.DismissedToStart) {
|
||||||
|
itemScope.launch {
|
||||||
|
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||||
|
if (composeState.value.editing) {
|
||||||
|
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||||
|
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
|
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
val swipeableModifier = SwipeToDismissModifier(
|
||||||
|
state = dismissState,
|
||||||
|
directions = setOf(DismissDirection.EndToStart),
|
||||||
|
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||||
|
)
|
||||||
|
val sent = cItem.chatDir.sent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) {
|
fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
|
||||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
Box(
|
||||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
modifier = modifier.padding(
|
||||||
}) {
|
bottom = if (itemSeparation.largeGap) {
|
||||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, 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.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
if (i == 0) {
|
||||||
|
8.dp
|
||||||
|
} else {
|
||||||
|
4.dp
|
||||||
|
}
|
||||||
|
} else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
|
||||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||||
if (it == DismissValue.DismissedToStart) {
|
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
||||||
itemScope.launch {
|
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
|
||||||
if (composeState.value.editing) {
|
return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
|
||||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
}
|
||||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
|
||||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
Box {
|
||||||
}
|
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||||
|
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
|
||||||
|
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
||||||
|
val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
|
||||||
|
if (chatInfo is ChatInfo.Group) {
|
||||||
|
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||||
|
val member = cItem.chatDir.groupMember
|
||||||
|
val (prevMember, memCount) =
|
||||||
|
if (range != null) {
|
||||||
|
chatModel.getPrevHiddenMember(member, range)
|
||||||
|
} else {
|
||||||
|
null to 1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
val swipeableModifier = SwipeToDismissModifier(
|
|
||||||
state = dismissState,
|
|
||||||
directions = setOf(DismissDirection.EndToStart),
|
|
||||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
|
||||||
)
|
|
||||||
val sent = cItem.chatDir.sent
|
|
||||||
|
|
||||||
@Composable
|
if (showMemberImage(member, prevItem) || showAvatar) {
|
||||||
fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
|
Column(
|
||||||
Box(
|
Modifier
|
||||||
modifier = modifier.padding(
|
.padding(top = 8.dp)
|
||||||
bottom = if (itemSeparation.largeGap) {
|
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||||
if (i == 0) {
|
.fillMaxWidth()
|
||||||
8.dp
|
.then(swipeableModifier),
|
||||||
} else {
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
4.dp
|
horizontalAlignment = Alignment.Start
|
||||||
}
|
) {
|
||||||
} else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp
|
@Composable
|
||||||
),
|
fun MemberNameAndRole() {
|
||||||
contentAlignment = Alignment.CenterStart
|
Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
) {
|
Text(
|
||||||
content()
|
memberNames(member, prevMember, memCount),
|
||||||
}
|
Modifier
|
||||||
}
|
.padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
|
||||||
|
.weight(1f, false),
|
||||||
|
fontSize = 13.5.sp,
|
||||||
|
color = MaterialTheme.colors.secondary,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
||||||
|
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||||
|
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
||||||
|
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
|
|
||||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
|
||||||
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
|
||||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
|
||||||
|
|
||||||
return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
|
||||||
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
|
|
||||||
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
|
||||||
val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
|
|
||||||
if (chatInfo is ChatInfo.Group) {
|
|
||||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
|
||||||
val member = cItem.chatDir.groupMember
|
|
||||||
val (prevMember, memCount) =
|
|
||||||
if (range != null) {
|
|
||||||
chatModel.getPrevHiddenMember(member, range)
|
|
||||||
} else {
|
|
||||||
null to 1
|
|
||||||
}
|
|
||||||
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(swipeableModifier),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
horizontalAlignment = Alignment.Start
|
|
||||||
) {
|
|
||||||
@Composable
|
|
||||||
fun MemberNameAndRole() {
|
|
||||||
Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text(
|
Text(
|
||||||
memberNames(member, prevMember, memCount),
|
member.memberRole.text,
|
||||||
Modifier
|
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
|
||||||
.padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
|
|
||||||
.weight(1f, false),
|
|
||||||
fontSize = 13.5.sp,
|
fontSize = 13.5.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colors.secondary,
|
color = MaterialTheme.colors.secondary,
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
|
||||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
|
||||||
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
|
||||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
|
||||||
|
|
||||||
Text(
|
|
||||||
member.memberRole.text,
|
|
||||||
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
|
|
||||||
fontSize = 13.5.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colors.secondary,
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Item() {
|
|
||||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
|
||||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
|
||||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
|
||||||
}
|
|
||||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
|
||||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
|
||||||
MemberImage(member)
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
|
||||||
ChatItemViewShortHand(cItem, itemSeparation, range, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cItem.content.showMemberName) {
|
|
||||||
DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
|
|
||||||
MemberNameAndRole()
|
|
||||||
Item()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Item()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ChatItemBox {
|
@Composable
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
fun Item() {
|
||||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||||
|
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||||
|
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||||
|
}
|
||||||
|
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||||
|
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||||
|
MemberImage(member)
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
||||||
|
ChatItemViewShortHand(cItem, itemSeparation, range, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row(
|
}
|
||||||
Modifier
|
if (cItem.content.showMemberName) {
|
||||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
|
||||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
MemberNameAndRole()
|
||||||
.then(swipeableOrSelectionModifier)
|
Item()
|
||||||
) {
|
|
||||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Item()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ChatItemBox {
|
ChatItemBox {
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||||
}
|
}
|
||||||
Box(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
|
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||||
.then(if (selectionVisible) Modifier else swipeableModifier)
|
.then(swipeableOrSelectionModifier)
|
||||||
) {
|
) {
|
||||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // direct message
|
} else {
|
||||||
ChatItemBox {
|
ChatItemBox {
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.padding(
|
Modifier
|
||||||
start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
|
.padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
|
||||||
end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
|
|
||||||
)
|
|
||||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||||
.then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
.then(if (selectionVisible) Modifier else swipeableModifier)
|
||||||
) {
|
) {
|
||||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectionVisible) {
|
} else { // direct message
|
||||||
Box(Modifier.matchParentSize().clickable {
|
ChatItemBox {
|
||||||
val checked = selectedChatItems.value?.contains(cItem.id) == true
|
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
|
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier.padding(
|
||||||
|
start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
|
||||||
|
end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
|
||||||
|
)
|
||||||
|
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||||
|
.then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
||||||
|
) {
|
||||||
|
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (selectionVisible) {
|
||||||
|
Box(Modifier.matchParentSize().clickable {
|
||||||
val (currIndex, nextItem) = chatModel.getNextChatItem(cItem)
|
val checked = selectedChatItems.value?.contains(cItem.id) == true
|
||||||
val ciCategory = cItem.mergeCategory
|
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
|
||||||
if (ciCategory != null && ciCategory == nextItem?.mergeCategory) {
|
})
|
||||||
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
|
|
||||||
} else {
|
|
||||||
val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
|
||||||
|
|
||||||
val itemSeparation = getItemSeparation(cItem, nextItem)
|
|
||||||
val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null
|
|
||||||
|
|
||||||
if (itemSeparation.date != null) {
|
|
||||||
DateSeparator(itemSeparation.date)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||||
val reversed = reversedChatItems.value
|
LaunchedEffect(cItem.id) {
|
||||||
if (revealed.value && range != null) {
|
itemScope.launch {
|
||||||
reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
delay(600)
|
||||||
val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1]
|
val itemRange = if (range != null) {
|
||||||
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
|
val firstItem = reversedChatItems.value.getOrNull(range.first)
|
||||||
|
val lastItem = reversedChatItems.value.getOrNull(range.last)
|
||||||
|
if (lastItem != null && firstItem != null) {
|
||||||
|
CC.ItemRange(lastItem.id, firstItem.id)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CC.ItemRange(cItem.id, cItem.id)
|
||||||
}
|
}
|
||||||
} else {
|
if (itemRange != null) {
|
||||||
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
markRead(itemRange, null)
|
||||||
}
|
|
||||||
|
|
||||||
if (i == reversed.lastIndex) {
|
|
||||||
DateSeparator(cItem.meta.itemTs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
|
||||||
LaunchedEffect(cItem.id) {
|
|
||||||
itemScope.launch {
|
|
||||||
delay(600)
|
|
||||||
markRead(CC.ItemRange(cItem.id, cItem.id), null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val itemSeparation = getItemSeparation(cItem, nextItem)
|
||||||
|
val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null
|
||||||
|
|
||||||
|
if (itemSeparation.date != null) {
|
||||||
|
DateSeparator(itemSeparation.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumnWithScrollBar(
|
||||||
|
Modifier.align(Alignment.BottomCenter),
|
||||||
|
state = listState,
|
||||||
|
reverseLayout = true,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = topPaddingToContent(),
|
||||||
|
bottom = composeViewHeight.value
|
||||||
|
),
|
||||||
|
additionalBarOffset = composeViewHeight
|
||||||
|
) {
|
||||||
|
for (area in sections.value) {
|
||||||
|
for ((sIdx, section) in area.items.withIndex()) {
|
||||||
|
if (section.revealed) {
|
||||||
|
itemsIndexed(section.items, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
||||||
|
// index here is just temporary, should be removed at all or put in the section items
|
||||||
|
val prevItem = area.getPreviousShownItem(sIdx, i)
|
||||||
|
val nextItem = area.getNextShownItem(sIdx, i)
|
||||||
|
ChatViewListItem(area.itemPositions[cItem.id] ?: -1, section.originalItemsRange.takeIf { cItem.mergeCategory != null }, section.showAvatar.contains(cItem.id), cItem, prevItem, nextItem, )
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val item = section.items.first()
|
||||||
|
item(key = item.id to item.meta.createdAt.toEpochMilliseconds()) {
|
||||||
|
// here you make one collapsed item from multiple items (should be already in section items)
|
||||||
|
val prevItem = area.getPreviousShownItem(sIdx, section.items.lastIndex)
|
||||||
|
val nextItem = area.getNextShownItem(sIdx, section.items.lastIndex)
|
||||||
|
ChatViewListItem(area.itemPositions[item.id] ?: -1, section.originalItemsRange, section.showAvatar.contains(item.id), item, prevItem, nextItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reversedChatItems.value.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
DateSeparator(reversedChatItems.value.last().meta.itemTs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) {
|
||||||
|
preloadItemsEnabled.value = false
|
||||||
|
scope.launch {
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
preloadItemsEnabled.value = true
|
||||||
|
sections.value.dropTemporarySections()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState)
|
|
||||||
|
|
||||||
FloatingDate(
|
FloatingDate(
|
||||||
Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
|
Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
|
||||||
@@ -1276,13 +1421,13 @@ fun BoxScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) {
|
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>, enabled: State<Boolean>) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// Helps to scroll to bottom after moving from Group to Direct chat
|
// Helps to scroll to bottom after moving from Group to Direct chat
|
||||||
// and prevents scrolling to bottom on orientation change
|
// and prevents scrolling to bottom on orientation change
|
||||||
var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) }
|
var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) }
|
||||||
LaunchedEffect(chatId, shouldAutoScroll) {
|
LaunchedEffect(chatId, shouldAutoScroll) {
|
||||||
if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0) {
|
if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0 && enabled.value) {
|
||||||
scope.launch { listState.scrollToItem(0) }
|
scope.launch { listState.scrollToItem(0) }
|
||||||
}
|
}
|
||||||
// Don't autoscroll next time until it will be needed
|
// Don't autoscroll next time until it will be needed
|
||||||
@@ -1296,7 +1441,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { chatItems.value.lastOrNull()?.id }
|
snapshotFlow { chatItems.value.lastOrNull()?.id }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it && enabled.value }
|
||||||
.collect {
|
.collect {
|
||||||
try {
|
try {
|
||||||
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) {
|
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) {
|
||||||
@@ -1326,7 +1471,8 @@ fun BoxScope.FloatingButtons(
|
|||||||
chatInfo: ChatInfo,
|
chatInfo: ChatInfo,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||||
listState: LazyListState
|
listState: LazyListState,
|
||||||
|
scrollToLatestItem: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
|
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
|
||||||
@@ -1348,9 +1494,7 @@ fun BoxScope.FloatingButtons(
|
|||||||
showBottomButtonWithCounter,
|
showBottomButtonWithCounter,
|
||||||
showBottomButtonWithArrow,
|
showBottomButtonWithArrow,
|
||||||
composeViewHeight,
|
composeViewHeight,
|
||||||
onClickArrowDown = {
|
onClickArrowDown = scrollToLatestItem,
|
||||||
scope.launch { listState.animateScrollToItem(0) }
|
|
||||||
},
|
|
||||||
onClickCounter = {
|
onClickCounter = {
|
||||||
val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
|
val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
|
||||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
|
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
|
||||||
@@ -1397,12 +1541,35 @@ fun PreloadItems(
|
|||||||
chatId: String,
|
chatId: String,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
remaining: Int = 10,
|
remaining: Int = 10,
|
||||||
onLoadMore: (ChatId) -> Unit,
|
enabled: State<Boolean>,
|
||||||
|
boundaries: State<List<ChatSectionAreaBoundary>>,
|
||||||
|
onLoadMore: (ChatId, ScrollDirection, Triple<Long, Int, ChatSectionArea>) -> Unit,
|
||||||
) {
|
) {
|
||||||
// Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection
|
// Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection
|
||||||
val allowLoad = remember { mutableStateOf(false) }
|
val allowLoad = remember { mutableStateOf(false) }
|
||||||
val chatId = rememberUpdatedState(chatId)
|
val chatId = rememberUpdatedState(chatId)
|
||||||
val onLoadMore = rememberUpdatedState(onLoadMore)
|
val onLoadMore = rememberUpdatedState(onLoadMore)
|
||||||
|
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||||
|
var previousIndex by remember { mutableStateOf(0) }
|
||||||
|
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||||
|
val currentIndex = listState.firstVisibleItemIndex
|
||||||
|
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||||
|
val threshold = 25
|
||||||
|
|
||||||
|
scrollDirection = when {
|
||||||
|
currentIndex > previousIndex -> ScrollDirection.Up
|
||||||
|
currentIndex < previousIndex -> ScrollDirection.Down
|
||||||
|
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Up
|
||||||
|
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Down
|
||||||
|
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||||
|
else -> scrollDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
previousIndex = currentIndex
|
||||||
|
previousScrollOffset = currentScrollOffset
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { chatId.value }
|
snapshotFlow { chatId.value }
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
@@ -1412,19 +1579,41 @@ fun PreloadItems(
|
|||||||
allowLoad.value = true
|
allowLoad.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyChangeEffect(allowLoad.value) {
|
KeyChangeEffect(allowLoad.value, enabled.value) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val lInfo = listState.layoutInfo
|
val lInfo = listState.layoutInfo
|
||||||
val totalItemsNumber = lInfo.totalItemsCount
|
val totalItemsNumber = lInfo.totalItemsCount
|
||||||
val lastVisibleItemIndex = (lInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
|
val lastVisibleItemIndex = (lInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
|
||||||
if (allowLoad.value && lastVisibleItemIndex > (totalItemsNumber - remaining) && totalItemsNumber >= ChatPagination.INITIAL_COUNT)
|
val section = if (scrollDirection == ScrollDirection.Up) {
|
||||||
totalItemsNumber + ChatPagination.PRELOAD_COUNT
|
boundaries.value.find { lastVisibleItemIndex in it.minIndex..it.maxIndex }
|
||||||
else
|
} else if (scrollDirection == ScrollDirection.Down) {
|
||||||
0
|
boundaries.value.find { listState.firstVisibleItemIndex in it.minIndex..it.maxIndex }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = if (allowLoad.value && section != null && enabled.value) {
|
||||||
|
val itemIdx = when {
|
||||||
|
scrollDirection == ScrollDirection.Up && lastVisibleItemIndex > (section.maxIndex - remaining) -> {
|
||||||
|
chatModel.chatItems.size - 1 - section.maxIndex
|
||||||
|
}
|
||||||
|
scrollDirection == ScrollDirection.Down && section.area != ChatSectionArea.Bottom && listState.firstVisibleItemIndex < (section.minIndex + remaining) && totalItemsNumber > remaining -> {
|
||||||
|
chatModel.chatItems.size - 1 - section.minIndex
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val itemId = itemIdx?.let { chatModel.chatItems.value.getOrNull(it)?.id }
|
||||||
|
itemId?.let { Triple(it, itemIdx, section.area) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
request
|
||||||
}
|
}
|
||||||
.filter { it > 0 }
|
.distinctUntilChanged()
|
||||||
|
.filterNotNull()
|
||||||
.collect {
|
.collect {
|
||||||
onLoadMore.value(chatId.value)
|
onLoadMore.value(chatId.value, scrollDirection, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2102,7 +2291,7 @@ fun PreviewChatLayout() {
|
|||||||
back = {},
|
back = {},
|
||||||
info = {},
|
info = {},
|
||||||
showMemberInfo = { _, _ -> },
|
showMemberInfo = { _, _ -> },
|
||||||
loadPrevMessages = {},
|
loadMessages = { _, _, _ -> },
|
||||||
deleteMessage = { _, _ -> },
|
deleteMessage = { _, _ -> },
|
||||||
deleteMessages = { _ -> },
|
deleteMessages = { _ -> },
|
||||||
receiveFile = { _ -> },
|
receiveFile = { _ -> },
|
||||||
@@ -2174,7 +2363,7 @@ fun PreviewGroupChatLayout() {
|
|||||||
back = {},
|
back = {},
|
||||||
info = {},
|
info = {},
|
||||||
showMemberInfo = { _, _ -> },
|
showMemberInfo = { _, _ -> },
|
||||||
loadPrevMessages = {},
|
loadMessages = { _, _, _ -> },
|
||||||
deleteMessage = { _, _ -> },
|
deleteMessage = { _, _ -> },
|
||||||
deleteMessages = {},
|
deleteMessages = {},
|
||||||
receiveFile = { _ -> },
|
receiveFile = { _ -> },
|
||||||
|
|||||||
+3
-2
@@ -70,8 +70,9 @@ fun GroupMemberInfoView(
|
|||||||
getContactChat = { chatModel.getContactChat(it) },
|
getContactChat = { chatModel.getContactChat(it) },
|
||||||
openDirectChat = {
|
openDirectChat = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
|
val r = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||||
if (c != null) {
|
if (r != null) {
|
||||||
|
val (c) = r
|
||||||
withChats {
|
withChats {
|
||||||
if (chatModel.getContactChat(it) == null) {
|
if (chatModel.getContactChat(it) == null) {
|
||||||
addChat(c)
|
addChat(c)
|
||||||
|
|||||||
+7
-1
@@ -129,7 +129,13 @@ fun FramedItemView(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { showMenu.value = true },
|
onLongClick = { showMenu.value = true },
|
||||||
onClick = { scrollToItem(qi.itemId?: return@combinedClickable) }
|
onClick = {
|
||||||
|
if (qi.itemId == null) {
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_get_chat_item_not_found_title), generalGetString(MR.strings.failed_to_get_chat_item_not_found_description))
|
||||||
|
} else {
|
||||||
|
scrollToItem(qi.itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.onRightClick { showMenu.value = true }
|
.onRightClick { showMenu.value = true }
|
||||||
) {
|
) {
|
||||||
|
|||||||
+27
-14
@@ -1,7 +1,6 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -32,6 +31,7 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -204,43 +204,56 @@ suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope {
|
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope {
|
||||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId)
|
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||||
if (chat != null && isActive) {
|
if (chat != null && isActive) {
|
||||||
openLoadedChat(chat, chatModel)
|
openLoadedChat(chat.first, chatModel, chat.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope {
|
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope {
|
||||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId)
|
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||||
if (chat != null && isActive) {
|
if (chat != null && isActive) {
|
||||||
openLoadedChat(chat, chatModel)
|
openLoadedChat(chat.first, chatModel, chat.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope {
|
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope {
|
||||||
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT))
|
||||||
if (chat != null && isActive) {
|
if (chat != null && isActive) {
|
||||||
openLoadedChat(chat, chatModel)
|
openLoadedChat(chat.first, chatModel, chat.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
fun openLoadedChat(chat: Chat, chatModel: ChatModel, landingSection: ChatLandingSection = ChatLandingSection.Latest) {
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.replaceAll(chat.chatItems)
|
||||||
chatModel.chatId.value = chat.chatInfo.id
|
chatModel.chatId.value = chat.chatInfo.id
|
||||||
|
chatModel.chatItemsSectionArea.clear()
|
||||||
|
chatModel.chatItems.value.associateTo(chatModel.chatItemsSectionArea) { it.id to landingSectionToArea(landingSection) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
|
suspend fun apiLoadMessages(
|
||||||
val chatInfo = ch.chatInfo
|
rhId: Long?,
|
||||||
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
|
chatInfo: ChatInfo,
|
||||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
|
chatModel: ChatModel,
|
||||||
|
itemId: Long,
|
||||||
|
search: String,
|
||||||
|
chatSectionLoader: ChatSectionLoader,
|
||||||
|
pagination: ChatPagination = ChatPagination.Before(itemId, ChatPagination.PRELOAD_COUNT)
|
||||||
|
) {
|
||||||
|
val (chat) = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
|
||||||
if (chatModel.chatId.value != chat.id) return
|
if (chatModel.chatId.value != chat.id) return
|
||||||
chatModel.chatItems.addAll(0, chat.chatItems)
|
withContext(Dispatchers.Main) {
|
||||||
|
val itemsToAdd = chatSectionLoader.prepareItems(chat.chatItems)
|
||||||
|
if (itemsToAdd.isNotEmpty()) {
|
||||||
|
chatModel.chatItems.addAll(chatSectionLoader.position, itemsToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
|
suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
|
||||||
val chatInfo = ch.chatInfo
|
val chatInfo = ch.chatInfo
|
||||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
val (chat) = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
||||||
if (chatModel.chatId.value != chat.id) return
|
if (chatModel.chatId.value != chat.id) return
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.replaceAll(chat.chatItems)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -105,7 +105,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||||
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
runAtomically { toRemove.removeAll { elem -> modalViews.removeAt(elem); true } }
|
||||||
}
|
}
|
||||||
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
|
// 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
|
// to prevent unneeded animation on different situations
|
||||||
@@ -184,7 +184,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
}
|
}
|
||||||
// This is needed because if we delete from modalViews immediately on request, animation will be bad
|
// This is needed because if we delete from modalViews immediately on request, animation will be bad
|
||||||
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
|
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
|
||||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
runAtomically { toRemove.removeAll { elem -> modalViews.removeAt(elem); true } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
<string name="error_loading_xftp_servers">Error loading XFTP servers</string>
|
<string name="error_loading_xftp_servers">Error loading XFTP servers</string>
|
||||||
<string name="error_setting_network_config">Error updating network configuration</string>
|
<string name="error_setting_network_config">Error updating network configuration</string>
|
||||||
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
||||||
|
<string name="failed_to_get_chat_item_not_found_title">Message no longer available</string>
|
||||||
|
<string name="failed_to_get_chat_item_not_found_description">The quoted message you are trying to access has been deleted.</string>
|
||||||
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
||||||
<string name="contact_developers">Please update the app and contact developers.</string>
|
<string name="contact_developers">Please update the app and contact developers.</string>
|
||||||
<string name="failed_to_create_user_title">Error creating profile!</string>
|
<string name="failed_to_create_user_title">Error creating profile!</string>
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
|||||||
listGroups count pending =
|
listGroups count pending =
|
||||||
readTVarIO (groupRegs st) >>= \groups -> do
|
readTVarIO (groupRegs st) >>= \groups -> do
|
||||||
grs <-
|
grs <-
|
||||||
if pending
|
if pending
|
||||||
then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups
|
then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups
|
||||||
else pure groups
|
else pure groups
|
||||||
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||||
@@ -643,7 +643,7 @@ getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId)
|
|||||||
where
|
where
|
||||||
resp :: ChatResponse -> Maybe Contact
|
resp :: ChatResponse -> Maybe Contact
|
||||||
resp = \case
|
resp = \case
|
||||||
CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) -> Just ct
|
CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) _ -> Just ct
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo)
|
getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo)
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20240920_user_order
|
Simplex.Chat.Migrations.M20240920_user_order
|
||||||
Simplex.Chat.Migrations.M20241008_indexes
|
Simplex.Chat.Migrations.M20241008_indexes
|
||||||
Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
||||||
|
Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
|
|||||||
+8
-6
@@ -735,14 +735,14 @@ processChatCommand' vr = \case
|
|||||||
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
|
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
|
||||||
-- TODO optimize queries calculating ChatStats, currently they're disabled
|
-- TODO optimize queries calculating ChatStats, currently they're disabled
|
||||||
CTDirect -> do
|
CTDirect -> do
|
||||||
directChat <- withFastStore (\db -> getDirectChat db vr user cId pagination search)
|
(directChat, section) <- withFastStore (\db -> getDirectChat db vr user cId pagination search)
|
||||||
pure $ CRApiChat user (AChat SCTDirect directChat)
|
pure $ CRApiChat user (AChat SCTDirect directChat) section
|
||||||
CTGroup -> do
|
CTGroup -> do
|
||||||
groupChat <- withFastStore (\db -> getGroupChat db vr user cId pagination search)
|
(groupChat, section) <- withFastStore (\db -> getGroupChat db vr user cId pagination search)
|
||||||
pure $ CRApiChat user (AChat SCTGroup groupChat)
|
pure $ CRApiChat user (AChat SCTGroup groupChat) section
|
||||||
CTLocal -> do
|
CTLocal -> do
|
||||||
localChat <- withFastStore (\db -> getLocalChat db user cId pagination search)
|
(localChat, section) <- withFastStore (\db -> getLocalChat db user cId pagination search)
|
||||||
pure $ CRApiChat user (AChat SCTLocal localChat)
|
pure $ CRApiChat user (AChat SCTLocal localChat) section
|
||||||
CTContactRequest -> pure $ chatCmdError (Just user) "not implemented"
|
CTContactRequest -> pure $ chatCmdError (Just user) "not implemented"
|
||||||
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
|
CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
|
||||||
APIGetChatItems pagination search -> withUser $ \user -> do
|
APIGetChatItems pagination search -> withUser $ \user -> do
|
||||||
@@ -8301,6 +8301,8 @@ chatCommandP =
|
|||||||
(CPLast <$ "count=" <*> A.decimal)
|
(CPLast <$ "count=" <*> A.decimal)
|
||||||
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
|
<|> (CPAround <$ "around=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
|
<|> (CPInitial <$ "initial=" <*> A.decimal)
|
||||||
paginationByTimeP =
|
paginationByTimeP =
|
||||||
(PTLast <$ "count=" <*> A.decimal)
|
(PTLast <$ "count=" <*> A.decimal)
|
||||||
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
|
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ data ChatResponse
|
|||||||
| CRChatSuspended
|
| CRChatSuspended
|
||||||
| CRApiChats {user :: User, chats :: [AChat]}
|
| CRApiChats {user :: User, chats :: [AChat]}
|
||||||
| CRChats {chats :: [AChat]}
|
| CRChats {chats :: [AChat]}
|
||||||
| CRApiChat {user :: User, chat :: AChat}
|
| CRApiChat {user :: User, chat :: AChat, section :: ChatLandingSection}
|
||||||
| CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]}
|
| CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]}
|
||||||
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
|
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
|
||||||
| CRChatItemId User (Maybe ChatItemId)
|
| CRChatItemId User (Maybe ChatItemId)
|
||||||
@@ -839,8 +839,15 @@ data ChatPagination
|
|||||||
= CPLast Int
|
= CPLast Int
|
||||||
| CPAfter ChatItemId Int
|
| CPAfter ChatItemId Int
|
||||||
| CPBefore ChatItemId Int
|
| CPBefore ChatItemId Int
|
||||||
|
| CPAround ChatItemId Int
|
||||||
|
| CPInitial Int
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
data ChatLandingSection
|
||||||
|
= CLSLatest
|
||||||
|
| CLSUnread
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
data PaginationByTime
|
data PaginationByTime
|
||||||
= PTLast Int
|
= PTLast Int
|
||||||
| PTAfter UTCTime Int
|
| PTAfter UTCTime Int
|
||||||
@@ -1591,6 +1598,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCSR") ''RemoteCtrlStopReason)
|
|||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason)
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason)
|
||||||
|
|
||||||
|
$(JQ.deriveJSON (enumJSON $ dropPrefix "CLS") ''ChatLandingSection)
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
|
||||||
|
|
||||||
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
|
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
|
||||||
|
|||||||
@@ -239,6 +239,12 @@ chatItemTs (CChatItem _ ci) = chatItemTs' ci
|
|||||||
chatItemTs' :: ChatItem c d -> UTCTime
|
chatItemTs' :: ChatItem c d -> UTCTime
|
||||||
chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs
|
chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs
|
||||||
|
|
||||||
|
chatItemCreatedAt :: CChatItem c -> UTCTime
|
||||||
|
chatItemCreatedAt (CChatItem _ ci) = chatItemCreatedAt' ci
|
||||||
|
|
||||||
|
chatItemCreatedAt' :: ChatItem c d -> UTCTime
|
||||||
|
chatItemCreatedAt' ChatItem {meta = CIMeta {createdAt}} = createdAt
|
||||||
|
|
||||||
chatItemTimed :: ChatItem c d -> Maybe CITimed
|
chatItemTimed :: ChatItem c d -> Maybe CITimed
|
||||||
chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed
|
chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20241023_chat_item_autoincrement_id :: Query
|
||||||
|
m20241023_chat_item_autoincrement_id =
|
||||||
|
[sql|
|
||||||
|
INSERT INTO sqlite_sequence (name, seq)
|
||||||
|
SELECT 'chat_items', MAX(ROWID) FROM chat_items;
|
||||||
|
|
||||||
|
PRAGMA writable_schema=1;
|
||||||
|
|
||||||
|
UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT')
|
||||||
|
WHERE name = 'chat_items' AND type = 'table';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=0;
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20241023_chat_item_autoincrement_id :: Query
|
||||||
|
down_m20241023_chat_item_autoincrement_id =
|
||||||
|
[sql|
|
||||||
|
DELETE FROM sqlite_sequence WHERE name = 'chat_items';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=1;
|
||||||
|
|
||||||
|
UPDATE sqlite_master
|
||||||
|
SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY')
|
||||||
|
WHERE name = 'chat_items' AND type = 'table';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=0;
|
||||||
|
|]
|
||||||
@@ -360,7 +360,7 @@ CREATE TABLE pending_group_messages(
|
|||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||||
);
|
);
|
||||||
CREATE TABLE chat_items(
|
CREATE TABLE chat_items(
|
||||||
chat_item_id INTEGER PRIMARY KEY,
|
chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||||
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
|
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
|
||||||
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
||||||
@@ -399,6 +399,7 @@ CREATE TABLE chat_items(
|
|||||||
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
|
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
|
||||||
via_proxy INTEGER
|
via_proxy INTEGER
|
||||||
);
|
);
|
||||||
|
CREATE TABLE sqlite_sequence(name,seq);
|
||||||
CREATE TABLE chat_item_messages(
|
CREATE TABLE chat_item_messages(
|
||||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||||
message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE,
|
message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE,
|
||||||
@@ -429,7 +430,6 @@ CREATE TABLE commands(
|
|||||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||||
);
|
);
|
||||||
CREATE TABLE sqlite_sequence(name,seq);
|
|
||||||
CREATE TABLE settings(
|
CREATE TABLE settings(
|
||||||
settings_id INTEGER PRIMARY KEY,
|
settings_id INTEGER PRIMARY KEY,
|
||||||
chat_item_ttl INTEGER,
|
chat_item_ttl INTEGER,
|
||||||
|
|||||||
+328
-162
@@ -138,7 +138,7 @@ import Data.Time (addUTCTime)
|
|||||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..))
|
import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..))
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), PaginationByTime (..))
|
import Simplex.Chat.Controller (ChatLandingSection (CLSLatest, CLSUnread), ChatListQuery (..), ChatPagination (..), PaginationByTime (..))
|
||||||
import Simplex.Chat.Markdown
|
import Simplex.Chat.Markdown
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Messages.CIContent
|
import Simplex.Chat.Messages.CIContent
|
||||||
@@ -947,37 +947,41 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
|
|||||||
aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
|
aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
|
||||||
in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat
|
in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat
|
||||||
|
|
||||||
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect)
|
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect, ChatLandingSection)
|
||||||
getDirectChat db vr user contactId pagination search_ = do
|
getDirectChat db vr user contactId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
ct <- getContact db vr user contactId
|
ct <- getContact db vr user contactId
|
||||||
liftIO $ case pagination of
|
case pagination of
|
||||||
CPLast count -> getDirectChatLast_ db user ct count search
|
CPLast count -> liftIO $ (,CLSLatest) <$> getDirectChatLast_ db user ct count search
|
||||||
CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search
|
CPAfter afterId count -> (,CLSLatest) <$> getDirectChatAfter_ db user ct afterId count search
|
||||||
CPBefore beforeId count -> getDirectChatBefore_ db user ct beforeId count search
|
CPBefore beforeId count -> (,CLSLatest) <$> getDirectChatBefore_ db user ct beforeId count search
|
||||||
|
CPAround aroundId count -> (,CLSLatest) <$> getDirectChatAround_ db user ct aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getDirectChatInitial_ db user ct count
|
||||||
|
|
||||||
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
|
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
|
||||||
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect)
|
||||||
getDirectChatLast_ db user@User {userId} ct@Contact {contactId} count search = do
|
getDirectChatLast_ db user ct count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemIdsLast_
|
chatItemIds <- getDirectChatItemIdsLast_ db user ct count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getDirectChatItemIdsLast_ :: IO [ChatItemId]
|
getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId]
|
||||||
getDirectChatItemIdsLast_ =
|
getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, count)
|
(userId, contactId, search, count)
|
||||||
|
|
||||||
safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect)
|
safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect)
|
||||||
safeGetDirectItem db user ct currentTs itemId =
|
safeGetDirectItem db user ct currentTs itemId =
|
||||||
@@ -1021,82 +1025,129 @@ getDirectChatItemLast db user@User {userId} contactId = do
|
|||||||
(userId, contactId)
|
(userId, contactId)
|
||||||
getDirectChatItem db user contactId chatItemId
|
getDirectChatItem db user contactId chatItemId
|
||||||
|
|
||||||
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
getDirectChatAfter_ db user@User {userId} ct@Contact {contactId} afterChatItemId count search = do
|
getDirectChatAfter_ db user ct@Contact {contactId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemIdsAfter_
|
afterChatItem <- getDirectChatItem db user contactId afterChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getDirectChatItemIdsAfter_ db user ct afterChatItemId count search (chatItemCreatedAt afterChatItem)
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) chatItems stats
|
pure $ Chat (DirectChat ct) chatItems stats
|
||||||
where
|
|
||||||
getDirectChatItemIdsAfter_ :: IO [ChatItemId]
|
|
||||||
getDirectChatItemIdsAfter_ =
|
|
||||||
map fromOnly
|
|
||||||
<$> DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT chat_item_id
|
|
||||||
FROM chat_items
|
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
|
||||||
AND chat_item_id > ?
|
|
||||||
ORDER BY created_at ASC, chat_item_id ASC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|
||||||
(userId, contactId, search, afterChatItemId, count)
|
|
||||||
|
|
||||||
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatItemIdsAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getDirectChatBefore_ db user@User {userId} ct@Contact {contactId} beforeChatItemId count search = do
|
getDirectChatItemIdsAfter_ db User {userId} Contact {contactId} afterChatItemId count search afterChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
|
||||||
|
ORDER BY created_at ASC, chat_item_id ASC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, contactId, search, afterChatItemCreatedAt, afterChatItemCreatedAt, afterChatItemId, count)
|
||||||
|
|
||||||
|
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
|
getDirectChatBefore_ db user ct@Contact {contactId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemsIdsBefore_
|
beforeChatItem <- getDirectChatItem db user contactId beforeChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getDirectChatItemsIdsBefore_ db user ct beforeChatItemId count search (chatItemCreatedAt beforeChatItem)
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getDirectChatItemsIdsBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getDirectChatItemsIdsBefore_ db User {userId} Contact {contactId} beforeChatItemId count search beforeChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, contactId, search, beforeChatItemCreatedAt, beforeChatItemCreatedAt, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
|
getDirectChatAround_ db user ct@Contact {contactId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getDirectChatItem db user contactId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getDirectChatItemsIdsBefore_ db user ct aroundItemId fetchCountBefore search (chatItemCreatedAt middleChatItem)
|
||||||
|
afterIds <- liftIO $ getDirectChatItemIdsAfter_ db user ct aroundItemId fetchCountAfter search (chatItemCreatedAt middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ reverse <$> mapM (safeGetDirectItem db user ct currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) afterIds
|
||||||
|
let chatItems = beforeChatItems <> [middleChatItem] <> afterChatItems
|
||||||
|
pure $ Chat (DirectChat ct) chatItems stats
|
||||||
|
|
||||||
|
getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect, ChatLandingSection)
|
||||||
|
getDirectChatInitial_ db user@User {userId} ct@Contact {contactId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getDirectChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> do
|
||||||
|
chat <- getDirectChatAround_ db user ct firstUnreadItemId count ""
|
||||||
|
lastItemId <- liftIO $ getDirectChatItemIdsLast_ db user ct 1 ""
|
||||||
|
pure (chat, landingSection chat lastItemId)
|
||||||
|
Nothing -> liftIO $ (,CLSLatest) <$> getDirectChatLast_ db user ct count ""
|
||||||
where
|
where
|
||||||
getDirectChatItemsIdsBefore_ :: IO [ChatItemId]
|
getDirectChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getDirectChatItemsIdsBefore_ =
|
getDirectChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND contact_id = ? AND item_status = ?
|
||||||
AND chat_item_id < ?
|
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, beforeChatItemId, count)
|
(userId, contactId, CISRcvNew)
|
||||||
|
|
||||||
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
landingSection :: Chat c -> [ChatItemId] -> ChatLandingSection
|
||||||
|
landingSection Chat {chatItems} [lastItemId] = do
|
||||||
|
let lastItemIdInChat = foldr (\ci acc -> acc || cchatItemId ci == lastItemId) False chatItems
|
||||||
|
if lastItemIdInChat then CLSLatest else CLSUnread
|
||||||
|
landingSection _ _ = CLSUnread
|
||||||
|
|
||||||
|
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, ChatLandingSection)
|
||||||
getGroupChat db vr user groupId pagination search_ = do
|
getGroupChat db vr user groupId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
g <- getGroupInfo db vr user groupId
|
g <- getGroupInfo db vr user groupId
|
||||||
case pagination of
|
case pagination of
|
||||||
CPLast count -> liftIO $ getGroupChatLast_ db user g count search
|
CPLast count -> liftIO $ (,CLSLatest) <$> getGroupChatLast_ db user g count search
|
||||||
CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search
|
CPAfter afterId count -> (,CLSLatest) <$> getGroupChatAfter_ db user g afterId count search
|
||||||
CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search
|
CPBefore beforeId count -> (,CLSLatest) <$> getGroupChatBefore_ db user g beforeId count search
|
||||||
|
CPAround aroundId count -> (,CLSLatest) <$> getGroupChatAround_ db user g aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getGroupChatInitial_ db user g count
|
||||||
|
|
||||||
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup)
|
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup)
|
||||||
getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do
|
getGroupChatLast_ db user g count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getGroupChatItemIdsLast_
|
chatItemIds <- getGroupChatItemIdsLast_ db user g count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getGroupChatItemIdsLast_ :: IO [ChatItemId]
|
getGroupChatItemIdsLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO [ChatItemId]
|
||||||
getGroupChatItemIdsLast_ =
|
getGroupChatItemIdsLast_ db User {userId} GroupInfo {groupId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY item_ts DESC, chat_item_id DESC
|
ORDER BY item_ts DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, count)
|
(userId, groupId, search, count)
|
||||||
|
|
||||||
safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup)
|
safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup)
|
||||||
safeGetGroupItem db user g currentTs itemId =
|
safeGetGroupItem db user g currentTs itemId =
|
||||||
@@ -1141,83 +1192,122 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do
|
|||||||
getGroupChatItem db user groupId chatItemId
|
getGroupChatItem db user groupId chatItemId
|
||||||
|
|
||||||
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId count search = do
|
getGroupChatAfter_ db user g@GroupInfo {groupId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
afterChatItem <- getGroupChatItem db user groupId afterChatItemId
|
afterChatItem <- getGroupChatItem db user groupId afterChatItemId
|
||||||
chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem)
|
chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ db user g afterChatItemId count search (chatItemTs afterChatItem)
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) chatItems stats
|
pure $ Chat (GroupChat g) chatItems stats
|
||||||
where
|
|
||||||
getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId]
|
getGroupChatItemIdsAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getGroupChatItemIdsAfter_ afterChatItemTs =
|
getGroupChatItemIdsAfter_ db User {userId} GroupInfo {groupId} afterChatItemId count search afterChatItemTs =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
|
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
|
||||||
ORDER BY item_ts ASC, chat_item_id ASC
|
ORDER BY item_ts ASC, chat_item_id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count)
|
(userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count)
|
||||||
|
|
||||||
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId count search = do
|
getGroupChatBefore_ db user g@GroupInfo {groupId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId
|
beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId
|
||||||
chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem)
|
chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ db user g beforeChatItemId count search (chatItemTs beforeChatItem)
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getGroupChatItemIdsBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getGroupChatItemIdsBefore_ db User {userId} GroupInfo {groupId} beforeChatItemId count search beforeChatItemTs =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY item_ts DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
|
getGroupChatAround_ db user g@GroupInfo {groupId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getGroupChatItem db user groupId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getGroupChatItemIdsBefore_ db user g aroundItemId fetchCountBefore search (chatItemTs middleChatItem)
|
||||||
|
afterIds <- liftIO $ getGroupChatItemIdsAfter_ db user g aroundItemId fetchCountAfter search (chatItemTs middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ reverse <$> mapM (safeGetGroupItem db user g currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) afterIds
|
||||||
|
let chatItems = beforeChatItems <> [middleChatItem] <> afterChatItems
|
||||||
|
pure $ Chat (GroupChat g) chatItems stats
|
||||||
|
|
||||||
|
getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Int -> ExceptT StoreError IO (Chat 'CTGroup, ChatLandingSection)
|
||||||
|
getGroupChatInitial_ db user@User {userId} g@GroupInfo {groupId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getGroupChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> do
|
||||||
|
chat <- getGroupChatAround_ db user g firstUnreadItemId count ""
|
||||||
|
lastItemId <- liftIO $ getGroupChatItemIdsLast_ db user g 1 ""
|
||||||
|
pure (chat, landingSection chat lastItemId)
|
||||||
|
Nothing -> liftIO $ (,CLSLatest) <$> getGroupChatLast_ db user g count ""
|
||||||
where
|
where
|
||||||
getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId]
|
getGroupChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getGroupChatItemIdsBefore_ beforeChatItemTs =
|
getGroupChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_status = ?
|
||||||
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
|
|
||||||
ORDER BY item_ts DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count)
|
(userId, groupId, CISRcvNew)
|
||||||
|
|
||||||
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal)
|
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, ChatLandingSection)
|
||||||
getLocalChat db user folderId pagination search_ = do
|
getLocalChat db user folderId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
nf <- getNoteFolder db user folderId
|
nf <- getNoteFolder db user folderId
|
||||||
liftIO $ case pagination of
|
case pagination of
|
||||||
CPLast count -> getLocalChatLast_ db user nf count search
|
CPLast count -> liftIO $ (,CLSLatest) <$> getLocalChatLast_ db user nf count search
|
||||||
CPAfter afterId count -> getLocalChatAfter_ db user nf afterId count search
|
CPAfter afterId count -> (,CLSLatest) <$> getLocalChatAfter_ db user nf afterId count search
|
||||||
CPBefore beforeId count -> getLocalChatBefore_ db user nf beforeId count search
|
CPBefore beforeId count -> (,CLSLatest) <$> getLocalChatBefore_ db user nf beforeId count search
|
||||||
|
CPAround aroundId count -> (,CLSLatest) <$> getLocalChatAround_ db user nf aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getLocalChatInitial_ db user nf count
|
||||||
|
|
||||||
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal)
|
||||||
getLocalChatLast_ db user@User {userId} nf@NoteFolder {noteFolderId} count search = do
|
getLocalChatLast_ db user nf count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsLast_
|
chatItemIds <- getLocalChatItemIdsLast_ db user nf count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getLocalChatItemIdsLast_ :: IO [ChatItemId]
|
getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId]
|
||||||
getLocalChatItemIdsLast_ =
|
getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, noteFolderId, search, count)
|
(userId, noteFolderId, search, count)
|
||||||
|
|
||||||
safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal)
|
safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal)
|
||||||
safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId =
|
safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId =
|
||||||
@@ -1245,51 +1335,88 @@ safeToLocalItem currentTs itemId = \case
|
|||||||
file = Nothing
|
file = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
getLocalChatAfter_ db user@User {userId} nf@NoteFolder {noteFolderId} afterChatItemId count search = do
|
getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsAfter_
|
afterChatItem <- getLocalChatItem db user noteFolderId afterChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getLocalChatItemIdsAfter_ db user nf afterChatItemId count search (chatItemCreatedAt afterChatItem)
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) chatItems stats
|
pure $ Chat (LocalChat nf) chatItems stats
|
||||||
where
|
|
||||||
getLocalChatItemIdsAfter_ :: IO [ChatItemId]
|
|
||||||
getLocalChatItemIdsAfter_ =
|
|
||||||
map fromOnly
|
|
||||||
<$> DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT chat_item_id
|
|
||||||
FROM chat_items
|
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
|
||||||
AND chat_item_id > ?
|
|
||||||
ORDER BY created_at ASC, chat_item_id ASC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|
||||||
(userId, noteFolderId, search, afterChatItemId, count)
|
|
||||||
|
|
||||||
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatItemIdsAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getLocalChatBefore_ db user@User {userId} nf@NoteFolder {noteFolderId} beforeChatItemId count search = do
|
getLocalChatItemIdsAfter_ db User {userId} NoteFolder {noteFolderId} afterChatItemId count search afterChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
|
||||||
|
ORDER BY created_at ASC, chat_item_id ASC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, noteFolderId, search, afterChatItemCreatedAt, afterChatItemCreatedAt, afterChatItemId, count)
|
||||||
|
|
||||||
|
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
|
getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsBefore_
|
beforeChatItem <- getLocalChatItem db user noteFolderId beforeChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getLocalChatItemIdsBefore_ db user nf beforeChatItemId count search (chatItemCreatedAt beforeChatItem)
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getLocalChatItemIdsBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getLocalChatItemIdsBefore_ db User {userId} NoteFolder {noteFolderId} beforeChatItemId count search beforeChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, noteFolderId, search, beforeChatItemCreatedAt, beforeChatItemCreatedAt, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
|
getLocalChatAround_ db user nf@NoteFolder {noteFolderId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getLocalChatItem db user noteFolderId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getLocalChatItemIdsBefore_ db user nf aroundItemId fetchCountBefore search (chatItemCreatedAt middleChatItem)
|
||||||
|
afterIds <- liftIO $ getLocalChatItemIdsAfter_ db user nf aroundItemId fetchCountAfter search (chatItemCreatedAt middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ reverse <$> mapM (safeGetLocalItem db user nf currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) afterIds
|
||||||
|
let chatItems = beforeChatItems <> [middleChatItem] <> afterChatItems
|
||||||
|
pure $ Chat (LocalChat nf) chatItems stats
|
||||||
|
|
||||||
|
getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal, ChatLandingSection)
|
||||||
|
getLocalChatInitial_ db user@User {userId} nf@NoteFolder {noteFolderId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getLocalChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> do
|
||||||
|
chat <- getLocalChatAround_ db user nf firstUnreadItemId count ""
|
||||||
|
lastItemId <- liftIO $ getLocalChatItemIdsLast_ db user nf 1 ""
|
||||||
|
pure (chat, landingSection chat lastItemId)
|
||||||
|
Nothing -> liftIO $ (,CLSLatest) <$> getLocalChatLast_ db user nf count ""
|
||||||
where
|
where
|
||||||
getLocalChatItemIdsBefore_ :: IO [ChatItemId]
|
getLocalChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getLocalChatItemIdsBefore_ =
|
getLocalChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND note_folder_id = ? AND item_status = ?
|
||||||
AND chat_item_id < ?
|
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, noteFolderId, search, beforeChatItemId, count)
|
(userId, noteFolderId, CISRcvNew)
|
||||||
|
|
||||||
toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId)
|
toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId)
|
||||||
toChatItemRef = \case
|
toChatItemRef = \case
|
||||||
@@ -1581,6 +1708,13 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
|
|||||||
CPLast count -> liftIO $ getAllChatItemsLast_ count
|
CPLast count -> liftIO $ getAllChatItemsLast_ count
|
||||||
CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId
|
CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId
|
||||||
CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId
|
CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId
|
||||||
|
CPAround aroundId count -> liftIO . getAllChatItemsAround_ aroundId count . aChatItemTs =<< getAChatItem_ aroundId
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
firstUnreadItemId <- liftIO getFirstUnreadItemId_
|
||||||
|
case firstUnreadItemId of
|
||||||
|
Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId
|
||||||
|
Nothing -> liftIO $ getAllChatItemsLast_ count
|
||||||
mapM (uncurry (getAChatItem db vr user)) itemRefs
|
mapM (uncurry (getAChatItem db vr user)) itemRefs
|
||||||
where
|
where
|
||||||
search = fromMaybe "" search_
|
search = fromMaybe "" search_
|
||||||
@@ -1624,6 +1758,31 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, search, beforeTs, beforeTs, beforeId, count)
|
(userId, search, beforeTs, beforeTs, beforeId, count)
|
||||||
|
getChatItem chatId =
|
||||||
|
DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id, contact_id, group_id, note_folder_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE chat_item_id = ?
|
||||||
|
|]
|
||||||
|
(Only chatId)
|
||||||
|
getAllChatItemsAround_ aroundId count aroundTs = do
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
itemsBefore <- getAllChatItemsBefore_ aroundId fetchCountBefore aroundTs
|
||||||
|
item <- getChatItem aroundId
|
||||||
|
itemsAfter <- getAllChatItemsAfter_ aroundId fetchCountAfter aroundTs
|
||||||
|
pure $ itemsBefore <> item <> itemsAfter
|
||||||
|
getFirstUnreadItemId_ =
|
||||||
|
fmap join . maybeFirstRow fromOnly $
|
||||||
|
DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT MIN(chat_item_id)
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND item_status = ?
|
||||||
|
|]
|
||||||
|
(userId, CISRcvNew)
|
||||||
|
|
||||||
getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId]
|
getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId]
|
||||||
getChatItemIdsByAgentMsgId db connId msgId =
|
getChatItemIdsByAgentMsgId db connId msgId =
|
||||||
@@ -2650,3 +2809,10 @@ getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, rcvMsgContentTag, sndMsgContentTag, count)
|
(userId, groupId, rcvMsgContentTag, sndMsgContentTag, count)
|
||||||
|
|
||||||
|
divideFetchCountAround_ :: Int -> (Int, Int)
|
||||||
|
divideFetchCountAround_ count =
|
||||||
|
let fetchCountEachSide = count `div` 2
|
||||||
|
fetchCountBefore = fetchCountEachSide + if count `mod` 2 /= 0 then 1 else 0
|
||||||
|
fetchCountAfter = fetchCountEachSide
|
||||||
|
in (fetchCountBefore, fetchCountAfter)
|
||||||
@@ -114,6 +114,7 @@ import Simplex.Chat.Migrations.M20240827_calls_uuid
|
|||||||
import Simplex.Chat.Migrations.M20240920_user_order
|
import Simplex.Chat.Migrations.M20240920_user_order
|
||||||
import Simplex.Chat.Migrations.M20241008_indexes
|
import Simplex.Chat.Migrations.M20241008_indexes
|
||||||
import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
||||||
|
import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@@ -227,7 +228,8 @@ schemaMigrations =
|
|||||||
("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid),
|
("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid),
|
||||||
("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order),
|
("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order),
|
||||||
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes),
|
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes),
|
||||||
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id)
|
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id),
|
||||||
|
("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRChatSuspended -> ["chat suspended"]
|
CRChatSuspended -> ["chat suspended"]
|
||||||
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats]
|
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats]
|
||||||
CRChats chats -> viewChats ts tz chats
|
CRChats chats -> viewChats ts tz chats
|
||||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
|
CRApiChat u chat lsec -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] <> (["newer messages available" | lsec == CLSUnread])
|
||||||
CRApiParsedMarkdown ft -> [viewJSON ft]
|
CRApiParsedMarkdown ft -> [viewJSON ft]
|
||||||
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
||||||
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ testAddContact = versionTestMatrix2 runTestAddContact
|
|||||||
-- pagination
|
-- pagination
|
||||||
alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")])
|
alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")])
|
||||||
alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")])
|
alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")])
|
||||||
|
alice #$> ("/_get chat @2 around=" <> itemId 2 <> " count=3", chat, [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")])
|
||||||
-- search
|
-- search
|
||||||
alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")])
|
alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")])
|
||||||
-- read messages
|
-- read messages
|
||||||
@@ -791,7 +792,7 @@ testDirectMessageDelete =
|
|||||||
alice @@@ [("@bob", lastChatFeature)]
|
alice @@@ [("@bob", lastChatFeature)]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
||||||
|
|
||||||
-- alice: msg id 1
|
-- alice: msg id 3
|
||||||
bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice")
|
bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice")
|
||||||
bob <# "@alice [edited] > hello 🙂"
|
bob <# "@alice [edited] > hello 🙂"
|
||||||
bob <## " hey alice"
|
bob <## " hey alice"
|
||||||
@@ -806,12 +807,12 @@ testDirectMessageDelete =
|
|||||||
alice @@@ [("@bob", "hey alice [marked deleted]")]
|
alice @@@ [("@bob", "hey alice [marked deleted]")]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")])
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")])
|
||||||
|
|
||||||
-- alice: deletes msg id 1 that was broadcast deleted by bob
|
-- alice: deletes msg id 3 that was broadcast deleted by bob
|
||||||
alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted")
|
alice #$> ("/_delete item @2 " <> itemId 3 <> " internal", id, "message deleted")
|
||||||
alice @@@ [("@bob", lastChatFeature)]
|
alice @@@ [("@bob", lastChatFeature)]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
||||||
|
|
||||||
-- alice: msg id 1, bob: msg id 3 (quoting message alice deleted locally)
|
-- alice: msg id 4, bob: msg id 3 (quoting message alice deleted locally)
|
||||||
bob `send` "> @alice (hello 🙂) do you receive my messages?"
|
bob `send` "> @alice (hello 🙂) do you receive my messages?"
|
||||||
bob <# "@alice > hello 🙂"
|
bob <# "@alice > hello 🙂"
|
||||||
bob <## " do you receive my messages?"
|
bob <## " do you receive my messages?"
|
||||||
@@ -819,14 +820,14 @@ testDirectMessageDelete =
|
|||||||
alice <## " do you receive my messages?"
|
alice <## " do you receive my messages?"
|
||||||
alice @@@ [("@bob", "do you receive my messages?")]
|
alice @@@ [("@bob", "do you receive my messages?")]
|
||||||
alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))])
|
alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))])
|
||||||
alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item")
|
alice #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "cannot delete this item")
|
||||||
|
|
||||||
-- alice: msg id 2, bob: msg id 4
|
-- alice: msg id 5, bob: msg id 4
|
||||||
bob #> "@alice how are you?"
|
bob #> "@alice how are you?"
|
||||||
alice <# "bob> how are you?"
|
alice <# "bob> how are you?"
|
||||||
|
|
||||||
-- alice: deletes msg id 2
|
-- alice: deletes msg id 5
|
||||||
alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted")
|
alice #$> ("/_delete item @2 " <> itemId 5 <> " internal", id, "message deleted")
|
||||||
|
|
||||||
-- bob: marks deleted msg id 4 (that alice deleted locally)
|
-- bob: marks deleted msg id 4 (that alice deleted locally)
|
||||||
bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted")
|
bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted")
|
||||||
@@ -2340,6 +2341,12 @@ testUserPrivacy =
|
|||||||
"bob> Voice messages: enabled",
|
"bob> Voice messages: enabled",
|
||||||
"bob> Audio/video calls: enabled"
|
"bob> Audio/video calls: enabled"
|
||||||
]
|
]
|
||||||
|
alice ##> "/_get items around=11 count=3"
|
||||||
|
alice
|
||||||
|
<##? [ "bob> Message reactions: enabled",
|
||||||
|
"bob> Voice messages: enabled",
|
||||||
|
"bob> Audio/video calls: enabled"
|
||||||
|
]
|
||||||
alice ##> "/_get items after=12 count=10"
|
alice ##> "/_get items after=12 count=10"
|
||||||
alice
|
alice
|
||||||
<##? [ "@bob hello",
|
<##? [ "@bob hello",
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ testGroupShared alice bob cath checkMessages directConnections = do
|
|||||||
-- so we take into account group event items as well as sent group invitations in direct chats
|
-- so we take into account group event items as well as sent group invitations in direct chats
|
||||||
alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")])
|
alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")])
|
||||||
alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")])
|
alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")])
|
||||||
|
alice #$> ("/_get chat #1 around=" <> msgItem2 <> " count=6", chat, [(0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) -- expecting only 4 items since there is only 1 item after
|
||||||
alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")])
|
alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")])
|
||||||
bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
|
bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
|
||||||
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
|
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
|||||||
alice ##> "/chats"
|
alice ##> "/chats"
|
||||||
|
|
||||||
alice /* "ahoy!"
|
alice /* "ahoy!"
|
||||||
alice ##> "/_update item *1 1 text Greetings."
|
alice ##> "/_update item *1 2 text Greetings."
|
||||||
alice ##> "/tail *"
|
alice ##> "/tail *"
|
||||||
alice <# "* Greetings."
|
alice <# "* Greetings."
|
||||||
|
|
||||||
@@ -102,6 +102,10 @@ testChatPagination tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
|||||||
|
|
||||||
alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")])
|
alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")])
|
||||||
|
alice #$> ("/_get chat *1 around=2 count=1", chat, [(1, "memento mori")])
|
||||||
|
alice #$> ("/_get chat *1 around=2 count=3", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock")])
|
||||||
|
alice #$> ("/_get chat *1 around=3 count=10", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
||||||
|
alice #$> ("/_get chat *1 around=4 count=3", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
||||||
|
|||||||
+3
-1
@@ -102,7 +102,9 @@ skipComparisonForDownMigrations =
|
|||||||
-- table and indexes move down to the end of the file
|
-- table and indexes move down to the end of the file
|
||||||
"20231215_recreate_msg_deliveries",
|
"20231215_recreate_msg_deliveries",
|
||||||
-- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file
|
-- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file
|
||||||
"20240313_drop_agent_ack_cmd_id"
|
"20240313_drop_agent_ack_cmd_id",
|
||||||
|
-- on down migration chat_item_autoincrement_id makes sequence table creation move down on the file
|
||||||
|
"20241023_chat_item_autoincrement_id"
|
||||||
]
|
]
|
||||||
|
|
||||||
getSchema :: FilePath -> FilePath -> IO String
|
getSchema :: FilePath -> FilePath -> IO String
|
||||||
|
|||||||
Reference in New Issue
Block a user