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]
userList: UserListView
proc delete(self: ChatMessageList) =
proc delete*(self: ChatMessageList) =
self.messages = @[]
self.isEdited = initTable[string, bool]()
self.messageIndex = initTable[string, int]()
self.timedoutMessages = initHashSet[string]()
self.QAbstractListModel.delete
proc setup(self: ChatMessageList) =
self.QAbstractListModel.setup
proc countChanged*(self: ChatMessageList) {.signal.}
proc fetchMoreMessagesButton(self: ChatMessageList): Message =
result = Message()
result.contentType = ContentType.FetchMoreMessagesButton;
@ -83,20 +78,33 @@ QtObject:
self.messages.add(self.fetchMoreMessagesButton())
self.messages.add(self.chatIdentifier(self.id))
proc newChatMessageList*(chatId: string, status: Status, addFakeMessages: bool = true): ChatMessageList =
new(result, delete)
result.messages = @[]
result.id = chatId
proc setup*(self: ChatMessageList, chatId: string, status: Status, addFakeMessages: bool) =
self.messages = @[]
self.id = chatId
if addFakeMessages:
result.addFakeMessages()
self.addFakeMessages()
result.messageIndex = initTable[string, int]()
result.timedoutMessages = initHashSet[string]()
result.isEdited = initTable[string, bool]()
result.userList = newUserListView(status)
result.status = status
result.setup
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
proc hasMessage*(self: ChatMessageList, messageId: string): bool =
return self.messageIndex.hasKey(messageId)
@ -157,13 +165,6 @@ QtObject:
method rowCount(self: ChatMessageList, index: QModelIndex = nil): int =
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 =
if not self.messageReactions.hasKey(messageId): return ""
result = self.messageReactions[messageId]
@ -303,18 +304,28 @@ QtObject:
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.userList.add(message)
self.countChanged()
self.endInsertRows()
self.countChanged()
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:
if self.messageIndex.hasKey(message.id): continue
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.userList.add(message)
self.countChanged()
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 =
if (not self.messageIndex.hasKey(messageId)): return
@ -323,9 +334,11 @@ QtObject:
proc clear*(self: ChatMessageList, addFakeMessages: bool = true) =
self.beginResetModel()
self.messages = @[]
self.messageIndex.clear
if (addFakeMessages):
self.addFakeMessages()
self.endResetModel()
self.countChanged()
proc setMessageReactions*(self: ChatMessageList, messageId: string, newReactions: string)=
self.messageReactions[messageId] = newReactions
@ -391,12 +404,3 @@ QtObject:
QtProperty[QVariant] userList:
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/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
import ../../../status/libstatus/chat as libstatus_chat
@ -68,10 +68,10 @@ QtObject:
type MessageView* = ref object of QAbstractListModel
status: Status
messageList*: OrderedTable[string, ChatMessageList]
searchResultMessageModel*: MessageListProxyModel
pinnedMessagesList*: OrderedTable[string, ChatMessageList]
channelView*: ChannelView
communities*: CommunitiesView
observedMessageItem*: MessageItem
pubKey*: string
loadingMessages*: bool
unreadMessageCnt: int
@ -88,7 +88,7 @@ QtObject:
self.pinnedMessagesList = initOrderedTable[string, ChatMessageList]()
self.channelOpenTime = initTable[string, int64]()
self.QAbstractListModel.delete
self.observedMessageItem.delete
self.searchResultMessageModel.delete
proc newMessageView*(status: Status, channelView: ChannelView, communitiesView: CommunitiesView): MessageView =
new(result, delete)
@ -100,7 +100,7 @@ QtObject:
result.messageList[status_utils.getTimelineChatId()] = newChatMessageList(status_utils.getTimelineChatId(), result.status, false)
result.loadingMessages = false
result.unreadMessageCnt = 0
result.observedMessageItem = newMessageItem(status)
result.searchResultMessageModel = newMessageListProxyModel(status)
result.unreadDirectMessagesAndMentionsCount = 0
result.setup
@ -296,23 +296,6 @@ QtObject:
read = getMessageList
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.} =
self.upsertChannel(self.channelView.activeChannel.id)
return newQVariant(self.pinnedMessagesList[self.channelView.activeChannel.id])
@ -513,3 +496,17 @@ QtObject:
if (message.id == messageId):
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"
Popup {
property var searchResults
property string chatId: chatsModel.channelView.activeChannel.id
id: popup
@ -24,7 +23,8 @@ Popup {
const noResultHeight = 122
let minHeight = 560
const maxHeight = parent.height - 200
if (!searchResults || !searchResults.length || !searchResultContent.visible) {
if (!searchResultContent.visible) {
return noResultHeight
}
@ -32,10 +32,10 @@ Popup {
return maxHeight
}
if (messageColumn.height < minHeight - noResultHeight) {
if (listView.height < minHeight - noResultHeight) {
return minHeight
}
if (messageColumn.height > maxHeight - noResultHeight) {
if (listView.height > maxHeight - noResultHeight) {
return maxHeight
}
}
@ -75,19 +75,7 @@ Popup {
}
property var searchMessages: Backpressure.debounce(searchInput, 400, function (value) {
if (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)
}
chatsModel.messageView.searchMessages(value)
})
StyledTextField {
@ -105,7 +93,9 @@ Popup {
background: Rectangle {
color: Style.current.transparent
}
onTextChanged: Qt.callLater(searchHeader.searchMessages, searchInput.text)
onTextChanged: {
searchHeader.searchMessages(searchInput.text)
}
}
Separator {
@ -150,7 +140,7 @@ Popup {
Item {
id: searchResultContent
visible: !!popup.searchResults && popup.searchResults.length > 0
visible: chatsModel.messageView.searchResultMessageModel.count > 0
width: parent.width
anchors.bottom: parent.bottom
anchors.top: channelBadge.bottom
@ -183,66 +173,53 @@ Popup {
width: parent.width
clip: true
Column {
id: messageColumn
width: parent.width
spacing: 0
Repeater {
model: popup.searchResults
ListView{
id: listView
model: chatsModel.messageView.searchResultMessageModel
delegate: Message {
property var messageItem: ({})
function getMessage() {
chatsModel.messageView.setObservedMessageItem(popup.chatId, modelData)
return chatsModel.messageView.observedMessageItem
}
Component.onCompleted: {
messageItem = getMessage()
}
anchors.right: undefined
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
messageId: model.messageId
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
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: messageItem.linkUrls
communityId: messageItem.communityId
hasMention: messageItem.hasMention
stickerPackId: messageItem.stickerPackId
pinnedBy: messageItem.pinnedBy
pinnedMessage: messageItem.isPinned
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 = messageItem.fromAuthor
const pk = model.fromAuthor
const userProfileImage = appMain.getProfileImage(pk)
return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
}
popup.close()
positionAtMessage(messageItem.messageId)
positionAtMessage(model.messageId)
}
prevMessageIndex: -1
prevMsgTimestamp: ""
}
}
}
}
}