feat: add basic local chat search

Fixes #2771
This commit is contained in:
Jonathan Rainville 2021-07-07 11:58:59 -04:00 committed by Iuri Matias
parent 47d1546893
commit 6e218ad924
11 changed files with 501 additions and 187 deletions

View File

@ -9,7 +9,7 @@ import ../../status/ens as status_ens
import ../../status/chat/[chat, message]
import ../../status/profile/profile
import web3/[conversions, ethtypes]
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages]
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item]
import ../utils/image_utils
import ../../status/tasks/[qt, task_runner_impl]
import ../../status/tasks/marathon/mailserver/worker

View File

@ -41,6 +41,11 @@ QtObject:
QtProperty[string] id:
read = id
proc communityId*(self: ChatItemView): string {.slot.} = result = ?.self.chatItem.communityId
QtProperty[string] communityId:
read = communityId
proc description*(self: ChatItemView): string {.slot.} = result = ?.self.chatItem.description
QtProperty[string] description:

View File

@ -15,6 +15,11 @@ QtObject:
proc delete*(self: MessageItem) =
self.QObject.delete
proc newMessageItem*(status: Status): MessageItem =
new(result, delete)
result.status = status
result.setup
proc newMessageItem*(status: Status, message: Message): MessageItem =
new(result, delete)
result.messageItem = message

View File

@ -391,4 +391,13 @@ QtObject:
newQVariant(self.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

@ -10,7 +10,7 @@ import ../../../status/chat/[chat, message]
import ../../../status/profile/profile
import ../../../status/tasks/[qt, task_runner_impl]
import communities, chat_item, channels_list, communities, community_list, message_list, channel
import communities, chat_item, channels_list, communities, community_list, message_list, channel, message_item
# TODO: remove me
import ../../../status/libstatus/chat as libstatus_chat
@ -70,6 +70,7 @@ QtObject:
pinnedMessagesList*: OrderedTable[string, ChatMessageList]
channelView*: ChannelView
communities*: CommunitiesView
observedMessageItem*: MessageItem
pubKey*: string
loadingMessages*: bool
unreadMessageCnt: int
@ -84,8 +85,8 @@ QtObject:
self.messageList = initOrderedTable[string, ChatMessageList]()
self.pinnedMessagesList = initOrderedTable[string, ChatMessageList]()
self.channelOpenTime = initTable[string, int64]()
# self.QObject.delete
self.QAbstractListModel.delete
self.observedMessageItem.delete
proc newMessageView*(status: Status, channelView: ChannelView, communitiesView: CommunitiesView): MessageView =
new(result, delete)
@ -97,6 +98,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.setup
# proc getMessageListIndexById(self: MessageView, id: string): int
@ -284,6 +286,23 @@ 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])

View File

@ -287,7 +287,6 @@ StackLayout {
}
}
searchButton.visible: false
membersButton.visible: appSettings.showOnlineUsers && chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
notificationButton.visible: appSettings.isActivityCenterEnabled
notificationCount: chatsModel.activityNotificationList.unreadCount

View File

@ -4,6 +4,7 @@ import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
import ".."
import "../../components"
Rectangle {
property string chatId: ""
@ -34,11 +35,13 @@ Rectangle {
Loader {
active: true
height: parent.height
anchors.left: parent.left
anchors.leftMargin: 4
sourceComponent: {
switch (model.notificationType) {
case Constants.activityCenterNotificationTypeMention: return wrapper.communityId ? communityComponent : channelComponent
case Constants.activityCenterNotificationTypeMention: return communityOrChannelContentComponent
case Constants.activityCenterNotificationTypeReply: return replyComponent
default: return channelComponent
default: return communityOrChannelContentComponent
}
}
}
@ -63,7 +66,6 @@ Rectangle {
height: 16
source: "../../../../img/reply-small-arrow.svg"
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter:parent.verticalCenter
}
@ -86,185 +88,13 @@ Rectangle {
}
Component {
id: communityComponent
id: communityOrChannelContentComponent
Item {
property int communityIndex: chatsModel.communities.joinedCommunities.getCommunityIndex(wrapper.communityId)
property string image: communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "thumbnailImage") : ""
property string iconColor: !image && communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "communityColor"): ""
property bool useLetterIdenticon: !image
property string communityName: communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "name") : ""
property string channelName: chatsModel.getChannelNameById(wrapper.chatId)
id: communityBadge
width: childrenRect.width
height: parent.height
SVGImage {
id: communityIcon
width: 16
height: 16
source: "../../../../img/communities.svg"
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter:parent.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.secondaryText
}
}
Loader {
id: communityImageLoader
active: true
anchors.left: communityIcon.right
anchors.leftMargin: 2
anchors.verticalCenter: parent.verticalCenter
sourceComponent: communityBadge.useLetterIdenticon ? letterIdenticon :imageIcon
}
Component {
id: imageIcon
RoundedImage {
source: communityBadge.image
noMouseArea: true
noHover: true
width: 16
height: 16
}
}
Component {
id: letterIdenticon
StatusLetterIdenticon {
width: 16
height: 16
letterSize: 12
chatName: communityBadge.communityName
color: communityBadge.iconColor
}
}
function getLinkStyle(link, hoveredLink) {
return `<style type="text/css">` +
`a {` +
`color: ${Style.current.secondaryText};` +
`text-decoration: none;` +
`}` +
(hoveredLink !== "" ? `a[href="${hoveredLink}"] { text-decoration: underline; }` : "") +
`</style>` +
`<a href="${link}">${link}</a>`
}
StyledTextEdit {
id: communityName
text: communityBadge.getLinkStyle(communityBadge.communityName, hoveredLink)
height: 18
readOnly: true
textFormat: Text.RichText
width: implicitWidth > 300 ? 300 : implicitWidth
clip: true
anchors.left: communityImageLoader.right
anchors.leftMargin: 4
color: Style.current.secondaryText
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
onLinkActivated: function () {
chatsModel.communities.setActiveCommunity(wrapper.communityId)
}
}
SVGImage {
id: caretImage
source: "../../../../img/show-category.svg"
width: 16
height: 16
anchors.left: communityName.right
anchors.verticalCenter: parent.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.secondaryText
}
}
StyledTextEdit {
id: channelName
text: communityBadge.getLinkStyle(communityBadge.channelName || wrapper.name, hoveredLink)
height: 18
readOnly: true
textFormat: Text.RichText
width: implicitWidth > 300 ? 300 : implicitWidth
clip: true
anchors.left: caretImage.right
color: Style.current.secondaryText
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
onLinkActivated: function () {
chatsModel.communities.setActiveCommunity(wrapper.communityId)
chatsModel.setActiveChannel(model.message.chatId)
}
}
}
}
Component {
id: channelComponent
Item {
width: childrenRect.width
height: parent.height
Connections {
enabled: realChatType === Constants.chatTypeOneToOne
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === wrapper.chatId) {
wrapper.profileImage = appMain.getProfileImage(wrapper.chatId)
}
}
}
SVGImage {
id: channelIcon
width: 16
height: 16
fillMode: Image.PreserveAspectFit
source: "../../../../img/channel-icon-" + (wrapper.realChatType === Constants.chatTypePublic ? "public-chat.svg" : "group.svg")
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter:parent.verticalCenter
}
StatusIdenticon {
id: contactImage
height: 16
width: 16
chatId: wrapper.chatId
chatName: wrapper.name
chatType: wrapper.realChatType
identicon: wrapper.profileImage || wrapper.identicon
anchors.left: channelIcon.right
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
letterSize: 11
}
StyledText {
id: contactInfo
text: wrapper.realChatType !== Constants.chatTypePublic ?
Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(wrapper.name))) :
"#" + Utils.filterXSS(wrapper.name)
anchors.left: contactImage.right
anchors.leftMargin: 4
color: Style.current.secondaryText
font.weight: Font.Medium
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
}
BadgeContent {
chatId: wrapper.chatId
name: wrapper.name
identicon: wrapper.identicon
communityId: wrapper.communityId
}
}
}

View File

@ -17,7 +17,7 @@ Item {
property bool isCurrentUser: false
property string timestamp: "1234567"
property string sticker: "Qme8vJtyrEHxABcSVGPF95PtozDgUyfr1xGjePmFdZgk9v"
property int contentType: 2 // constants don't work in default props
property int contentType: 1 // constants don't work in default props
property string chatId: "chatId"
property string outgoingStatus: ""
property string responseTo: ""

View File

@ -0,0 +1,214 @@
import QtQuick 2.13
import QtGraphicalEffects 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
Item {
property color textColor: Style.current.textColor
property string chatId: ""
property string name: "channelName"
property string identicon
property string communityId
property int chatType: chatsModel.channelView.chats.getChannelType(chatId)
property int realChatType: {
if (chatType === Constants.chatTypeCommunity) {
// TODO add a check for private community chats once it is created
return Constants.chatTypePubliccommunityComponent
}
return chatType
}
property string profileImage: realChatType === Constants.chatTypeOneToOne ? appMain.getProfileImage(chatId) || "" : ""
id: wrapper
height: 24
width: childrenRect.width
Loader {
active: true
height: parent.height
sourceComponent: wrapper.communityId ? communityComponent : channelComponent
}
Component {
id: communityComponent
Item {
property int communityIndex: chatsModel.communities.joinedCommunities.getCommunityIndex(wrapper.communityId)
property string image: communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "thumbnailImage") : ""
property string iconColor: !image && communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "communityColor"): ""
property bool useLetterIdenticon: !image
property string communityName: communityIndex > -1 ? chatsModel.communities.joinedCommunities.rowData(communityIndex, "name") : ""
property string channelName: chatsModel.getChannelNameById(wrapper.chatId)
id: communityBadge
width: childrenRect.width
height: parent.height
SVGImage {
id: communityIcon
width: 16
height: 16
source: "../../../img/communities.svg"
anchors.left: parent.left
anchors.verticalCenter:parent.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: wrapper.textColor
}
}
Loader {
id: communityImageLoader
active: true
anchors.left: communityIcon.right
anchors.leftMargin: 2
anchors.verticalCenter: parent.verticalCenter
sourceComponent: communityBadge.useLetterIdenticon ? letterIdenticon :imageIcon
}
Component {
id: imageIcon
RoundedImage {
source: communityBadge.image
noMouseArea: true
noHover: true
width: 16
height: 16
}
}
Component {
id: letterIdenticon
StatusLetterIdenticon {
width: 16
height: 16
letterSize: 12
chatName: communityBadge.communityName
color: communityBadge.iconColor
}
}
function getLinkStyle(link, hoveredLink) {
return `<style type="text/css">` +
`a {` +
`color: ${wrapper.textColor};` +
`text-decoration: none;` +
`}` +
(hoveredLink !== "" ? `a[href="${hoveredLink}"] { text-decoration: underline; }` : "") +
`</style>` +
`<a href="${link}">${link}</a>`
}
StyledTextEdit {
id: communityName
text: communityBadge.getLinkStyle(communityBadge.communityName, hoveredLink)
height: 18
readOnly: true
textFormat: Text.RichText
width: implicitWidth > 300 ? 300 : implicitWidth
clip: true
anchors.left: communityImageLoader.right
anchors.leftMargin: 4
color: wrapper.textColor
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
onLinkActivated: function () {
chatsModel.communities.setActiveCommunity(wrapper.communityId)
}
}
SVGImage {
id: caretImage
source: "../../../img/show-category.svg"
width: 16
height: 16
anchors.left: communityName.right
anchors.verticalCenter: parent.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: wrapper.textColor
}
}
StyledTextEdit {
id: channelName
text: communityBadge.getLinkStyle(communityBadge.channelName || wrapper.name, hoveredLink)
height: 18
readOnly: true
textFormat: Text.RichText
width: implicitWidth > 300 ? 300 : implicitWidth
clip: true
anchors.left: caretImage.right
color: wrapper.textColor
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
onLinkActivated: function () {
chatsModel.communities.setActiveCommunity(wrapper.communityId)
chatsModel.setActiveChannel(model.message.chatId)
}
}
}
}
Component {
id: channelComponent
Item {
width: childrenRect.width
height: parent.height
Connections {
enabled: realChatType === Constants.chatTypeOneToOne
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === wrapper.chatId) {
wrapper.profileImage = appMain.getProfileImage(wrapper.chatId)
}
}
}
SVGImage {
id: channelIcon
width: 16
height: 16
fillMode: Image.PreserveAspectFit
source: "../../../img/channel-icon-" + (wrapper.realChatType === Constants.chatTypePublic ? "public-chat.svg" : "group.svg")
anchors.left: parent.left
anchors.verticalCenter:parent.verticalCenter
}
StatusIdenticon {
id: contactImage
height: 16
width: 16
chatId: wrapper.chatId
chatName: wrapper.name
chatType: wrapper.realChatType
identicon: wrapper.profileImage || wrapper.identicon
anchors.left: channelIcon.right
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
letterSize: 11
}
StyledText {
id: contactInfo
text: wrapper.realChatType !== Constants.chatTypePublic ?
Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(wrapper.name))) :
"#" + Utils.filterXSS(wrapper.name)
anchors.left: contactImage.right
anchors.leftMargin: 4
color: wrapper.textColor
font.weight: Font.Medium
font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter
}
}
}
}

View File

@ -0,0 +1,231 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "../ChatColumn"
Popup {
property var searchResults
property string chatId: chatsModel.channelView.activeChannel.id
id: popup
modal: true
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.4)
}
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
parent: Overlay.overlay
x: Math.round(((parent ? parent.width : 0) - width) / 2)
y: Math.round(((parent ? parent.height : 0) - height) / 2)
width: 690
height: {
if (!searchResults || !searchResults.length) {
return 122
}
// FIXME childrenRect has a binding loop for some reason
const childrenHeight = searchHeader.height + channelBadge.height + channelBadge.anchors.topMargin +
searchResultContent.height + searchResultContent.anchors.topMargin
// min height
if (childrenHeight < 560) {
return 560
}
// max height
if (childrenHeight > 970) {
return 970
}
return childrenHeight
}
background: Rectangle {
color: Style.current.background
radius: 16
}
onOpened: {
popupOpened = true
searchInput.forceActiveFocus(Qt.MouseFocusReason)
}
onClosed: {
popupOpened = false
destroy()
}
padding: 0
Item {
id: searchHeader
width: parent.width
height: 64
SVGImage {
id: searchImage
source: "../../../img/search.svg"
width: 28
height: 28
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
}
property var searchMessages: Backpressure.debounce(searchInput, 400, function (value) {
if (value === "") {
searchResults = []
return
}
// TODO add loading?
const messageIdsStr = chatsModel.messageView.messageList.messageSearch(value)
try {
searchResults = JSON.parse(messageIdsStr)
} catch (e) {
console.error ("Error parsing search result", e)
}
})
StyledTextField {
id: searchInput
anchors.left: searchImage.right
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
placeholderText: qsTr("Search")
placeholderTextColor: Style.current.secondaryText
selectByMouse: true
font.pixelSize: 28
background: Rectangle {
color: Style.current.transparent
}
Keys.onReleased: Qt.callLater(searchHeader.searchMessages, searchInput.text)
}
Separator {
anchors.bottom: parent.bottom
anchors.topMargin: 0
}
}
Rectangle {
id: channelBadge
color: Style.current.inputBackground
border.width: 0
radius: Style.current.radius
height: 32
width: childrenRect.width + 2 * inText.anchors.leftMargin
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.top: searchHeader.bottom
anchors.topMargin: 12
StyledText {
id: inText
text: qsTr("In:")
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 15
}
BadgeContent {
chatId: chatId
name: chatsModel.channelView.activeChannel.name
identicon: chatsModel.channelView.activeChannel.identicon
communityId: chatsModel.channelView.activeChannel.communityId
anchors.left: inText.right
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
id: searchResultContent
visible: !!popup.searchResults && popup.searchResults.length > 0
width: parent.width
height: visible ? implicitHeight + anchors.topMargin : 0
implicitHeight: childrenRect.height
anchors.top: channelBadge.bottom
anchors.topMargin: visible ? 13 : 0
Separator {
id: sep2
anchors.top: parent.top
anchors.topMargin: 0
}
StyledText {
id: sectionTitle
text: qsTr("Messages")
font.pixelSize: 15
color: Style.current.secondaryText
anchors.top: sep2.bottom
anchors.topMargin: Style.current.smallPadding
anchors.left: parent.left
anchors.leftMargin: Style.current.bigPadding
}
Column {
anchors.top: sectionTitle.bottom
anchors.topMargin: 4
width: parent.width
height: searchResultContent.visible ? childrenRect.height : 0
spacing: 0
Repeater {
model: popup.searchResults
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
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: ""
}
}
}
}
}

View File

@ -158,6 +158,7 @@ DISTFILES += \
app/AppLayouts/Chat/ContactsColumn/AddChat.qml \
app/AppLayouts/Chat/ContactsColumn/ClosedEmptyView.qml \
app/AppLayouts/Chat/components/AcceptRejectOptionsButtons.qml \
app/AppLayouts/Chat/components/BadgeContent.qml \
app/AppLayouts/Chat/components/ChooseBrowserPopup.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityList.qml \
@ -179,6 +180,7 @@ DISTFILES += \
app/AppLayouts/Chat/components/InviteFriendsPopup.qml \
app/AppLayouts/Chat/components/MessageContextMenu.qml \
app/AppLayouts/Chat/components/NicknamePopup.qml \
app/AppLayouts/Chat/components/SearchPopup.qml \
app/AppLayouts/Chat/components/SuggestedChannels.qml \
app/AppLayouts/Chat/components/GroupInfoPopup.qml \
app/AppLayouts/Chat/data/channelList.js \