refactor(app-search): app search module added

This commit is contained in:
Sale Djenic 2021-11-09 15:13:37 +01:00
parent 9045b0bbc4
commit d67278b23e
24 changed files with 1179 additions and 23 deletions

View File

@ -0,0 +1,41 @@
type
BaseItem* {.pure inheritable.} = ref object of RootObj
value: string
text: string
image: string
icon: string
iconColor: string
isIdenticon: bool
proc setup*(self: BaseItem, value, text, image, icon, iconColor: string, isIdenticon: bool) =
self.value = value
self.text = text
self.image = image
self.icon = icon
self.iconColor = iconColor
self.isIdenticon = isIdenticon
proc initBaseItem*(value, text, image, icon, iconColor: string, isIdenticon: bool): BaseItem =
result = BaseItem()
result.setup(value, text, image, icon, iconColor, isIdenticon)
proc delete*(self: BaseItem) =
discard
method value*(self: BaseItem): string {.inline base.} =
self.value
method text*(self: BaseItem): string {.inline base.} =
self.text
method image*(self: BaseItem): string {.inline base.} =
self.image
method icon*(self: BaseItem): string {.inline base.} =
self.icon
method iconColor*(self: BaseItem): string {.inline base.} =
self.iconColor
method isIdenticon*(self: BaseItem): bool {.inline base.} =
self.isIdenticon

View File

@ -0,0 +1,114 @@
import controller_interface
import io_interface
import ../../../boot/app_sections_config as conf
import ../../../../app_service/service/chat/service as chat_service
import ../../../../app_service/service/community/service as community_service
import ../../../../app_service/service/message/service as message_service
import eventemitter
import status/[signals]
export controller_interface
type
Controller* = ref object of controller_interface.AccessInterface
delegate: io_interface.AccessInterface
events: EventEmitter
chatService: chat_service.ServiceInterface
communityService: community_service.ServiceInterface
messageService: message_service.Service
activeSectionId: string
activeChatId: string
searchLocation: string
searchSubLocation: string
searchTerm: string
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service): Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.chatService = chatService
result.communityService = communityService
result.messageService = messageService
method delete*(self: Controller) =
discard
method init*(self: Controller) =
self.events.on(SIGNAL_SEARCH_MESSAGES_LOADED) do(e:Args):
let args = SearchMessagesLoadedArgs(e)
self.delegate.onSearchMessagesDone(args.messages)
method activeSectionId*(self: Controller): string =
return self.activeSectionId
method activeChatId*(self: Controller): string =
return self.activeChatId
method setActiveSectionIdAndChatId*(self: Controller, sectionId: string, chatId: string) =
self.activeSectionId = sectionId
self.activeChatId = chatId
method searchTerm*(self: Controller): string =
return self.searchTerm
method searchLocation*(self: Controller): string =
return self.searchLocation
method searchSubLocation*(self: Controller): string =
return self.searchSubLocation
method setSearchLocation*(self: Controller, location: string, subLocation: string) =
## Setting location and subLocation to an empty string means we're
## searching in all available chats/channels/communities.
self.searchLocation = location
self.searchSubLocation = subLocation
method getCommunities*(self: Controller): seq[CommunityDto] =
return self.communityService.getCommunities()
method getCommunityById*(self: Controller, communityId: string): CommunityDto =
return self.communityService.getCommunityById(communityId)
method getAllChatsForCommunity*(self: Controller, communityId: string): seq[Chat] =
return self.communityService.getAllChats(communityId)
method getChatDetailsForChatTypes*(self: Controller, types: seq[ChatType]): seq[ChatDto] =
return self.chatService.getChatsOfChatTypes(types)
method getChatDetails*(self: Controller, communityId, chatId: string): ChatDto =
let fullId = communityId & chatId
return self.chatService.getChatById(fullId)
method searchMessages*(self: Controller, searchTerm: string) =
self.searchTerm = searchTerm
var chats: seq[string]
var communities: seq[string]
if (self.searchSubLocation.len > 0):
chats.add(self.searchSubLocation)
elif (self.searchLocation.len > 0):
# If "Chat" is set for the meassgeSearchLocation that means we need to search in all chats from the chat section.
if (self.searchLocation != conf.CHAT_SECTION_ID):
communities.add(self.searchLocation)
else:
let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat]
let displayedChats = self.getChatDetailsForChatTypes(types)
for c in displayedChats:
chats.add(c.id)
if (communities.len == 0 and chats.len == 0):
let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat]
let displayedChats = self.getChatDetailsForChatTypes(types)
for c in displayedChats:
chats.add(c.id)
let communitiesIds = self.communityService.getCommunityIds()
for cId in communitiesIds:
communities.add(cId)
self.messageService.asyncSearchMessages(communities, chats, self.searchTerm, false)

View File

@ -0,0 +1,51 @@
import ../../../../app_service/service/chat/dto/chat
import ../../../../app_service/service/community/dto/community
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
## Abstract class for any input/interaction with this module.
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method init*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method activeSectionId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method activeChatId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method setActiveSectionIdAndChatId*(self: AccessInterface, sectionId: string, chatId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method searchTerm*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method searchLocation*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method searchSubLocation*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method setSearchLocation*(self: AccessInterface, location: string, subLocation: string) {.base.} =
raise newException(ValueError, "No implementation available")
method getCommunities*(self: AccessInterface): seq[CommunityDto] {.base.} =
raise newException(ValueError, "No implementation available")
method getCommunityById*(self: AccessInterface, communityId: string): CommunityDto {.base.} =
raise newException(ValueError, "No implementation available")
method getAllChatsForCommunity*(self: AccessInterface, communityId: string): seq[Chat] {.base.} =
raise newException(ValueError, "No implementation available")
method getChatDetailsForChatTypes*(self: AccessInterface, types: seq[ChatType]): seq[ChatDto] {.base.} =
raise newException(ValueError, "No implementation available")
method getChatDetails*(self: AccessInterface, communityId, chatId: string): ChatDto {.base.} =
raise newException(ValueError, "No implementation available")
method searchMessages*(self: AccessInterface, searchTerm: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,11 @@
# Defines how parent module accesses this module
include ./private_interfaces/module_base_interface
include ./private_interfaces/module_access_interface
# Defines how this module view communicates with this module
include ./private_interfaces/module_view_delegate_interface
# Defines how this controller communicates with this module
include ./private_interfaces/module_controller_delegate_interface
# Defines how submodules of this module communicate with this module

View File

@ -0,0 +1,47 @@
import json, strformat
import base_item, location_menu_sub_model, location_menu_sub_item
type
Item* = ref object of BaseItem
subItems: SubModel
proc initItem*(value, text, image, icon, iconColor: string = "", isIdenticon: bool = true): Item =
result = Item()
result.setup(value, text, image, icon, iconColor, isIdenticon)
result.subItems = newSubModel()
proc delete*(self: Item) =
self.subItems.delete
self.BaseItem.delete
proc subItems*(self: Item): SubModel {.inline.} =
self.subItems
proc `$`*(self: Item): string =
result = fmt"""SearchMenuItem(
value: {self.value},
title: {self.text},
imageSource: {self.image},
iconName: {self.icon},
iconColor: {self.iconColor},
isIdenticon: {self.isIdenticon},
subItems:[
{$self.subItems}
]"""
proc toJsonNode*(self: Item): JsonNode =
result = %* {
"value": self.value,
"title": self.text,
"imageSource": self.image,
"iconName": self.icon,
"iconColor": self.iconColor,
"isIdenticon": self.isIdenticon
}
proc setSubItems*(self: Item, subItems: seq[SubItem]) =
self.subItems.setItems(subItems)
proc getSubItemForValue*(self: Item, value: string): SubItem =
self.subItems.getItemForValue(value)

View File

@ -0,0 +1,81 @@
import NimQml, Tables, strutils
import location_menu_item, location_menu_sub_item
type
ModelRole {.pure.} = enum
Value = UserRole + 1
Title
ImageSource
IconName
IconColor
IsIdenticon
SubItems
QtObject:
type
Model* = ref object of QAbstractListModel
items: seq[Item]
proc delete(self: Model) =
for i in 0 ..< self.items.len:
self.items[i].delete
self.items = @[]
self.QAbstractListModel.delete
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newModel*(): Model =
new(result, delete)
result.setup()
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.Value.int:"value",
ModelRole.Title.int:"title",
ModelRole.ImageSource.int:"imageSource",
ModelRole.IconName.int:"iconName",
ModelRole.IconColor.int:"iconColor",
ModelRole.IsIdenticon.int:"isIdenticon",
ModelRole.SubItems.int:"subItems"
}.toTable
method data(self: Model, 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.ModelRole
case enumRole:
of ModelRole.Value:
result = newQVariant(item.value)
of ModelRole.Title:
result = newQVariant(item.text)
of ModelRole.ImageSource:
result = newQVariant(item.image)
of ModelRole.IconName:
result = newQVariant(item.icon)
of ModelRole.IconColor:
result = newQVariant(item.iconColor)
of ModelRole.IsIdenticon:
result = newQVariant(item.isIdenticon)
of ModelRole.SubItems:
result = newQVariant(item.subItems)
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.items = items
self.endResetModel()
proc getItemForValue*(self: Model, value: string): Item =
for i in self.items:
if (i.value == value):
return i

View File

@ -0,0 +1,34 @@
import json, strformat
import base_item
export base_item
type
SubItem* = ref object of BaseItem
proc initSubItem*(value, text, image, icon, iconColor: string = "", isIdenticon: bool = true): SubItem =
result = SubItem()
result.setup(value, text, image, icon, iconColor, isIdenticon)
proc delete*(self: SubItem) =
self.BaseItem.delete
proc `$`*(self: SubItem): string =
result = fmt"""SearchMenuSubItem(
value: {self.value},
text: {self.text},
imageSource: {self.image},
iconName: {self.icon},
iconColor: {self.iconColor},
isIdenticon: {self.isIdenticon}
]"""
proc toJsonNode*(self: SubItem): JsonNode =
result = %* {
"value": self.value,
"text": self.text,
"imageSource": self.image,
"iconName": self.icon,
"iconColor": self.iconColor,
"isIdenticon": self.isIdenticon
}

View File

@ -0,0 +1,92 @@
import NimQml, Tables, strutils, strformat
import location_menu_sub_item
type
SubModelRole {.pure.} = enum
Value = UserRole + 1
Text
Image
Icon
IconColor
IsIdenticon
QtObject:
type
SubModel* = ref object of QAbstractListModel
items: seq[SubItem]
proc delete*(self: SubModel) =
for i in 0 ..< self.items.len:
self.items[i].delete
self.items = @[]
self.QAbstractListModel.delete
proc setup(self: SubModel) =
self.QAbstractListModel.setup
proc newSubModel*(): SubModel =
new(result, delete)
result.setup()
proc `$`*(self: SubModel): string =
for i in 0 ..< self.items.len:
result &= fmt"""
[{i}]:({$self.items[i]})
"""
proc countChanged*(self: SubModel) {.signal.}
proc count*(self: SubModel): int {.slot.} =
self.items.len
QtProperty[int] count:
read = count
notify = countChanged
method rowCount(self: SubModel, index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: SubModel): Table[int, string] =
{
SubModelRole.Value.int:"value",
SubModelRole.Text.int:"text",
SubModelRole.Image.int:"imageSource",
SubModelRole.Icon.int:"iconName",
SubModelRole.IconColor.int:"iconColor",
SubModelRole.IsIdenticon.int:"isIdenticon"
}.toTable
method data(self: SubModel, 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.SubModelRole
case enumRole:
of SubModelRole.Value:
result = newQVariant(item.value)
of SubModelRole.Text:
result = newQVariant(item.text)
of SubModelRole.Image:
result = newQVariant(item.image)
of SubModelRole.Icon:
result = newQVariant(item.icon)
of SubModelRole.IconColor:
result = newQVariant(item.iconColor)
of SubModelRole.IsIdenticon:
result = newQVariant(item.isIdenticon)
proc setItems*(self: SubModel, items: seq[SubItem]) =
self.beginResetModel()
self.items = items
self.endResetModel()
proc getItemForValue*(self: SubModel, value: string): SubItem =
for i in self.items:
if (i.value == value):
return i

View File

@ -0,0 +1,205 @@
import NimQml
import json, strutils, chronicles
import io_interface
import ../io_interface as delegate_interface
import view, controller
import location_menu_model, location_menu_item
import location_menu_sub_model, location_menu_sub_item
import result_model, result_item
import ../../shared_models/message_item
import ../../../boot/app_sections_config as conf
import ../../../../app_service/service/chat/service as chat_service
import ../../../../app_service/service/community/service as community_service
import ../../../../app_service/service/message/service as message_service
import eventemitter
export io_interface
logScope:
topics = "app-search-module"
# Constants used in this module
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"
type
Module* = ref object of io_interface.AccessInterface
delegate: delegate_interface.AccessInterface
view: View
viewVariant: QVariant
controller: controller.AccessInterface
moduleLoaded: bool
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, chatService: chat_service.Service,
communityService: community_service.Service, messageService: message_service.Service):
Module =
result = Module()
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, chatService, communityService, messageService)
result.moduleLoaded = false
method delete*(self: Module) =
self.view.delete
self.viewVariant.delete
self.controller.delete
method load*(self: Module) =
self.controller.init()
self.view.load()
method isLoaded*(self: Module): bool =
return self.moduleLoaded
method viewDidLoad*(self: Module) =
self.delegate.chatSectionDidLoad()
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
proc buildLocationMenuForChat(self: Module): location_menu_item.Item =
var item = location_menu_item.initItem(conf.CHAT_SECTION_ID, SEARCH_MENU_LOCATION_CHAT_SECTION_NAME, "", "chat", "",
false)
let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat]
let displayedChats = self.controller.getChatDetailsForChatTypes(types)
var subItems: seq[location_menu_sub_item.SubItem]
for c in displayedChats:
var text = if(c.name.endsWith(".stateofus.eth")): c.name[0 .. ^15] else: c.name
let subItem = location_menu_sub_item.initSubItem(c.id, text, c.identicon, "", c.color, c.identicon.len == 0)
subItems.add(subItem)
item.setSubItems(subItems)
return item
proc buildLocationMenuForCommunity(self: Module, community: CommunityDto): location_menu_item.Item =
var item = location_menu_item.initItem(community.id, community.name, community.images.thumbnail, "", community.color,
community.images.thumbnail.len == 0)
var subItems: seq[location_menu_sub_item.SubItem]
let chats = self.controller.getAllChatsForCommunity(community.id)
for c in chats:
let chatDto = self.controller.getChatDetails(community.id, c.id)
let subItem = location_menu_sub_item.initSubItem(chatDto.id, chatDto.name, chatDto.identicon, "", chatDto.color,
chatDto.identicon.len == 0)
subItems.add(subItem)
item.setSubItems(subItems)
return item
method prepareLocationMenuModel*(self: Module) =
var items: seq[location_menu_item.Item]
items.add(self.buildLocationMenuForChat())
let communities = self.controller.getCommunities()
for c in communities:
items.add(self.buildLocationMenuForCommunity(c))
self.view.locationMenuModel().setItems(items)
method onActiveChatChange*(self: Module, sectionId: string, chatId: string) =
self.controller.setActiveSectionIdAndChatId(sectionId, chatId)
method setSearchLocation*(self: Module, location: string, subLocation: string) =
self.controller.setSearchLocation(location, subLocation)
method getSearchLocationObject*(self: Module): string =
## This method returns location and subLocation with their details so we
## may set initial search location on the side of qml.
var jsonObject = %* {
"location": "",
"subLocation": ""
}
if(self.controller.activeSectionId().len == 0):
return Json.encode(jsonObject)
let item = self.view.locationMenuModel().getItemForValue(self.controller.activeSectionId())
if(not item.isNil):
jsonObject["location"] = item.toJsonNode()
if(self.controller.activeChatId().len > 0):
let subItem = item.getSubItemForValue(self.controller.activeChatId())
if(not subItem.isNil):
jsonObject["subLocation"] = subItem.toJsonNode()
return Json.encode(jsonObject)
method searchMessages*(self: Module, searchTerm: string) =
if (searchTerm.len == 0):
self.view.searchResultModel().clear()
return
self.controller.searchMessages(searchTerm)
method onSearchMessagesDone*(self: Module, messages: seq[MessageDto]) =
var items: seq[result_item.Item]
var channels: seq[result_item.Item]
let myPublicKey = "" # This will be updated once we add userProfile #getSetting[string](Setting.PublicKey, "0x0")
# Add communities
let communities = self.controller.getCommunities()
for co in communities:
if(self.controller.searchLocation().len == 0 and co.name.toLower.startsWith(self.controller.searchTerm().toLower)):
let item = result_item.initItem(co.id, "", "", co.id, co.name, SEARCH_RESULT_COMMUNITIES_SECTION_NAME,
co.images.thumbnail, co.color, "", "", co.images.thumbnail, co.color)
items.add(item)
# Add channels
if(self.controller.searchSubLocation().len == 0 and self.controller.searchLocation().len == 0 or
self.controller.searchLocation() == co.name):
for c in co.chats:
let chatDto = self.controller.getChatDetails(co.id, c.id)
if(c.name.toLower.startsWith(self.controller.searchTerm().toLower)):
let item = result_item.initItem(chatDto.id, "", "", chatDto.id, chatDto.name,
SEARCH_RESULT_CHANNELS_SECTION_NAME, chatDto.identicon, chatDto.color, "", "", chatDto.identicon, chatDto.color,
chatDto.identicon.len > 0)
channels.add(item)
# Add chats
if(self.controller.searchLocation().len == 0 or self.controller.searchLocation() == conf.CHAT_SECTION_ID):
let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat]
let displayedChats = self.controller.getChatDetailsForChatTypes(types)
for c in displayedChats:
if(c.name.toLower.startsWith(self.controller.searchTerm().toLower)):
let item = result_item.initItem(c.id, "", "", c.id, c.name, SEARCH_RESULT_CHATS_SECTION_NAME, c.identicon,
c.color, "", "", c.identicon, c.color, c.identicon.len > 0)
items.add(item)
# Add channels in order as requested by the design
items.add(channels)
# Add messages
for m in messages:
if (m.contentType.ContentType != ContentType.Message):
continue
let chatDto = self.controller.getChatDetails("", m.chatId)
let image = if(m.image.len > 0): m.image else: m.identicon
if(chatDto.communityId.len == 0):
let item = result_item.initItem(m.id, m.text, $m.timestamp, m.`from`, m.`from`,
SEARCH_RESULT_MESSAGES_SECTION_NAME, image, "", chatDto.name, "", chatDto.identicon, chatDto.color,
chatDto.identicon.len == 0)
items.add(item)
else:
let community = self.controller.getCommunityById(chatDto.communityId)
let item = result_item.initItem(m.id, m.text, $m.timestamp, m.`from`, m.`from`,
SEARCH_RESULT_MESSAGES_SECTION_NAME, image, "", community.name, chatDto.name, community.images.thumbnail,
community.color, community.images.thumbnail.len == 0)
items.add(item)
self.view.searchResultModel().set(items)

View File

@ -0,0 +1,16 @@
import NimQml
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method isLoaded*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onActiveChatChange*(self: AccessInterface, sectionId: string, chatId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,5 @@
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
# Since nim doesn't support using concepts in second level nested types we
# define delegate interfaces within access interface.

View File

@ -0,0 +1,4 @@
import ../../../../../app_service/service/message/dto/message
method onSearchMessagesDone*(self: AccessInterface, messages: seq[MessageDto]) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,14 @@
method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method prepareLocationMenuModel*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method setSearchLocation*(self: AccessInterface, location: string, subLocation: string) {.base.} =
raise newException(ValueError, "No implementation available")
method getSearchLocationObject*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method searchMessages*(self: AccessInterface, searchTerm: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,90 @@
import strformat
type Item* = object
itemId: string
content: string
time: string
titleId: string
title: string
sectionName: string
image: string
color: string
badgePrimaryText: string
badgeSecondaryText: string
badgeImage: string
badgeIconColor: string
badgeIsLetterIdenticon: bool
proc initItem*(itemId, content, time, titleId, title, sectionName: string, image, color, badgePrimaryText,
badgeSecondaryText, badgeImage, badgeIconColor: string = "", badgeIsLetterIdenticon: bool = false):
Item =
result.itemId = itemId
result.content = content
result.time = time
result.titleId = titleId
result.title = title
result.sectionName = sectionName
result.image = image
result.color = color
result.badgePrimaryText = badgePrimaryText
result.badgeSecondaryText = badgeSecondaryText
result.badgeImage = badgeImage
result.badgeIconColor = badgeIconColor
result.badgeIsLetterIdenticon = badgeIsLetterIdenticon
proc `$`*(self: Item): string =
result = "SearchResultItem("
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"image:{self.image}"
result &= fmt"color:{self.color}"
result &= fmt"badgePrimaryText:{self.badgePrimaryText}"
result &= fmt"badgeSecondaryText:{self.badgeSecondaryText}"
result &= fmt"badgeImage:{self.badgeImage}"
result &= fmt"badgeIconColor:{self.badgeIconColor}"
result &= fmt"badgeIsLetterIdenticon:{self.badgeIsLetterIdenticon}"
result &= ")"
method itemId*(self: Item): string {.inline.} =
return self.itemId
method content*(self: Item): string {.inline.} =
return self.content
method time*(self: Item): string {.inline.} =
return self.time
method titleId*(self: Item): string {.inline.} =
return self.titleId
method title*(self: Item): string {.inline.} =
return self.title
method sectionName*(self: Item): string {.inline.} =
return self.sectionName
method image*(self: Item): string {.inline.} =
return self.image
method color*(self: Item): string {.inline.} =
return self.color
method badgePrimaryText*(self: Item): string {.inline.} =
return self.badgePrimaryText
method badgeSecondaryText*(self: Item): string {.inline.} =
return self.badgeSecondaryText
method badgeImage*(self: Item): string {.inline.} =
return self.badgeImage
method badgeIconColor*(self: Item): string {.inline.} =
return self.badgeIconColor
method badgeIsLetterIdenticon*(self: Item): bool {.inline.} =
return self.badgeIsLetterIdenticon

View File

@ -0,0 +1,120 @@
import NimQml, Tables, strutils
import result_item
type
ModelRole {.pure.} = enum
ItemId = UserRole + 1
Content
Time
TitleId
Title
SectionName
Image
Color
BadgePrimaryText
BadgeSecondaryText
BadgeImage
BadgeIconColor
BadgeIsLetterIdenticon
QtObject:
type
Model* = ref object of QAbstractListModel
resultList: seq[Item]
proc delete(self: Model) =
self.QAbstractListModel.delete
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newModel*(): Model =
new(result, delete)
result.setup()
#################################################
# Properties
#################################################
proc countChanged*(self: Model) {.signal.}
proc count*(self: Model): int {.slot.} =
self.resultList.len
QtProperty[int] count:
read = count
notify = countChanged
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.resultList.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.ItemId.int:"itemId",
ModelRole.Content.int:"content",
ModelRole.Time.int:"time",
ModelRole.TitleId.int:"titleId",
ModelRole.Title.int:"title",
ModelRole.SectionName.int:"sectionName",
ModelRole.Image.int:"image",
ModelRole.Color.int:"color",
ModelRole.BadgePrimaryText.int:"badgePrimaryText",
ModelRole.BadgeSecondaryText.int:"badgeSecondaryText",
ModelRole.BadgeImage.int:"badgeImage",
ModelRole.BadgeIconColor.int:"badgeIconColor",
ModelRole.BadgeIsLetterIdenticon.int:"badgeIsLetterIdenticon"
}.toTable
method data(self: Model, 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.ModelRole
case enumRole:
of ModelRole.ItemId:
result = newQVariant(item.itemId)
of ModelRole.Content:
result = newQVariant(item.content)
of ModelRole.Time:
result = newQVariant(item.time)
of ModelRole.TitleId:
result = newQVariant(item.titleId)
of ModelRole.Title:
result = newQVariant(item.title)
of ModelRole.SectionName:
result = newQVariant(item.sectionName)
of ModelRole.Image:
result = newQVariant(item.image)
of ModelRole.Color:
result = newQVariant(item.color)
of ModelRole.BadgePrimaryText:
result = newQVariant(item.badgePrimaryText)
of ModelRole.BadgeSecondaryText:
result = newQVariant(item.badgeSecondaryText)
of ModelRole.BadgeImage:
result = newQVariant(item.badgeImage)
of ModelRole.BadgeIconColor:
result = newQVariant(item.badgeIconColor)
of ModelRole.BadgeIsLetterIdenticon:
result = newQVariant(item.badgeIsLetterIdentIcon)
proc add*(self: Model, item: Item) =
self.beginInsertRows(newQModelIndex(), self.resultList.len, self.resultList.len)
self.resultList.add(item)
self.endInsertRows()
proc set*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.resultList = items
self.endResetModel()
proc clear*(self: Model) =
self.beginResetModel()
self.resultList = @[]
self.endResetModel()

View File

@ -0,0 +1,61 @@
import NimQml
import result_model, location_menu_model
import io_interface
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
searchResultModel: result_model.Model
searchResultModelVariant: QVariant
locationMenuModel: location_menu_model.Model
locationMenuModelVariant: QVariant
proc delete*(self: View) =
self.searchResultModel.delete
self.searchResultModelVariant.delete
self.locationMenuModel.delete
self.locationMenuModelVariant.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
result.searchResultModel = result_model.newModel()
result.searchResultModelVariant = newQVariant(result.searchResultModel)
result.locationMenuModel = location_menu_model.newModel()
result.locationMenuModelVariant = newQVariant(result.locationMenuModel)
proc load*(self: View) =
self.delegate.viewDidLoad()
proc searchResultModel*(self: View): result_model.Model =
return self.searchResultModel
proc locationMenuModel*(self: View): location_menu_model.Model =
return self.locationMenuModel
proc getSearchResultModel*(self: View): QVariant {.slot.} =
return newQVariant(self.searchResultModel)
QtProperty[QVariant] resultModel:
read = getSearchResultModel
proc getLocationMenuModel*(self: View): QVariant {.slot.} =
newQVariant(self.locationMenuModel)
QtProperty[QVariant] locationMenuModel:
read = getLocationMenuModel
proc prepareLocationMenuModel*(self: View) {.slot.} =
self.delegate.prepareLocationMenuModel()
proc setSearchLocation*(self: View, location: string = "", subLocation: string = "") {.slot.} =
self.delegate.setSearchLocation(location, subLocation)
proc getSearchLocationObject*(self: View): string {.slot.} =
self.delegate.getSearchLocationObject()
proc searchMessages*(self: View, searchTerm: string) {.slot.} =
self.delegate.searchMessages(searchTerm)

View File

@ -8,6 +8,7 @@ import chat_section/module as chat_section_module
import wallet_section/module as wallet_section_module
import browser_section/module as browser_section_module
import profile_section/module as profile_section_module
import app_search/module as app_search_module
import ../../../app_service/service/keychain/service as keychain_service
import ../../../app_service/service/chat/service as chat_service
@ -46,6 +47,7 @@ type
walletSectionModule: wallet_section_module.AccessInterface
browserSectionModule: browser_section_module.AccessInterface
profileSectionModule: profile_section_module.AccessInterface
appSearchModule: app_search_module.AccessInterface
moduleLoaded: bool
proc newModule*[T](
@ -90,6 +92,7 @@ proc newModule*[T](
dappPermissionsService, providerService)
result.profileSectionModule = profile_section_module.newModule(result, events, accountsService, settingsService,
profileService, contactsService, aboutService, languageService, mnemonicService, privacyService)
result.appSearchModule = app_search_module.newModule(result, events, chatService, communityService, messageService)
method delete*[T](self: Module[T]) =
self.chatSectionModule.delete
@ -99,6 +102,7 @@ method delete*[T](self: Module[T]) =
self.communitySectionsModule.clear
self.walletSectionModule.delete
self.browserSectionModule.delete
self.appSearchModule.delete
self.view.delete
self.viewVariant.delete
self.controller.delete
@ -197,6 +201,7 @@ method load*[T](self: Module[T], events: EventEmitter, chatService: chat_service
# self.timelineSectionModule.load()
# self.nodeManagementSectionModule.load()
self.profileSectionModule.load()
self.appSearchModule.load()
# Set active section on app start
self.setActiveSection(activeSection)
@ -304,4 +309,7 @@ method getCommunitySectionModule*[T](self: Module[T], communityId: string): QVar
return self.communitySectionsModule[communityId].getModuleAsVariant()
method onActiveChatChange*[T](self: Module[T], sectionId: string, chatId: string) =
discard
self.appSearchModule.onActiveChatChange(sectionId, chatId)
method getAppSearchModule*[T](self: Module[T]): QVariant =
self.appSearchModule.getModuleAsVariant()

View File

@ -17,4 +17,7 @@ method getChatSectionModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getCommunitySectionModule*(self: AccessInterface, communityId: string): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getAppSearchModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -117,4 +117,10 @@ QtObject:
return communityVariant
proc getChatSectionModule*(self: View): QVariant {.slot.} =
return self.delegate.getChatSectionModule()
return self.delegate.getChatSectionModule()
proc getAppSearchModule(self: View): QVariant {.slot.} =
return self.delegate.getAppSearchModule()
QtProperty[QVariant] appSearchModule:
read = getAppSearchModule

View File

@ -32,7 +32,7 @@ method init*(self: Service) =
proc(x: JsonNode): CommunityDto = x.toCommunityDto())
for community in communities:
self.communities[community.id] = community
self.communities[community.id] = community
except Exception as e:
let errDesription = e.msg
@ -42,6 +42,13 @@ method init*(self: Service) =
method getCommunities*(self: Service): seq[CommunityDto] =
return toSeq(self.communities.values)
method getCommunityById*(self: Service, communityId: string): CommunityDto =
if(not self.communities.hasKey(communityId)):
error "error: requested community doesn't exists"
return
return self.communities[communityId]
method getCommunityIds*(self: Service): seq[string] =
return toSeq(self.communities.keys)
@ -63,7 +70,7 @@ proc sortDesc[T](t1, t2: T): int =
method getCategories*(self: Service, communityId: string, order: SortOrder = SortOrder.Ascending): seq[Category] =
if(not self.communities.contains(communityId)):
error "trying to get community for an unexisting community id"
error "trying to get community categories for an unexisting community id"
return
result = self.communities[communityId].categories
@ -73,8 +80,11 @@ method getCategories*(self: Service, communityId: string, order: SortOrder = Sor
result.sort(sortDesc[Category])
method getChats*(self: Service, communityId: string, categoryId = "", order = SortOrder.Ascending): seq[Chat] =
## By default returns chats which don't belong to any category, for passed `communityId`.
## If `categoryId` is set then only chats belonging to that category for passed `communityId` will be returned.
## Returned chats are sorted by position following set `order` parameter.
if(not self.communities.contains(communityId)):
error "trying to get community for an unexisting community id"
error "trying to get community chats for an unexisting community id"
return
for chat in self.communities[communityId].chats:
@ -87,3 +97,17 @@ method getChats*(self: Service, communityId: string, categoryId = "", order = So
result.sort(sortAsc[Chat])
else:
result.sort(sortDesc[Chat])
method getAllChats*(self: Service, communityId: string, order = SortOrder.Ascending): seq[Chat] =
## Returns all chats belonging to the community with passed `communityId`, sorted by position.
## Returned chats are sorted by position following set `order` parameter.
if(not self.communities.contains(communityId)):
error "trying to get all community chats for an unexisting community id"
return
result = self.communities[communityId].chats
if(order == SortOrder.Ascending):
result.sort(sortAsc[Chat])
else:
result.sort(sortDesc[Chat])

View File

@ -16,6 +16,9 @@ method init*(self: ServiceInterface) {.base.} =
method getCommunities*(self: ServiceInterface): seq[CommunityDto] {.base.} =
raise newException(ValueError, "No implementation available")
method getCommunityById*(self: ServiceInterface, communityId: string): CommunityDto {.base.} =
raise newException(ValueError, "No implementation available")
method getCommunityIds*(self: ServiceInterface): seq[string] {.base.} =
raise newException(ValueError, "No implementation available")
@ -25,4 +28,7 @@ method getCategories*(self: ServiceInterface, communityId: string, order: SortOr
method getChats*(self: ServiceInterface, communityId: string, categoryId = "", order = SortOrder.Ascending): seq[Chat]
{.base.} =
raise newException(ValueError, "No implementation available")
method getAllChats*(self: ServiceInterface, communityId: string, order = SortOrder.Ascending): seq[Chat] {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -43,4 +43,52 @@ const asyncFetchChatMessagesTask: Task = proc(argEncoded: string) {.gcsafe, nimc
"reactions": reactionsArr
}
arg.finish(responseJson)
#################################################
# Async search messages
#################################################
type
AsyncSearchMessagesTaskArg = ref object of QObjectTaskArg
searchTerm: string
caseSensitive: bool
#################################################
# 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)
let response = status_go.fetchAllMessagesFromChatWhichMatchTerm(arg.chatId, arg.searchTerm, arg.caseSensitive)
let responseJson = %*{
"chatId": arg.chatId,
"messages": response.result
}
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)
let response = status_go.fetchAllMessagesFromChatsAndCommunitiesWhichMatchTerm(arg.communityIds, arg.chatIds,
arg.searchTerm, arg.caseSensitive)
let responseJson = %*{
"communityIds": arg.communityIds,
"chatIds": arg.chatIds,
"messages": response.result
}
arg.finish(responseJson)

View File

@ -24,8 +24,12 @@ const MESSAGES_PER_PAGE = 20
const SIGNAL_MESSAGES_LOADED* = "new-messagesLoaded" #Once we are done with refactoring we should remove "new-" from all signals
const SIGNAL_MESSAGE_PINNED* = "new-messagePinned"
const SIGNAL_MESSAGE_UNPINNED* = "new-messageUnpinned"
const SIGNAL_SEARCH_MESSAGES_LOADED* = "new-searchMessagesLoaded"
type
SearchMessagesLoadedArgs* = ref object of Args
messages*: seq[MessageDto]
MessagesLoadedArgs* = ref object of Args
chatId*: string
messages*: seq[MessageDto]
@ -66,7 +70,7 @@ QtObject:
return self.pinnedMsgCursor[chatId]
proc onLoadMoreMessagesForChat*(self: Service, response: string) {.slot.} =
proc onAsyncLoadMoreMessagesForChat*(self: Service, response: string) {.slot.} =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
info "load more messages response is not a json object"
@ -112,15 +116,15 @@ QtObject:
self.events.emit(SIGNAL_MESSAGES_LOADED, data)
proc loadMoreMessagesForChat*(self: Service, chatId: string) =
proc asyncLoadMoreMessagesForChat*(self: Service, chatId: string) =
if (chatId.len == 0):
error "empty chat id", methodName="loadMoreMessagesForChat"
error "empty chat id", methodName="asyncLoadMoreMessagesForChat"
return
let arg = AsyncFetchChatMessagesTaskArg(
tptr: cast[ByteAddress](asyncFetchChatMessagesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onLoadMoreMessagesForChat",
slot: "onAsyncLoadMoreMessagesForChat",
chatId: chatId,
msgCursor: self.getCurrentMessageCursor(chatId),
pinnedMsgCursor: self.getCurrentPinnedMessageCursor(chatId),
@ -129,12 +133,12 @@ QtObject:
self.threadpool.start(arg)
proc loadInitialMessagesForChat*(self: Service, chatId: string) =
proc asyncLoadInitialMessagesForChat*(self: Service, chatId: string) =
if(self.getCurrentMessageCursor(chatId).len > 0):
return
# we're here if initial messages are not loaded yet
self.loadMoreMessagesForChat(chatId)
self.asyncLoadMoreMessagesForChat(chatId)
proc addReaction*(self: Service, chatId: string, messageId: string, emojiId: int):
@ -207,4 +211,75 @@ QtObject:
except Exception as e:
result.error = e.msg
error "error: ", methodName="getDetailsForMessage", errName = e.name, errDesription = e.msg
error "error: ", methodName="getDetailsForMessage", errName = e.name, errDesription = e.msg
proc finishAsyncSearchMessagesWithError*(self: Service, errorMessage: string) =
error "error: ", methodName="onAsyncSearchMessages", errDescription = errorMessage
self.events.emit(SIGNAL_SEARCH_MESSAGES_LOADED, SearchMessagesLoadedArgs())
proc onAsyncSearchMessages*(self: Service, response: string) {.slot.} =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
self.finishAsyncSearchMessagesWithError("search messages response is not an json object")
return
var messagesObj: JsonNode
if (not responseObj.getProp("messages", messagesObj)):
self.finishAsyncSearchMessagesWithError("search messages response doesn't contain messages property")
return
var messagesArray: JsonNode
if (not messagesObj.getProp("messages", messagesArray)):
self.finishAsyncSearchMessagesWithError("search messages response doesn't contain messages array")
return
if (messagesArray.kind != JArray):
self.finishAsyncSearchMessagesWithError("expected messages json array is not of JArray type")
return
var messages = map(messagesArray.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto())
let data = SearchMessagesLoadedArgs(messages: messages)
self.events.emit(SIGNAL_SEARCH_MESSAGES_LOADED, data)
proc asyncSearchMessages*(self: Service, 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):
error "error: empty channel id set for fetching more messages", methodName="asyncSearchMessages"
return
if (searchTerm.len == 0):
return
let arg = AsyncSearchMessagesInChatTaskArg(
tptr: cast[ByteAddress](asyncSearchMessagesInChatTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncSearchMessages",
chatId: chatId,
searchTerm: searchTerm,
caseSensitive: caseSensitive
)
self.threadpool.start(arg)
proc asyncSearchMessages*(self: Service, communityIds: seq[string], chatIds: seq[string], searchTerm: string,
caseSensitive: bool) =
## Asynchronous search for messages which contain the searchTerm and belong to any chat/channel from chatIds array
## or any channel of community from communityIds array.
if (communityIds.len == 0 and chatIds.len == 0):
error "either community ids or chat ids or both must be set", methodName="asyncSearchMessages"
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.threadpool.start(arg)

View File

@ -52,21 +52,21 @@ StatusAppThreePanelLayout {
StatusSearchLocationMenu {
id: searchPopupMenu
searchPopup: searchPopup
locationModel: root.rootStore.chatsModelInst.messageSearchViewController.locationMenuModel
locationModel: mainModule.appSearchModule.locationMenuModel
onItemClicked: {
root.rootStore.chatsModelInst.messageSearchViewController.setSearchLocation(firstLevelItemValue, secondLevelItemValue)
mainModule.appSearchModule.setSearchLocation(firstLevelItemValue, secondLevelItemValue)
if(searchPopup.searchText !== "")
searchMessages(searchPopup.searchText)
}
}
property var searchMessages: Backpressure.debounce(searchPopup, 400, function (value) {
root.rootStore.chatsModelInst.messageSearchViewController.searchMessages(value)
mainModule.appSearchModule.searchMessages(value)
})
Connections {
target: root.rootStore.chatsModelInst.messageSearchViewController.locationMenuModel
target: mainModule.appSearchModule.locationMenuModel
onModelAboutToBeReset: {
for (var i = 2; i <= searchPopupMenu.count; i++) {
//clear menu
@ -84,7 +84,7 @@ StatusAppThreePanelLayout {
defaultSearchLocationText: qsTr("Anywhere")
searchOptionsPopupMenu: searchPopupMenu
searchResults: root.rootStore.chatsModelInst.messageSearchViewController.resultModel
searchResults: mainModule.appSearchModule.resultModel
formatTimestampFn: function (ts) {
return new Date(parseInt(ts, 10)).toLocaleString(Qt.locale(localAppSettings.locale))
@ -104,9 +104,9 @@ StatusAppThreePanelLayout {
onOpened: {
searchPopup.resetSearchSelection();
searchPopup.forceActiveFocus()
root.rootStore.chatsModelInst.messageSearchViewController.prepareLocationMenuModel()
mainModule.appSearchModule.prepareLocationMenuModel()
const jsonObj = root.rootStore.chatsModelInst.messageSearchViewController.getSearchLocationObject()
const jsonObj = mainModule.appSearchModule.getSearchLocationObject()
if (!jsonObj) {
return
@ -115,7 +115,7 @@ StatusAppThreePanelLayout {
let obj = JSON.parse(jsonObj)
if (obj.location === "") {
if(obj.subLocation === "") {
root.rootStore.chatsModelInst.messageSearchViewController.setSearchLocation("", "")
mainModule.appSearchModule.setSearchLocation("", "")
}
else {
searchPopup.setSearchSelection(obj.subLocation.text,
@ -125,7 +125,7 @@ StatusAppThreePanelLayout {
obj.subLocation.iconName,
obj.subLocation.identiconColor)
root.rootStore.chatsModelInst.messageSearchViewController.setSearchLocation("", obj.subLocation.value)
mainModule.appSearchModule.setSearchLocation("", obj.subLocation.value)
}
}
else {
@ -137,7 +137,7 @@ StatusAppThreePanelLayout {
obj.subLocation.iconName,
obj.subLocation.identiconColor)
root.rootStore.chatsModelInst.messageSearchViewController.setSearchLocation(obj.location.value, obj.subLocation.value)
mainModule.appSearchModule.setSearchLocation(obj.location.value, obj.subLocation.value)
}
else {
searchPopup.setSearchSelection(obj.location.title,
@ -147,7 +147,7 @@ StatusAppThreePanelLayout {
obj.location.iconName,
obj.location.identiconColor)
root.rootStore.chatsModelInst.messageSearchViewController.setSearchLocation(obj.location.value, obj.subLocation.value)
mainModule.appSearchModule.setSearchLocation(obj.location.value, obj.subLocation.value)
}
}
}