mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-18 16:06:53 +00:00
Merge branch 'master' into chat-relays
This commit is contained in:
@@ -444,17 +444,17 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?) async throws -> [MsgContentTag] {
|
||||
func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope? = nil) async throws -> [MsgContentTag] {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope))
|
||||
if case let .chatContentTypes(types) = r { return types }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async {
|
||||
await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems)
|
||||
func loadChat(chat: Chat, im: ItemsModel, contentTag: MsgContentTag? = nil, search: String = "", clearItems: Bool = true) async {
|
||||
await loadChat(chatId: chat.chatInfo.id, im: im, contentTag: contentTag, search: search, clearItems: clearItems)
|
||||
}
|
||||
|
||||
func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
func loadChat(chatId: ChatId, im: ItemsModel, contentTag: MsgContentTag? = nil, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
await MainActor.run {
|
||||
if clearItems {
|
||||
im.reversedChatItems = []
|
||||
@@ -468,10 +468,11 @@ func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundIte
|
||||
openAroundItemId != nil
|
||||
? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage)
|
||||
: (
|
||||
search == ""
|
||||
contentTag == nil && search == ""
|
||||
? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)
|
||||
)
|
||||
),
|
||||
contentTag,
|
||||
search,
|
||||
openAroundItemId,
|
||||
{ 0...0 }
|
||||
|
||||
@@ -15,6 +15,7 @@ func apiLoadMessages(
|
||||
_ chatId: ChatId,
|
||||
_ im: ItemsModel,
|
||||
_ pagination: ChatPagination,
|
||||
_ contentTag: MsgContentTag? = nil,
|
||||
_ search: String = "",
|
||||
_ openAroundItemId: ChatItem.ID? = nil,
|
||||
_ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> = { 0 ... 0 }
|
||||
@@ -22,7 +23,7 @@ func apiLoadMessages(
|
||||
let chat: Chat
|
||||
let navInfo: NavigationInfo
|
||||
do {
|
||||
(chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search)
|
||||
(chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: contentTag ?? im.contentTag, pagination: pagination, search: search)
|
||||
} catch let error {
|
||||
logger.error("apiLoadMessages error: \(responseError(error))")
|
||||
return
|
||||
|
||||
@@ -44,6 +44,8 @@ struct ChatView: View {
|
||||
@State private var showSearch = false
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
@State private var contentFilter: ContentFilter? = nil
|
||||
@State private var availableContent: [ContentFilter] = [.images, .files, .links]
|
||||
// opening GroupMemberInfoView on member icon
|
||||
@State private var selectedMember: GMember? = nil
|
||||
// opening GroupLinkView on link button (incognito)
|
||||
@@ -528,16 +530,19 @@ struct ChatView: View {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
} else {
|
||||
contentFilterMenu(withLabel: false)
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .audio)
|
||||
} label: {
|
||||
Label("Audio call", systemImage: "phone")
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
@@ -545,6 +550,9 @@ struct ChatView: View {
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
contentFilterMenu(withLabel: true)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
@@ -554,23 +562,24 @@ struct ChatView: View {
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
}
|
||||
}
|
||||
contentFilterMenu(withLabel: false)
|
||||
Menu {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
}
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
@@ -578,7 +587,10 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
case .local:
|
||||
searchButton()
|
||||
HStack {
|
||||
contentFilterMenu(withLabel: false)
|
||||
searchButton()
|
||||
}
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
@@ -685,6 +697,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAvailableContent()
|
||||
}
|
||||
if chatModel.draftChatId == cInfo.id && !composeState.forwarding,
|
||||
let draft = chatModel.draft {
|
||||
@@ -698,6 +711,22 @@ struct ChatView: View {
|
||||
floatingButtonModel.updateOnListChange(scrollView.listState)
|
||||
}
|
||||
|
||||
private func updateAvailableContent() {
|
||||
Task {
|
||||
let content: [ContentFilter]
|
||||
do {
|
||||
let contentTags = Set(try await apiGetChatContentTypes(chatId: chat.chatInfo.id)).union(ContentFilter.alwaysShow)
|
||||
content = ContentFilter.allCases.filter { contentTags.contains($0.contentTag) }
|
||||
} catch let error {
|
||||
logger.error("apiGetChatContentTypes error: \(responseError(error))")
|
||||
content = ContentFilter.allCases
|
||||
}
|
||||
await MainActor.run {
|
||||
availableContent = content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToItem(_ itemId: ChatItem.ID) {
|
||||
Task {
|
||||
do {
|
||||
@@ -732,10 +761,14 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private func searchToolbar() -> some View {
|
||||
HStack(spacing: 12) {
|
||||
let placeholder: LocalizedStringKey = contentFilter?.searchPlaceholder ?? "Search"
|
||||
return HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
TextField("Search", text: $searchText)
|
||||
if let contentFilter {
|
||||
Image(systemName: contentFilter.icon)
|
||||
}
|
||||
TextField(placeholder, text: $searchText)
|
||||
.focused($searchFocussed)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -1052,7 +1085,7 @@ struct ChatView: View {
|
||||
|
||||
private func searchTextChanged(_ s: String) {
|
||||
Task {
|
||||
await loadChat(chat: chat, im: im, search: s)
|
||||
await loadChat(chat: chat, im: im, contentTag: contentFilter?.contentTag, search: s)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
await MainActor.run {
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
@@ -1255,16 +1288,52 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func contentFilterMenu(withLabel: Bool) -> some View {
|
||||
Menu {
|
||||
ForEach(availableContent, id: \.self) { type in
|
||||
Button {
|
||||
setContentFilter(type)
|
||||
} label: {
|
||||
Label(type.label, systemImage: contentFilter == type ? type.iconFilled : type.icon)
|
||||
}
|
||||
}
|
||||
if contentFilter != nil {
|
||||
Button {
|
||||
closeSearch()
|
||||
} label: {
|
||||
Label("All messages", systemImage: "bubble.left.and.text.bubble.right")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
let icon = contentFilter == nil ? "photo.on.rectangle" : "photo.on.rectangle.fill"
|
||||
if withLabel {
|
||||
Label("Filter", systemImage: icon)
|
||||
} else {
|
||||
Image(systemName: icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func focusSearch() {
|
||||
showSearch = true
|
||||
searchFocussed = true
|
||||
searchText = ""
|
||||
}
|
||||
|
||||
private func setContentFilter(_ type: ContentFilter) {
|
||||
if (contentFilter == type) { return }
|
||||
contentFilter = type
|
||||
showSearch = true
|
||||
searchText = ""
|
||||
searchTextChanged("")
|
||||
}
|
||||
|
||||
private func closeSearch() {
|
||||
showSearch = false
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
contentFilter = nil
|
||||
updateAvailableContent()
|
||||
}
|
||||
|
||||
private func closeKeyboardAndRun(_ action: @escaping () -> Void) {
|
||||
@@ -1285,7 +1354,7 @@ struct ChatView: View {
|
||||
Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } }
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
Label("Invite member", systemImage: "person.crop.circle.badge.plus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1374,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "link.badge.plus")
|
||||
Label("Group link", systemImage: "link.badge.plus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1473,6 +1542,7 @@ struct ChatView: View {
|
||||
chat.chatInfo.id,
|
||||
im,
|
||||
pagination,
|
||||
contentFilter?.contentTag,
|
||||
searchText,
|
||||
nil,
|
||||
{ visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) }
|
||||
@@ -2957,6 +3027,66 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
|
||||
}
|
||||
}
|
||||
|
||||
enum ContentFilter: CaseIterable {
|
||||
case images
|
||||
case videos
|
||||
case voice
|
||||
case files
|
||||
case links
|
||||
|
||||
static let alwaysShow: Set<MsgContentTag> = [.image, .link]
|
||||
|
||||
var contentTag: MsgContentTag {
|
||||
switch self {
|
||||
case .images: .image
|
||||
case .videos: .video
|
||||
case .voice: .voice
|
||||
case .files: .file
|
||||
case .links: .link
|
||||
}
|
||||
}
|
||||
|
||||
var label: LocalizedStringKey {
|
||||
switch self {
|
||||
case .images: "Images"
|
||||
case .videos: "Videos"
|
||||
case .voice: "Voice messages"
|
||||
case .files: "Files"
|
||||
case .links: "Links"
|
||||
}
|
||||
}
|
||||
|
||||
var searchPlaceholder: LocalizedStringKey {
|
||||
switch self {
|
||||
case .images: "Search images"
|
||||
case .videos: "Search videos"
|
||||
case .voice: "Search voice messages"
|
||||
case .files: "Search files"
|
||||
case .links: "Search links"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .images: "photo"
|
||||
case .videos: "video"
|
||||
case .voice: "mic"
|
||||
case .files: "doc"
|
||||
case .links: "link"
|
||||
}
|
||||
}
|
||||
|
||||
var iconFilled: String {
|
||||
switch self {
|
||||
case .images: "photo.fill"
|
||||
case .videos: "video.fill"
|
||||
case .voice: "mic.fill"
|
||||
case .files: "doc.fill"
|
||||
case .links: "link.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/.idea
|
||||
!/.idea/codeStyles/*
|
||||
|
||||
+1
-1
@@ -3735,7 +3735,7 @@ sealed class CC {
|
||||
}
|
||||
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
}
|
||||
is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)})"
|
||||
is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)}"
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
|
||||
+2
-1
@@ -27,11 +27,12 @@ suspend fun apiLoadMessages(
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
pagination: ChatPagination,
|
||||
contentTag: MsgContentTag? = null,
|
||||
search: String = "",
|
||||
openAroundItemId: Long? = null,
|
||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||
) = coroutineScope {
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), contentTag ?: chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
/** When [openAroundItemId] is provided, chatId can be different too */
|
||||
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
|
||||
|
||||
+277
-98
@@ -45,6 +45,7 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.common.views.newchat.alertProfileImageSize
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.*
|
||||
@@ -144,6 +145,9 @@ fun ChatView(
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
||||
val showCommandsMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val contentFilter = rememberSaveable { mutableStateOf<ContentFilter?>(null) }
|
||||
val availableContent = remember { mutableStateOf<List<ContentFilter>>(ContentFilter.initialList) }
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
@@ -170,7 +174,12 @@ fun ChatView(
|
||||
}
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
contentFilter.value = null
|
||||
availableContent.value = ContentFilter.initialList
|
||||
selectedChatItems.value = null
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||
}
|
||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
@@ -229,11 +238,11 @@ fun ChatView(
|
||||
val sameText = searchText.value == value
|
||||
// showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it
|
||||
// (required on Android to have this check to prevent call to search with old text)
|
||||
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null
|
||||
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null && contentFilter.value == null
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
if ((sameText && contentFilter.value == null) || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
withBGApi {
|
||||
apiFindMessages(chatsCtx, c, value)
|
||||
apiFindMessages(chatsCtx, c, contentFilter.value?.contentTag, value)
|
||||
searchText.value = value
|
||||
}
|
||||
}
|
||||
@@ -486,7 +495,7 @@ fun ChatView(
|
||||
val c = chatModel.getChat(chatId)
|
||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||
if (c != null) {
|
||||
apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes)
|
||||
apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, contentFilter.value?.contentTag, searchText.value, null, visibleItemIndexes)
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
@@ -742,14 +751,23 @@ fun ChatView(
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
closeSearch = {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
contentFilter.value = null
|
||||
// Update available content types when search closes
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
showSearch = showSearch,
|
||||
showCommandsMenu = showCommandsMenu
|
||||
showCommandsMenu = showCommandsMenu,
|
||||
contentFilter = contentFilter,
|
||||
availableContent = availableContent,
|
||||
searchPlaceholder = contentFilter.value?.searchPlaceholder?.let { generalGetString(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -785,6 +803,23 @@ fun ChatView(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAvailableContent(chatRh: Long?, activeChat: State<Chat?>, availableContent: MutableState<List<ContentFilter>>) {
|
||||
withBGApi {
|
||||
Log.e(TAG, "updateAvailableContent")
|
||||
val chatInfo = activeChat.value?.chatInfo
|
||||
if (chatInfo == null) return@withBGApi
|
||||
val types = chatModel.controller.apiGetChatContentTypes(chatRh, chatInfo.chatType, chatInfo.apiId, null)
|
||||
if (activeChat.value?.chatInfo?.id != chatInfo.id) return@withBGApi
|
||||
if (types == null) {
|
||||
availableContent.value = ContentFilter.entries
|
||||
} else {
|
||||
val typeSet: Set<MsgContentTag> = types.union(ContentFilter.alwaysShow)
|
||||
Log.e(TAG, "updateAvailableContent $typeSet")
|
||||
availableContent.value = ContentFilter.entries.filter { it -> typeSet.contains(it.contentTag) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectingText(chatInfo: ChatInfo): String? {
|
||||
return when (chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
@@ -879,7 +914,10 @@ fun ChatLayout(
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
showSearch: MutableState<Boolean>,
|
||||
showCommandsMenu: MutableState<Boolean>
|
||||
showCommandsMenu: MutableState<Boolean>,
|
||||
contentFilter: MutableState<ContentFilter?>,
|
||||
availableContent: State<List<ContentFilter>>,
|
||||
searchPlaceholder: String?
|
||||
) {
|
||||
val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -1063,7 +1101,7 @@ fun ChatLayout(
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch, contentFilter, availableContent, searchPlaceholder)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||
@@ -1096,10 +1134,14 @@ fun BoxScope.ChatInfoToolbar(
|
||||
openGroupLink: (GroupInfo) -> Unit,
|
||||
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
showSearch: MutableState<Boolean>
|
||||
showSearch: MutableState<Boolean>,
|
||||
contentFilter: MutableState<ContentFilter?>,
|
||||
availableContent: State<List<ContentFilter>>,
|
||||
searchPlaceholder: String?
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val showContentFilterMenu = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
@@ -1107,6 +1149,7 @@ fun BoxScope.ChatInfoToolbar(
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
contentFilter.value = null
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) {
|
||||
@@ -1115,104 +1158,141 @@ fun BoxScope.ChatInfoToolbar(
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
if (chatInfo is ChatInfo.Local) {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
stringResource(MR.strings.search_verb).capitalize(Locale.current),
|
||||
tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
if (connectedAt != null) {
|
||||
val time = remember { mutableStateOf(durationText(0)) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
|
||||
Text(time.value, Modifier.widthIn(min = sp50))
|
||||
}
|
||||
}
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
endCall()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
null,
|
||||
tint = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
val showContentFilterButton = availableContent.value.isNotEmpty()
|
||||
val activeCallInChat = chatInfo is ChatInfo.Direct && activeCall?.contact?.id == chatInfo.id
|
||||
|
||||
// Content filter button - shown in bar, or moved to menu during active call
|
||||
if (showContentFilterButton) {
|
||||
val enabled = chatInfo !is ChatInfo.Local || chatInfo.noteFolder.ready
|
||||
if (activeCallInChat) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
addMembers(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_menu_item),
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
showContentFilterMenu.value = true
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
openGroupLink(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary)
|
||||
IconButton(
|
||||
{ showContentFilterMenu.value = true },
|
||||
enabled = enabled
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat-type specific buttons
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Local -> {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
stringResource(MR.strings.search_verb).capitalize(Locale.current),
|
||||
tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.Direct -> {
|
||||
if (activeCall?.contact?.id == chatInfo.id) {
|
||||
if (appPlatform.isDesktop) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
if (connectedAt != null) {
|
||||
val time = remember { mutableStateOf(durationText(0)) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
|
||||
Text(time.value, Modifier.widthIn(min = sp50))
|
||||
}
|
||||
}
|
||||
}
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
endCall()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
null,
|
||||
tint = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Call buttons moved to menu
|
||||
if (chatInfo.contact.mergedPreferences.calls.enabled.forUser && chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
})
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
is ChatInfo.Group -> {
|
||||
// Add members / group link moved to menu
|
||||
if (chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chatInfo.incognito) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_add_members), painterResource(MR.images.ic_person_add_500), onClick = {
|
||||
showMenu.value = false
|
||||
addMembers(chatInfo.groupInfo)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.group_link), painterResource(MR.images.ic_add_link), onClick = {
|
||||
showMenu.value = false
|
||||
openGroupLink(chatInfo.groupInfo)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val enableNtfs = chatInfo.chatSettings?.enableNtfs
|
||||
if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) {
|
||||
val ntfMode = remember { mutableStateOf(enableNtfs) }
|
||||
@@ -1242,13 +1322,27 @@ fun BoxScope.ChatInfoToolbar(
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val chatBottomBar = remember { appPrefs.chatBottomBar.state }
|
||||
val searchTrailingContent: @Composable (() -> Unit)? = if (showContentFilterButton) {{
|
||||
IconButton({ showContentFilterMenu.value = true }) {
|
||||
Icon(
|
||||
painterResource(if (contentFilter.value == null) MR.images.ic_photo_library else MR.images.ic_photo_library_filled),
|
||||
null,
|
||||
Modifier.padding(4.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}} else null
|
||||
|
||||
DefaultAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch.value,
|
||||
searchAlwaysVisible = contentFilter.value != null,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
searchPlaceholder = searchPlaceholder,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
searchTrailingContent = searchTrailingContent,
|
||||
buttons = { barButtons.forEach { it() } }
|
||||
)
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||
@@ -1269,6 +1363,65 @@ fun BoxScope.ChatInfoToolbar(
|
||||
menuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
val contentFilterWidth = remember { mutableStateOf(250.dp) }
|
||||
val contentFilterHeight = remember { mutableStateOf(0.dp) }
|
||||
DefaultDropdownMenu(
|
||||
showContentFilterMenu,
|
||||
modifier = Modifier.onSizeChanged { with(density) {
|
||||
contentFilterWidth.value = it.width.toDp().coerceAtLeast(250.dp)
|
||||
if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) contentFilterHeight.value = it.height.toDp()
|
||||
} },
|
||||
offset = DpOffset(-contentFilterWidth.value, if (oneHandUI.value && chatBottomBar.value) -contentFilterHeight.value else AppBarHeight)
|
||||
) {
|
||||
val contentFilterMenuItems: List<@Composable () -> Unit> = buildList {
|
||||
availableContent.value.forEach { filter ->
|
||||
val isSelected = contentFilter.value == filter
|
||||
add {
|
||||
ItemAction(
|
||||
stringResource(filter.label),
|
||||
painterResource(if (isSelected) filter.iconFilled else filter.icon),
|
||||
color = if (isSelected) MaterialTheme.colors.primary else Color.Unspecified,
|
||||
onClick = {
|
||||
showContentFilterMenu.value = false
|
||||
if (contentFilter.value == filter) return@ItemAction
|
||||
contentFilter.value = filter
|
||||
showSearch.value = true
|
||||
scope.launch {
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (c != null) {
|
||||
apiFindMessages(chatsCtx, c, filter.contentTag, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showSearch.value) {
|
||||
add {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_all_messages),
|
||||
painterResource(MR.images.ic_forum),
|
||||
onClick = {
|
||||
showContentFilterMenu.value = false
|
||||
contentFilter.value = null
|
||||
showSearch.value = false
|
||||
scope.launch {
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (c != null) {
|
||||
apiFindMessages(chatsCtx, c, null, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value && chatBottomBar.value) {
|
||||
contentFilterMenuItems.asReversed().forEach { it() }
|
||||
} else {
|
||||
contentFilterMenuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3425,7 +3578,10 @@ fun PreviewChatLayout() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) },
|
||||
showCommandsMenu = remember { mutableStateOf(false) }
|
||||
showCommandsMenu = remember { mutableStateOf(false) },
|
||||
contentFilter = remember { mutableStateOf(null) },
|
||||
availableContent = remember { mutableStateOf(ContentFilter.initialList) },
|
||||
searchPlaceholder = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3505,7 +3661,30 @@ fun PreviewGroupChatLayout() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) },
|
||||
showCommandsMenu = remember { mutableStateOf(false) }
|
||||
showCommandsMenu = remember { mutableStateOf(false) },
|
||||
contentFilter = remember { mutableStateOf(null) },
|
||||
availableContent = remember { mutableStateOf(ContentFilter.initialList) },
|
||||
searchPlaceholder = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ContentFilter(
|
||||
val contentTag: MsgContentTag,
|
||||
val label: StringResource,
|
||||
val searchPlaceholder: StringResource,
|
||||
val icon: ImageResource,
|
||||
val iconFilled: ImageResource
|
||||
) {
|
||||
Images(MsgContentTag.Image, MR.strings.content_filter_images, MR.strings.placeholder_search_images, MR.images.ic_image, MR.images.ic_image_filled),
|
||||
Videos(MsgContentTag.Video, MR.strings.content_filter_videos, MR.strings.placeholder_search_videos, MR.images.ic_videocam, MR.images.ic_videocam_filled),
|
||||
Voice(MsgContentTag.Voice, MR.strings.content_filter_voice_messages, MR.strings.placeholder_search_voice_messages, MR.images.ic_mic, MR.images.ic_mic_filled),
|
||||
Files(MsgContentTag.File, MR.strings.content_filter_files, MR.strings.placeholder_search_files, MR.images.ic_draft, MR.images.ic_draft_filled),
|
||||
Links(MsgContentTag.Link, MR.strings.content_filter_links, MR.strings.placeholder_search_links, MR.images.ic_link, MR.images.ic_link);
|
||||
|
||||
companion object {
|
||||
val alwaysShow: Set<MsgContentTag> = setOf(MsgContentTag.Image, MsgContentTag.Link)
|
||||
|
||||
val initialList: List<ContentFilter> = listOf(ContentFilter.Images, ContentFilter.Files, ContentFilter.Links)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -228,6 +228,7 @@ suspend fun openChat(
|
||||
} else {
|
||||
ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
},
|
||||
contentTag = null,
|
||||
"",
|
||||
openAroundItemId
|
||||
)
|
||||
@@ -241,11 +242,12 @@ suspend fun openLoadedChat(chat: Chat) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) {
|
||||
suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, contentTag: MsgContentTag?, search: String) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.chatItems.clearAndNotify()
|
||||
}
|
||||
apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search)
|
||||
val pagination = if (search.isNotEmpty() || contentTag != null) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination, contentTag, search)
|
||||
}
|
||||
|
||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
|
||||
|
||||
+4
-1
@@ -29,7 +29,9 @@ fun DefaultAppBar(
|
||||
onTop: Boolean,
|
||||
showSearch: Boolean = false,
|
||||
searchAlwaysVisible: Boolean = false,
|
||||
searchPlaceholder: String? = null,
|
||||
onSearchValueChanged: (String) -> Unit = {},
|
||||
searchTrailingContent: @Composable (() -> Unit)? = null,
|
||||
buttons: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
||||
@@ -78,7 +80,8 @@ fun DefaultAppBar(
|
||||
AppBar(
|
||||
title = {
|
||||
if (showSearch) {
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||
val placeholder = searchPlaceholder ?: stringResource(MR.strings.search_verb)
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, placeholder = placeholder, trailingContent = searchTrailingContent, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||
} else if (title != null) {
|
||||
title()
|
||||
} else if (titleText.value.isNotEmpty() && connection != null) {
|
||||
|
||||
+19
-10
@@ -10,6 +10,7 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -112,18 +113,26 @@ fun SearchTextField(
|
||||
placeholder = {
|
||||
Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
||||
IconButton({
|
||||
if (alwaysVisible) {
|
||||
keyboard?.hide()
|
||||
focusManager.clearFocus()
|
||||
trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.offset(x = 8.dp)
|
||||
) {
|
||||
if (searchText.value.text.isNotEmpty()) {
|
||||
IconButton({
|
||||
if (alwaysVisible) {
|
||||
keyboard?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
searchText.value = TextFieldValue("")
|
||||
onValueChange("")
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
searchText.value = TextFieldValue("");
|
||||
onValueChange("")
|
||||
}, Modifier.offset(x = reducedCloseButtonPadding)) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
||||
trailingContent?.invoke()
|
||||
}
|
||||
}} else trailingContent,
|
||||
}} else null,
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
|
||||
@@ -368,6 +368,18 @@
|
||||
<string name="edit_verb">Edit</string>
|
||||
<string name="info_menu">Info</string>
|
||||
<string name="search_verb">Search</string>
|
||||
<string name="placeholder_search_images">Search images</string>
|
||||
<string name="placeholder_search_videos">Search videos</string>
|
||||
<string name="placeholder_search_voice_messages">Search voice messages</string>
|
||||
<string name="placeholder_search_files">Search files</string>
|
||||
<string name="placeholder_search_links">Search links</string>
|
||||
<string name="content_filter_images">Images</string>
|
||||
<string name="content_filter_videos">Videos</string>
|
||||
<string name="content_filter_voice_messages">Voice messages</string>
|
||||
<string name="content_filter_files">Files</string>
|
||||
<string name="content_filter_links">Links</string>
|
||||
<string name="content_filter_all_messages">All messages</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="archive_verb">Archive</string>
|
||||
<string name="archive_report">Archive report</string>
|
||||
<string name="archive_reports">Archive reports</string>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M182-124.5q-22.97 0-40.23-17.27Q124.5-159.03 124.5-182v-596q0-22.97 17.27-40.23Q159.03-835.5 182-835.5h596q22.97 0 40.23 17.27Q835.5-800.97 835.5-778v596q0 22.97-17.27 40.23Q800.97-124.5 778-124.5H182Zm58-154h481.5L577-471 446-301.5l-92-125-114 148Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M345.5-376H731L606.5-544l-103 135-67.5-86.5L345.5-376Zm-88 176q-22.97 0-40.23-17.27Q200-234.53 200-257.5v-560q0-22.97 17.27-40.23Q234.53-875 257.5-875h560q22.97 0 40.23 17.27Q875-840.47 875-817.5v560q0 22.97-17.27 40.23Q840.47-200 817.5-200h-560Zm0-57.5h560v-560h-560v560ZM142.5-85q-22.97 0-40.23-17.27Q85-119.53 85-142.5V-760h57.5v617.5H760V-85H142.5Zm115-732.5v560-560Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
+4
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M345.5-376H731L606.5-544l-103 135-67.5-86.5L345.5-376Zm-88 176q-22.97 0-40.23-17.27Q200-234.53 200-257.5v-560q0-22.97 17.27-40.23Q234.53-875 257.5-875h560q22.97 0 40.23 17.27Q875-840.47 875-817.5v560q0 22.97-17.27 40.23Q840.47-200 817.5-200h-560Zm-115 115q-22.97 0-40.23-17.27Q85-119.53 85-142.5V-760h57.5v617.5H760V-85H142.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
@@ -768,6 +768,7 @@ Group:
|
||||
- itemTimed: [CITimed](#citimed)?
|
||||
- itemLive: bool?
|
||||
- userMention: bool
|
||||
- hasLink: bool
|
||||
- deletable: bool
|
||||
- editable: bool
|
||||
- forwardedByMember: int64?
|
||||
|
||||
@@ -778,6 +778,7 @@ export interface CIMeta {
|
||||
itemTimed?: CITimed
|
||||
itemLive?: boolean
|
||||
userMention: boolean
|
||||
hasLink: boolean
|
||||
deletable: boolean
|
||||
editable: boolean
|
||||
forwardedByMember?: number // int64
|
||||
|
||||
+4
-2
@@ -127,7 +127,8 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260109_chat_relays
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -277,7 +278,8 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260109_chat_relays
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
@@ -2265,7 +2265,8 @@ processChatCommand vr nm = \case
|
||||
(errs, ctSndMsgs :: [(Contact, SndMessage)]) <-
|
||||
partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_
|
||||
timestamp <- liftIO getCurrentTime
|
||||
lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs
|
||||
let hasLink = msgContentHasLink mc $ parseMaybeMarkdownList $ msgContentText mc
|
||||
lift . void $ withStoreBatch' $ \db -> map (createCI db user hasLink timestamp) ctSndMsgs
|
||||
pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp}
|
||||
where
|
||||
addContactConn :: Contact -> [(Contact, Connection)] -> [(Contact, Connection)]
|
||||
@@ -2280,9 +2281,9 @@ processChatCommand vr nm = \case
|
||||
combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg')
|
||||
combineResults _ (Left e) _ = Left e
|
||||
combineResults _ _ (Left e) = Left e
|
||||
createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO ()
|
||||
createCI db user createdAt (ct, sndMsg) =
|
||||
void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt
|
||||
createCI :: DB.Connection -> User -> Bool -> UTCTime -> (Contact, SndMessage) -> IO ()
|
||||
createCI db user hasLink createdAt (ct, sndMsg) =
|
||||
void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False hasLink createdAt
|
||||
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
|
||||
contactId <- withFastStore $ \db -> getContactIdByName db user cName
|
||||
quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
|
||||
|
||||
@@ -2265,9 +2265,10 @@ saveSndChatItems user cd itemsData itemTimed live = do
|
||||
where
|
||||
createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd))
|
||||
createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do
|
||||
ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt
|
||||
let hasLink_ = ciContentHasLink content (snd itemTexts)
|
||||
ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live hasLink_ createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
let ci = mkChatItem_ cd False ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt
|
||||
let ci = mkChatItem_ cd False ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False hasLink_ createdAt Nothing createdAt
|
||||
Right <$> case cd of
|
||||
CDGroupSnd g _scope | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions
|
||||
_ -> pure ci
|
||||
@@ -2300,9 +2301,10 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared
|
||||
if (ciRequiresAttention content || contactChatDeleted cd)
|
||||
then updateChatTsStats db vr user cd createdAt (memberChatStats userMention)
|
||||
else pure $ toChatInfo cd
|
||||
(ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt
|
||||
let hasLink_ = ciContentHasLink content ft_
|
||||
(ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention hasLink_ brokerTs createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
let ci = mkChatItem_ cd False ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt
|
||||
let ci = mkChatItem_ cd False ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention hasLink_ brokerTs forwardedByMember createdAt
|
||||
ci' <- case cd of
|
||||
CDGroupRcv g _scope _m | not (null mentions') -> createGroupCIMentions db g ci mentions'
|
||||
_ -> pure ci
|
||||
@@ -2318,15 +2320,26 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared
|
||||
-- TODO [mentions] optimize by avoiding unnecessary parsing
|
||||
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||
mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs =
|
||||
let ts = ciContentTexts content
|
||||
in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs
|
||||
let ts@(_, ft_) = ciContentTexts content
|
||||
hasLink_ = ciContentHasLink content ft_
|
||||
in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember currentTs
|
||||
|
||||
mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||
mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs =
|
||||
mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||
mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember currentTs =
|
||||
let itemStatus = ciCreateStatus content
|
||||
meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention currentTs itemTs forwardedByMember showGroupAsSender currentTs currentTs
|
||||
meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender currentTs currentTs
|
||||
in ChatItem {chatDir = toCIDirection cd, meta, content, mentions = M.empty, formattedText, quotedItem, reactions = [], file}
|
||||
|
||||
ciContentHasLink :: CIContent d -> Maybe MarkdownList -> Bool
|
||||
ciContentHasLink content ft_ = case ciMsgContent content of
|
||||
Just mc -> msgContentHasLink mc ft_
|
||||
Nothing -> False
|
||||
|
||||
msgContentHasLink :: MsgContent -> Maybe MarkdownList -> Bool
|
||||
msgContentHasLink mc ft_ = case msgContentTag mc of
|
||||
MCLink_ -> True
|
||||
_ -> maybe False hasLinks ft_
|
||||
|
||||
createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId)
|
||||
createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do
|
||||
cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction
|
||||
@@ -2587,7 +2600,8 @@ createChatItems user itemTs_ dirsCIContents = do
|
||||
createACIs db itemTs createdAt (cd, showGroupAsSender, contents) = map createACI contents
|
||||
where
|
||||
createACI (content, sharedMsgId) = do
|
||||
ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId itemTs createdAt
|
||||
let hasLink_ = ciContentHasLink content Nothing
|
||||
ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ itemTs createdAt
|
||||
let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt
|
||||
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
||||
|
||||
@@ -2617,10 +2631,11 @@ createLocalChatItems user cd itemsData createdAt = do
|
||||
pure items
|
||||
where
|
||||
createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd)
|
||||
createItem db (content, ciFile, itemForwarded, ts) = do
|
||||
ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt
|
||||
createItem db (content, ciFile, itemForwarded, ts@(_, ft_)) = do
|
||||
let hasLink_ = ciContentHasLink content ft_
|
||||
ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False hasLink_ createdAt Nothing createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
pure $ mkChatItem_ cd False ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt
|
||||
pure $ mkChatItem_ cd False ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False hasLink_ createdAt Nothing createdAt
|
||||
|
||||
withUser' :: (User -> CM ChatResponse) -> CM ChatResponse
|
||||
withUser' action =
|
||||
|
||||
@@ -178,6 +178,16 @@ isSimplexLink = \case
|
||||
SimplexLink {} -> True
|
||||
_ -> False
|
||||
|
||||
isLink :: Format -> Bool
|
||||
isLink = \case
|
||||
Uri -> True
|
||||
HyperLink {} -> True
|
||||
SimplexLink {} -> True
|
||||
_ -> False
|
||||
|
||||
hasLinks :: MarkdownList -> Bool
|
||||
hasLinks = any $ \(FormattedText f _) -> maybe False isLink f
|
||||
|
||||
markdownP :: Parser Markdown
|
||||
markdownP = mconcat <$> A.many' fragmentP
|
||||
where
|
||||
|
||||
@@ -499,6 +499,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
|
||||
itemTimed :: Maybe CITimed,
|
||||
itemLive :: Maybe Bool,
|
||||
userMention :: Bool, -- True for messages that mention user or reply to user messages
|
||||
hasLink :: BoolDef,
|
||||
deletable :: Bool,
|
||||
editable :: Bool,
|
||||
forwardedByMember :: Maybe GroupMemberId,
|
||||
@@ -510,11 +511,12 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
|
||||
|
||||
type ShowGroupAsSender = Bool
|
||||
|
||||
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt =
|
||||
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt =
|
||||
let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs
|
||||
editable = deletable && isNothing itemForwarded
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, deletable, editable, forwardedByMember, showGroupAsSender, createdAt, updatedAt}
|
||||
hasLink = BoolDef hasLink_
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, hasLink, deletable, editable, forwardedByMember, showGroupAsSender, createdAt, updatedAt}
|
||||
|
||||
deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool
|
||||
deletable' itemContent itemDeleted itemTs allowedInterval currentTs =
|
||||
@@ -540,6 +542,7 @@ dummyMeta itemId ts itemText =
|
||||
itemTimed = Nothing,
|
||||
itemLive = Nothing,
|
||||
userMention = False,
|
||||
hasLink = BoolDef False,
|
||||
deletable = False,
|
||||
editable = False,
|
||||
forwardedByMember = Nothing,
|
||||
|
||||
@@ -525,9 +525,9 @@ setSupportChatMemberAttention db vr user g m memberAttention = do
|
||||
m_ <- runExceptT $ getGroupMemberById db vr user (groupMemberId' m)
|
||||
pure $ either (const m) id m_ -- Left shouldn't happen, but types require it
|
||||
|
||||
createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId
|
||||
createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt =
|
||||
createNewChatItem_ db user chatDirection False createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt
|
||||
createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> IO ChatItemId
|
||||
createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live hasLink createdAt =
|
||||
createNewChatItem_ db user chatDirection False createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False hasLink createdAt Nothing createdAt
|
||||
where
|
||||
createdByMsgId = if msgId == 0 then Nothing else Just msgId
|
||||
quoteRow :: NewQuoteRow
|
||||
@@ -541,9 +541,9 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon
|
||||
CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId)
|
||||
CIQGroupRcv Nothing -> (Just False, Nothing)
|
||||
|
||||
createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom)
|
||||
createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do
|
||||
ciId <- createNewChatItem_ db user chatDirection False (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt
|
||||
createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom)
|
||||
createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention hasLink itemTs createdAt = do
|
||||
ciId <- createNewChatItem_ db user chatDirection False (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember createdAt
|
||||
quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg
|
||||
pure (ciId, quotedItem, itemForwarded)
|
||||
where
|
||||
@@ -558,15 +558,15 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw
|
||||
CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ _ ->
|
||||
(Just $ Just userMemberId == memberId, memberId)
|
||||
|
||||
createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> UTCTime -> UTCTime -> IO ChatItemId
|
||||
createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ itemTs =
|
||||
createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False itemTs Nothing
|
||||
createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> UTCTime -> UTCTime -> IO ChatItemId
|
||||
createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink itemTs =
|
||||
createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing
|
||||
where
|
||||
quoteRow :: NewQuoteRow
|
||||
quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
|
||||
createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId
|
||||
createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do
|
||||
createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId
|
||||
createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember createdAt = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
@@ -575,20 +575,20 @@ createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ share
|
||||
user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_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, include_in_history, created_at, updated_at, item_live, user_mention, show_group_as_sender, timed_ttl, timed_delete_at,
|
||||
forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, show_group_as_sender, 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 :. groupScopeRow :. 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, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, 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), BI userMention, BI showGroupAsSender) :. ciTimedRow timed
|
||||
itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, BoolInt, 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, BI userMention, BI hasLink, BI showGroupAsSender) :. ciTimedRow timed
|
||||
quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e)
|
||||
idsRow :: (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId, Maybe NoteFolderId)
|
||||
idsRow = case chatDirection of
|
||||
@@ -1045,7 +1045,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
|
||||
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -1078,7 +1078,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
|
||||
_ -> Just (CIDeleted @'CTLocal deletedTs)
|
||||
itemEdited' = maybe False unBI itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing False createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs Nothing False createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -1470,6 +1470,12 @@ getChatItemIDs db User {userId} cInfo contentFilter range count search = case cI
|
||||
(grCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ")
|
||||
(userId, groupId)
|
||||
"item_ts"
|
||||
(Nothing, Just MCLink_) ->
|
||||
liftIO $
|
||||
idsQuery
|
||||
(grCond <> " AND has_link = 1 ")
|
||||
(userId, groupId)
|
||||
"item_ts"
|
||||
(Nothing, Just mcTag) ->
|
||||
liftIO $
|
||||
idsQuery
|
||||
@@ -1488,11 +1494,13 @@ getChatItemIDs db User {userId} cInfo contentFilter range count search = case cI
|
||||
grCond = " user_id = ? AND group_id = ? "
|
||||
DirectChat Contact {contactId} -> liftIO $ case contentFilter of
|
||||
Nothing -> idsQuery ctCond (userId, contactId) "created_at"
|
||||
Just MCLink_ -> idsQuery (ctCond <> " AND has_link = 1 ") (userId, contactId) "created_at"
|
||||
Just mcTag -> idsQuery (ctCond <> " AND msg_content_tag = ? ") (userId, contactId, mcTag) "created_at"
|
||||
where
|
||||
ctCond = " user_id = ? AND contact_id = ? "
|
||||
LocalChat NoteFolder {noteFolderId} -> liftIO $ case contentFilter of
|
||||
Nothing -> idsQuery nfCond (userId, noteFolderId) "created_at"
|
||||
Just MCLink_ -> idsQuery (nfCond <> " AND has_link = 1 ") (userId, noteFolderId) "created_at"
|
||||
Just mcTag -> idsQuery (nfCond <> " AND msg_content_tag = ? ") (userId, noteFolderId, mcTag) "created_at"
|
||||
where
|
||||
nfCond = " user_id = ? AND note_folder_id = ? "
|
||||
@@ -2191,7 +2199,7 @@ updateLocalChatItemsRead db User {userId} noteFolderId = do
|
||||
|
||||
type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol)
|
||||
|
||||
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt)
|
||||
type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt)
|
||||
|
||||
type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||
|
||||
@@ -2215,7 +2223,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -2248,7 +2256,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
|
||||
_ -> Just (CIDeleted @'CTDirect deletedTs)
|
||||
itemEdited' = maybe False unBI itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing False createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs Nothing False createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -2286,7 +2294,7 @@ toGroupChatItem
|
||||
( ( (itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId)
|
||||
:. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt)
|
||||
:. forwardedFromRow
|
||||
:. (timedTTL, timedDeleteAt, itemLive, BI userMention)
|
||||
:. (timedTTL, timedDeleteAt, itemLive, BI userMention, BI hasLink)
|
||||
:. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)
|
||||
)
|
||||
:. (forwardedByMember, BI showGroupAsSender)
|
||||
@@ -2331,7 +2339,7 @@ toGroupChatItem
|
||||
_ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
|
||||
itemEdited' = maybe False unBI itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention hasLink currentTs itemTs forwardedByMember showGroupAsSender createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
@@ -2604,7 +2612,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
@@ -2959,7 +2967,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
@@ -3068,7 +3076,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol
|
||||
FROM chat_items i
|
||||
|
||||
@@ -25,7 +25,8 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260109_chat_relays
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -51,7 +52,8 @@ schemaMigrations =
|
||||
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
|
||||
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables),
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260109_chat_relays", m20260109_chat_relays, Just down_m20260109_chat_relays)
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260122_has_link :: Text
|
||||
m20260122_has_link =
|
||||
[r|
|
||||
ALTER TABLE chat_items ADD COLUMN has_link SMALLINT NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE chat_items SET msg_content_tag = 'text' WHERE msg_content_tag = 'liveText';
|
||||
|
||||
UPDATE chat_items SET has_link = 1
|
||||
WHERE msg_content_tag = 'link' OR item_text LIKE '%https://%';
|
||||
|
||||
CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items(user_id, group_id, has_link, item_ts);
|
||||
CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items(user_id, contact_id, has_link, created_at);
|
||||
CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items(user_id, note_folder_id, has_link, created_at);
|
||||
|]
|
||||
|
||||
down_m20260122_has_link :: Text
|
||||
down_m20260122_has_link =
|
||||
[r|
|
||||
DROP INDEX idx_chat_items_note_folder_has_link_created_at;
|
||||
DROP INDEX idx_chat_items_contacts_has_link_created_at;
|
||||
DROP INDEX idx_chat_items_groups_has_link_item_ts;
|
||||
|
||||
ALTER TABLE chat_items DROP COLUMN has_link;
|
||||
|]
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260109_chat_relays where
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260109_chat_relays :: Text
|
||||
m20260109_chat_relays =
|
||||
m20260222_chat_relays :: Text
|
||||
m20260222_chat_relays =
|
||||
T.pack
|
||||
[r|
|
||||
CREATE TABLE chat_relays(
|
||||
@@ -59,8 +59,8 @@ CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id);
|
||||
ALTER TABLE group_members ADD COLUMN relay_link BYTEA;
|
||||
|]
|
||||
|
||||
down_m20260109_chat_relays :: Text
|
||||
down_m20260109_chat_relays =
|
||||
down_m20260222_chat_relays :: Text
|
||||
down_m20260222_chat_relays =
|
||||
T.pack
|
||||
[r|
|
||||
ALTER TABLE users DROP COLUMN is_user_chat_relay;
|
||||
@@ -342,7 +342,8 @@ CREATE TABLE test_chat_schema.chat_items (
|
||||
user_mention smallint DEFAULT 0 NOT NULL,
|
||||
group_scope_tag text,
|
||||
group_scope_group_member_id bigint,
|
||||
show_group_as_sender smallint DEFAULT 0 NOT NULL
|
||||
show_group_as_sender smallint DEFAULT 0 NOT NULL,
|
||||
has_link smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -1886,6 +1887,10 @@ CREATE INDEX idx_chat_items_contacts_created_at ON test_chat_schema.chat_items U
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_contacts_has_link_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, has_link, created_at);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, msg_content_tag, created_at);
|
||||
|
||||
|
||||
@@ -1946,6 +1951,10 @@ CREATE INDEX idx_chat_items_groups ON test_chat_schema.chat_items USING btree (u
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_groups_has_link_item_ts ON test_chat_schema.chat_items USING btree (user_id, group_id, has_link, item_ts);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_groups_history ON test_chat_schema.chat_items USING btree (user_id, group_id, include_in_history, item_deleted, item_ts, chat_item_id);
|
||||
|
||||
|
||||
@@ -1974,6 +1983,10 @@ CREATE INDEX idx_chat_items_item_status ON test_chat_schema.chat_items USING btr
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, has_link, created_at);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, msg_content_tag, created_at);
|
||||
|
||||
|
||||
|
||||
@@ -148,7 +148,8 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260109_chat_relays
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -297,7 +298,8 @@ schemaMigrations =
|
||||
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
|
||||
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables),
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260109_chat_relays", m20260109_chat_relays, Just down_m20260109_chat_relays)
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260122_has_link :: Query
|
||||
m20260122_has_link =
|
||||
[sql|
|
||||
UPDATE chat_items SET msg_content_tag = 'text' WHERE msg_content_tag = 'liveText';
|
||||
|
||||
UPDATE chat_items SET msg_content_tag = CAST(msg_content_tag as TEXT) WHERE typeof(msg_content_tag) = 'blob';
|
||||
|
||||
ALTER TABLE chat_items ADD COLUMN has_link INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE chat_items SET has_link = 1
|
||||
WHERE msg_content_tag = 'link' OR item_text LIKE '%https://%';
|
||||
|
||||
CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items(user_id, group_id, has_link, item_ts);
|
||||
CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items(user_id, contact_id, has_link, created_at);
|
||||
CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items(user_id, note_folder_id, has_link, created_at);
|
||||
|]
|
||||
|
||||
down_m20260122_has_link :: Query
|
||||
down_m20260122_has_link =
|
||||
[sql|
|
||||
DROP INDEX idx_chat_items_note_folder_has_link_created_at;
|
||||
DROP INDEX idx_chat_items_contacts_has_link_created_at;
|
||||
DROP INDEX idx_chat_items_groups_has_link_item_ts;
|
||||
|
||||
ALTER TABLE chat_items DROP COLUMN has_link;
|
||||
|]
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260109_chat_relays where
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
@@ -18,8 +18,8 @@ import Database.SQLite.Simple.QQ (sql)
|
||||
-- - group_members.relay_link - relay link, saved on member record for user joining group
|
||||
-- - groups.relay_own_status - indicates for a relay client that it is chat relay for the group (RelayStatus)
|
||||
-- - groups.relay_request_* - relay request "work item" fields
|
||||
m20260109_chat_relays :: Query
|
||||
m20260109_chat_relays =
|
||||
m20260222_chat_relays :: Query
|
||||
m20260222_chat_relays =
|
||||
[sql|
|
||||
CREATE TABLE chat_relays(
|
||||
chat_relay_id INTEGER PRIMARY KEY,
|
||||
@@ -72,8 +72,8 @@ CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id);
|
||||
ALTER TABLE group_members ADD COLUMN relay_link BLOB;
|
||||
|]
|
||||
|
||||
down_m20260109_chat_relays :: Query
|
||||
down_m20260109_chat_relays =
|
||||
down_m20260222_chat_relays :: Query
|
||||
down_m20260222_chat_relays =
|
||||
[sql|
|
||||
ALTER TABLE users DROP COLUMN is_user_chat_relay;
|
||||
|
||||
@@ -867,7 +867,7 @@ Query:
|
||||
LIMIT ?
|
||||
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?)
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
@@ -878,7 +878,7 @@ Query:
|
||||
LIMIT ?
|
||||
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?)
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
@@ -1247,7 +1247,7 @@ Query:
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol
|
||||
FROM chat_items i
|
||||
@@ -1264,7 +1264,7 @@ Query:
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
-- CIMeta forwardedByMember, showGroupAsSender
|
||||
@@ -1317,7 +1317,7 @@ Query:
|
||||
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
|
||||
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
|
||||
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention,
|
||||
i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, i.has_link,
|
||||
-- CIFile
|
||||
f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol,
|
||||
-- DirectQuote
|
||||
@@ -1448,7 +1448,7 @@ Query:
|
||||
LIMIT ?
|
||||
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
|
||||
SEARCH chat_items USING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?)
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
@@ -4405,7 +4405,7 @@ Query:
|
||||
Plan:
|
||||
SEARCH files USING INDEX idx_files_user_id (user_id=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?)
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?)
|
||||
@@ -4451,12 +4451,12 @@ Query:
|
||||
user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_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, include_in_history, created_at, updated_at, item_live, user_mention, show_group_as_sender, timed_ttl, timed_delete_at,
|
||||
forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, show_group_as_sender, 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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|
||||
Plan:
|
||||
|
||||
@@ -5437,7 +5437,7 @@ Query:
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ?
|
||||
Plan:
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?)
|
||||
SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query:
|
||||
@@ -5446,7 +5446,7 @@ Query:
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.contact_id = ?
|
||||
Plan:
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query:
|
||||
@@ -5464,7 +5464,7 @@ Query:
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.group_id = ?
|
||||
Plan:
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?)
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?)
|
||||
SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query:
|
||||
@@ -5482,7 +5482,7 @@ Query:
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.note_folder_id = ?
|
||||
Plan:
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH i USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query:
|
||||
@@ -5798,6 +5798,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id =
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND has_link = 1 ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=? AND has_link=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag=?)
|
||||
@@ -5818,6 +5822,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND has_link = 1 ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=? AND has_link=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?)
|
||||
@@ -5826,6 +5834,10 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_i
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND has_link = 1 ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=? AND has_link=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag=?)
|
||||
@@ -5878,7 +5890,7 @@ SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_
|
||||
|
||||
Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?)
|
||||
SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?)
|
||||
@@ -5902,7 +5914,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? AND item_content_tag != 'chatBanner'
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH chat_items USING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?)
|
||||
SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?)
|
||||
@@ -5914,7 +5926,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?)
|
||||
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?)
|
||||
SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?)
|
||||
@@ -5938,7 +5950,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND item_content_tag != 'chatBanner'
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?)
|
||||
SEARCH chat_items USING INDEX idx_chat_items_groups_has_link_item_ts (user_id=? AND group_id=?)
|
||||
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?)
|
||||
SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?)
|
||||
@@ -5950,7 +5962,7 @@ SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?)
|
||||
|
||||
Query: DELETE FROM chat_items WHERE user_id = ? AND note_folder_id = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=? AND note_folder_id=?)
|
||||
SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?)
|
||||
SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?)
|
||||
SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?)
|
||||
@@ -6241,7 +6253,7 @@ SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?)
|
||||
SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?)
|
||||
SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?)
|
||||
SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_has_link_created_at (user_id=?)
|
||||
SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?)
|
||||
SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
|
||||
SEARCH connections USING COVERING INDEX idx_connections_to_subscribe (user_id=?)
|
||||
@@ -6405,7 +6417,7 @@ Query: SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id
|
||||
Plan:
|
||||
SCAN CONSTANT ROW
|
||||
SCALAR SUBQUERY 1
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
|
||||
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_has_link_created_at (user_id=? AND contact_id=?)
|
||||
|
||||
Query: SELECT EXISTS (SELECT 1 FROM group_members WHERE group_id = ? AND member_id = ?)
|
||||
Plan:
|
||||
|
||||
@@ -452,7 +452,8 @@ CREATE TABLE chat_items(
|
||||
user_mention INTEGER NOT NULL DEFAULT 0,
|
||||
group_scope_tag TEXT,
|
||||
group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE,
|
||||
show_group_as_sender INTEGER NOT NULL DEFAULT 0
|
||||
show_group_as_sender INTEGER NOT NULL DEFAULT 0,
|
||||
has_link INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE sqlite_sequence(name,seq);
|
||||
CREATE TABLE chat_item_messages(
|
||||
@@ -1234,6 +1235,24 @@ CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items
|
||||
msg_content_tag,
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_chat_items_groups_has_link_item_ts ON chat_items(
|
||||
user_id,
|
||||
group_id,
|
||||
has_link,
|
||||
item_ts
|
||||
);
|
||||
CREATE INDEX idx_chat_items_contacts_has_link_created_at ON chat_items(
|
||||
user_id,
|
||||
contact_id,
|
||||
has_link,
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_chat_items_note_folder_has_link_created_at ON chat_items(
|
||||
user_id,
|
||||
note_folder_id,
|
||||
has_link,
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_chat_relays_user_id ON chat_relays(user_id);
|
||||
CREATE UNIQUE INDEX idx_chat_relays_user_id_address ON chat_relays(
|
||||
user_id,
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ relayTestOpts = testOpts {coreOptions = testCoreOpts {chatRelay = True}}
|
||||
|
||||
#if !defined(dbPostgres)
|
||||
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
|
||||
getTestOpts maintenance dbKey = testOpts {coreOptions = testCoreOpts {dbOptions = (dbOptions testCoreOpts) {dbKey}, maintenance}}
|
||||
getTestOpts maintenance dbKey = testOpts {coreOptions = testCoreOpts {maintenance, dbOptions = (dbOptions testCoreOpts) {dbKey}}}
|
||||
#endif
|
||||
|
||||
termSettings :: VirtualTerminalSettings
|
||||
|
||||
@@ -156,6 +156,8 @@ chatDirectTests = do
|
||||
describe "delivery receipts" $ do
|
||||
it "should send delivery receipts" testSendDeliveryReceipts
|
||||
it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts
|
||||
describe "link content filter" $ do
|
||||
it "filter chat by link content" testLinkContentFilter
|
||||
describe "negotiate connection peer chat protocol version range" $ do
|
||||
describe "peer version range correctly set for new connection via invitation" $ do
|
||||
testInvVRange supportedChatVRange supportedChatVRange
|
||||
@@ -3311,3 +3313,43 @@ contactInfoChatVRange cc (VersionRange minVer maxVer) = do
|
||||
cc <## "connection not verified, use /code command to see security code"
|
||||
cc <## "quantum resistant end-to-end encryption"
|
||||
cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")")
|
||||
|
||||
testLinkContentFilter :: HasCallStack => TestParams -> IO ()
|
||||
testLinkContentFilter =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
|
||||
alice ##> "/c"
|
||||
simplexLink <- getInvitation alice
|
||||
|
||||
let linkPreview = "{\"msgContent\": {\"type\": \"link\", \"text\": \"https://simplex.chat\", \"preview\": {\"uri\": \"https://simplex.chat\", \"title\": \"SimpleX Chat\", \"description\": \"SimpleX Chat\", \"image\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
alice ##> ("/_send @2 json [" <> linkPreview <> "]")
|
||||
alice <# "@bob https://simplex.chat"
|
||||
bob <# "alice> https://simplex.chat"
|
||||
|
||||
alice #> "@bob check out https://example.com"
|
||||
bob <# "alice> check out https://example.com"
|
||||
|
||||
bob #> "@alice visit http://test.org"
|
||||
alice <# "bob> visit http://test.org"
|
||||
|
||||
alice #> ("@bob " <> simplexLink)
|
||||
bob <#. "alice> https://simplex.chat/invitation#"
|
||||
|
||||
bob #> "@alice [click here](https://link.example.com)"
|
||||
alice <# "bob> [click here](https://link.example.com)"
|
||||
|
||||
alice #> "@bob visit example.com for info"
|
||||
bob <# "alice> visit example.com for info"
|
||||
|
||||
alice #> "@bob hello, no links here"
|
||||
bob <# "alice> hello, no links here"
|
||||
|
||||
alice ##> "/_get content types @2"
|
||||
alice <## "Chat content types: link, text"
|
||||
alice #$> ("/_get chat @2 content=link count=100", chat, [(1, "https://simplex.chat"), (1, "check out https://example.com"), (0, "visit http://test.org"), (1, simplexLink), (0, "[click here](https://link.example.com)"), (1, "visit example.com for info")])
|
||||
|
||||
bob ##> "/_get content types @2"
|
||||
bob <## "Chat content types: link, text"
|
||||
bob #$> ("/_get chat @2 content=link count=100", chat, [(0, "https://simplex.chat"), (0, "check out https://example.com"), (1, "visit http://test.org"), (0, simplexLink), (1, "[click here](https://link.example.com)"), (0, "visit example.com for info")])
|
||||
|
||||
@@ -100,6 +100,26 @@ runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withX
|
||||
bob <## "Chat content types: file, text"
|
||||
bob #$> ("/_get chat @2 content=file count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")])
|
||||
|
||||
-- Test file with link in text - should appear in both file and link filters
|
||||
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"check https://example.com for docs\"}}]"
|
||||
alice <# "@bob check https://example.com for docs"
|
||||
alice <# "/f @bob ./tests/fixtures/test.pdf"
|
||||
alice <## "use /fc 2 to cancel sending"
|
||||
bob <# "alice> check https://example.com for docs"
|
||||
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
|
||||
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
|
||||
alice <## "completed uploading file 2 (test.pdf) for bob"
|
||||
|
||||
alice ##> "/_get content types @2"
|
||||
alice <## "Chat content types: file, text"
|
||||
alice #$> ("/_get chat @2 content=file count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg"), ((1, "check https://example.com for docs"), Just "./tests/fixtures/test.pdf")])
|
||||
alice #$> ("/_get chat @2 content=link count=100", chatF, [((1, "check https://example.com for docs"), Just "./tests/fixtures/test.pdf")])
|
||||
|
||||
bob ##> "/_get content types @2"
|
||||
bob <## "Chat content types: file, text"
|
||||
bob #$> ("/_get chat @2 content=file count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg"), ((0, "check https://example.com for docs"), Nothing)])
|
||||
bob #$> ("/_get chat @2 content=link count=100", chatF, [((0, "check https://example.com for docs"), Nothing)])
|
||||
|
||||
testSendImage :: HasCallStack => TestParams -> IO ()
|
||||
testSendImage =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
@@ -375,7 +395,7 @@ testGroupSendImage =
|
||||
bob ##> "/_get content types #1"
|
||||
bob <## "Chat content types: image, text"
|
||||
bob #$> ("/_get chat #1 content=image count=100", chatF, [((0, ""), Just "./tests/tmp/test.jpg")])
|
||||
|
||||
|
||||
cath #$> ("/_get chat #1 count=3", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg"), ((0, "received"), Nothing), ((1, "received too"), Nothing)])
|
||||
cath ##> "/_get content types #1"
|
||||
cath <## "Chat content types: image, text"
|
||||
|
||||
@@ -133,6 +133,8 @@ chatGroupTests = do
|
||||
describe "group delivery receipts" $ do
|
||||
it "should send delivery receipts in group" testSendGroupDeliveryReceipts
|
||||
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
|
||||
describe "link content filter" $ do
|
||||
it "filter group chat by link content" testGroupLinkContentFilter
|
||||
describe "direct connections in group are not established based on chat protocol version" $ do
|
||||
it "direct contacts are not created" testNoGroupDirectConns
|
||||
it "members have different local display names in different groups" testNoDirectDifferentLDNs
|
||||
@@ -8691,3 +8693,40 @@ testChannels2RelaysIncognito ps =
|
||||
dan ?#> "#team hey"
|
||||
[bob, cath] *<# ("#team " <> danIncognito <> "> hey")
|
||||
[alice, eve, frank] *<# ("#team " <> danIncognito <> "> hey [>>]")
|
||||
|
||||
testGroupLinkContentFilter :: HasCallStack => TestParams -> IO ()
|
||||
testGroupLinkContentFilter =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
let linkPreview = "{\"msgContent\": {\"type\": \"link\", \"text\": \"https://simplex.chat\", \"preview\": {\"uri\": \"https://simplex.chat\", \"title\": \"SimpleX Chat\", \"description\": \"SimpleX Chat\", \"image\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
alice ##> ("/_send #1 json [" <> linkPreview <> "]")
|
||||
alice <# "#team https://simplex.chat"
|
||||
concurrently_
|
||||
(bob <# "#team alice> https://simplex.chat")
|
||||
(cath <# "#team alice> https://simplex.chat")
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
bob #> "#team check out https://example.com"
|
||||
concurrently_
|
||||
(alice <# "#team bob> check out https://example.com")
|
||||
(cath <# "#team bob> check out https://example.com")
|
||||
|
||||
cath #> "#team hello, no links here"
|
||||
concurrently_
|
||||
(alice <# "#team cath> hello, no links here")
|
||||
(bob <# "#team cath> hello, no links here")
|
||||
|
||||
alice ##> "/_get content types #1"
|
||||
alice <## "Chat content types: link, text"
|
||||
alice #$> ("/_get chat #1 content=link count=100", chat, [(1, "https://simplex.chat"), (0, "check out https://example.com")])
|
||||
|
||||
bob ##> "/_get content types #1"
|
||||
bob <## "Chat content types: link, text"
|
||||
bob #$> ("/_get chat #1 content=link count=100", chat, [(0, "https://simplex.chat"), (1, "check out https://example.com")])
|
||||
|
||||
cath ##> "/_get content types #1"
|
||||
cath <## "Chat content types: link, text"
|
||||
cath #$> ("/_get chat #1 content=link count=100", chat, [(0, "https://simplex.chat"), (0, "check out https://example.com")])
|
||||
|
||||
@@ -26,6 +26,8 @@ chatLocalChatsTests = do
|
||||
describe "batch create messages" $ do
|
||||
it "create multiple messages api" testCreateMulti
|
||||
it "create multiple messages with files" testCreateMultiFiles
|
||||
describe "link content filter" $ do
|
||||
it "filter notes by link content" testLinkContentFilter
|
||||
|
||||
testNotes :: TestParams -> IO ()
|
||||
testNotes ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
@@ -231,3 +233,18 @@ testCreateMultiFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_get chat *1 count=3"
|
||||
r <- chatF <$> getTermLine alice
|
||||
r `shouldBe` [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]
|
||||
|
||||
testLinkContentFilter :: TestParams -> IO ()
|
||||
testLinkContentFilter ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
createCCNoteFolder alice
|
||||
|
||||
let linkPreview = "{\"msgContent\": {\"type\": \"link\", \"text\": \"https://simplex.chat\", \"preview\": {\"uri\": \"https://simplex.chat\", \"title\": \"SimpleX Chat\", \"description\": \"SimpleX Chat\", \"image\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
alice ##> ("/_create *1 json [" <> linkPreview <> "]")
|
||||
alice <# "* https://simplex.chat"
|
||||
|
||||
alice >* "check out https://example.com"
|
||||
alice >* "hello, no links here"
|
||||
|
||||
alice ##> "/_get content types *1"
|
||||
alice <## "Chat content types: link, text"
|
||||
alice #$> ("/_get chat *1 content=link count=100", chat, [(1, "https://simplex.chat"), (1, "check out https://example.com")])
|
||||
|
||||
Reference in New Issue
Block a user