feature(@desktop/chat): implement search on sqlcipher (status-go side)

Current code adapted to handle future changes on message search (like searching message in multiple
channels).

Memory leak which was happening in qml assigning (copying) MessageItem to qml variable messageItem
(where that qml variable messageItem was never deleted) is fixed.

Fixes: #2912
This commit is contained in:
Sale Djenic 2021-07-20 20:26:26 +02:00 committed by Iuri Matias
parent 5fc85af04b
commit 1573d7b928
4 changed files with 152 additions and 132 deletions

View File

@ -58,18 +58,13 @@ QtObject:
timedoutMessages: HashSet[string] timedoutMessages: HashSet[string]
userList: UserListView userList: UserListView
proc delete(self: ChatMessageList) = proc delete*(self: ChatMessageList) =
self.messages = @[] self.messages = @[]
self.isEdited = initTable[string, bool]() self.isEdited = initTable[string, bool]()
self.messageIndex = initTable[string, int]() self.messageIndex = initTable[string, int]()
self.timedoutMessages = initHashSet[string]() self.timedoutMessages = initHashSet[string]()
self.QAbstractListModel.delete self.QAbstractListModel.delete
proc setup(self: ChatMessageList) =
self.QAbstractListModel.setup
proc countChanged*(self: ChatMessageList) {.signal.}
proc fetchMoreMessagesButton(self: ChatMessageList): Message = proc fetchMoreMessagesButton(self: ChatMessageList): Message =
result = Message() result = Message()
result.contentType = ContentType.FetchMoreMessagesButton; result.contentType = ContentType.FetchMoreMessagesButton;
@ -83,20 +78,33 @@ QtObject:
self.messages.add(self.fetchMoreMessagesButton()) self.messages.add(self.fetchMoreMessagesButton())
self.messages.add(self.chatIdentifier(self.id)) self.messages.add(self.chatIdentifier(self.id))
proc newChatMessageList*(chatId: string, status: Status, addFakeMessages: bool = true): ChatMessageList = proc setup*(self: ChatMessageList, chatId: string, status: Status, addFakeMessages: bool) =
new(result, delete) self.messages = @[]
result.messages = @[] self.id = chatId
result.id = chatId
if addFakeMessages: if addFakeMessages:
result.addFakeMessages() self.addFakeMessages()
result.messageIndex = initTable[string, int]() self.messageIndex = initTable[string, int]()
result.timedoutMessages = initHashSet[string]() self.timedoutMessages = initHashSet[string]()
result.isEdited = initTable[string, bool]() self.isEdited = initTable[string, bool]()
result.userList = newUserListView(status) self.userList = newUserListView(status)
result.status = status self.status = status
result.setup
self.QAbstractListModel.setup
proc newChatMessageList*(chatId: string, status: Status, addFakeMessages: bool): ChatMessageList =
new(result, delete)
result.setup(chatId, status, addFakeMessages)
proc countChanged*(self: ChatMessageList) {.signal.}
proc count*(self: ChatMessageList): int {.slot.} =
self.messages.len
QtProperty[int] count:
read = count
notify = countChanged
proc hasMessage*(self: ChatMessageList, messageId: string): bool = proc hasMessage*(self: ChatMessageList, messageId: string): bool =
return self.messageIndex.hasKey(messageId) return self.messageIndex.hasKey(messageId)
@ -157,13 +165,6 @@ QtObject:
method rowCount(self: ChatMessageList, index: QModelIndex = nil): int = method rowCount(self: ChatMessageList, index: QModelIndex = nil): int =
return self.messages.len return self.messages.len
proc count*(self: ChatMessageList): int {.slot.} =
self.messages.len
QtProperty[int] count:
read = count
notify = countChanged
proc getReactions*(self:ChatMessageList, messageId: string):string = proc getReactions*(self:ChatMessageList, messageId: string):string =
if not self.messageReactions.hasKey(messageId): return "" if not self.messageReactions.hasKey(messageId): return ""
result = self.messageReactions[messageId] result = self.messageReactions[messageId]
@ -303,18 +304,28 @@ QtObject:
self.messageIndex[message.id] = self.messages.len self.messageIndex[message.id] = self.messages.len
self.messages.add(message) self.messages.add(message)
self.userList.add(message) self.userList.add(message)
self.countChanged()
self.endInsertRows() self.endInsertRows()
self.countChanged()
proc add*(self: ChatMessageList, messages: seq[Message]) = proc add*(self: ChatMessageList, messages: seq[Message]) =
self.beginInsertRows(newQModelIndex(), self.messages.len, self.messages.len) self.beginInsertRows(newQModelIndex(), self.messages.len, self.messages.len + messages.len - 1)
for message in messages: for message in messages:
if self.messageIndex.hasKey(message.id): continue if self.messageIndex.hasKey(message.id): continue
self.messageIndex[message.id] = self.messages.len self.messageIndex[message.id] = self.messages.len
self.messages.add(message) self.messages.add(message)
self.userList.add(message) self.userList.add(message)
self.countChanged()
self.endInsertRows() self.endInsertRows()
self.countChanged()
proc remove*(self: ChatMessageList, messageId: string) =
if not self.messageIndex.hasKey(messageId): return
let index = self.getMessageIndex(messageId)
self.beginRemoveRows(newQModelIndex(), index, index)
self.messages.delete(index)
self.messageIndex.del(messageId)
self.endRemoveRows()
self.countChanged()
proc getMessageById*(self: ChatMessageList, messageId: string): Message = proc getMessageById*(self: ChatMessageList, messageId: string): Message =
if (not self.messageIndex.hasKey(messageId)): return if (not self.messageIndex.hasKey(messageId)): return
@ -323,9 +334,11 @@ QtObject:
proc clear*(self: ChatMessageList, addFakeMessages: bool = true) = proc clear*(self: ChatMessageList, addFakeMessages: bool = true) =
self.beginResetModel() self.beginResetModel()
self.messages = @[] self.messages = @[]
self.messageIndex.clear
if (addFakeMessages): if (addFakeMessages):
self.addFakeMessages() self.addFakeMessages()
self.endResetModel() self.endResetModel()
self.countChanged()
proc setMessageReactions*(self: ChatMessageList, messageId: string, newReactions: string)= proc setMessageReactions*(self: ChatMessageList, messageId: string, newReactions: string)=
self.messageReactions[messageId] = newReactions self.messageReactions[messageId] = newReactions
@ -391,12 +404,3 @@ QtObject:
QtProperty[QVariant] userList: QtProperty[QVariant] userList:
read = getUserList read = getUserList
proc messageSearch*(self: ChatMessageList, searchTerm: string): string {.slot.} =
let lowercaseTerm = searchTerm.toLowerAscii
var messageIds: seq[string] = @[]
for message in self.messages:
if message.text.toLowerAscii.contains(lowercaseTerm):
# TODO try to send a Variant
messageIds.add(message.id)
return messageIds.toJson

View File

@ -0,0 +1,42 @@
import NimQml, strutils
import message_list
import ../../../status/[status]
import ../../../status/chat/[message]
QtObject:
type
MessageListProxyModel* = ref object of ChatMessageList
sourceMessages: seq[Message]
proc delete(self: MessageListProxyModel) =
self.ChatMessageList.delete
proc setup(self: MessageListProxyModel, status: Status) =
self.ChatMessageList.setup("", status, false)
proc newMessageListProxyModel*(status: Status): MessageListProxyModel =
new(result, delete)
result.setup(status)
proc setSourceMessages*(self: MessageListProxyModel, messages: seq[Message]) =
self.sourceMessages = messages
proc setFilter*(self: MessageListProxyModel, filter: string, caseSensitive: bool) =
self.clear(false)
if (filter.len == 0):
return
let pattern = if(caseSensitive): filter else: filter.toLowerAscii
var matchedMessages: seq[Message] = @[]
for message in self.sourceMessages:
if (caseSensitive and message.text.contains(pattern) or
not caseSensitive and message.text.toLowerAscii.contains(pattern)):
matchedMessages.add(message)
if (matchedMessages.len == 0):
return
self.add(matchedMessages)

View File

@ -11,7 +11,7 @@ import ../../../status/profile/profile
import ../../../status/tasks/[qt, task_runner_impl] import ../../../status/tasks/[qt, task_runner_impl]
import ../../../status/tasks/marathon/mailserver/worker import ../../../status/tasks/marathon/mailserver/worker
import communities, chat_item, channels_list, communities, community_list, message_list, channel, message_item import communities, chat_item, channels_list, communities, community_list, message_list, channel, message_item, message_list_proxy
# TODO: remove me # TODO: remove me
import ../../../status/libstatus/chat as libstatus_chat import ../../../status/libstatus/chat as libstatus_chat
@ -68,10 +68,10 @@ QtObject:
type MessageView* = ref object of QAbstractListModel type MessageView* = ref object of QAbstractListModel
status: Status status: Status
messageList*: OrderedTable[string, ChatMessageList] messageList*: OrderedTable[string, ChatMessageList]
searchResultMessageModel*: MessageListProxyModel
pinnedMessagesList*: OrderedTable[string, ChatMessageList] pinnedMessagesList*: OrderedTable[string, ChatMessageList]
channelView*: ChannelView channelView*: ChannelView
communities*: CommunitiesView communities*: CommunitiesView
observedMessageItem*: MessageItem
pubKey*: string pubKey*: string
loadingMessages*: bool loadingMessages*: bool
unreadMessageCnt: int unreadMessageCnt: int
@ -88,7 +88,7 @@ QtObject:
self.pinnedMessagesList = initOrderedTable[string, ChatMessageList]() self.pinnedMessagesList = initOrderedTable[string, ChatMessageList]()
self.channelOpenTime = initTable[string, int64]() self.channelOpenTime = initTable[string, int64]()
self.QAbstractListModel.delete self.QAbstractListModel.delete
self.observedMessageItem.delete self.searchResultMessageModel.delete
proc newMessageView*(status: Status, channelView: ChannelView, communitiesView: CommunitiesView): MessageView = proc newMessageView*(status: Status, channelView: ChannelView, communitiesView: CommunitiesView): MessageView =
new(result, delete) new(result, delete)
@ -100,7 +100,7 @@ QtObject:
result.messageList[status_utils.getTimelineChatId()] = newChatMessageList(status_utils.getTimelineChatId(), result.status, false) result.messageList[status_utils.getTimelineChatId()] = newChatMessageList(status_utils.getTimelineChatId(), result.status, false)
result.loadingMessages = false result.loadingMessages = false
result.unreadMessageCnt = 0 result.unreadMessageCnt = 0
result.observedMessageItem = newMessageItem(status) result.searchResultMessageModel = newMessageListProxyModel(status)
result.unreadDirectMessagesAndMentionsCount = 0 result.unreadDirectMessagesAndMentionsCount = 0
result.setup result.setup
@ -296,23 +296,6 @@ QtObject:
read = getMessageList read = getMessageList
notify = activeChannelChanged notify = activeChannelChanged
proc observedMessageItemChanged*(self: MessageView) {.signal.}
proc setObservedMessageItem*(self: MessageView, chatId: string, messageId: string) {.slot.} =
if(messageId == ""): return
if (not self.messageList.hasKey(chatId)): return
let message = self.messageList[chatId].getMessageById(messageId)
if (message.id == ""):
return
self.observedMessageItem.setMessageItem(message)
self.observedMessageItemChanged()
proc getObservedMessageItem*(self: MessageView): QVariant {.slot.} = newQVariant(self.observedMessageItem)
QtProperty[QVariant] observedMessageItem:
read = getObservedMessageItem
write = setObservedMessageItem
notify = observedMessageItemChanged
proc getPinnedMessagesList(self: MessageView): QVariant {.slot.} = proc getPinnedMessagesList(self: MessageView): QVariant {.slot.} =
self.upsertChannel(self.channelView.activeChannel.id) self.upsertChannel(self.channelView.activeChannel.id)
return newQVariant(self.pinnedMessagesList[self.channelView.activeChannel.id]) return newQVariant(self.pinnedMessagesList[self.channelView.activeChannel.id])
@ -513,3 +496,17 @@ QtObject:
if (message.id == messageId): if (message.id == messageId):
return chatId return chatId
proc getSearchResultMessageModel*(self: MessageView): QVariant {.slot.} =
newQVariant(self.searchResultMessageModel)
# we just need to expose model to qml, there is no need for exposing notifying signal
QtProperty[QVariant] searchResultMessageModel:
read = getSearchResultMessageModel
proc searchMessages*(self: MessageView, searchTerm: string) {.slot.} =
# channelId is used here only to support message search in currently selected channel
# later when we decide to apply message search over multiple channels MessageListProxyModel
# will be updated to support setting list of sourcer messages.
let channelId = self.channelView.activeChannel.id
self.searchResultMessageModel.setSourceMessages(self.messageList[channelId].messages)
self.searchResultMessageModel.setFilter(searchTerm, false)

View File

@ -6,7 +6,6 @@ import "../../../../shared/status"
import "../ChatColumn" import "../ChatColumn"
Popup { Popup {
property var searchResults
property string chatId: chatsModel.channelView.activeChannel.id property string chatId: chatsModel.channelView.activeChannel.id
id: popup id: popup
@ -24,7 +23,8 @@ Popup {
const noResultHeight = 122 const noResultHeight = 122
let minHeight = 560 let minHeight = 560
const maxHeight = parent.height - 200 const maxHeight = parent.height - 200
if (!searchResults || !searchResults.length || !searchResultContent.visible) {
if (!searchResultContent.visible) {
return noResultHeight return noResultHeight
} }
@ -32,10 +32,10 @@ Popup {
return maxHeight return maxHeight
} }
if (messageColumn.height < minHeight - noResultHeight) { if (listView.height < minHeight - noResultHeight) {
return minHeight return minHeight
} }
if (messageColumn.height > maxHeight - noResultHeight) { if (listView.height > maxHeight - noResultHeight) {
return maxHeight return maxHeight
} }
} }
@ -75,19 +75,7 @@ Popup {
} }
property var searchMessages: Backpressure.debounce(searchInput, 400, function (value) { property var searchMessages: Backpressure.debounce(searchInput, 400, function (value) {
if (value === "") { chatsModel.messageView.searchMessages(value)
searchResultContent.visible = false
return
}
// TODO add loading?
const messageIdsStr = chatsModel.messageView.messageList.messageSearch(value)
try {
searchResultContent.visible = true
searchResults = JSON.parse(messageIdsStr)
} catch (e) {
console.error ("Error parsing search result", e)
}
}) })
StyledTextField { StyledTextField {
@ -105,7 +93,9 @@ Popup {
background: Rectangle { background: Rectangle {
color: Style.current.transparent color: Style.current.transparent
} }
onTextChanged: Qt.callLater(searchHeader.searchMessages, searchInput.text) onTextChanged: {
searchHeader.searchMessages(searchInput.text)
}
} }
Separator { Separator {
@ -150,7 +140,7 @@ Popup {
Item { Item {
id: searchResultContent id: searchResultContent
visible: !!popup.searchResults && popup.searchResults.length > 0 visible: chatsModel.messageView.searchResultMessageModel.count > 0
width: parent.width width: parent.width
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.top: channelBadge.bottom anchors.top: channelBadge.bottom
@ -183,66 +173,53 @@ Popup {
width: parent.width width: parent.width
clip: true clip: true
Column { ListView{
id: messageColumn id: listView
width: parent.width model: chatsModel.messageView.searchResultMessageModel
spacing: 0
Repeater {
model: popup.searchResults
delegate: Message { delegate: Message {
property var messageItem: ({})
function getMessage() {
chatsModel.messageView.setObservedMessageItem(popup.chatId, modelData)
return chatsModel.messageView.observedMessageItem
}
Component.onCompleted: {
messageItem = getMessage()
}
anchors.right: undefined anchors.right: undefined
messageId: messageItem.messageId messageId: model.messageId
fromAuthor: messageItem.fromAuthor fromAuthor: model.fromAuthor
chatId: messageItem.chatId chatId: model.chatId
userName: messageItem.userName userName: model.userName
alias: messageItem.alias alias: model.alias
localName: messageItem.localName localName: model.localName
message: messageItem.message message: model.message
plainText: messageItem.plainText plainText: model.plainText
identicon: messageItem.identicon identicon: model.identicon
isCurrentUser: messageItem.isCurrentUser isCurrentUser: model.isCurrentUser
timestamp: messageItem.timestamp timestamp: model.timestamp
sticker: messageItem.sticker sticker: model.sticker
contentType: messageItem.contentType contentType: model.contentType
outgoingStatus: messageItem.outgoingStatus outgoingStatus: model.outgoingStatus
responseTo: messageItem.responseTo responseTo: model.responseTo
imageClick: imagePopup.openPopup.bind(imagePopup) imageClick: imagePopup.openPopup.bind(imagePopup)
linkUrls: messageItem.linkUrls linkUrls: model.linkUrls
communityId: messageItem.communityId communityId: model.communityId
hasMention: messageItem.hasMention hasMention: model.hasMention
stickerPackId: messageItem.stickerPackId stickerPackId: model.stickerPackId
pinnedBy: messageItem.pinnedBy pinnedBy: model.pinnedBy
pinnedMessage: messageItem.isPinned pinnedMessage: model.isPinned
activityCenterMessage: true activityCenterMessage: true
clickMessage: function (isProfileClick) { clickMessage: function (isProfileClick) {
if (isProfileClick) { if (isProfileClick) {
const pk = messageItem.fromAuthor const pk = model.fromAuthor
const userProfileImage = appMain.getProfileImage(pk) const userProfileImage = appMain.getProfileImage(pk)
return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk)) return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
} }
popup.close() popup.close()
positionAtMessage(messageItem.messageId) positionAtMessage(model.messageId)
} }
prevMessageIndex: -1 prevMessageIndex: -1
prevMsgTimestamp: "" prevMsgTimestamp: ""
} }
}
} }
} }
} }