refactor(online-users): adding online users for chat/channel

This commit is contained in:
Sale Djenic 2021-11-30 15:49:45 +01:00
parent afe6d34735
commit f138fecdd2
35 changed files with 763 additions and 325 deletions

View File

@ -362,6 +362,7 @@ proc load(self: AppController) =
# load main module
self.mainModule.load(
self.statusFoundation.status.events,
self.contactsService,
self.chatService,
self.communityService,
self.messageService

View File

@ -4,7 +4,7 @@ import
proc handleSignals(self: ChatController) =
self.status.events.on(SignalType.Message.event) do(e:Args):
var data = MessageSignal(e)
self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages, data.activityCenterNotification, data.statusUpdates, data.deletedMessages)
#self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages, data.activityCenterNotification, data.statusUpdates, data.deletedMessages)
self.status.events.on(SignalType.DiscoverySummary.event) do(e:Args):
## Handle mailserver peers being added and removed
@ -39,8 +39,9 @@ proc handleSignals(self: ChatController) =
self.view.messageView.messageList[chatId].checkTimeout(messageId)
self.status.events.on(SignalType.CommunityFound.event) do(e: Args):
var data = CommunitySignal(e)
self.view.communities.addCommunityToList(data.community)
discard
# var data = CommunitySignal(e)
# self.view.communities.addCommunityToList(data.community)
self.status.events.on(SignalType.MailserverRequestCompleted.event) do(e:Args):
# TODO: if the signal contains a cursor, request additional messages

View File

@ -2,13 +2,13 @@ import json
import base
import status/types/community
import ../../../../app_service/service/community/dto/[community]
import signal_type
type CommunitySignal* = ref object of Signal
community*: Community
community*: CommunityDto
proc fromEvent*(T: type CommunitySignal, event: JsonNode): CommunitySignal =
result = CommunitySignal()
result.signalType = SignalType.CommunityFound
result.community = event["event"].toCommunity()
result.community = event["event"].toCommunityDto()

View File

@ -2,20 +2,26 @@ import json
import base
import status/types/[message, chat, community, profile, installation,
activity_center_notification, status_update, removed_message]
# Step by step we should remove all these types from `status-lib`
import status/types/[installation, activity_center_notification, removed_message]
import status/types/community as old_community
import ../../../../app_service/service/message/dto/[message, pinned_message, reaction]
import ../../../../app_service/service/chat/dto/[chat]
import ../../../../app_service/service/community/dto/[community]
import ../../../../app_service/service/contacts/dto/[contacts, status_update]
type MessageSignal* = ref object of Signal
messages*: seq[Message]
pinnedMessages*: seq[Message]
chats*: seq[Chat]
contacts*: seq[Profile]
messages*: seq[MessageDto]
pinnedMessages*: seq[PinnedMessageDto]
chats*: seq[ChatDto]
contacts*: seq[ContactsDto]
installations*: seq[Installation]
emojiReactions*: seq[Reaction]
communities*: seq[Community]
membershipRequests*: seq[CommunityMembershipRequest]
emojiReactions*: seq[ReactionDto]
communities*: seq[CommunityDto]
membershipRequests*: seq[old_community.CommunityMembershipRequest]
activityCenterNotification*: seq[ActivityCenterNotification]
statusUpdates*: seq[StatusUpdate]
statusUpdates*: seq[StatusUpdateDto]
deletedMessages*: seq[RemovedMessage]
proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
@ -25,27 +31,21 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
if event["event"]{"contacts"} != nil:
for jsonContact in event["event"]["contacts"]:
signal.contacts.add(jsonContact.toProfile())
var chatsWithMentions: seq[string] = @[]
signal.contacts.add(jsonContact.toContactsDto())
if event["event"]{"messages"} != nil:
for jsonMsg in event["event"]["messages"]:
var message = jsonMsg.toMessage()
if message.hasMention:
chatsWithMentions.add(message.chatId)
var message = jsonMsg.toMessageDto()
signal.messages.add(message)
if event["event"]{"chats"} != nil:
for jsonChat in event["event"]["chats"]:
var chat = jsonChat.toChat
if chatsWithMentions.contains(chat.id):
chat.mentionsCount.inc
var chat = jsonChat.toChatDto()
signal.chats.add(chat)
if event["event"]{"statusUpdates"} != nil:
for jsonStatusUpdate in event["event"]["statusUpdates"]:
var statusUpdate = jsonStatusUpdate.toStatusUpdate
var statusUpdate = jsonStatusUpdate.toStatusUpdateDto()
signal.statusUpdates.add(statusUpdate)
if event["event"]{"installations"} != nil:
@ -54,11 +54,11 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
if event["event"]{"emojiReactions"} != nil:
for jsonReaction in event["event"]["emojiReactions"]:
signal.emojiReactions.add(jsonReaction.toReaction)
signal.emojiReactions.add(jsonReaction.toReactionDto())
if event["event"]{"communities"} != nil:
for jsonCommunity in event["event"]["communities"]:
signal.communities.add(jsonCommunity.toCommunity)
signal.communities.add(jsonCommunity.toCommunityDto())
if event["event"]{"requestsToJoinCommunity"} != nil:
for jsonCommunity in event["event"]["requestsToJoinCommunity"]:
@ -73,23 +73,25 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
signal.activityCenterNotification.add(jsonNotification.toActivityCenterNotification())
if event["event"]{"pinMessages"} != nil:
for jsonPinnedMessage in event["event"]["pinMessages"]:
var contentType: ContentType
try:
contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt)
except:
contentType = ContentType.Message
signal.pinnedMessages.add(Message(
id: jsonPinnedMessage{"message_id"}.getStr,
chatId: jsonPinnedMessage{"chat_id"}.getStr,
localChatId: jsonPinnedMessage{"localChatId"}.getStr,
pinnedBy: jsonPinnedMessage{"from"}.getStr,
identicon: jsonPinnedMessage{"identicon"}.getStr,
alias: jsonPinnedMessage{"alias"}.getStr,
clock: jsonPinnedMessage{"clock"}.getInt,
isPinned: jsonPinnedMessage{"pinned"}.getBool,
contentType: contentType
))
discard
# Need to refactor this
# for jsonPinnedMessage in event["event"]["pinMessages"]:
# var contentType: ContentType
# try:
# contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt)
# except:
# contentType = ContentType.Message
# signal.pinnedMessages.add(Message(
# id: jsonPinnedMessage{"message_id"}.getStr,
# chatId: jsonPinnedMessage{"chat_id"}.getStr,
# localChatId: jsonPinnedMessage{"localChatId"}.getStr,
# pinnedBy: jsonPinnedMessage{"from"}.getStr,
# identicon: jsonPinnedMessage{"identicon"}.getStr,
# alias: jsonPinnedMessage{"alias"}.getStr,
# clock: jsonPinnedMessage{"clock"}.getInt,
# isPinned: jsonPinnedMessage{"pinned"}.getBool,
# contentType: contentType
# ))
result = signal

View File

@ -33,6 +33,8 @@ QtObject:
self.activeSubItemChanged()
proc getId(self: ActiveItem): string {.slot.} =
if(self.item.isNil):
return ""
return self.item.id
QtProperty[string] id:
@ -48,54 +50,72 @@ QtObject:
read = getIsSubItemActive
proc getName(self: ActiveItem): string {.slot.} =
if(self.item.isNil):
return ""
return self.item.name
QtProperty[string] name:
read = getName
proc getIcon(self: ActiveItem): string {.slot.} =
if(self.item.isNil):
return ""
return self.item.icon
QtProperty[string] icon:
read = getIcon
proc getColor(self: ActiveItem): string {.slot.} =
if(self.item.isNil):
return ""
return self.item.color
QtProperty[string] color:
read = getColor
proc getDescription(self: ActiveItem): string {.slot.} =
if(self.item.isNil):
return ""
return self.item.description
QtProperty[string] description:
read = getDescription
proc getType(self: ActiveItem): int {.slot.} =
if(self.item.isNil):
return 0
return self.item.`type`
QtProperty[int] type:
read = getType
proc getHasUnreadMessages(self: ActiveItem): bool {.slot.} =
if(self.item.isNil):
return false
return self.item.hasUnreadMessages
QtProperty[bool] hasUnreadMessages:
read = getHasUnreadMessages
proc getNotificationCount(self: ActiveItem): int {.slot.} =
if(self.item.isNil):
return 0
return self.item.notificationsCount
QtProperty[int] notificationCount:
read = getNotificationCount
proc getMuted(self: ActiveItem): bool {.slot.} =
if(self.item.isNil):
return false
return self.item.muted
QtProperty[bool] muted:
read = getMuted
proc getPosition(self: ActiveItem): int {.slot.} =
if(self.item.isNil):
return 0
return self.item.position
QtProperty[int] position:

View File

@ -18,18 +18,20 @@ type
events: EventEmitter
chatId: string
belongsToCommunity: bool
isUsersListAvailable: bool #users list is not available for 1:1 chat
chatService: chat_service.ServiceInterface
communityService: community_service.ServiceInterface
messageService: message_service.Service
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, chatId: string,
belongsToCommunity: bool, chatService: chat_service.ServiceInterface,
belongsToCommunity: bool, isUsersListAvailable: bool, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service): Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.chatId = chatId
result.belongsToCommunity = belongsToCommunity
result.isUsersListAvailable = isUsersListAvailable
result.chatService = chatService
result.communityService = communityService
result.messageService = messageService
@ -67,4 +69,7 @@ method unpinMessage*(self: Controller, messageId: string) =
method getMessageDetails*(self: Controller, messageId: string):
tuple[message: MessageDto, reactions: seq[ReactionDto], error: string] =
return self.messageService.getDetailsForMessage(self.chatId, messageId)
return self.messageService.getDetailsForMessage(self.chatId, messageId)
method isUsersListAvailable*(self: Controller): bool =
return self.isUsersListAvailable

View File

@ -21,4 +21,7 @@ method unpinMessage*(self: AccessInterface, messageId: string) {.base.} =
method getMessageDetails*(self: AccessInterface, messageId: string):
tuple[message: MessageDto, reactions: seq[ReactionDto], error: string] {.base.} =
raise newException(ValueError, "No implementation available")
method isUsersListAvailable*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -10,6 +10,7 @@ import input_area/module as input_area_module
import messages/module as messages_module
import users/module as users_module
import ../../../../../app_service/service/contacts/service as contact_service
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
@ -32,7 +33,8 @@ type
usersModule: users_module.AccessInterface
moduleLoaded: bool
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, chatId: string, belongsToCommunity: bool,
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service):
Module =
@ -40,14 +42,15 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, chatId, belongsToCommunity, chatService, communityService,
messageService)
result.controller = controller.newController(result, events, chatId, belongsToCommunity, isUsersListAvailable,
chatService, communityService, messageService)
result.moduleLoaded = false
result.inputAreaModule = input_area_module.newModule(result, chatId, belongsToCommunity, chatService, communityService)
result.messagesModule = messages_module.newModule(result, events, chatId, belongsToCommunity, chatService,
communityService, messageService)
result.usersModule = users_module.newModule(result, chatId, belongsToCommunity, chatService, communityService)
result.usersModule = users_module.newModule(result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable,
contactService, communityService, messageService)
method delete*(self: Module) =
self.inputAreaModule.delete
@ -147,4 +150,6 @@ method onPinMessage*(self: Module, messageId: string) =
return
self.view.model.appendItem(item)
method isUsersListAvailable*(self: Module): bool =
self.controller.isUsersListAvailable()

View File

@ -13,4 +13,7 @@ method getUsersModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method unpinMessage*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method isUsersListAvailable*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,33 +1,80 @@
import sequtils, sugar
import controller_interface
import io_interface
import ../../../../../../app_service/service/contacts/service as contact_service
import ../../../../../../app_service/service/community/service_interface as community_service
import ../../../../../../app_service/service/message/service as message_service
import eventemitter
export controller_interface
type
Controller* = ref object of controller_interface.AccessInterface
delegate: io_interface.AccessInterface
events: EventEmitter
sectionId: string
chatId: string
belongsToCommunity: bool
isUsersListAvailable: bool #users list is not available for 1:1 chat
contactService: contact_service.Service
communityService: community_service.ServiceInterface
messageService: message_service.Service
proc newController*(delegate: io_interface.AccessInterface, chatId: string, belongsToCommunity: bool,
communityService: community_service.ServiceInterface): Controller =
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service,
communityService: community_service.ServiceInterface, messageService: message_service.Service):
Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.sectionId = sectionId
result.chatId = chatId
result.belongsToCommunity = belongsToCommunity
result.isUsersListAvailable = isUsersListAvailable
result.contactService = contactService
result.communityService = communityService
result.messageService = messageService
method delete*(self: Controller) =
discard
method init*(self: Controller) =
discard
if(self.isUsersListAvailable):
self.events.on(SIGNAL_MESSAGES_LOADED) do(e:Args):
let args = MessagesLoadedArgs(e)
if(self.chatId != args.chatId):
return
method getChatId*(self: Controller): string =
return self.chatId
self.delegate.newMessagesLoaded(args.messages)
method belongsToCommunity*(self: Controller): bool =
return self.belongsToCommunity
self.events.on(SIGNAL_CONTACT_NICKNAME_CHANGED) do(e: Args):
var args = ContactNicknameUpdatedArgs(e)
self.delegate.contactNicknameChanged(args.contactId, args.nickname)
self.events.on(SIGNAL_CONTACTS_STATUS_UPDATED) do(e: Args):
var args = ContactsStatusUpdatedArgs(e)
self.delegate.contactsStatusUpdated(args.statusUpdates)
self.events.on(SIGNAL_CONTACT_UPDATED) do(e: Args):
var args = ContactUpdatedArgs(e)
self.delegate.contactUpdated(args.contact)
self.events.on(SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED) do(e: Args):
self.delegate.loggedInUserImageChanged()
method getMembersPublicKeys*(self: Controller): seq[string] =
# in case of chat section, there is no a members list
if(not self.belongsToCommunity):
return
let communityDto = self.communityService.getCommunityById(self.sectionId)
result = communityDto.members.map(x => x.id)
method getContactNameAndImage*(self: Controller, contactId: string):
tuple[name: string, image: string, isIdenticon: bool] =
return self.contactService.getContactNameAndImage(contactId)
method getStatusForContact*(self: Controller, contactId: string): StatusUpdateDto =
return self.contactService.getStatusForContactWithId(contactId)

View File

@ -1,4 +1,4 @@
import ../../../../../../app_service/service/community/service_interface as community_service
import ../../../../../../app_service/service/contacts/service as contacts_service
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -13,7 +13,12 @@ method init*(self: AccessInterface) {.base.} =
method getChatId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method belongsToCommunity*(self: AccessInterface): bool {.base.} =
method getMembersPublicKeys*(self: AccessInterface): seq[string] {.base.} =
raise newException(ValueError, "No implementation available")
method getContactNameAndImage*(self: AccessInterface, contactId: string):
tuple[name: string, image: string, isIdenticon: bool] {.base.} =
raise newException(ValueError, "No implementation available")
method getStatusForContact*(self: AccessInterface, contactId: string): StatusUpdateDto {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,24 +1,26 @@
type
OnlineStatus* {.pure.} = enum
Online = 0
Idle
Offline = 0
Online
DoNotDisturb
Idle
Invisible
Offline
type
Item* = ref object
id: string
name: string
onlineStatus: OnlineStatus
identicon: string
icon: string
isIdenticon: bool
proc initItem*(id: string, name: string, onlineStatus: OnlineStatus, identicon: string): Item =
proc initItem*(id: string, name: string, onlineStatus: OnlineStatus, icon: string, isidenticon: bool): Item =
result = Item()
result.id = id
result.name = name
result.onlineStatus = onlineStatus
result.identicon = identicon
result.icon = icon
result.isIdenticon = isidenticon
proc id*(self: Item): string {.inline.} =
self.id
@ -35,5 +37,14 @@ proc onlineStatus*(self: Item): OnlineStatus {.inline.} =
proc `onlineStatus=`*(self: Item, value: OnlineStatus) {.inline.} =
self.onlineStatus = value
proc identicon*(self: Item): string {.inline.} =
self.identicon
proc icon*(self: Item): string {.inline.} =
self.icon
proc `icon=`*(self: Item, value: string) {.inline.} =
self.icon = value
proc isIdenticon*(self: Item): bool {.inline.} =
self.isIdenticon
proc `isIdenticon=`*(self: Item, value: bool) {.inline.} =
self.isIdenticon = value

View File

@ -7,7 +7,8 @@ type
Id = UserRole + 1
Name
OnlineStatus
Identicon
Icon
IsIdenticon
QtObject:
type
@ -33,7 +34,8 @@ QtObject:
ModelRole.Id.int:"id",
ModelRole.Name.int:"name",
ModelRole.OnlineStatus.int:"onlineStatus",
ModelRole.Identicon.int:"identicon",
ModelRole.Icon.int:"icon",
ModelRole.IsIdenticon.int:"isIdenticon",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -53,13 +55,30 @@ QtObject:
result = newQVariant(item.name)
of ModelRole.OnlineStatus:
result = newQVariant(item.onlineStatus.int)
of ModelRole.Identicon:
result = newQVariant(item.identicon)
of ModelRole.Icon:
result = newQVariant(item.icon)
of ModelRole.IsIdenticon:
result = newQVariant(item.isIdenticon)
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.items = items
self.endResetModel()
proc addItem*(self: Model, item: Item) =
# we need to maintain online contact on top, that means
# if we add an item online status we add it as the last online item (before the first offline item)
# if we add an item with offline status we add it as the first offline item (after the last online item)
var position = -1
for i in 0 ..< self.items.len:
if(self.items[i].onlineStatus == OnlineStatus.Offline):
position = i
break
if(position == -1):
position = self.items.len
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginInsertRows(parentModelIndex, position, position)
self.items.insert(item, position)
self.endInsertRows()
proc findIndexForMessageId(self: Model, id: string): int =
for i in 0 ..< self.items.len:
@ -68,6 +87,17 @@ QtObject:
return -1
proc removeItemWithIndex(self: Model, index: int) =
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginRemoveRows(parentModelIndex, index, index)
self.items.delete(index)
self.endRemoveRows()
proc isContactWithIdAdded*(self: Model, id: string): bool =
return self.findIndexForMessageId(id) != -1
proc setName*(self: Model, id: string, name: string) =
let ind = self.findIndexForMessageId(id)
if(ind == -1):
@ -78,12 +108,38 @@ QtObject:
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.Name.int])
proc setIcon*(self: Model, id: string, icon: string, isIdenticon: bool) =
let ind = self.findIndexForMessageId(id)
if(ind == -1):
return
self.items[ind].icon = icon
self.items[ind].isIdenticon = isIdenticon
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.Icon.int, ModelRole.IsIdenticon.int])
proc updateItem*(self: Model, id: string, name: string, icon: string, isIdenticon: bool) =
let ind = self.findIndexForMessageId(id)
if(ind == -1):
return
self.items[ind].name = name
self.items[ind].icon = icon
self.items[ind].isIdenticon = isIdenticon
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.Name.int, ModelRole.Icon.int, ModelRole.IsIdenticon.int])
proc setOnlineStatus*(self: Model, id: string, onlineStatus: OnlineStatus) =
let ind = self.findIndexForMessageId(id)
if(ind == -1):
return
self.items[ind].onlineStatus = onlineStatus
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.OnlineStatus.int])
if(self.items[ind].onlineStatus == onlineStatus):
return
var item = self.items[ind]
item.onlineStatus = onlineStatus
self.removeItemWithIndex(ind)
self.addItem(item)

View File

@ -1,11 +1,14 @@
import NimQml
import io_interface
import ../io_interface as delegate_interface
import view, controller
import view, item, model, controller
import ../../../../../global/global_singleton
import ../../../../../../app_service/service/chat/service_interface as chat_service
import ../../../../../../app_service/service/contacts/service as contact_service
import ../../../../../../app_service/service/community/service_interface as community_service
import ../../../../../../app_service/service/message/service as message_service
import eventemitter
export io_interface
@ -17,14 +20,16 @@ type
controller: controller.AccessInterface
moduleLoaded: bool
proc newModule*(delegate: delegate_interface.AccessInterface, chatId: string, belongsToCommunity: bool,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface):
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service,
communityService: community_service.ServiceInterface, 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, chatId, belongsToCommunity, communityService)
result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable,
contactService, communityService, messageService)
result.moduleLoaded = false
method delete*(self: Module) =
@ -33,8 +38,6 @@ method delete*(self: Module) =
self.controller.delete
method load*(self: Module) =
singletonInstance.engine.setRootContextProperty("usersModule", self.viewVariant)
self.controller.init()
self.view.load()
@ -42,8 +45,56 @@ method isLoaded*(self: Module): bool =
return self.moduleLoaded
method viewDidLoad*(self: Module) =
# add me as the first user to the list
let loggedInUserDisplayName = singletonInstance.userProfile.getName() & "(You)"
self.view.model().addItem(initItem(singletonInstance.userProfile.getPubKey(), loggedInUserDisplayName,
OnlineStatus.Online, singletonInstance.userProfile.getIcon(), singletonInstance.userProfile.getIsIdenticon()))
# add other memebers
let usersKeys = self.controller.getMembersPublicKeys()
for k in usersKeys:
let (name, image, isIdenticon) = self.controller.getContactNameAndImage(k)
let statusUpdateDto = self.controller.getStatusForContact(k)
let status = statusUpdateDto.statusType.int.OnlineStatus
self.view.model().addItem(initItem(k, name, status, image, isidenticon))
self.moduleLoaded = true
self.delegate.usersDidLoad()
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
return self.viewVariant
method newMessagesLoaded*(self: Module, messages: seq[MessageDto]) =
for m in messages:
if(self.view.model().isContactWithIdAdded(m.`from`)):
continue
let (name, image, isIdenticon) = self.controller.getContactNameAndImage(m.`from`)
let statusUpdateDto = self.controller.getStatusForContact(m.`from`)
let status = statusUpdateDto.statusType.int.OnlineStatus
self.view.model().addItem(initItem(m.`from`, name, status, image, isidenticon))
method contactNicknameChanged*(self: Module, publicKey: string, nickname: string) =
if(nickname.len == 0):
let (name, _, _) = self.controller.getContactNameAndImage(publicKey)
self.view.model().setName(publicKey, name)
else:
self.view.model().setName(publicKey, nickname)
method contactsStatusUpdated*(self: Module, statusUpdates: seq[StatusUpdateDto]) =
for s in statusUpdates:
let status = s.statusType.int.OnlineStatus
self.view.model().setOnlineStatus(s.publicKey, status)
method contactUpdated*(self: Module, contact: ContactsDto) =
var icon = contact.identicon
var isIdenticon = contact.identicon.len > 0
if(contact.image.thumbnail.len > 0):
icon = contact.image.thumbnail
isIdenticon = false
self.view.model().updateItem(contact.id, contact.userNameOrAlias(), icon, isIdenticon)
method loggedInUserImageChanged*(self: Module) =
self.view.model().setIcon(singletonInstance.userProfile.getPubKey(), singletonInstance.userProfile.getThumbnailImage(),
singletonInstance.userProfile.getIsIdenticon())

View File

@ -0,0 +1,17 @@
import ../../../../../../../app_service/service/message/dto/[message]
import ../../../../../../../app_service/service/contacts/dto/[contacts, status_update]
method newMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method contactNicknameChanged*(self: AccessInterface, publicKey: string, nickname: string) {.base.} =
raise newException(ValueError, "No implementation available")
method contactsStatusUpdated*(self: AccessInterface, statusUpdates: seq[StatusUpdateDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method contactUpdated*(self: AccessInterface, contact: ContactsDto) {.base.} =
raise newException(ValueError, "No implementation available")
method loggedInUserImageChanged*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -7,9 +7,11 @@ QtObject:
View* = ref object of QObject
delegate: io_interface.AccessInterface
model: Model
modelVariant: QVariant
proc delete*(self: View) =
self.model.delete
self.modelVariant.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
@ -17,6 +19,19 @@ QtObject:
result.QObject.setup
result.delegate = delegate
result.model = newModel()
result.modelVariant = newQVariant(result.model)
proc model*(self: View): Model =
return self.model
proc load*(self: View) =
self.delegate.viewDidLoad()
self.delegate.viewDidLoad()
proc modelChanged*(self: View) {.signal.}
proc getModel(self: View): QVariant {.slot.} =
return self.modelVariant
QtProperty[QVariant] model:
read = getModel
notify = modelChanged

View File

@ -52,4 +52,7 @@ QtObject:
read = getModel
proc unpinMessage*(self: View, messageId: string) {.slot.} =
self.delegate.unpinMessage(messageId)
self.delegate.unpinMessage(messageId)
proc isUsersListAvailable*(self: View): bool {.slot.} =
return self.delegate.isUsersListAvailable()

View File

@ -3,6 +3,7 @@ import Tables
import controller_interface
import io_interface
import ../../../../app_service/service/contacts/service as contact_service
import ../../../../app_service/service/chat/service_interface as chat_service
import ../../../../app_service/service/community/service_interface as community_service
import ../../../../app_service/service/message/service as message_service
@ -19,19 +20,20 @@ type
activeItemId: string
activeSubItemId: string
events: EventEmitter
contactService: contact_service.Service
chatService: chat_service.ServiceInterface
communityService: community_service.ServiceInterface
messageService: message_service.Service
proc newController*(delegate: io_interface.AccessInterface, sectionId: string, isCommunity: bool, events: EventEmitter,
chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface,
messageService: message_service.Service): Controller =
contactService: contact_service.Service, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service): Controller =
result = Controller()
result.delegate = delegate
result.sectionId = sectionId
result.isCommunitySection = isCommunity
result.events = events
result.contactService = contactService
result.chatService = chatService
result.communityService = communityService
result.messageService = messageService
@ -87,5 +89,5 @@ method getOneToOneChatNameAndImage*(self: Controller, chatId: string):
method createPublicChat*(self: Controller, chatId: string) =
let response = self.chatService.createPublicChat(chatId)
if(response.success):
self.delegate.addNewPublicChat(response.chatDto, self.events, self.chatService, self.communityService,
self.messageService)
self.delegate.addNewPublicChat(response.chatDto, self.events, self.contactService, self.chatService,
self.communityService, self.messageService)

View File

@ -5,6 +5,7 @@ import view, controller, item, sub_item, model, sub_model
import chat_content/module as chat_content_module
import ../../../../app_service/service/contacts/service as contact_service
import ../../../../app_service/service/chat/service_interface as chat_service
import ../../../../app_service/service/community/service_interface as community_service
import ../../../../app_service/service/message/service as message_service
@ -31,6 +32,7 @@ proc newModule*(
events: EventEmitter,
sectionId: string,
isCommunity: bool,
contactService: contact_service.Service,
chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface,
messageService: message_service.Service
@ -39,8 +41,8 @@ proc newModule*(
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, sectionId, isCommunity, events, chatService, communityService,
messageService)
result.controller = controller.newController(result, sectionId, isCommunity, events, contactService, chatService,
communityService, messageService)
result.moduleLoaded = false
result.chatContentModule = initOrderedTable[string, chat_content_module.AccessInterface]()
@ -56,14 +58,15 @@ method delete*(self: Module) =
method isCommunity*(self: Module): bool =
return self.controller.isCommunity()
proc addSubmodule(self: Module, chatId: string, belongToCommunity: bool, events: EventEmitter,
proc addSubmodule(self: Module, chatId: string, belongToCommunity: bool, isUsersListAvailable: bool, events: EventEmitter,
contactService: contact_service.Service, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) =
self.chatContentModule[chatId] = chat_content_module.newModule(self, events, self.controller.getMySectionId(), chatId,
belongToCommunity, isUsersListAvailable, contactService, chatService, communityService, messageService)
proc buildChatUI(self: Module, events: EventEmitter, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) =
self.chatContentModule[chatId] = chat_content_module.newModule(self, events, chatId, belongToCommunity, chatService,
communityService, messageService)
proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) =
let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat]
let chats = self.controller.getChatDetailsForChatTypes(types)
@ -74,13 +77,15 @@ proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.S
var chatName = c.name
var chatImage = c.identicon
var isIdenticon = false
var isUsersListAvailable = true
if(c.chatType == ChatType.OneToOne):
isUsersListAvailable = false
(chatName, chatImage, isIdenticon) = self.controller.getOneToOneChatNameAndImage(c.id)
let item = initItem(c.id, chatName, chatImage, isIdenticon, c.color, c.description, c.chatType.int, hasNotification,
notificationsCount, c.muted, false, 0)
self.view.appendItem(item)
self.addSubmodule(c.id, false, events, chatService, communityService, messageService)
self.addSubmodule(c.id, false, isUsersListAvailable, events, contactService, chatService, communityService, messageService)
# make the first Public chat active when load the app
if(selectedItemId.len == 0 and c.chatType == ChatType.Public):
@ -88,8 +93,9 @@ proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.S
self.setActiveItemSubItem(selectedItemId, "")
proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) =
proc buildCommunityUI(self: Module, events: EventEmitter, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) =
var selectedItemId = ""
var selectedSubItemId = ""
let communityIds = self.controller.getCommunityIds()
@ -107,7 +113,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv
let channelItem = initItem(chatDto.id, chatDto.name, chatDto.identicon, false, chatDto.color, chatDto.description,
chatDto.chatType.int, hasNotification, notificationsCount, chatDto.muted, false, c.position)
self.view.appendItem(channelItem)
self.addSubmodule(chatDto.id, true, events, chatService, communityService, messageService)
self.addSubmodule(chatDto.id, true, false, events, contactService, chatService, communityService, messageService)
# make the first channel which doesn't belong to any category active when load the app
if(selectedItemId.len == 0):
@ -133,7 +139,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv
let channelItem = initSubItem(chatDto.id, cat.id, chatDto.name, chatDto.identicon, false, chatDto.color,
chatDto.description, hasNotification, notificationsCount, chatDto.muted, false, c.position)
categoryChannels.add(channelItem)
self.addSubmodule(chatDto.id, true, events, chatService, communityService, messageService)
self.addSubmodule(chatDto.id, true, false, events, contactService, chatService, communityService, messageService)
# in case there is no channels beyond categories,
# make the first channel of the first category active when load the app
@ -148,15 +154,16 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv
self.setActiveItemSubItem(selectedItemId, selectedSubItemId)
method load*(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) =
method load*(self: Module, events: EventEmitter, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) =
self.controller.init()
self.view.load()
if(self.controller.isCommunity()):
self.buildCommunityUI(events, chatService, communityService, messageService)
self.buildCommunityUI(events, contactService, chatService, communityService, messageService)
else:
self.buildChatUI(events, chatService, communityService, messageService)
self.buildChatUI(events, contactService, chatService, communityService, messageService)
for cModule in self.chatContentModule.values:
cModule.load()
@ -230,14 +237,15 @@ method createPublicChat*(self: Module, chatId: string) =
self.controller.createPublicChat(chatId)
method addNewPublicChat*(self: Module, chatDto: ChatDto, events: EventEmitter, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) =
method addNewPublicChat*(self: Module, chatDto: ChatDto, events: EventEmitter, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) =
let hasNotification = chatDto.unviewedMessagesCount > 0 or chatDto.unviewedMentionsCount > 0
let notificationsCount = chatDto.unviewedMentionsCount
let item = initItem(chatDto.id, chatDto.name, chatDto.identicon, true, chatDto.color, chatDto.description,
chatDto.chatType.int, hasNotification, notificationsCount, chatDto.muted, false, 0)
self.view.appendItem(item)
self.addSubmodule(chatDto.id, false, events, chatService, communityService, messageService)
self.addSubmodule(chatDto.id, false, true, events, contactService, chatService, communityService, messageService)
# make new added chat active one
self.setActiveItemSubItem(item.id, "")

View File

@ -1,5 +1,6 @@
import NimQml
import ../../../../../app_service/service/contacts/service as contact_service
import ../../../../../app_service/service/chat/service_interface as chat_service
import ../../../../../app_service/service/community/service_interface as community_service
import ../../../../../app_service/service/message/service as message_service
@ -9,8 +10,9 @@ import eventemitter
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface, events: EventEmitter, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) {.base.} =
method load*(self: AccessInterface, events: EventEmitter, contactService: contact_service.Service,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) {.base.} =
raise newException(ValueError, "No implementation available")
method isLoaded*(self: AccessInterface): bool {.base.} =

View File

@ -2,6 +2,6 @@ method activeItemSubItemSet*(self: AccessInterface, itemId: string, subItemId: s
raise newException(ValueError, "No implementation available")
method addNewPublicChat*(self: AccessInterface, chatDto: ChatDto, events: EventEmitter,
chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface,
messageService: message_service.Service) {.base.} =
contactService: contact_service.Service, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, messageService: message_service.Service) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -84,8 +84,8 @@ proc newModule*[T](
result.moduleLoaded = false
# Submodules
result.chatSectionModule = chat_section_module.newModule(result, events, conf.CHAT_SECTION_ID, false, chatService,
communityService, messageService)
result.chatSectionModule = chat_section_module.newModule(result, events, conf.CHAT_SECTION_ID, false, contactsService,
chatService, communityService, messageService)
result.communitySectionsModule = initOrderedTable[string, chat_section_module.AccessInterface]()
result.walletSectionModule = wallet_section_module.newModule[Module[T]](result, events, tokenService,
transactionService, collectible_service, walletAccountService, settingsService)
@ -114,6 +114,7 @@ method delete*[T](self: Module[T]) =
method load*[T](
self: Module[T],
events: EventEmitter,
contactsService: contacts_service.Service,
chatService: chat_service.Service,
communityService: community_service.Service,
messageService: message_service.Service
@ -131,6 +132,7 @@ method load*[T](
events,
c.id,
true,
contactsService,
chatService,
communityService,
messageService
@ -209,9 +211,9 @@ method load*[T](
activeSection = profileSettingsSectionItem
# Load all sections
self.chatSectionModule.load(events, chatService, communityService, messageService)
self.chatSectionModule.load(events, contactsService, chatService, communityService, messageService)
for cModule in self.communitySectionsModule.values:
cModule.load(events, chatService, communityService, messageService)
cModule.load(events, contactsService, chatService, communityService, messageService)
self.walletSectionModule.load()
# self.walletV2SectionModule.load()
self.browserSectionModule.load()

View File

@ -1,3 +1,4 @@
import ../../../../app_service/service/contacts/service as contacts_service
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
@ -10,6 +11,7 @@ method delete*(self: AccessInterface) {.base.} =
method load*(
self: AccessInterface,
events: EventEmitter,
contactsService: contacts_service.Service,
chatService: chat_service.Service,
communityService: community_service.Service,
messageService: message_service.Service

View File

@ -17,6 +17,14 @@ template getProp(obj: JsonNode, prop: string, value: var typedesc[int64]): bool
success
template getProp(obj: JsonNode, prop: string, value: var typedesc[uint64]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):
value = uint64(obj[prop].getBiggestInt)
success = true
success
template getProp(obj: JsonNode, prop: string, value: var typedesc[string]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):

View File

@ -1,3 +1,4 @@
import os
import status/ens as status_ens
include ../../common/json_utils
@ -17,3 +18,16 @@ const lookupContactTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
if not id.startsWith("0x"):
id = status_ens.pubkey(id)
arg.finish(id)
#################################################
# Async timer
#################################################
type
TimerTaskArg = ref object of QObjectTaskArg
timeoutInMilliseconds: int
const timerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[TimerTaskArg](argEncoded)
sleep(arg.timeoutInMilliseconds)
arg.finish("done")

View File

@ -17,6 +17,7 @@ type ContactsDto* = object
alias*: string
identicon*: string
lastUpdated*: int64
lastUpdatedLocally*: int64
localNickname*: string
image*: Images
added*: bool
@ -36,9 +37,10 @@ proc `$`*(self: ContactsDto): string =
id: {self.id},
name: {self.name},
ensVerified: {self.ensVerified},
alias: {self.alias},
identicon: {self.identicon},
lastUpdated: {self.lastUpdated},
alias: {self.alias},
identicon: {self.identicon},
lastUpdated: {self.lastUpdated},
lastUpdatedLocally: {self.lastUpdatedLocally},
localNickname: {self.localNickname},
image:[
{$self.image}
@ -69,6 +71,7 @@ proc toContactsDto*(jsonObj: JsonNode): ContactsDto =
discard jsonObj.getProp("alias", result.alias)
discard jsonObj.getProp("identicon", result.identicon)
discard jsonObj.getProp("lastUpdated", result.lastUpdated)
discard jsonObj.getProp("lastUpdatedLocally", result.lastUpdatedLocally)
discard jsonObj.getProp("localNickname", result.localNickname)
var imageObj: JsonNode

View File

@ -0,0 +1,26 @@
import json
include ../../../common/json_utils
type StatusType* {.pure.}= enum
Offline = 0,
Online
DoNotDisturb
Idle
Invisible
type StatusUpdateDto* = object
publicKey*: string
statusType*: StatusType
clock*: uint64
text*: string
proc toStatusUpdateDto*(jsonObj: JsonNode): StatusUpdateDto =
discard jsonObj.getProp("publicKey", result.publicKey)
discard jsonObj.getProp("clock", result.clock)
discard jsonObj.getProp("text", result.text)
result.statusType = StatusType.Offline
var statusTypeInt: int
if (jsonObj.getProp("statusType", statusTypeInt) and
(statusTypeInt >= ord(low(StatusType)) or statusTypeInt <= ord(high(StatusType)))):
result.statusType = StatusType(statusTypeInt)

View File

@ -1,15 +1,18 @@
import NimQml, Tables, json, sequtils, strformat, chronicles, strutils
import NimQml, Tables, json, sequtils, strformat, chronicles, strutils, times
import eventemitter
import ../../../app/global/global_singleton
import ../../../app/core/signals/types
import ../../../app/core/tasks/[qt, threadpool]
import ./dto/contacts as contacts_dto
import ./dto/status_update as status_update_dto
import status/statusgo_backend_new/contacts as status_contacts
import status/statusgo_backend_new/accounts as status_accounts
import status/statusgo_backend_new/chat as status_chat
import status/statusgo_backend_new/utils as status_utils
export contacts_dto
export contacts_dto, status_update_dto
include async_tasks
@ -26,6 +29,17 @@ type
ContactAddedArgs* = ref object of Args
contact*: ContactsDto
ContactUpdatedArgs* = ref object of Args
contact*: ContactsDto
ContactsStatusUpdatedArgs* = ref object of Args
statusUpdates*: seq[StatusUpdateDto]
# Local Constants:
const CheckStatusIntervalInMilliseconds = 5000 # 5 seconds, this is timeout how often do we check for user status.
const OnlineLimitInSeconds = int(5.5 * 60) # 5.5 minutes
const IdleLimitInSeconds = int(7 * 60) # 7 minutes
# Signals which may be emitted by this service:
const SIGNAL_CONTACT_LOOKED_UP* = "SIGNAL_CONTACT_LOOKED_UP"
# Remove new when old code is removed
@ -34,23 +48,43 @@ const SIGNAL_CONTACT_BLOCKED* = "new-contactBlocked"
const SIGNAL_CONTACT_UNBLOCKED* = "new-contactUnblocked"
const SIGNAL_CONTACT_REMOVED* = "new-contactRemoved"
const SIGNAL_CONTACT_NICKNAME_CHANGED* = "new-contactNicknameChanged"
const SIGNAL_CONTACTS_STATUS_UPDATED* = "new-contactsStatusUpdated"
const SIGNAL_CONTACT_UPDATED* = "new-contactUpdated"
const SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED* = "new-loggedInUserImageChanged"
QtObject:
type Service* = ref object of QObject
threadpool: ThreadPool
contacts: Table[string, ContactsDto] # [contact_id, ContactsDto]
contactsStatus: Table[string, StatusUpdateDto] # [contact_id, StatusUpdateDto]
events: EventEmitter
closingApp: bool
# Forward declaration
proc getContactById*(self: Service, id: string): ContactsDto
proc saveContact(self: Service, contact: ContactsDto)
proc startCheckingContactStatuses(self: Service)
proc delete*(self: Service) =
self.closingApp = true
self.contacts.clear
self.contactsStatus.clear
self.QObject.delete
proc newService*(events: EventEmitter, threadpool: ThreadPool): Service =
new(result, delete)
result.QObject.setup
result.closingApp = false
result.events = events
result.threadpool = threadpool
result.contacts = initTable[string, ContactsDto]()
signalConnect(singletonInstance.userProfile, "imageChanged()", result, "onLoggedInUserImageChange()", 2)
proc addContact(self: Service, contact: ContactsDto) =
# Private proc, used for adding contacts only.
self.contacts[contact.id] = contact
self.contactsStatus[contact.id] = StatusUpdateDto(publicKey: contact.id, statusType: StatusType.Offline)
proc fetchContacts*(self: Service) =
try:
@ -59,15 +93,45 @@ QtObject:
let contacts = map(response.result.getElems(), proc(x: JsonNode): ContactsDto = x.toContactsDto())
for contact in contacts:
self.contacts[contact.id] = contact
self.addContact(contact)
except Exception as e:
let errDesription = e.msg
error "error: ", errDesription
return
proc doConnect(self: Service) =
self.events.on(SignalType.Message.event) do(e:Args):
var receivedData = MessageSignal(e)
if(receivedData.statusUpdates.len > 0):
for s in receivedData.statusUpdates:
if(not self.contactsStatus.hasKey(s.publicKey)):
# we shouldn't be here ever, but the following line ensures we have added a contact before setting status for it
discard self.getContactById(s.publicKey)
self.contactsStatus[s.publicKey] = s
let data = ContactsStatusUpdatedArgs(statusUpdates: receivedData.statusUpdates)
self.events.emit(SIGNAL_CONTACTS_STATUS_UPDATED, data)
if(receivedData.contacts.len > 0):
for c in receivedData.contacts:
let localContact = self.getContactById(c.id)
var receivedContact = c
receivedContact.localNickname = localContact.localNickname
self.saveContact(receivedContact)
let data = ContactUpdatedArgs(contact: receivedContact)
self.events.emit(SIGNAL_CONTACT_UPDATED, data)
proc init*(self: Service) =
self.fetchContacts()
self.doConnect()
self.startCheckingContactStatuses()
proc onLoggedInUserImageChange*(self: Service) {.slot.} =
let data = Args()
self.events.emit(SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED, data)
proc getContacts*(self: Service): seq[ContactsDto] =
return toSeq(self.contacts.values)
@ -77,7 +141,10 @@ QtObject:
let response = status_contacts.getContactByID(id)
result = response.result.toContactsDto()
self.contacts[result.id] = result
if result.id.len == 0:
return
self.addContact(result)
except Exception as e:
let errDesription = e.msg
@ -109,6 +176,15 @@ QtObject:
blocked: false,
hasAddedUs: false
)
self.addContact(result)
proc getStatusForContactWithId*(self: Service, publicKey: string): StatusUpdateDto =
# This method will fetch current accurate status from `status-go` once we add an api point there for it.
if(not self.contactsStatus.hasKey(publicKey)):
# following line ensures that we have added a contact before setting status for it
discard self.getContactById(publicKey)
return self.contactsStatus[publicKey]
proc getContactNameAndImage*(self: Service, publicKey: string): tuple[name: string, image: string, isIdenticon: bool] =
## This proc should be used accross the app in order to have for the same contact
@ -207,6 +283,8 @@ QtObject:
self.events.emit(SIGNAL_CONTACT_LOOKED_UP, data)
proc lookupContact*(self: Service, value: string) =
if(self.closingApp):
return
let arg = LookupContactTaskArg(
tptr: cast[ByteAddress](lookupContactTask),
vptr: cast[ByteAddress](self.vptr),
@ -214,3 +292,42 @@ QtObject:
value: value
)
self.threadpool.start(arg)
proc checkContactsStatus*(self: Service, response: string) {.slot.} =
let nowInMyLocalZone = now()
let timestampNow = uint64(nowInMyLocalZone.toTime().toUnix())
var updatedStatuses: seq[StatusUpdateDto]
for status in self.contactsStatus.mvalues:
if(timestampNow - status.clock < uint64(OnlineLimitInSeconds)):
if(status.statusType == StatusType.Online):
continue
else:
status.statusType = StatusType.Online
updatedStatuses.add(status)
elif(timestampNow - status.clock < uint64(IdleLimitInSeconds)):
if(status.statusType == StatusType.Idle):
continue
else:
status.statusType = StatusType.Idle
updatedStatuses.add(status)
elif(status.statusType != StatusType.Offline):
status.statusType = StatusType.Offline
updatedStatuses.add(status)
if(updatedStatuses.len > 0):
let data = ContactsStatusUpdatedArgs(statusUpdates: updatedStatuses)
self.events.emit(SIGNAL_CONTACTS_STATUS_UPDATED, data)
self.startCheckingContactStatuses()
proc startCheckingContactStatuses(self: Service) =
if(self.closingApp):
return
let arg = TimerTaskArg(
tptr: cast[ByteAddress](timerTask),
vptr: cast[ByteAddress](self.vptr),
slot: "checkContactsStatus",
timeoutInMilliseconds: CheckStatusIntervalInMilliseconds
)
self.threadpool.start(arg)

View File

@ -29,6 +29,18 @@ StatusAppThreePanelLayout {
// Each `ChatLayout` has its own chatCommunitySectionModule
// (on the backend chat and community sections share the same module since they are actually the same)
property var chatCommunitySectionModule
// Since qml component doesn't follow encaptulation from the backend side, we're introducing
// a method which will return appropriate chat content module for selected chat/channel
function currentChatContentModule(){
// When we decide to have the same struct as it's on the backend we will remove this function.
// So far this is a way to deal with refactord backend from the current qml structure.
if(chatCommunitySectionModule.activeItem.isSubItemActive)
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id)
else
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id)
return chatCommunitySectionModule.getChatContentModule()
}
// Not Refactored
property var messageStore
@ -72,35 +84,37 @@ StatusAppThreePanelLayout {
}
}
showRightPanel: (localAccountSensitiveSettings.expandUsersList && (localAccountSensitiveSettings.showOnlineUsers || chatsModel.communities.activeCommunity.active)
&& (chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne))
showRightPanel: {
// Check if user list is available as an option for particular chat content module.
let usersListAvailable = currentChatContentModule().isUsersListAvailable()
return localAccountSensitiveSettings.showOnlineUsers && usersListAvailable && localAccountSensitiveSettings.expandUsersList
}
rightPanel: localAccountSensitiveSettings.communitiesEnabled && chatCommunitySectionModule.isCommunity()? communityUserListComponent : userListComponent
Component {
id: communityUserListComponent
CommunityUserListPanel {
//Not Refactored Yet
//currentTime: chatColumn.currentTime
messageContextMenu: quickActionMessageOptionsMenu
// profilePubKey: userProfile.pubKey
// community: root.rootStore.chatsModelInst.communities.activeCommunity
// currentUserName: Utils.removeStatusEns(root.rootStore.profileModelInst.ens.preferredUsername
// || root.rootStore.profileModelInst.profile.username)
// currentUserOnline: root.store.userProfileInst.userStatus
// contactsList: root.rootStore.allContacts
usersModule: {
if(chatCommunitySectionModule.activeItem.isSubItemActive)
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id)
else
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id)
return chatCommunitySectionModule.getChatContentModule().usersModule
}
}
}
Component {
id: userListComponent
UserListPanel {
//Not Refactored Yet
//currentTime: chatColumn.currentTime
//userList: chatColumn.userList
messageContextMenu: quickActionMessageOptionsMenu
// profilePubKey: userProfile.pubKey
// contactsList: root.rootStore.allContacts
// isOnline: root.rootStore.chatsModelInst.isOnline
usersModule: {
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id)
return chatCommunitySectionModule.getChatContentModule().usersModule
}
}
}

View File

@ -9,95 +9,113 @@ import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
StatusListItem {
id: root
Item {
id: wrapper
anchors.right: parent.right
anchors.rightMargin: 8
anchors.left: parent.left
anchors.leftMargin: 8
implicitHeight: 44
leftPadding: 8
rightPadding: 8
height: rectangle.height + 4
property var contactsList
property int statusType: -1
property string name: ""
property string publicKey: ""
property string profilePubKey: ""
property string name: ""
property string identicon: ""
property bool isCurrentUser: (publicKey === profilePubKey)
property string profileImage: appMain.getProfileImage(publicKey) || ""
property bool highlighted: false
property string lastSeen: ""
property bool isOnline: false
property var currentTime
property bool isIdenticon: true
property int userStatus: Constants.userStatus.offline
property var messageContextMenu
title: isCurrentUser ? qsTr("You") : Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(root.name)))
image.source: profileImage || identicon
image.isIdenticon: !profileImage
image.width: 24
image.height: 24
statusListItemIcon.anchors.topMargin: 10
statusListItemTitle.elide: Text.ElideRight
statusListItemTitle.wrapMode: Text.NoWrap
color: sensor.containsMouse ?
Theme.palette.statusChatListItem.hoverBackgroundColor :
Theme.palette.baseColor4
sensor.onClicked: {
if (mouse.button === Qt.LeftButton) {
//TODO remove dynamic scoping
openProfilePopup(root.name, root.publicKey, (root.profileImage || root.identicon), "", appMain.getUserNickname(root.publicKey));
}
else if (mouse.button === Qt.RightButton && !!messageContextMenu) {
// Set parent, X & Y positions for the messageContextMenu
messageContextMenu.parent = root
messageContextMenu.setXPosition = function() { return 0}
messageContextMenu.setYPosition = function() { return root.height}
messageContextMenu.isProfile = true;
messageContextMenu.show(root.name, root.publicKey, (root.profileImage || root.identicon), "", appMain.getUserNickname(root.publicKey))
property bool enableMouseArea: true
property color color: {
if (wrapper.hovered) {
return Style.current.menuBackgroundHover
}
return Style.current.transparent
}
Connections {
enabled: !!root.contactsList
target: root.contactsList
onContactChanged: {
if (pubkey === root.publicKey) {
root.profileImage = !!appMain.getProfileImage(root.publicKey) ?
appMain.getProfileImage(root.publicKey) : ""
Rectangle {
id: rectangle
width: parent.width
height: 40
radius: 8
color: wrapper.color
StatusSmartIdenticon {
id: contactImage
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
image: StatusImageSettings {
width: 28
height: 28
source: wrapper.identicon
isIdenticon: wrapper.isIdenticon
}
icon: StatusIconSettings {
width: 28
height: 28
letterSize: 15
}
name: wrapper.name
}
StyledText {
id: contactInfo
text: wrapper.name
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
elide: Text.ElideRight
color: Style.current.textColor
font.weight: Font.Medium
font.pixelSize: 15
anchors.left: contactImage.right
anchors.leftMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
StatusBadge {
id: statusBadge
width: 15
height: 15
anchors.left: contactImage.right
anchors.leftMargin: -Style.current.smallPadding
anchors.bottom: contactImage.bottom
visible: wrapper.userStatus !== Constants.userStatus.offline
border.width: 3
border.color: Theme.palette.statusAppNavBar.backgroundColor
color: {
if(wrapper.userStatus === Constants.userStatus.online)
return Style.current.green
else if(wrapper.userStatus === Constants.userStatus.idle)
return Style.current.orange
else if(wrapper.userStatus === Constants.userStatus.doNotDisturb)
return Style.current.red
return "transparent"
}
}
}
StatusBadge {
id: statusBadge
width: 15
height: 15
anchors.left: parent.left
anchors.leftMargin: 22
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
visible: root.isOnline && !((root.statusType === -1) && (lastSeenMinutesAgo > 7))
border.width: 3
border.color: root.sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4
property real lastSeenMinutesAgo: ((currentTime/1000 - parseInt(lastSeen)) / 60);
color: {
if (visible) {
if (statusType === Constants.statusType_DoNotDisturb) {
return Style.current.red;
} else if (isCurrentUser || (lastSeenMinutesAgo < 5.5)) {
return Style.current.green;
} else if (((statusType !== -1) && (lastSeenMinutesAgo > 5.5)) ||
((statusType === -1) && (lastSeenMinutesAgo < 7))) {
return Style.current.orange;
} else if ((statusType === -1) && (lastSeenMinutesAgo > 7)) {
return "transparent";
MouseArea {
enabled: enableMouseArea
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
hoverEnabled: true
onEntered: {
wrapper.hovered = true
}
onExited: {
wrapper.hovered = false
}
onClicked: {
if (mouse.button === Qt.LeftButton) {
//TODO remove dynamic scoping
openProfilePopup(wrapper.name, wrapper.publicKey, wrapper.identicon, "", wrapper.name);
}
else if (mouse.button === Qt.RightButton && !!messageContextMenu) {
// Set parent, X & Y positions for the messageContextMenu
messageContextMenu.parent = rectangle
messageContextMenu.setXPosition = function() { return 0}
messageContextMenu.setYPosition = function() { return rectangle.height}
messageContextMenu.isProfile = true;
messageContextMenu.show(wrapper.name, wrapper.publicKey, wrapper.identicon, "", wrapper.name)
}
} else {
return "transparent";
}
}
}

View File

@ -1,11 +1,5 @@
import QtQuick 2.13
import Qt.labs.platform 1.1
import QtQuick.Controls 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.13
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import shared 1.0
import shared.panels 1.0
import shared.status 1.0
@ -17,11 +11,11 @@ import utils 1.0
Item {
id: root
anchors.fill: parent
property var userList
property var currentTime
property bool isOnline
property var contactsList
property string profilePubKey
// Important:
// Each chat/community has its own ChatContentModule and each ChatContentModule has a single usersModule
// usersModule on the backend contains everything needed for this component
property var usersModule
property var messageContextMenu
StyledText {
@ -52,26 +46,14 @@ Item {
bottomMargin: Style.current.bigPadding
}
boundsBehavior: Flickable.StopAtBounds
model: userListDelegate
}
DelegateModelGeneralized {
id: userListDelegate
lessThan: [
function (left, right) {
return (left.lastSeen > right.lastSeen);
}
]
model: root.userList
model: usersModule.model
delegate: UserDelegate {
name: model.userName
publicKey: model.publicKey
profilePubKey: root.profilePubKey
identicon: model.identicon
contactsList: root.contactsList
lastSeen: model.lastSeen / 1000
currentTime: root.currentTime
isOnline: root.isOnline
publicKey: model.id
name: model.name
identicon: model.icon
isIdenticon: model.isIdenticon
userStatus: model.onlineStatus
messageContextMenu: root.messageContextMenu
}
}
}

View File

@ -1,11 +1,5 @@
import QtQuick 2.13
import Qt.labs.platform 1.1
import QtQuick.Controls 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.13
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import shared 1.0
import shared.panels 1.0
import shared.status 1.0
@ -20,14 +14,12 @@ import StatusQ.Core.Theme 0.1
Item {
id: root
anchors.fill: parent
property var userList
property var currentTime
property var contactsList
property string profilePubKey
// Important:
// Each chat/community has its own ChatContentModule and each ChatContentModule has a single usersModule
// usersModule on the backend contains everything needed for this component
property var usersModule
property var messageContextMenu
property var community
property string currentUserName: ""
property bool currentUserOnline: true
StatusBaseText {
id: titleText
@ -59,46 +51,32 @@ Item {
bottomMargin: Style.current.bigPadding
}
boundsBehavior: Flickable.StopAtBounds
model: userListDelegate
section.property: "online"
section.delegate: (root.width > 58) ? sectionDelegateComponent : null
Component {
id: sectionDelegateComponent
Item {
width: parent.width
height: 24
StatusBaseText {
anchors.fill: parent
anchors.leftMargin: Style.current.padding
verticalAlignment: Text.AlignVCenter
font.pixelSize: Style.current.additionalTextSize
color: Theme.palette.baseColor1
text: section === 'true' ? qsTr("Online") : qsTr("Offline")
}
}
}
}
DelegateModelGeneralized {
id: userListDelegate
lessThan: [
function(left, right) {
return left.sortKey.localeCompare(right.sortKey) < 0
}
]
model: community.members
model: usersModule.model
delegate: UserDelegate {
name: model.userName
publicKey: model.pubKey
profilePubKey: root.profilePubKey
identicon: model.identicon
contactsList: root.contactsList
lastSeen: model.lastSeen
statusType: model.statusType
currentTime: root.currentTime
isOnline: (model.userName === root.currentUserName) ?
root.currentUserOnline : model.online
publicKey: model.id
name: model.name
identicon: model.icon
isIdenticon: model.isIdenticon
userStatus: model.onlineStatus
messageContextMenu: root.messageContextMenu
}
section.property: "onlineStatus"
section.delegate: (root.width > 58) ? sectionDelegateComponent : null
}
Component {
id: sectionDelegateComponent
Item {
width: parent.width
height: 24
StyledText {
anchors.fill: parent
anchors.leftMargin: Style.current.padding
verticalAlignment: Text.AlignVCenter
font.pixelSize: Style.current.additionalTextSize
color: Theme.palette.baseColor1
text: model.onlineStatus === Constants.userStatus.online? qsTr("Online") : qsTr("Offline")
}
}
}
}

View File

@ -28,6 +28,18 @@ Item {
// Important: we have parent module in this context only cause qml components
// don't follow struct from we have on the backend.
property var parentModule
// Since qml component doesn't follow encaptulation from the backend side, we're introducing
// a method which will return appropriate chat content module for selected chat/channel
function currentChatContentModule(){
// When we decide to have the same struct as it's on the backend we will remove this function.
// So far this is a way to deal with refactord backend from the current qml structure.
if(parentModule.activeItem.isSubItemActive)
parentModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id)
else
parentModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id)
return parentModule.getChatContentModule()
}
property var rootStore
property alias pinnedMessagesPopupComponent: pinnedMessagesPopupComponent
@ -210,8 +222,11 @@ Item {
}
}
membersButton.visible: (localAccountSensitiveSettings.showOnlineUsers || root.rootStore.chatsModelInst.communities.activeCommunity.active)
&& root.rootStore.chatsModelInst.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
membersButton.visible: {
// Check if user list is available as an option for particular chat content module.
let usersListAvailable = currentChatContentModule().isUsersListAvailable()
return localAccountSensitiveSettings.showOnlineUsers && usersListAvailable
}
membersButton.highlighted: localAccountSensitiveSettings.expandUsersList
notificationButton.visible: localAccountSensitiveSettings.isActivityCenterEnabled
notificationButton.tooltip.offset: localAccountSensitiveSettings.expandUsersList ? 0 : 14

View File

@ -320,19 +320,18 @@ Item {
badge.implicitHeight: 15
badge.implicitWidth: 15
badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor
badge.color: {
return userProfile.sendUserStatus ? Style.current.green : Style.current.midGrey
/*
// Use this code once support for custom user status is added
switch(userProfile.currentUserStatus){
case Constants.statusType_Online:
return Style.current.green;
case Constants.statusType_DoNotDisturb:
return Style.current.red;
default:
return Style.current.midGrey;
}*/
}
/*
//This is still not in use. Read a comment for `currentUserStatus` in UserProfile on the nim side.
// Use this code once support for custom user status is added
switch(userProfile.currentUserStatus){
case Constants.userStatus.online:
return Style.current.green;
case Constants.userStatus.doNotDisturb:
return Style.current.red;
default:
return Style.current.midGrey;
}*/
badge.color: appMain.rootStore.userProfileInst.userStatus ? Style.current.green : Style.current.midGrey
badge.border.width: 3
onClicked: {
userStatusContextMenu.opened ?

View File

@ -40,6 +40,13 @@ QtObject {
readonly property int newMessage: 6
}
readonly property QtObject userStatus: QtObject{
readonly property int offline: 0
readonly property int online: 1
readonly property int doNotDisturb: 2
readonly property int idle: 3
}
readonly property int communityImported: 0
readonly property int communityImportingInProgress: 1
readonly property int communityImportingError: 2
@ -113,10 +120,6 @@ QtObject {
readonly property string linux: "linux"
readonly property string mac: "mac"
readonly property int statusType_Unknown: 0
readonly property int statusType_Online: 1
readonly property int statusType_DoNotDisturb: 2
// Transaction states
readonly property int addressRequested: 1
readonly property int declined: 2