Merge branch 'master' into av/ios-infinity-scroll

This commit is contained in:
Avently
2025-01-22 13:34:51 -08:00
21 changed files with 457 additions and 89 deletions
@@ -1484,6 +1484,7 @@ data class Contact(
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean,
val chatTags: List<Long>,
val chatItemTTL: Long?,
override val chatDeleted: Boolean,
val uiThemes: ThemeModeOverrides? = null,
): SomeChat, NamedChat {
@@ -1567,7 +1568,8 @@ data class Contact(
contactGrpInvSent = false,
chatDeleted = false,
uiThemes = null,
chatTags = emptyList()
chatTags = emptyList(),
chatItemTTL = null,
)
}
}
@@ -1726,6 +1728,7 @@ data class GroupInfo (
val chatTs: Instant?,
val uiThemes: ThemeModeOverrides? = null,
val chatTags: List<Long>,
val chatItemTTL: Long?,
override val localAlias: String,
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
@@ -1774,7 +1777,8 @@ data class GroupInfo (
chatTs = Clock.System.now(),
uiThemes = null,
chatTags = emptyList(),
localAlias = ""
localAlias = "",
chatItemTTL = null
)
}
}
@@ -4186,32 +4190,49 @@ enum class SwitchPhase {
@SerialName("completed") Completed
}
sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
sealed class ChatItemTTL: Comparable<ChatItemTTL> {
object Day: ChatItemTTL()
object Week: ChatItemTTL()
object Month: ChatItemTTL()
object Year: ChatItemTTL()
data class Seconds(val secs: Long): ChatItemTTL()
object None: ChatItemTTL()
override fun compareTo(other: ChatItemTTL?): Int = (seconds ?: Long.MAX_VALUE).compareTo(other?.seconds ?: Long.MAX_VALUE)
override fun compareTo(other: ChatItemTTL): Int =
(seconds.takeIf { it != 0L } ?: Long.MAX_VALUE)
.compareTo(other.seconds.takeIf { it != 0L } ?: Long.MAX_VALUE)
val seconds: Long?
val seconds: Long
get() =
when (this) {
is None -> null
is None -> 0
is Day -> 86400L
is Week -> 7 * 86400L
is Month -> 30 * 86400L
is Year -> 365 * 86400L
is Seconds -> secs
}
val text: String
get() = when(this) {
is None -> generalGetString(MR.strings.chat_item_ttl_none)
is Day -> generalGetString(MR.strings.chat_item_ttl_day)
is Week -> generalGetString(MR.strings.chat_item_ttl_week)
is Month -> generalGetString(MR.strings.chat_item_ttl_month)
is Year -> generalGetString(MR.strings.chat_item_ttl_year)
is Seconds -> String.format(generalGetString(MR.strings.chat_item_ttl_seconds), secs)
}
val neverExpires: Boolean get() = this is None
companion object {
fun fromSeconds(seconds: Long?): ChatItemTTL =
fun fromSeconds(seconds: Long): ChatItemTTL =
when (seconds) {
null -> None
0L -> None
86400L -> Day
7 * 86400L -> Week
30 * 86400L -> Month
365 * 86400L -> Year
else -> Seconds(seconds)
}
}
@@ -1182,7 +1182,13 @@ object ChatController {
suspend fun getChatItemTTL(rh: Long?): ChatItemTTL {
val userId = currentUserId("getChatItemTTL")
val r = sendCmd(rh, CC.APIGetChatItemTTL(userId))
if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL)
if (r is CR.ChatItemTTL) {
return if (r.chatItemTTL != null) {
ChatItemTTL.fromSeconds(r.chatItemTTL)
} else {
ChatItemTTL.None
}
}
throw Exception("failed to get chat item TTL: ${r.responseType} ${r.details}")
}
@@ -1193,6 +1199,13 @@ object ChatController {
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
}
suspend fun setChatTTL(rh: Long?, chatType: ChatType, id: Long, chatItemTTL: ChatItemTTL?) {
val userId = currentUserId("setChatTTL")
val r = sendCmd(rh, CC.APISetChatTTL(userId, chatType, id, chatItemTTL?.seconds))
if (r is CR.CmdOk) return
throw Exception("failed to set chat TTL: ${r.responseType} ${r.details}")
}
suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean {
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
return when (r) {
@@ -3383,8 +3396,9 @@ sealed class CC {
class ApiGetUsageConditions(): CC()
class ApiSetConditionsNotified(val conditionsId: Long): CC()
class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List<Long>): CC()
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
class APISetChatItemTTL(val userId: Long, val seconds: Long): CC()
class APIGetChatItemTTL(val userId: Long): CC()
class APISetChatTTL(val userId: Long, val chatType: ChatType, val id: Long, val seconds: Long?): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
class APIGetNetworkConfig: CC()
class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC()
@@ -3567,6 +3581,7 @@ sealed class CC {
is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}"
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
is APIGetNetworkConfig -> "/network"
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
@@ -3727,6 +3742,7 @@ sealed class CC {
is ApiAcceptConditions -> "apiAcceptConditions"
is APISetChatItemTTL -> "apiSetChatItemTTL"
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetChatTTL -> "apiSetChatTTL"
is APISetNetworkConfig -> "apiSetNetworkConfig"
is APIGetNetworkConfig -> "apiGetNetworkConfig"
is APISetNetworkInfo -> "apiSetNetworkInfo"
@@ -3812,7 +3828,7 @@ sealed class CC {
data class ItemRange(val from: Long, val to: Long)
fun chatItemTTLStr(seconds: Long?): String {
if (seconds == null) return "none"
if (seconds == null) return "default"
return seconds.toString()
}
@@ -16,8 +16,10 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.*
@@ -36,12 +38,14 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.group.ChatTTLSection
import chat.simplex.common.views.chat.group.ProgressIndicator
import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.database.*
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@@ -74,6 +78,9 @@ fun ChatInfoView(
}
val chatRh = chat.remoteHostId
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) }
val deletingItems = rememberSaveable(contact.id) { mutableStateOf(false) }
ChatInfoLayout(
chat,
contact,
@@ -84,6 +91,16 @@ fun ChatInfoView(
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
sendReceipts.value = sendRcpts
},
chatItemTTL = chatItemTTL,
setChatItemTTL = {
if (it == chatItemTTL.value) {
return@ChatInfoLayout
}
val previousChatTTL = chatItemTTL.value
chatItemTTL.value = it
setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
connStats = connStats,
contactNetworkStatus.value,
customUserProfile,
@@ -173,7 +190,8 @@ fun ChatInfoView(
}
},
close = close,
onSearchClicked = onSearchClicked
onSearchClicked = onSearchClicked,
deletingItems = deletingItems
)
}
}
@@ -504,6 +522,8 @@ fun ChatInfoLayout(
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
chatItemTTL: MutableState<ChatItemTTL?>,
setChatItemTTL: (ChatItemTTL?) -> Unit,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
@@ -520,7 +540,8 @@ fun ChatInfoLayout(
syncContactConnectionForce: () -> Unit,
verifyClicked: () -> Unit,
close: () -> Unit,
onSearchClicked: () -> Unit
onSearchClicked: () -> Unit,
deletingItems: State<Boolean>
) {
val cStats = connStats.value
val scrollState = rememberScrollState()
@@ -597,6 +618,9 @@ fun ChatInfoLayout(
}
SectionDividerSpaced(maxBottomPadding = false)
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
val conn = contact.activeConn
if (conn != null) {
SectionView {
@@ -1308,6 +1332,80 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo)
}
fun setChatTTLAlert(
rhId: Long?,
chatInfo: ChatInfo,
selectedChatTTL: MutableState<ChatItemTTL?>,
previousChatTTL: ChatItemTTL?,
progressIndicator: MutableState<Boolean>
) {
val defaultTTL = chatModel.chatItemTTL.value
val previouslyUsedTTL = previousChatTTL ?: defaultTTL
val newTTLToUse = selectedChatTTL.value ?: defaultTTL
AlertManager.shared.showAlertDialog(
title = generalGetString(
if (newTTLToUse.neverExpires) {
MR.strings.disable_automatic_deletion_question
} else if (!previouslyUsedTTL.neverExpires || selectedChatTTL.value == null) {
MR.strings.change_automatic_deletion_question
} else MR.strings.enable_automatic_deletion_question),
text = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion_message else MR.strings.change_automatic_chat_deletion_message),
confirmText = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion else MR.strings.delete_messages),
onConfirm = { setChatTTL(rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) },
onDismiss = { selectedChatTTL.value = previousChatTTL },
onDismissRequest = { selectedChatTTL.value = previousChatTTL },
destructive = true,
)
}
private fun setChatTTL(
rhId: Long?,
chatInfo: ChatInfo,
chatTTL: MutableState<ChatItemTTL?>,
progressIndicator: MutableState<Boolean>,
previousChatTTL: ChatItemTTL?
) {
progressIndicator.value = true
withBGApi {
try {
chatModel.controller.setChatTTL(rhId, chatInfo.chatType, chatInfo.apiId, chatTTL.value)
afterSetChatTTL(rhId, chatInfo, progressIndicator)
} catch (e: Exception) {
chatTTL.value = previousChatTTL
afterSetChatTTL(rhId, chatInfo, progressIndicator)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_message_deletion), e.stackTraceToString())
}
}
}
private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) {
try {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
if (chat.chatItems.isEmpty()) {
// replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview
withChats {
val oldChat = getChat(chat.id)
if (oldChat != null) {
replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList()))
}
}
}
if (chat.remoteHostId != chatModel.remoteHostId() || chat.id != chatModel.chatId.value) return
processLoadedChat(
chat,
navInfo,
contentTag = null,
pagination = pagination
)
} catch (e: Exception) {
Log.e(TAG, "apiGetChat error: ${e.stackTraceToString()}")
} finally {
progressIndicator.value = false
}
}
@Preview
@Composable
fun PreviewChatInfoLayout() {
@@ -1322,6 +1420,8 @@ fun PreviewChatInfoLayout() {
User.sampleData,
sendReceipts = remember { mutableStateOf(SendReceipts.Yes) },
setSendReceipts = {},
chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) },
setChatItemTTL = {},
localAlias = "",
connectionCode = "123",
developerTools = false,
@@ -1338,7 +1438,8 @@ fun PreviewChatInfoLayout() {
syncContactConnectionForce = {},
verifyClicked = {},
close = {},
onSearchClicked = {}
onSearchClicked = {},
deletingItems = remember { mutableStateOf(false) }
)
}
}
@@ -34,7 +34,16 @@ suspend fun apiLoadMessages(
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last)
|| !isActive) return@coroutineScope
processLoadedChat(chat, navInfo, contentTag, pagination, visibleItemIndexesNonReversed)
}
suspend fun processLoadedChat(
chat: Chat,
navInfo: NavigationInfo,
contentTag: MsgContentTag?,
pagination: ChatPagination,
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) {
val chatState = chatModel.chatStateForContent(contentTag)
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
val oldItems = chatModel.chatItemsForContent(contentTag).value
@@ -70,7 +79,7 @@ suspend fun apiLoadMessages(
is ChatPagination.Before -> {
newItems.addAll(oldItems)
val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId }
if (indexInCurrentItems == -1) return@coroutineScope
if (indexInCurrentItems == -1) return
val (newIds, _) = mapItemsToIds(chat.chatItems)
val wasSize = newItems.size
val (oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) = removeDuplicatesAndModifySplitsOnBeforePagination(
@@ -87,7 +96,7 @@ suspend fun apiLoadMessages(
is ChatPagination.After -> {
newItems.addAll(oldItems)
val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId }
if (indexInCurrentItems == -1) return@coroutineScope
if (indexInCurrentItems == -1) return
val mappedItems = mapItemsToIds(chat.chatItems)
val newIds = mappedItems.first
@@ -17,6 +17,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
@@ -39,6 +40,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.database.TtlOptions
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@@ -55,7 +57,10 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
val groupInfo = chat.chatInfo.groupInfo
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) }
val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) }
val scope = rememberCoroutineScope()
GroupChatInfoLayout(
chat,
groupInfo,
@@ -66,6 +71,16 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
sendReceipts.value = sendRcpts
},
chatItemTTL = chatItemTTL,
setChatItemTTL = {
if (it == chatItemTTL.value) {
return@GroupChatInfoLayout
}
val previousChatTTL = chatItemTTL.value
chatItemTTL.value = it
setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
members = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedByDescending { it.memberRole },
@@ -125,7 +140,8 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
manageGroupLink = {
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
},
onSearchClicked = onSearchClicked
onSearchClicked = onSearchClicked,
deletingItems = deletingItems
)
}
}
@@ -285,6 +301,8 @@ fun ModalData.GroupChatInfoLayout(
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
chatItemTTL: MutableState<ChatItemTTL?>,
setChatItemTTL: (ChatItemTTL?) -> Unit,
members: List<GroupMember>,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
@@ -300,7 +318,8 @@ fun ModalData.GroupChatInfoLayout(
leaveGroup: () -> Unit,
manageGroupLink: () -> Unit,
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
onSearchClicked: () -> Unit
onSearchClicked: () -> Unit,
deletingItems: State<Boolean>
) {
val listState = remember { appBarHandler.listState }
val scope = rememberCoroutineScope()
@@ -394,7 +413,10 @@ fun ModalData.GroupChatInfoLayout(
}
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
SectionTextFooter(stringResource(footerId))
SectionDividerSpaced(maxTopPadding = true)
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
@@ -463,6 +485,26 @@ fun ModalData.GroupChatInfoLayout(
}
}
@Composable
fun ChatTTLSection(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) {
Box {
SectionView {
TtlOptions(
chatItemTTL,
enabled = remember { derivedStateOf { !deletingItems.value } },
onSelected = setChatItemTTL,
default = chatModel.chatItemTTL
)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
if (deletingItems.value) {
Box(Modifier.matchParentSize()) {
ProgressIndicator()
}
}
}
}
@Composable
private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
Column(
@@ -770,12 +812,14 @@ fun PreviewGroupChatInfoLayout() {
User.sampleData,
sendReceipts = remember { mutableStateOf(SendReceipts.Yes) },
setSendReceipts = {},
chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) },
setChatItemTTL = {},
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false,
onLocalAliasChanged = {},
groupLink = null,
scrollToItemId = remember { mutableStateOf(null) },
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {},
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
)
}
}
@@ -1,9 +1,8 @@
package chat.simplex.common.views.chat.group
import SectionBottomSpacer
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -12,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.*
@@ -108,6 +109,9 @@ fun DatabaseView() {
}
},
onChatItemTTLSelected = {
if (it == null) {
return@DatabaseLayout
}
val oldValue = chatItemTTL.value
chatItemTTL.value = it
if (it < oldValue) {
@@ -158,7 +162,7 @@ fun DatabaseLayout(
exportArchive: () -> Unit,
deleteChatAlert: () -> Unit,
deleteAppFilesAndMedia: () -> Unit,
onChatItemTTLSelected: (ChatItemTTL) -> Unit,
onChatItemTTLSelected: (ChatItemTTL?) -> Unit,
disconnectAllHosts: () -> Unit,
) {
val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote
@@ -300,21 +304,25 @@ private fun setChatItemTTLAlert(
}
@Composable
private fun TtlOptions(current: State<ChatItemTTL>, enabled: State<Boolean>, onSelected: (ChatItemTTL) -> Unit) {
fun TtlOptions(
current: State<ChatItemTTL?>,
enabled: State<Boolean>,
onSelected: (ChatItemTTL?) -> Unit,
default: State<ChatItemTTL>? = null
) {
val values = remember {
val all: ArrayList<ChatItemTTL> = arrayListOf(ChatItemTTL.None, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day)
if (current.value is ChatItemTTL.Seconds) {
all.add(current.value)
val all: ArrayList<ChatItemTTL> = arrayListOf(ChatItemTTL.None, ChatItemTTL.Year, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day)
val currentValue = current.value
if (currentValue is ChatItemTTL.Seconds) {
all.add(currentValue)
}
all.map {
when (it) {
is ChatItemTTL.None -> it to generalGetString(MR.strings.chat_item_ttl_none)
is ChatItemTTL.Day -> it to generalGetString(MR.strings.chat_item_ttl_day)
is ChatItemTTL.Week -> it to generalGetString(MR.strings.chat_item_ttl_week)
is ChatItemTTL.Month -> it to generalGetString(MR.strings.chat_item_ttl_month)
is ChatItemTTL.Seconds -> it to String.format(generalGetString(MR.strings.chat_item_ttl_seconds), it.secs)
}
val options: MutableList<Pair<ChatItemTTL?, String>> = all.map { it to it.text }.toMutableList()
if (default != null) {
options.add(null to String.format(generalGetString(MR.strings.chat_item_ttl_default), default.value.text))
}
options
}
ExposedDropDownSettingRow(
generalGetString(MR.strings.delete_messages_after),
@@ -540,6 +540,12 @@
<!-- Chat Info Settings - ChatInfoView.kt -->
<string name="notifications">Notifications</string>
<string name="disable_automatic_deletion_question">Disable automatic message deletion?</string>
<string name="change_automatic_deletion_question">Change automatic message deletion?</string>
<string name="disable_automatic_deletion_message">Messages in this chat will never be deleted.</string>
<string name="change_automatic_chat_deletion_message">This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</string>
<string name="disable_automatic_deletion">Disable delete messages</string>
<string name="chat_ttl_options_footer">Delete chat messages from your device.</string>
<!-- Chat Info Actions - ChatInfoView.kt -->
<string name="info_view_connect_button">connect</string>
@@ -1396,7 +1402,9 @@
<string name="chat_item_ttl_day">1 day</string>
<string name="chat_item_ttl_week">1 week</string>
<string name="chat_item_ttl_month">1 month</string>
<string name="chat_item_ttl_year">1 year</string>
<string name="chat_item_ttl_seconds">%s second(s)</string>
<string name="chat_item_ttl_default">default (%s)</string>
<string name="messages_section_title">Messages</string>
<string name="messages_section_description">This setting applies to messages in your current chat profile</string>
<string name="delete_messages_after">Delete messages after</string>
+1
View File
@@ -220,6 +220,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20241230_reports
Simplex.Chat.Store.SQLite.Migrations.M20250105_indexes
Simplex.Chat.Store.SQLite.Migrations.M20250115_chat_ttl
Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history
other-modules:
Paths_simplex_chat
hs-source-dirs:
+5 -6
View File
@@ -741,7 +741,7 @@ data ChatResponse
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
| CRGroupInvitation {user :: User, shortGroupInfo :: ShortGroupInfo}
| CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
| CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
| CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
@@ -757,8 +757,7 @@ data ChatResponse
| CRUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember}
| CRUnknownMemberBlocked {user :: User, groupInfo :: GroupInfo, blockedByMember :: GroupMember, member :: GroupMember}
| CRUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember}
| CRGroupEmpty {user :: User, groupInfo :: GroupInfo}
| CRGroupRemoved {user :: User, groupInfo :: GroupInfo}
| CRGroupEmpty {user :: User, shortGroupInfo :: ShortGroupInfo}
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
| CRGroupProfile {user :: User, groupInfo :: GroupInfo}
@@ -773,9 +772,9 @@ data ChatResponse
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
| CRMemberSubError {user :: User, shortGroupInfo :: ShortGroupInfo, memberToSubscribe :: ShortGroupMember, chatError :: ChatError}
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]}
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
| CRGroupSubscribed {user :: User, shortGroupInfo :: ShortGroupInfo}
| CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]}
| CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
@@ -1051,7 +1050,7 @@ data ContactSubStatus = ContactSubStatus
deriving (Show)
data MemberSubStatus = MemberSubStatus
{ member :: GroupMember,
{ member :: ShortGroupMember,
memberError :: Maybe ChatError
}
deriving (Show)
+28 -22
View File
@@ -3347,17 +3347,17 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view
contactSubsToView rs cts ce
-- TODO possibly, we could either disable these events or replace with less noisy for API
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
rcvFileSubsToView rs rfts
pendingConnSubsToView rs pcs
unlessM (asks $ coreApi . config) $ do
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
rcvFileSubsToView rs rfts
pendingConnSubsToView rs pcs
where
addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case
RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs)
RcvGroupMsgConnection c _g m -> let ms' = addConn c (toShortMember m c) ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs)
@@ -3377,6 +3377,13 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
createdAt,
updatedAt = createdAt
}
toShortMember GroupMember {groupMemberId, groupId, localDisplayName} Connection {agentConnId} =
ShortGroupMember
{ groupMemberId,
groupId,
memberName = localDisplayName,
connId = agentConnId
}
getContactConns :: CM ([ConnId], Map ConnId Contact)
getContactConns = do
cts <- withStore_ (`getUserContacts` vr)
@@ -3387,11 +3394,13 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
(cs, ucs) <- unzip <$> withStore_ (`getUserContactLinks` vr)
let connIds = map aConnId cs
pure (connIds, M.fromList $ zip connIds ucs)
getGroupMemberConns :: CM ([Group], [ConnId], Map ConnId GroupMember)
getGroupMemberConns :: CM ([ShortGroup], [ConnId], Map ConnId ShortGroupMember)
getGroupMemberConns = do
gs <- withStore_ (`getUserGroups` vr)
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs
gs <- withStore_ getUserGroupsToSubscribe
let mPairs = concatMap (\(ShortGroup _ ms) -> map (\m -> (shortMemConnId m, m)) ms) gs
pure (gs, map fst mPairs, M.fromList mPairs)
where
shortMemConnId ShortGroupMember{connId = AgentConnId acId} = acId
getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer)
getSndFileTransferConns = do
sfts <- withStore_ getLiveSndFileTransfers
@@ -3435,30 +3444,27 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
-- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM ()
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> CM ()
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM ()
groupSubsToView rs gs ms ce = do
mapM_ groupSub $
sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs
sortOn (\(ShortGroup ShortGroupInfo {groupName = g} _) -> g) gs
toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs
where
mRs = resultsFor rs ms
groupSub :: Group -> CM ()
groupSub (Group g@GroupInfo {membership, groupId = gId} members) = do
groupSub :: ShortGroup -> CM ()
groupSub (ShortGroup g@ShortGroupInfo {groupId = gId, membershipStatus} members) = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors
toView groupEvent
where
mErrors :: [(GroupMember, ChatError)]
mErrors :: [(ShortGroupMember, ChatError)]
mErrors =
sortOn (\(GroupMember {localDisplayName = n}, _) -> n)
sortOn (\(ShortGroupMember {memberName = n}, _) -> n)
. filterErrors
$ filter (\(GroupMember {groupId}, _) -> groupId == gId) mRs
$ filter (\(ShortGroupMember {groupId}, _) -> groupId == gId) mRs
groupEvent :: ChatResponse
groupEvent
| memberStatus membership == GSMemInvited = CRGroupInvitation user g
| all (\GroupMember {activeConn} -> isNothing activeConn) members =
if memberActive membership
then CRGroupEmpty user g
else CRGroupRemoved user g
| membershipStatus == GSMemInvited = CRGroupInvitation user g
| null members = CRGroupEmpty user g
| otherwise = CRGroupSubscribed user g
sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView rs sfts = do
+56 -1
View File
@@ -57,6 +57,7 @@ module Simplex.Chat.Store.Groups
deleteGroupItemsAndMembers,
deleteGroup,
getUserGroups,
getUserGroupsToSubscribe,
getUserGroupDetails,
getUserGroupsWithSummary,
getGroupSummary,
@@ -164,9 +165,13 @@ import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version
import UnliftIO.STM
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
import Database.PostgreSQL.Simple.SqlQQ (sql)
#else
import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql)
#endif
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
@@ -588,6 +593,51 @@ getGroup db vr user groupId = do
members <- liftIO $ getGroupMembers db vr user gInfo
pure $ Group gInfo members
getGroupToSubscribe :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ShortGroup
getGroupToSubscribe db User {userId, userContactId} groupId = do
shortInfo <- getGroupInfoToSubscribe
members <- liftIO getGroupMembersToSubscribe
pure $ ShortGroup shortInfo members
where
getGroupInfoToSubscribe :: ExceptT StoreError IO ShortGroupInfo
getGroupInfoToSubscribe = ExceptT $ do
firstRow toInfo (SEGroupNotFound groupId) $
DB.query
db
[sql|
SELECT g.local_display_name, mu.member_status
FROM groups g
JOIN group_members mu ON mu.group_id = g.group_id
WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?
AND mu.member_status NOT IN (?,?,?)
|]
(groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
where
toInfo :: (GroupName, GroupMemberStatus) -> ShortGroupInfo
toInfo (groupName, membershipStatus) =
ShortGroupInfo groupId groupName membershipStatus
getGroupMembersToSubscribe :: IO [ShortGroupMember]
getGroupMembersToSubscribe = do
map toShortMember
<$> DB.query
db
[sql|
SELECT m.group_member_id, m.local_display_name, c.agent_conn_id
FROM group_members m
JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id
)
WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)
AND m.member_status NOT IN (?,?,?)
|]
(userId, userId, groupId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
where
toShortMember :: (GroupMemberId, ContactName, AgentConnId) -> ShortGroupMember
toShortMember (groupMemberId, localDisplayName, agentConnId) =
ShortGroupMember groupMemberId groupId localDisplayName agentConnId
deleteGroupConnectionsAndFiles :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO ()
deleteGroupConnectionsAndFiles db User {userId} GroupInfo {groupId} members = do
forM_ members $ \m -> DB.execute db "DELETE FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId' m)
@@ -642,6 +692,11 @@ getUserGroups db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroup db vr user) groupIds
getUserGroupsToSubscribe :: DB.Connection -> User -> IO [ShortGroup]
getUserGroupsToSubscribe db user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroupToSubscribe db user) groupIds
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
g_ <-
+12 -9
View File
@@ -415,20 +415,20 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id,
-- meta
item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id,
forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at,
forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, timed_ttl, timed_delete_at,
-- quote
quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id,
-- forwarded from
fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((userId, msgId_) :. idsRow :. itemRow :. quoteRow' :. forwardedFromRow)
ciId <- insertedRowId db
forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt
pure ciId
where
itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe BoolInt) :. (Maybe Int, Maybe UTCTime)
itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, BI <$> (justTrue live)) :. ciTimedRow timed
itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt) :. (Maybe Int, Maybe UTCTime)
itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> (justTrue live)) :. ciTimedRow timed
quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e)
idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
idsRow = case chatDirection of
@@ -438,6 +438,10 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing)
CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId)
CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId)
includeInHistory :: Bool
includeInHistory =
let (_, groupId_, _, _) = idsRow
in isJust groupId_ && isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_)
forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
forwardedFromRow = case itemForwarded of
Nothing ->
@@ -3070,12 +3074,11 @@ getGroupHistoryItems db user@User {userId} GroupInfo {groupId} m count = do
SELECT i.chat_item_id
FROM chat_items i
LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ?
WHERE i.user_id = ? AND i.group_id = ?
AND i.item_content_tag IN (?,?)
AND i.msg_content_tag NOT IN (?)
WHERE s.group_snd_item_status_id IS NULL
AND i.user_id = ? AND i.group_id = ?
AND i.include_in_history = 1
AND i.item_deleted = 0
AND s.group_snd_item_status_id IS NULL
ORDER BY i.item_ts DESC, i.chat_item_id DESC
LIMIT ?
|]
(groupMemberId' m, userId, groupId, rcvMsgContentTag, sndMsgContentTag, MCReport_, count)
(groupMemberId' m, userId, groupId, count)
@@ -425,7 +425,8 @@ CREATE TABLE chat_items(
fwd_from_group_id BIGINT REFERENCES groups ON DELETE SET NULL,
fwd_from_chat_item_id BIGINT REFERENCES chat_items ON DELETE SET NULL,
via_proxy SMALLINT,
msg_content_tag TEXT
msg_content_tag TEXT,
include_in_history SMALLINT NOT NULL DEFAULT 0
);
ALTER TABLE groups
ADD CONSTRAINT fk_groups_chat_items
@@ -1012,4 +1013,16 @@ CREATE INDEX idx_chat_items_groups_msg_content_tag_deleted ON chat_items(
item_deleted,
item_sent
);
CREATE INDEX idx_chat_items_groups_history ON chat_items(
user_id,
group_id,
include_in_history,
item_deleted,
item_ts,
chat_item_id
);
CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses(
chat_item_id,
group_member_id
);
|]
+3 -1
View File
@@ -124,6 +124,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20241223_chat_tags
import Simplex.Chat.Store.SQLite.Migrations.M20241230_reports
import Simplex.Chat.Store.SQLite.Migrations.M20250105_indexes
import Simplex.Chat.Store.SQLite.Migrations.M20250115_chat_ttl
import Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -247,7 +248,8 @@ schemaMigrations =
("20241223_chat_tags", m20241223_chat_tags, Just down_m20241223_chat_tags),
("20241230_reports", m20241230_reports, Just down_m20241230_reports),
("20250105_indexes", m20250105_indexes, Just down_m20250105_indexes),
("20250115_chat_ttl", m20250115_chat_ttl, Just down_m20250115_chat_ttl)
("20250115_chat_ttl", m20250115_chat_ttl, Just down_m20250115_chat_ttl),
("20250122_chat_items_include_in_history", m20250122_chat_items_include_in_history, Just down_m20250122_chat_items_include_in_history)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,39 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20250122_chat_items_include_in_history :: Query
m20250122_chat_items_include_in_history =
[sql|
ALTER TABLE chat_items ADD COLUMN include_in_history INTEGER NOT NULL DEFAULT 0;
CREATE INDEX idx_chat_items_groups_history ON chat_items(
user_id,
group_id,
include_in_history,
item_deleted,
item_ts,
chat_item_id
);
UPDATE chat_items
SET include_in_history = 1
WHERE group_id IS NOT NULL
AND item_content_tag IN ('rcvMsgContent', 'sndMsgContent')
AND msg_content_tag NOT IN ('report');
CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses(chat_item_id, group_member_id);
|]
down_m20250122_chat_items_include_in_history :: Query
down_m20250122_chat_items_include_in_history =
[sql|
DROP INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id;
DROP INDEX idx_chat_items_groups_history;
ALTER TABLE chat_items DROP COLUMN include_in_history;
|]
@@ -406,7 +406,8 @@ CREATE TABLE chat_items(
fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL,
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
via_proxy INTEGER,
msg_content_tag TEXT
msg_content_tag TEXT,
include_in_history INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE chat_item_messages(
@@ -978,3 +979,15 @@ CREATE INDEX idx_chat_items_groups_msg_content_tag_deleted ON chat_items(
item_deleted,
item_sent
);
CREATE INDEX idx_chat_items_groups_history ON chat_items(
user_id,
group_id,
include_in_history,
item_deleted,
item_ts,
chat_item_id
);
CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses(
chat_item_id,
group_member_id
);
+24
View File
@@ -373,6 +373,26 @@ optionalFullName displayName fullName
| T.null fullName || displayName == fullName = ""
| otherwise = " (" <> fullName <> ")"
data ShortGroup = ShortGroup
{ shortInfo :: ShortGroupInfo,
members :: [ShortGroupMember]
}
data ShortGroupInfo = ShortGroupInfo
{ groupId :: GroupId,
groupName :: GroupName,
membershipStatus :: GroupMemberStatus
}
deriving (Eq, Show)
data ShortGroupMember = ShortGroupMember
{ groupMemberId :: GroupMemberId,
groupId :: GroupId,
memberName :: ContactName,
connId :: AgentConnId
}
deriving (Show)
data Group = Group {groupInfo :: GroupInfo, members :: [GroupMember]}
deriving (Eq, Show)
@@ -1812,3 +1832,7 @@ $(JQ.deriveJSON defaultJSON ''ContactRef)
$(JQ.deriveJSON defaultJSON ''NoteFolder)
$(JQ.deriveJSON defaultJSON ''ChatTag)
$(JQ.deriveJSON defaultJSON ''ShortGroupInfo)
$(JQ.deriveJSON defaultJSON ''ShortGroupMember)
+15 -7
View File
@@ -292,7 +292,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
CRGroupInvitation u g -> ttyUser u [groupInvitationSub g]
CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m
@@ -307,8 +307,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g
CRDeletedMember u g by m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"]
CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"]
CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"]
CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"]
CRGroupEmpty u ShortGroupInfo {groupName = g} -> ttyUser u [ttyGroup g <> ": group is empty"]
CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"]
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
@@ -323,9 +322,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"]
CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct'
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
CRMemberSubError u ShortGroupInfo {groupName = g} ShortGroupMember {memberName = n} e -> ttyUser u [ttyGroup g <> " member " <> ttyContact n <> " error: " <> sShow e]
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
CRGroupSubscribed u ShortGroupInfo {groupName = g} -> ttyUser u $ viewGroupSubscribed g
CRPendingSubSummary u _ -> ttyUser u []
CRSndFileSubError u SndFileTransfer {fileId, fileName} e ->
ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
@@ -571,8 +570,8 @@ viewUsersList us =
<> ["muted" | not showNtfs]
<> [plain ("unread: " <> show count) | count /= 0]
viewGroupSubscribed :: GroupInfo -> [StyledString]
viewGroupSubscribed g = [membershipIncognito g <> ttyFullGroup g <> ": connected to server(s)"]
viewGroupSubscribed :: GroupName -> [StyledString]
viewGroupSubscribed g = [ttyGroup g <> ": connected to server(s)"]
showSMPServer :: SMPServer -> String
showSMPServer ProtocolServer {host} = B.unpack $ strEncode host
@@ -1216,6 +1215,15 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil
Just mp -> " to join as " <> incognitoProfile' (fromLocalProfile mp) <> ", "
Nothing -> " to join, "
groupInvitationSub :: ShortGroupInfo -> StyledString
groupInvitationSub ShortGroupInfo {groupName = ldn} =
highlight ("#" <> viewName ldn)
<> " - you are invited ("
<> highlight ("/j " <> viewName ldn)
<> " to join, "
<> highlight ("/d #" <> viewName ldn)
<> " to delete invitation)"
viewContactsMerged :: Contact -> Contact -> Contact -> [StyledString]
viewContactsMerged c1 c2 ct' =
[ "contact " <> ttyContact' c2 <> " is merged into " <> ttyContact' c1,
+4 -4
View File
@@ -923,13 +923,13 @@ testRestoreDirectory tmp = do
withTestChat tmp "cath" $ \cath -> do
bob <## "2 contacts connected (use /cs for the list)"
bob
<### [ "#privacy (Privacy): connected to server(s)",
"#security (Security): connected to server(s)"
<### [ "#privacy: connected to server(s)",
"#security: connected to server(s)"
]
cath <## "2 contacts connected (use /cs for the list)"
cath
<### [ "#privacy (Privacy): connected to server(s)",
"#anonymity (Anonymity): connected to server(s)"
<### [ "#privacy: connected to server(s)",
"#anonymity: connected to server(s)"
]
listGroups superUser bob cath
groupFoundN 3 bob "privacy"
-1
View File
@@ -169,7 +169,6 @@ testChatApi tmp = do
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown