feature(@desktop/chat): implement search results for communities, channels

Added a part for fetching messages from multiple chats/channels/communities.

Fixes: #2934
This commit is contained in:
Sale Djenic 2021-08-06 17:31:42 +02:00 committed by Iuri Matias
parent 4f8b072f10
commit e2628338de
23 changed files with 1065 additions and 269 deletions

View File

@ -1,7 +1,6 @@
import NimQml, Tables
import ../../../status/chat/[chat, message]
import ../../../status/status
import ../../../status/ens
import ../../../status/accounts
import strutils
@ -39,19 +38,6 @@ QtObject:
result.status = status
result.setup()
proc userNameOrAlias(self: ChannelsList, pubKey: string): string =
if self.status.chat.contacts.hasKey(pubKey):
return ens.userNameOrAlias(self.status.chat.contacts[pubKey])
generateAlias(pubKey)
proc chatName(self: ChannelsList, chatItem: Chat): string =
if not chatItem.chatType.isOneToOne: return chatItem.name
if self.status.chat.contacts.hasKey(chatItem.id) and self.status.chat.contacts[chatItem.id].hasNickname():
return self.status.chat.contacts[chatItem.id].localNickname
if chatItem.ensName != "":
return "@" & userName(chatItem.ensName).userName(true)
return self.userNameOrAlias(chatItem.id)
method rowCount*(self: ChannelsList, index: QModelIndex = nil): int = self.chats.len
proc renderBlock(self: ChannelsList, message: Message): string
@ -66,7 +52,7 @@ QtObject:
let chatItemRole = role.ChannelsRoles
case chatItemRole:
of ChannelsRoles.Name: result = newQVariant(self.chatName(chatItem))
of ChannelsRoles.Name: result = newQVariant(self.status.chat.chatName(chatItem))
of ChannelsRoles.Timestamp: result = newQVariant($chatItem.timestamp)
of ChannelsRoles.LastMessage: result = newQVariant(self.renderBlock(chatItem.lastMessage))
of ChannelsRoles.ContentType: result = newQVariant(chatItem.lastMessage.contentType.int)
@ -136,6 +122,13 @@ QtObject:
if chat.id == chatId:
return chat
proc getChannelById*(self: ChannelsList, chatId: string, found: var bool): Chat =
found = false
for chat in self.chats:
if chat.id == chatId:
found = true
return chat
proc getChannelByName*(self: ChannelsList, name: string): Chat =
for chat in self.chats:
if chat.name == name:
@ -213,7 +206,7 @@ QtObject:
proc renderInline(self: ChannelsList, elem: TextItem): string =
case elem.textType:
of "mention": result = self.userNameOrAlias(elem.literal)
of "mention": result = self.status.chat.userNameOrAlias(elem.literal)
of "link": result = elem.destination
else: result = escape_html(elem.literal)

View File

@ -313,4 +313,15 @@ QtObject:
return
self.communities[comIndex].recalculateMentions()
let index = self.createIndex(comIndex, 0, nil)
self.dataChanged(index, index, @[CommunityRoles.UnviewedMentionsCount.int])
self.dataChanged(index, index, @[CommunityRoles.UnviewedMentionsCount.int])
proc getChannelByIdAndBelongingCommunity*(self: CommunityList, chatId: string,
chat: var Chat, community: var Community): bool =
for co in self.communities:
for ch in co.chats:
if ch.id == chatId:
community = co
chat = ch
return true
return false

View File

@ -0,0 +1,6 @@
const SEARCH_MENU_LOCATION_CHAT_SECTION_NAME* = "Chat"
const SEARCH_RESULT_COMMUNITIES_SECTION_NAME* = "Communities"
const SEARCH_RESULT_CHATS_SECTION_NAME* = "Chats"
const SEARCH_RESULT_CHANNELS_SECTION_NAME* = "Channels"
const SEARCH_RESULT_MESSAGES_SECTION_NAME* = "Messages"

View File

@ -0,0 +1,82 @@
import json, strformat
import ../../../../status/chat/[chat]
import ../../../../status/[status]
import location_menu_sub_model, location_menu_sub_item
type
MessageSearchLocationMenuItem* = object
value: string
title: string
imageSource: string
iconName: string
iconColor: string
isIdenticon: bool
subItems: MessageSearchLocationMenuSubModel
proc initMessageSearchLocationMenuItem*(status: Status,
value, title, imageSource: string,
iconName, iconColor: string = "",
isIdenticon: bool = true): MessageSearchLocationMenuItem =
result.value = value
result.title = title
result.imageSource = imageSource
result.iconName = iconName
result.iconColor = iconColor
result.isIdenticon = isIdenticon
result.subItems = newMessageSearchLocationMenuSubModel(status)
proc `$`*(self: MessageSearchLocationMenuItem): string =
result = fmt"""MenuItem(
value: {self.value},
title: {self.title},
isIdenticon: {self.isIdenticon},
iconName: {self.iconName},
iconColor: {self.iconColor},
imageSource:{self.imageSource}
subItems:[
{$self.subItems}
]"""
proc getValue*(self: MessageSearchLocationMenuItem): string =
return self.value
proc getTitle*(self: MessageSearchLocationMenuItem): string =
return self.title
proc getImageSource*(self: MessageSearchLocationMenuItem): string =
return self.imageSource
proc getIconName*(self: MessageSearchLocationMenuItem): string =
return self.iconName
proc getIconColor*(self: MessageSearchLocationMenuItem): string =
return self.iconColor
proc getIsIdenticon*(self: MessageSearchLocationMenuItem): bool =
return self.isIdenticon
proc getSubItems*(self: MessageSearchLocationMenuItem):
MessageSearchLocationMenuSubModel =
self.subItems
proc prepareSubItems*(self: MessageSearchLocationMenuItem, chats: seq[Chat],
isCommunityChannel: bool) =
self.subItems.prepareItems(chats, isCommunityChannel)
proc getLocationSubItemForChatId*(self: MessageSearchLocationMenuItem,
chatId: string, found: var bool): MessageSearchLocationMenuSubItem =
self.subItems.getLocationSubItemForChatId(chatId, found)
proc toJsonNode*(self: MessageSearchLocationMenuItem): JsonNode =
result = %* {
"value": self.value,
"title": self.title,
"imageSource": self.imageSource,
"iconName": self.iconName,
"iconColor": self.iconColor,
"isIdenticon": self.isIdenticon
}

View File

@ -0,0 +1,113 @@
import NimQml, Tables, strutils
import ../../../../status/chat/[chat]
import ../../../../status/[status]
import location_menu_item, location_menu_sub_item, constants
type
MessageSearchLocationMenuModelRole {.pure.} = enum
Value = UserRole + 1
Title
ImageSource
IconName
IconColor
IsIdenticon
SubItems
QtObject:
type
MessageSearchLocationMenuModel* = ref object of QAbstractListModel
items: seq[MessageSearchLocationMenuItem]
proc delete(self: MessageSearchLocationMenuModel) =
self.QAbstractListModel.delete
proc setup(self: MessageSearchLocationMenuModel) =
self.QAbstractListModel.setup
proc newMessageSearchLocationMenuModel*(): MessageSearchLocationMenuModel =
new(result, delete)
result.setup()
method rowCount(self: MessageSearchLocationMenuModel,
index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: MessageSearchLocationMenuModel): Table[int, string] =
{
MessageSearchLocationMenuModelRole.Value.int:"value",
MessageSearchLocationMenuModelRole.Title.int:"title",
MessageSearchLocationMenuModelRole.ImageSource.int:"imageSource",
MessageSearchLocationMenuModelRole.IconName.int:"iconName",
MessageSearchLocationMenuModelRole.IconColor.int:"iconColor",
MessageSearchLocationMenuModelRole.IsIdenticon.int:"isIdenticon",
MessageSearchLocationMenuModelRole.SubItems.int:"subItems"
}.toTable
method data(self: MessageSearchLocationMenuModel, index: QModelIndex,
role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.items.len):
return
let item = self.items[index.row]
let enumRole = role.MessageSearchLocationMenuModelRole
case enumRole:
of MessageSearchLocationMenuModelRole.Value:
result = newQVariant(item.getValue)
of MessageSearchLocationMenuModelRole.Title:
result = newQVariant(item.getTitle)
of MessageSearchLocationMenuModelRole.ImageSource:
result = newQVariant(item.getImageSource)
of MessageSearchLocationMenuModelRole.IconName:
result = newQVariant(item.getIconName)
of MessageSearchLocationMenuModelRole.IconColor:
result = newQVariant(item.getIconColor)
of MessageSearchLocationMenuModelRole.IsIdenticon:
result = newQVariant(item.getIsIdenticon)
of MessageSearchLocationMenuModelRole.SubItems:
result = newQVariant(item.getSubItems)
proc prepareLocationMenu*(self: MessageSearchLocationMenuModel, status: Status,
chats: seq[Chat], communities: seq[Community]) =
self.beginResetModel()
self.items = @[]
var item = initMessageSearchLocationMenuItem(status,
SEARCH_MENU_LOCATION_CHAT_SECTION_NAME,
SEARCH_MENU_LOCATION_CHAT_SECTION_NAME, "", "chat", "", false)
item.prepareSubItems(chats, false)
self.items.add(item)
for co in communities:
item = initMessageSearchLocationMenuItem(status, co.id, co.name,
co.communityImage.thumbnail, "", co.communityColor,
co.communityImage.thumbnail.len == 0)
item.prepareSubItems(co.chats, true)
self.items.add(item)
self.endResetModel()
proc getLocationItemForCommunityId*(self: MessageSearchLocationMenuModel,
communityId: string, found: var bool): MessageSearchLocationMenuItem =
found = false
for i in self.items:
if (i.getValue() == communityId):
found = true
return i
proc getLocationSubItemForChatId*(self: MessageSearchLocationMenuModel,
chatId: string, found: var bool): MessageSearchLocationMenuSubItem =
for i in self.items:
let subItem = i.getLocationSubItemForChatId(chatId, found)
if (found):
return subItem

View File

@ -0,0 +1,59 @@
import json, strformat
type
MessageSearchLocationMenuSubItem* = object
value: string
text: string
imageSource: string
iconName: string
iconColor: string
isIdenticon: bool
proc initMessageSearchLocationMenuSubItem*(value, text, imageSource: string,
iconName, iconColor: string = "",
isIdenticon: bool = true): MessageSearchLocationMenuSubItem =
result.value = value
result.text = text
result.imageSource = imageSource
result.iconName = iconName
result.iconColor = iconColor
result.isIdenticon = isIdenticon
proc `$`*(self: MessageSearchLocationMenuSubItem): string =
result = fmt"""MenuSubItem:
value: {self.value},
text: {self.text},
isIdenticon: {self.isIdenticon},
iconName: {self.iconName},
iconColor: {self.iconColor},
imageSource:{self.imageSource}"""
proc getValue*(self: MessageSearchLocationMenuSubItem): string =
return self.value
proc getText*(self: MessageSearchLocationMenuSubItem): string =
return self.text
proc getImageSource*(self: MessageSearchLocationMenuSubItem): string =
return self.imageSource
proc getIconName*(self: MessageSearchLocationMenuSubItem): string =
return self.iconName
proc getIconColor*(self: MessageSearchLocationMenuSubItem): string =
return self.iconColor
proc getIsIdenticon*(self: MessageSearchLocationMenuSubItem): bool =
return self.isIdenticon
proc toJsonNode*(self: MessageSearchLocationMenuSubItem): JsonNode =
result = %* {
"value": self.value,
"text": self.text,
"imageSource": self.imageSource,
"iconName": self.iconName,
"iconColor": self.iconColor,
"isIdenticon": self.isIdenticon
}

View File

@ -0,0 +1,117 @@
import NimQml, Tables, strutils, strformat
import ../../../../status/chat/[chat]
import ../../../../status/[status]
import location_menu_sub_item
type
MessageSearchLocationMenuSubModelRole {.pure.} = enum
Value = UserRole + 1
Text
ImageSource
IconName
IconColor
IsIdenticon
QtObject:
type
MessageSearchLocationMenuSubModel* = ref object of QAbstractListModel
status: Status
items: seq[MessageSearchLocationMenuSubItem]
proc delete(self: MessageSearchLocationMenuSubModel) =
self.QAbstractListModel.delete
proc setup(self: MessageSearchLocationMenuSubModel) =
self.QAbstractListModel.setup
proc newMessageSearchLocationMenuSubModel*(status: Status):
MessageSearchLocationMenuSubModel =
new(result, delete)
result.status = status
result.setup()
proc `$`*(self: MessageSearchLocationMenuSubModel): string =
for i in 0 ..< self.items.len:
result &= fmt"""
[{i}]:({$self.items[i]})
"""
proc countChanged*(self: MessageSearchLocationMenuSubModel) {.signal.}
proc count*(self: MessageSearchLocationMenuSubModel): int {.slot.} =
self.items.len
QtProperty[int] count:
read = count
notify = countChanged
method rowCount(self: MessageSearchLocationMenuSubModel,
index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: MessageSearchLocationMenuSubModel):
Table[int, string] =
{
MessageSearchLocationMenuSubModelRole.Value.int:"value",
MessageSearchLocationMenuSubModelRole.Text.int:"text",
MessageSearchLocationMenuSubModelRole.ImageSource.int:"imageSource",
MessageSearchLocationMenuSubModelRole.IconName.int:"iconName",
MessageSearchLocationMenuSubModelRole.IconColor.int:"iconColor",
MessageSearchLocationMenuSubModelRole.IsIdenticon.int:"isIdenticon"
}.toTable
method data(self: MessageSearchLocationMenuSubModel, index: QModelIndex,
role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.items.len):
return
let item = self.items[index.row]
let enumRole = role.MessageSearchLocationMenuSubModelRole
case enumRole:
of MessageSearchLocationMenuSubModelRole.Value:
result = newQVariant(item.getValue)
of MessageSearchLocationMenuSubModelRole.Text:
result = newQVariant(item.getText)
of MessageSearchLocationMenuSubModelRole.ImageSource:
result = newQVariant(item.getImageSource)
of MessageSearchLocationMenuSubModelRole.IconName:
result = newQVariant(item.getIconName)
of MessageSearchLocationMenuSubModelRole.IconColor:
result = newQVariant(item.getIconColor)
of MessageSearchLocationMenuSubModelRole.IsIdenticon:
result = newQVariant(item.getIsIdenticon)
proc prepareItems*(self: MessageSearchLocationMenuSubModel, chats: seq[Chat],
isCommunityChannel: bool) =
self.beginResetModel()
self.items = @[]
for c in chats:
var text = self.status.chat.chatName(c)
if (isCommunityChannel):
self.items.add(initMessageSearchLocationMenuSubItem(c.id, text, "",
"channel", c.color, false))
else:
if (text.endsWith(".stateofus.eth")):
text = text[0 .. ^15]
self.items.add(initMessageSearchLocationMenuSubItem(c.id, text,
c.identicon, "", c.color, c.identicon.len == 0))
self.endResetModel()
proc getLocationSubItemForChatId*(self: MessageSearchLocationMenuSubModel,
chatId: string, found: var bool): MessageSearchLocationMenuSubItem =
found = false
for i in self.items:
if (i.getValue() == chatId):
found = true
return i

View File

@ -0,0 +1,80 @@
import strformat
type SearchResultItem* = object
itemId: string
content: string
time: string
titleId: string
title: string
sectionName: string
isLetterIdenticon: bool
badgeImage: string
badgePrimaryText: string
badgeSecondaryText: string
badgeIdenticonColor: string
proc initSearchResultItem*(itemId, content, time, titleId, title,
sectionName: string,
isLetterIdenticon: bool = false,
badgeImage, badgePrimaryText, badgeSecondaryText,
badgeIdenticonColor: string = ""): SearchResultItem =
result.itemId = itemId
result.content = content
result.time = time
result.titleId = titleId
result.title = title
result.sectionName = sectionName
result.isLetterIdenticon = isLetterIdenticon
result.badgeImage = badgeImage
result.badgePrimaryText = badgePrimaryText
result.badgeSecondaryText = badgeSecondaryText
result.badgeIdenticonColor = badgeIdenticonColor
proc `$`*(self: SearchResultItem): string =
result = "MessageSearchResultItem("
result &= fmt"itemId:{self.itemId}, "
result &= fmt"content:{self.content}, "
result &= fmt"time:{self.time}, "
result &= fmt"titleId:{self.titleId}, "
result &= fmt"title:{self.title}"
result &= fmt"sectionName:{self.sectionName}"
result &= fmt"isLetterIdenticon:{self.isLetterIdenticon}"
result &= fmt"badgeImage:{self.badgeImage}"
result &= fmt"badgePrimaryText:{self.badgePrimaryText}"
result &= fmt"badgeSecondaryText:{self.badgeSecondaryText}"
result &= fmt"badgeIdenticonColor:{self.badgeIdenticonColor}"
result &= ")"
method getItemId*(self: SearchResultItem): string {.base.} =
return self.itemId
method getContent*(self: SearchResultItem): string {.base.} =
return self.content
method getTime*(self: SearchResultItem): string {.base.} =
return self.time
method getTitleId*(self: SearchResultItem): string {.base.} =
return self.titleId
method getTitle*(self: SearchResultItem): string {.base.} =
return self.title
method getSectionName*(self: SearchResultItem): string {.base.} =
return self.sectionName
method getIsLetterIdentIcon*(self: SearchResultItem): bool {.base.} =
return self.isLetterIdenticon
method getBadgeImage*(self: SearchResultItem): string {.base.} =
return self.badgeImage
method getBadgePrimaryText*(self: SearchResultItem): string {.base.} =
return self.badgePrimaryText
method getBadgeSecondaryText*(self: SearchResultItem): string {.base.} =
return self.badgeSecondaryText
method getBadgeIdenticonColor*(self: SearchResultItem): string {.base.} =
return self.badgeIdenticonColor

View File

@ -0,0 +1,112 @@
import NimQml, Tables, strutils
import result_item
type
MessageSearchResultModelRole {.pure.} = enum
ItemId = UserRole + 1
Content
Time
TitleId
Title
SectionName
IsLetterIdenticon
BadgeImage
BadgePrimaryText
BadgeSecondaryText
BadgeIdenticonColor
QtObject:
type
MessageSearchResultModel* = ref object of QAbstractListModel
resultList: seq[SearchResultItem]
proc delete(self: MessageSearchResultModel) =
self.QAbstractListModel.delete
proc setup(self: MessageSearchResultModel) =
self.QAbstractListModel.setup
proc newMessageSearchResultModel*(): MessageSearchResultModel =
new(result, delete)
result.setup()
#################################################
# Properties
#################################################
proc countChanged*(self: MessageSearchResultModel) {.signal.}
proc count*(self: MessageSearchResultModel): int {.slot.} =
self.resultList.len
QtProperty[int] count:
read = count
notify = countChanged
method rowCount(self: MessageSearchResultModel, index: QModelIndex = nil): int =
return self.resultList.len
method roleNames(self: MessageSearchResultModel): Table[int, string] =
{
MessageSearchResultModelRole.ItemId.int:"itemId",
MessageSearchResultModelRole.Content.int:"content",
MessageSearchResultModelRole.Time.int:"time",
MessageSearchResultModelRole.TitleId.int:"titleId",
MessageSearchResultModelRole.Title.int:"title",
MessageSearchResultModelRole.SectionName.int:"sectionName",
MessageSearchResultModelRole.IsLetterIdenticon.int:"isLetterIdenticon",
MessageSearchResultModelRole.BadgeImage.int:"badgeImage",
MessageSearchResultModelRole.BadgePrimaryText.int:"badgePrimaryText",
MessageSearchResultModelRole.BadgeSecondaryText.int:"badgeSecondaryText",
MessageSearchResultModelRole.BadgeIdenticonColor.int:"badgeIdenticonColor"
}.toTable
method data(self: MessageSearchResultModel, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.resultList.len):
return
let item = self.resultList[index.row]
let enumRole = role.MessageSearchResultModelRole
case enumRole:
of MessageSearchResultModelRole.ItemId:
result = newQVariant(item.getItemId)
of MessageSearchResultModelRole.Content:
result = newQVariant(item.getContent)
of MessageSearchResultModelRole.Time:
result = newQVariant(item.getTime)
of MessageSearchResultModelRole.TitleId:
result = newQVariant(item.getTitleId)
of MessageSearchResultModelRole.Title:
result = newQVariant(item.getTitle)
of MessageSearchResultModelRole.SectionName:
result = newQVariant(item.getSectionName)
of MessageSearchResultModelRole.IsLetterIdenticon:
result = newQVariant(item.getIsLetterIdentIcon)
of MessageSearchResultModelRole.BadgeImage:
result = newQVariant(item.getBadgeImage)
of MessageSearchResultModelRole.BadgePrimaryText:
result = newQVariant(item.getBadgePrimaryText)
of MessageSearchResultModelRole.BadgeSecondaryText:
result = newQVariant(item.getBadgeSecondaryText)
of MessageSearchResultModelRole.BadgeIdenticonColor:
result = newQVariant(item.getBadgeIdenticonColor)
proc add*(self: MessageSearchResultModel, item: SearchResultItem) =
self.beginInsertRows(newQModelIndex(), self.resultList.len, self.resultList.len)
self.resultList.add(item)
self.endInsertRows()
proc set*(self: MessageSearchResultModel, items: seq[SearchResultItem]) =
self.beginResetModel()
self.resultList = items
self.endResetModel()
proc clear*(self: MessageSearchResultModel) =
self.beginResetModel()
self.resultList = @[]
self.endResetModel()

View File

@ -0,0 +1,265 @@
import NimQml, Tables, json, strutils, chronicles
import result_model, result_item, location_menu_model, location_menu_item, location_menu_sub_item
import constants as sr_constants
import ../../../../status/[status, types]
import ../../../../status/chat/[message, chat]
import ../../../../status/libstatus/[settings]
import ../communities
import ../channel
import ../chat_item
import ../channels_list
import ../community_list
logScope:
topics = "search-messages-view-controller"
type ResultItemInfo = object
communityId*: string
channelId*: string
messageId*: string
method isEmpty*(self: ResultItemInfo): bool {.base.} =
self.communityId.len == 0 and
self.channelId.len == 0 and
self.messageId.len == 0
QtObject:
type MessageSearchViewController* = ref object of QObject
status: Status
channelView: ChannelView
communitiesView: CommunitiesView
resultItems: Table[string, ResultItemInfo] # [resuiltItemId, ResultItemInfo]
messageSearchResultModel: MessageSearchResultModel
messageSearchLocationMenuModel: MessageSearchLocationMenuModel
meassgeSearchTerm: string
meassgeSearchLocation: string
meassgeSearchSubLocation: string
proc setup(self: MessageSearchViewController) =
self.QObject.setup
proc delete*(self: MessageSearchViewController) =
self.messageSearchResultModel.delete
self.messageSearchLocationMenuModel.delete
self.resultItems.clear
self.QObject.delete
proc newMessageSearchViewController*(status: Status, channelView: ChannelView,
communitiesView: CommunitiesView): MessageSearchViewController =
new(result, delete)
result.status = status
result.channelView = channelView
result.communitiesView = communitiesView
result.resultItems = initTable[string, ResultItemInfo]()
result.messageSearchResultModel = newMessageSearchResultModel()
result.messageSearchLocationMenuModel = newMessageSearchLocationMenuModel()
result.setup
proc getMessageSearchResultModel*(self: MessageSearchViewController):
QVariant {.slot.} =
newQVariant(self.messageSearchResultModel)
QtProperty[QVariant] resultModel:
read = getMessageSearchResultModel
proc getMessageSearchLocationMenuModel*(self: MessageSearchViewController):
QVariant {.slot.} =
newQVariant(self.messageSearchLocationMenuModel)
QtProperty[QVariant] locationMenuModel:
read = getMessageSearchLocationMenuModel
proc getSearchLocationObject*(self: MessageSearchViewController): string {.slot.} =
## This method returns location and subLocation with their details so we
## may set initial search location on the side of qml.
var found = false
let subItem = self.messageSearchLocationMenuModel.getLocationSubItemForChatId(
self.channelView.activeChannel.id, found
)
var jsonObject = %* {
"location": "",
"subLocation": ""
}
if(found):
jsonObject["subLocation"] = subItem.toJsonNode()
if(self.channelView.activeChannel.communityId.len == 0):
jsonObject["location"] = %* {
"value":sr_constants.SEARCH_MENU_LOCATION_CHAT_SECTION_NAME,
"title":sr_constants.SEARCH_MENU_LOCATION_CHAT_SECTION_NAME
}
else:
let item = self.messageSearchLocationMenuModel.getLocationItemForCommunityId(
self.channelView.activeChannel.communityId, found
)
if(found):
jsonObject["location"] = item.toJsonNode()
result = Json.encode(jsonObject)
proc prepareLocationMenuModel*(self: MessageSearchViewController)
{.slot.} =
self.messageSearchLocationMenuModel.prepareLocationMenu(
self.status,
self.channelView.chats.chats,
self.communitiesView.joinedCommunityList.communities)
proc setSearchLocation*(self: MessageSearchViewController, location: string = "",
subLocation: string = "") {.slot.} =
## Setting location and subLocation to an empty string means we're
## searching in all available chats/channels/communities.
self.meassgeSearchLocation = location
self.meassgeSearchSubLocation = subLocation
proc searchMessages*(self: MessageSearchViewController, searchTerm: string)
{.slot.} =
self.meassgeSearchTerm = searchTerm
self.resultItems.clear
if (self.meassgeSearchTerm.len == 0):
self.messageSearchResultModel.clear()
return
var chats: seq[string]
var communities: seq[string]
if (self.meassgeSearchSubLocation.len > 0):
chats.add(self.meassgeSearchSubLocation)
elif (self.meassgeSearchLocation.len > 0):
# If "Chat" is set for the meassgeSearchLocation that means we need to
# search in all chats from the chat section.
if (self.meassgeSearchLocation != sr_constants.SEARCH_MENU_LOCATION_CHAT_SECTION_NAME):
communities.add(self.meassgeSearchLocation)
else:
for c in self.channelView.chats.chats:
chats.add(c.id)
if (communities.len == 0 and chats.len == 0):
for c in self.channelView.chats.chats:
chats.add(c.id)
for co in self.communitiesView.joinedCommunityList.communities:
communities.add(co.id)
self.status.chat.asyncSearchMessages(communities, chats,
self.meassgeSearchTerm, false)
proc onSearchMessagesLoaded*(self: MessageSearchViewController,
messages: seq[Message]) =
self.resultItems.clear
var items: seq[SearchResultItem]
var channels: seq[SearchResultItem]
let myPublicKey = getSetting[string](Setting.PublicKey, "0x0")
# Add communities
for co in self.communitiesView.joinedCommunityList.communities:
if(self.meassgeSearchLocation.len == 0 and
co.name.toLower.startsWith(self.meassgeSearchTerm.toLower)):
let item = initSearchResultItem(co.id, "", "", co.id, co.name,
sr_constants.SEARCH_RESULT_COMMUNITIES_SECTION_NAME, false,
co.communityImage.thumbnail, "", "", co.communityColor)
self.resultItems.add(co.id, ResultItemInfo(communityId: co.id))
items.add(item)
# Add channels
if(self.meassgeSearchSubLocation.len == 0 and
self.meassgeSearchLocation.len == 0 or
self.meassgeSearchLocation == co.name):
for c in co.chats:
let chatName = self.status.chat.chatName(c)
var chatNameRaw = chatName
if(chatName.startsWith("@")):
chatNameRaw = chatName[1 ..^ 1]
if(chatNameRaw.toLower.startsWith(self.meassgeSearchTerm.toLower)):
let item = initSearchResultItem(c.id, "", "", c.id, chatName,
sr_constants.SEARCH_RESULT_CHANNELS_SECTION_NAME,
c.identicon.len > 0, c.identicon, "", "", c.color)
self.resultItems.add(c.id, ResultItemInfo(communityId: co.id,
channelId: c.id))
channels.add(item)
# Add chats
if(self.meassgeSearchLocation.len == 0 or
self.meassgeSearchLocation == sr_constants.SEARCH_RESULT_CHATS_SECTION_NAME):
for c in self.channelView.chats.chats:
let chatName = self.status.chat.chatName(c)
var chatNameRaw = chatName
if(chatName.startsWith("@")):
chatNameRaw = chatName[1 ..^ 1]
if(chatNameRaw.toLower.startsWith(self.meassgeSearchTerm.toLower)):
let item = initSearchResultItem(c.id, "", "", c.id, chatName,
sr_constants.SEARCH_RESULT_CHATS_SECTION_NAME,
c.identicon.len > 0, c.identicon, "", "", c.color)
self.resultItems.add(c.id, ResultItemInfo(communityId: "",
channelId: c.id))
items.add(item)
# Add channels in order as requested by design
items.add(channels)
# Add messages
for m in messages:
if (m.contentType != ContentType.Message):
continue
var found = false
var chat = self.channelView.chats.getChannelById(m.chatId, found)
if (found):
var channel = self.status.chat.chatName(chat)
if (channel.endsWith(".stateofus.eth")):
channel = channel[0 .. ^15]
var alias = self.status.chat.userNameOrAlias(m.fromAuthor, true)
if (myPublicKey == m.fromAuthor):
alias = "You"
let item = initSearchResultItem(m.id, m.text, m.timestamp, m.fromAuthor,
alias, sr_constants.SEARCH_RESULT_MESSAGES_SECTION_NAME,
chat.identicon.len == 0, chat.identicon, channel, "", chat.color)
self.resultItems.add(m.id, ResultItemInfo(communityId: "",
channelId: chat.id, messageId: m.id))
items.add(item)
else:
var community: Community
if (self.communitiesView.joinedCommunityList.
getChannelByIdAndBelongingCommunity(m.chatId, chat, community)):
var channel = self.status.chat.chatName(chat)
if (not channel.startsWith("#")):
channel = "#" & channel
if (channel.endsWith(".stateofus.eth")):
channel = channel[0 .. ^15]
var alias = self.status.chat.userNameOrAlias(m.fromAuthor, true)
if (myPublicKey == m.fromAuthor):
alias = "You"
let item = initSearchResultItem(m.id, m.text, m.timestamp, m.fromAuthor,
m.alias, sr_constants.SEARCH_RESULT_MESSAGES_SECTION_NAME,
community.communityImage.thumbnail.len == 0,
community.communityImage.thumbnail, community.name, channel,
community.communityColor)
self.resultItems.add(m.id, ResultItemInfo(communityId: community.id,
channelId: chat.id, messageId: m.id))
items.add(item)
self.messageSearchResultModel.set(items)
proc getItemInfo*(self: MessageSearchViewController, itemId: string):
ResultItemInfo =
self.resultItems.getOrDefault(itemId)

View File

@ -12,7 +12,7 @@ import contacts
import chat/[chat, message]
import tasks/[qt, task_runner_impl]
import signals/messages
import ens
import ens, accounts
logScope:
topics = "chat-model"
@ -621,6 +621,9 @@ QtObject:
return self.channels[chatId].communityId
proc asyncSearchMessages*(self: ChatModel, chatId: string, searchTerm: string, caseSensitive: bool) =
## Asynchronous search for messages which contain the searchTerm and belong
## to the chat with chatId.
if (chatId.len == 0):
info "empty channel id set for fetching more messages"
return
@ -628,8 +631,8 @@ QtObject:
if (searchTerm.len == 0):
return
let arg = AsyncSearchMessageTaskArg(
tptr: cast[ByteAddress](asyncSearchMessagesTask),
let arg = AsyncSearchMessagesInChatTaskArg(
tptr: cast[ByteAddress](asyncSearchMessagesInChatTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSearchMessages",
chatId: chatId,
@ -638,6 +641,29 @@ QtObject:
)
self.tasks.threadpool.start(arg)
proc asyncSearchMessages*(self: ChatModel, communityIds: seq[string], chatIds: seq[string], searchTerm: string, caseSensitive: bool) =
## Asynchronous search for messages which contain the searchTerm and belong
## to either any chat/channel from chatIds array or any channel of community
## from communityIds array.
if (communityIds.len == 0 and chatIds.len == 0):
info "either community ids or chat ids or both must be set"
return
if (searchTerm.len == 0):
return
let arg = AsyncSearchMessagesInChatsAndCommunitiesTaskArg(
tptr: cast[ByteAddress](asyncSearchMessagesInChatsAndCommunitiesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSearchMessages",
communityIds: communityIds,
chatIds: chatIds,
searchTerm: searchTerm,
caseSensitive: caseSensitive
)
self.tasks.threadpool.start(arg)
proc onAsyncSearchMessages*(self: ChatModel, response: string) {.slot.} =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
@ -774,3 +800,31 @@ QtObject:
self.events.emit("messagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: messages))
self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions))
self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: pinnedMessages))
proc userNameOrAlias*(self: ChatModel, pubKey: string,
prettyForm: bool = false): string =
## Returns ens name or alias, in case if prettyForm is true and ens name
## ends with ".stateofus.eth" that part will be removed.
var alias: string
if self.contacts.hasKey(pubKey):
alias = ens.userNameOrAlias(self.contacts[pubKey])
else:
alias = generateAlias(pubKey)
if (prettyForm and alias.endsWith(".stateofus.eth")):
alias = alias[0 .. ^15]
return alias
proc chatName*(self: ChatModel, chatItem: Chat): string =
if (not chatItem.chatType.isOneToOne):
return chatItem.name
if (self.contacts.hasKey(chatItem.id) and
self.contacts[chatItem.id].hasNickname()):
return self.contacts[chatItem.id].localNickname
if chatItem.ensName != "":
return "@" & userName(chatItem.ensName).userName(true)
return self.userNameOrAlias(chatItem.id)

View File

@ -1,14 +1,17 @@
#################################################
# Async search messages by term
#################################################
type
AsyncSearchMessageTaskArg = ref object of QObjectTaskArg
chatId: string
type
AsyncSearchMessagesTaskArg = ref object of QObjectTaskArg
searchTerm: string
caseSensitive: bool
const asyncSearchMessagesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSearchMessageTaskArg](argEncoded)
#################################################
# Async search messages in chat with chatId by term
#################################################
type
AsyncSearchMessagesInChatTaskArg = ref object of AsyncSearchMessagesTaskArg
chatId: string
const asyncSearchMessagesInChatTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSearchMessagesInChatTaskArg](argEncoded)
var messages: JsonNode
var success: bool
let response = status_chat.asyncSearchMessages(arg.chatId, arg.searchTerm, arg.caseSensitive, success)
@ -22,6 +25,30 @@ const asyncSearchMessagesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall
}
arg.finish(responseJson)
#################################################
# Async search messages in chats/channels and communities by term
#################################################
type
AsyncSearchMessagesInChatsAndCommunitiesTaskArg = ref object of AsyncSearchMessagesTaskArg
communityIds: seq[string]
chatIds: seq[string]
const asyncSearchMessagesInChatsAndCommunitiesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSearchMessagesInChatsAndCommunitiesTaskArg](argEncoded)
var messages: JsonNode
var success: bool
let response = status_chat.asyncSearchMessages(arg.communityIds, arg.chatIds, arg.searchTerm, arg.caseSensitive, success)
if(success):
messages = response.parseJson()["result"]
let responseJson = %*{
"communityIds": arg.communityIds,
"chatIds": arg.chatIds,
"messages": messages
}
arg.finish(responseJson)
#################################################
# Async load messages
#################################################

View File

@ -246,7 +246,6 @@ proc recalculateUnviewedMessages*(community: var Community) =
community.unviewedMessagesCount = total
proc recalculateMentions*(community: var Community) =
echo "(recalculateMentions) chatId: ", community.id, " before: ", community.unviewedMentionsCount
var total = 0
for chat in community.chats:
total += chat.unviewedMentionsCount

View File

@ -569,7 +569,15 @@ proc unreadActivityCenterNotificationsCount*(): int =
proc asyncSearchMessages*(chatId: string, searchTerm: string, caseSensitive: bool, success: var bool): string =
success = true
try:
result = callPrivateRPC("allChatMessagesWhichMatchTerm".prefix, %* [chatId, searchTerm, caseSensitive])
result = callPrivateRPC("allMessagesFromChatWhichMatchTerm".prefix, %* [chatId, searchTerm, caseSensitive])
except RpcException as e:
success = false
result = e.msg
proc asyncSearchMessages*(communityIds: seq[string], chatIds: seq[string], searchTerm: string, caseSensitive: bool, success: var bool): string =
success = true
try:
result = callPrivateRPC("allMessagesFromChatsAndCommunitiesWhichMatchTerm".prefix, %* [communityIds, chatIds, searchTerm, caseSensitive])
except RpcException as e:
success = false
result = e.msg

@ -1 +1 @@
Subproject commit 38b0207055f81c853830320e8b20e9532e84973b
Subproject commit c68ee3dbe412d8af7cab079b1cb67e0a2a3bf155

View File

@ -1,8 +1,8 @@
import QtQuick 2.13
import Qt.labs.platform 1.1
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
@ -165,6 +165,107 @@ Item {
}
}
StatusSearchLocationMenu {
id: searchPopupMenu
searchPopup: searchPopup
locatioModel: chatsModel.messageSearchViewController.locationMenuModel
onItemClicked: {
chatsModel.messageSearchViewController.setSearchLocation(firstLevelItemValue, secondLevelItemValue)
if(searchPopup.searchText !== "")
searchMessages(searchPopup.searchText)
}
}
property var searchMessages: Backpressure.debounce(searchPopup, 400, function (value) {
chatsModel.messageSearchViewController.searchMessages(value)
})
StatusSearchPopup {
id: searchPopup
noResultsLabel: qsTr("No results")
defaultSearchLocationText: qsTr("Anywhere")
searchOptionsPopupMenu: searchPopupMenu
searchResults: chatsModel.messageSearchViewController.resultModel
onSearchTextChanged: {
searchMessages(searchPopup.searchText);
}
onAboutToHide: {
if (searchPopupMenu.visible) {
searchPopupMenu.close();
}
//clear menu
for (var i = 2; i <= searchPopupMenu.count; i++) {
searchPopupMenu.removeItem(searchPopupMenu.takeItem(i));
}
}
onClosed: {
searchPopupMenu.dismiss();
}
onOpened: {
searchPopup.resetSearchSelection();
chatsModel.messageSearchViewController.prepareLocationMenuModel()
const jsonObj = chatsModel.messageSearchViewController.getSearchLocationObject()
if (!jsonObj) {
return
}
let obj = JSON.parse(jsonObj)
if (obj.location === "") {
if(obj.subLocation === "") {
chatsModel.messageSearchViewController.setSearchLocation("", "")
}
else {
searchPopup.setSearchSelection(obj.subLocation.text,
"",
obj.subLocation.imageSource,
obj.subLocation.isIdenticon,
obj.subLocation.iconName,
obj.subLocation.identiconColor)
chatsModel.messageSearchViewController.setSearchLocation("", obj.subLocation.value)
}
}
else {
if (obj.location.title === "Chat") {
searchPopup.setSearchSelection(obj.subLocation.text,
"",
obj.subLocation.imageSource,
obj.subLocation.isIdenticon,
obj.subLocation.iconName,
obj.subLocation.identiconColor)
chatsModel.messageSearchViewController.setSearchLocation(obj.location.value, obj.subLocation.value)
}
else {
searchPopup.setSearchSelection(obj.location.title,
obj.subLocation.text,
obj.location.imageSource,
obj.location.isIdenticon,
obj.location.iconName,
obj.location.identiconColor)
chatsModel.messageSearchViewController.setSearchLocation(obj.location.value, obj.subLocation.value)
}
}
}
onResultItemClicked: {
searchPopup.close()
chatsModel.switchToSearchedItem(itemId)
}
onResultItemTitleClicked: {
const pk = titleId
const userProfileImage = appMain.getProfileImage(pk)
return openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
}
}
StackLayout {
anchors.fill: parent
currentIndex: chatsModel.channelView.activeChannelIndex > -1 && chatGroupsListViewCount > 0 ? 0 : 1
@ -253,9 +354,6 @@ Item {
notificationCount: chatsModel.activityNotificationList.unreadCount
onSearchButtonClicked: searchPopup.open()
SearchPopup {
id: searchPopup
}
onMembersButtonClicked: showUsers = !showUsers
onNotificationButtonClicked: activityCenter.open()

View File

@ -1,227 +0,0 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "../ChatColumn"
Popup {
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: {
const noResultHeight = 122
let minHeight = 560
const maxHeight = parent.height - 200
if (!searchResultContent.visible) {
return noResultHeight
}
if (minHeight > maxHeight) {
return maxHeight
}
if (listView.height < minHeight - noResultHeight) {
return minHeight
}
if (listView.height > maxHeight - noResultHeight) {
return maxHeight
}
}
background: Rectangle {
color: Style.current.background
radius: 16
}
onOpened: {
popupOpened = true
searchInput.forceActiveFocus(Qt.MouseFocusReason)
}
onClosed: {
popupOpened = false
}
padding: 0
Connections {
target: chatsModel.channelView
onActiveChannelChanged: {
searchInput.text = ""
}
}
Item {
id: searchHeader
width: parent.width
height: 64
SVGImage {
id: searchImage
source: "../../../img/search.svg"
width: 40
height: 40
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
}
property var searchMessages: Backpressure.debounce(searchInput, 400, function (value) {
chatsModel.messageView.searchMessages(value)
})
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
//% "Search"
placeholderText: qsTrId("search")
placeholderTextColor: Style.current.secondaryText
selectByMouse: true
font.pixelSize: 28
background: Rectangle {
color: Style.current.transparent
}
onTextChanged: {
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
//% "In:"
text: qsTrId("in-")
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 15
}
BadgeContent {
chatId: popup.chatId
name: Utils.removeStatusEns(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
hideSecondIcon: true
}
}
Item {
id: searchResultContent
visible: chatsModel.messageView.searchResultMessageModel.count > 0
width: parent.width
anchors.bottom: parent.bottom
anchors.top: channelBadge.bottom
anchors.topMargin: visible ? 13 : 0
Separator {
id: sep2
anchors.top: parent.top
anchors.topMargin: 0
}
StyledText {
id: sectionTitle
//% "Messages"
text: qsTrId("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
}
ScrollView {
id: scrollView
anchors.top: sectionTitle.bottom
anchors.topMargin: 4
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.current.smallPadding
width: parent.width
clip: true
ListView{
id: listView
model: chatsModel.messageView.searchResultMessageModel
delegate: Message {
anchors.right: undefined
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: 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))
}
popup.close()
if(chatsModel.messageView.isMessageDisplayed(model.messageId))
positionAtMessage(model.messageId)
else
chatsModel.messageView.loadMessagesUntillMessageWithIdIsLoaded(model.messageId)
}
prevMessageIndex: -1
prevMsgTimestamp: ""
}
}
}
}
}

View File

@ -21,13 +21,13 @@ Item {
owner = backpressure;
}
owner.Component.onDestruction.connect(cleanup);
owner.Component.destruction.connect(cleanup);
var obj = Qt.createQmlObject('import QtQuick 2.0; Timer {running: false; repeat: false; interval: ' + timeout + '}', backpressure, "setTimeout");
obj.triggered.connect(function() {
callback();
obj.destroy();
owner.Component.onDestruction.disconnect(cleanup);
owner.Component.destruction.disconnect(cleanup);
delete _timers[tid];
});
obj.running = true;

View File

@ -179,7 +179,6 @@ 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 \

2
vendor/DOtherSide vendored

@ -1 +1 @@
Subproject commit c18777749a2574d743ca2b7e5abc654152154f00
Subproject commit 2bffa67581c4e7546cfb5784f187b85bc6f97388

@ -1 +1 @@
Subproject commit 2b6e50491786ae0d61a97f99edda27b70364838a
Subproject commit f4463f3955a96e162e9881b73ba02f819e0374a4

2
vendor/nimqml vendored

@ -1 +1 @@
Subproject commit 936a4fa8abd452c65cfd9d40f5ac261b3260a47e
Subproject commit 3a2026ebbc8a98ef1ffe15693685feea38286552

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 9f478db7ad06d0e84e43edf30c52ff586a9d266c
Subproject commit 33747cc283b65603565c008b522a0148c5645128