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,21 +78,34 @@ 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()
self.messageIndex = initTable[string, int]()
self.timedoutMessages = initHashSet[string]()
self.isEdited = initTable[string, bool]()
self.userList = newUserListView(status)
self.status = status
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
result.messageIndex = initTable[string, int]()
result.timedoutMessages = initHashSet[string]()
result.isEdited = initTable[string, bool]()
result.userList = newUserListView(status)
result.status = status
result.setup
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
@ -390,13 +403,4 @@ QtObject:
newQVariant(self.userList) newQVariant(self.userList)
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])
@ -512,4 +495,18 @@ QtObject:
for message in messageList.messages: for message in messageList.messages:
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 { delegate: Message {
model: popup.searchResults
delegate: Message { anchors.right: undefined
property var messageItem: ({}) messageId: model.messageId
fromAuthor: model.fromAuthor
function getMessage() { chatId: model.chatId
chatsModel.messageView.setObservedMessageItem(popup.chatId, modelData) userName: model.userName
return chatsModel.messageView.observedMessageItem alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
imageClick: imagePopup.openPopup.bind(imagePopup)
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedBy: model.pinnedBy
pinnedMessage: model.isPinned
activityCenterMessage: true
clickMessage: function (isProfileClick) {
if (isProfileClick) {
const pk = model.fromAuthor
const userProfileImage = appMain.getProfileImage(pk)
return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
} }
Component.onCompleted: { popup.close()
messageItem = getMessage()
}
anchors.right: undefined positionAtMessage(model.messageId)
messageId: messageItem.messageId
fromAuthor: messageItem.fromAuthor
chatId: messageItem.chatId
userName: messageItem.userName
alias: messageItem.alias
localName: messageItem.localName
message: messageItem.message
plainText: messageItem.plainText
identicon: messageItem.identicon
isCurrentUser: messageItem.isCurrentUser
timestamp: messageItem.timestamp
sticker: messageItem.sticker
contentType: messageItem.contentType
outgoingStatus: messageItem.outgoingStatus
responseTo: messageItem.responseTo
imageClick: imagePopup.openPopup.bind(imagePopup)
linkUrls: messageItem.linkUrls
communityId: messageItem.communityId
hasMention: messageItem.hasMention
stickerPackId: messageItem.stickerPackId
pinnedBy: messageItem.pinnedBy
pinnedMessage: messageItem.isPinned
activityCenterMessage: true
clickMessage: function (isProfileClick) {
if (isProfileClick) {
const pk = messageItem.fromAuthor
const userProfileImage = appMain.getProfileImage(pk)
return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
}
popup.close()
positionAtMessage(messageItem.messageId)
}
prevMessageIndex: -1
prevMsgTimestamp: ""
} }
prevMessageIndex: -1
prevMsgTimestamp: ""
} }
} }
} }
} }