From edba946a71ec057d50e72a9f8c177c07351aef5c Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 2 Aug 2023 20:03:52 +0400 Subject: [PATCH] feat(Community): Community messaging statistics chart (#11696) * feat(Community): Community messaging statistics chart Close 11152 - Use se `collectCommunityMessageMetrics` for messaging statistics chart in community overview * feat(Community): Transfer community metrics with dto objects * feat: impl simple string-based model for community metrics * fix(Community): Review fixes and fix for changing community when chat is open * Update src/app/modules/main/chat_section/controller.nim Co-authored-by: Jonathan Rainville --------- Co-authored-by: Jonathan Rainville --- .../modules/main/chat_section/controller.nim | 13 +++++ .../main/chat_section/io_interface.nim | 6 +++ src/app/modules/main/chat_section/module.nim | 6 +++ src/app/modules/main/chat_section/view.nim | 19 ++++++++ .../modules/main/communities/controller.nim | 2 +- .../service/community/async_tasks.nim | 23 +++++++++ .../service/community/dto/community.nim | 46 ++++++++++++++++++ src/app_service/service/community/service.nim | 48 +++++++++++++++++++ src/backend/communities.nim | 9 ++++ ui/app/AppLayouts/Chat/stores/RootStore.qml | 7 +++ .../panels/OverviewSettingsChart.qml | 16 +++++++ .../panels/OverviewSettingsPanel.qml | 14 ++++++ .../views/CommunitySettingsView.qml | 5 ++ vendor/status-go | 2 +- 14 files changed, 214 insertions(+), 2 deletions(-) diff --git a/src/app/modules/main/chat_section/controller.nim b/src/app/modules/main/chat_section/controller.nim index d7cc46bcf5..74ab849280 100644 --- a/src/app/modules/main/chat_section/controller.nim +++ b/src/app/modules/main/chat_section/controller.nim @@ -169,6 +169,16 @@ proc init*(self: Controller) = self.nodeConfigurationService, self.contactService, self.chatService, self.communityService, self.messageService, self.gifService, self.mailserversService, setChatAsActive = true) + self.events.on(SIGNAL_COMMUNITY_METRICS_UPDATED) do(e: Args): + let args = CommunityMetricsArgs(e) + if args.communityId == self.sectionId: + let metrics = self.communityService.getCommunityMetrics(args.communityId, args.metricsType) + var strings: seq[string] + for interval in metrics.intervals: + for timestamp in interval.timestamps: + strings.add($timestamp) + self.delegate.setOverviewChartData("[" & join(strings, ", ") & "]") + self.events.on(SIGNAL_COMMUNITY_CHANNEL_DELETED) do(e:Args): let args = CommunityChatIdArgs(e) if (args.communityId == self.sectionId): @@ -652,3 +662,6 @@ proc getContractAddressesForToken*(self: Controller, symbol: string): Table[int, proc getCommunityTokenList*(self: Controller): seq[CommunityTokenDto] = return self.communityTokensService.getCommunityTokens(self.getMySectionId()) + +proc collectCommunityMetricsMessagesTimestamps*(self: Controller, intervals: string) = + self.communityService.collectCommunityMetricsMessagesTimestamps(self.getMySectionId(), intervals) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/io_interface.nim b/src/app/modules/main/chat_section/io_interface.nim index 04507e8943..25ce6b55ea 100644 --- a/src/app/modules/main/chat_section/io_interface.nim +++ b/src/app/modules/main/chat_section/io_interface.nim @@ -349,6 +349,12 @@ method createOrEditCommunityTokenPermission*(self: AccessInterface, communityId: method deleteCommunityTokenPermission*(self: AccessInterface, communityId: string, permissionId: string) {.base.} = raise newException(ValueError, "No implementation available") +method collectCommunityMetricsMessagesTimestamps*(self: AccessInterface, intervals: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method setOverviewChartData*(self: AccessInterface, metrics: string) {.base.} = + raise newException(ValueError, "No implementation available") + method onCommunityTokenPermissionCreated*(self: AccessInterface, communityId: string, tokenPermission: CommunityTokenPermissionDto) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index 840ed5190f..c0408004c9 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -1322,3 +1322,9 @@ method deleteCommunityTokenPermission*(self: Module, communityId: string, permis method onDeactivateChatLoader*(self: Module, chatId: string) = self.view.chatsModel().disableChatLoader(chatId) + +method collectCommunityMetricsMessagesTimestamps*(self: Module, intervals: string) = + self.controller.collectCommunityMetricsMessagesTimestamps(intervals) + +method setOverviewChartData*(self: Module, metrics: string) = + self.view.setOverviewChartData(metrics) diff --git a/src/app/modules/main/chat_section/view.nim b/src/app/modules/main/chat_section/view.nim index 2d20ac710e..328ee218b0 100644 --- a/src/app/modules/main/chat_section/view.nim +++ b/src/app/modules/main/chat_section/view.nim @@ -27,6 +27,8 @@ QtObject: requiresTokenPermissionToJoin: bool amIMember: bool chatsLoaded: bool + communityMetrics: string # NOTE: later this should be replaced with QAbstractListModel-based model + proc delete*(self: View) = self.model.delete @@ -64,6 +66,7 @@ QtObject: result.amIMember = false result.requiresTokenPermissionToJoin = false result.chatsLoaded = false + result.communityMetrics = "[]" proc load*(self: View) = self.delegate.viewDidLoad() @@ -409,3 +412,19 @@ QtObject: QtProperty[bool] allTokenRequirementsMet: read = getAllTokenRequirementsMet notify = allTokenRequirementsMetChanged + + proc getOverviewChartData*(self: View): QVariant {.slot.} = + return newQVariant(self.communityMetrics) + + proc overviewChartDataChanged*(self: View) {.signal.} + + QtProperty[QVariant] overviewChartData: + read = getOverviewChartData + notify = overviewChartDataChanged + + proc setOverviewChartData*(self: View, communityMetrics: string) = + self.communityMetrics = communityMetrics + self.overviewChartDataChanged() + + proc collectCommunityMetricsMessagesTimestamps*(self: View, intervals: string) {.slot.} = + self.delegate.collectCommunityMetricsMessagesTimestamps(intervals) diff --git a/src/app/modules/main/communities/controller.nim b/src/app/modules/main/communities/controller.nim index 8a3921ff2a..5255885abd 100644 --- a/src/app/modules/main/communities/controller.nim +++ b/src/app/modules/main/communities/controller.nim @@ -1,4 +1,4 @@ -import stint +import stint, std/strutils import ./io_interface import ../../../core/signals/types diff --git a/src/app_service/service/community/async_tasks.nim b/src/app_service/service/community/async_tasks.nim index 95ce7f4466..14334c20bb 100644 --- a/src/app_service/service/community/async_tasks.nim +++ b/src/app_service/service/community/async_tasks.nim @@ -24,6 +24,29 @@ const asyncLoadCommunitiesDataTask: Task = proc(argEncoded: string) {.gcsafe, ni "error": e.msg, }) +type + AsyncCollectCommunityMetricsTaskArg = ref object of QObjectTaskArg + communityId: string + metricsType: CommunityMetricsType + intervals: JsonNode + +const asyncCollectCommunityMetricsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AsyncCollectCommunityMetricsTaskArg](argEncoded) + try: + let response = status_go.collectCommunityMetrics(arg.communityId, arg.metricsType.int, arg.intervals) + arg.finish(%* { + "communityId": arg.communityId, + "metricsType": arg.metricsType, + "response": response, + "error": "", + }) + except Exception as e: + arg.finish(%* { + "communityId": arg.communityId, + "metricsType": arg.metricsType, + "error": e.msg, + }) + type AsyncRequestCommunityInfoTaskArg = ref object of QObjectTaskArg communityId: string diff --git a/src/app_service/service/community/dto/community.nim b/src/app_service/service/community/dto/community.nim index b4b49d6bd8..2cc7af2ca9 100644 --- a/src/app_service/service/community/dto/community.nim +++ b/src/app_service/service/community/dto/community.nim @@ -25,6 +25,13 @@ type MutedType* {.pure.}= enum For1min = 6, Unmuted = 7 +type + CommunityMetricsType* {.pure.} = enum + MessagesTimestamps = 0, + MessagesCount, + Members, + ControlNodeUptime + type CommunityMembershipRequestDto* = object id*: string publicKey*: string @@ -88,6 +95,17 @@ type CheckPermissionsToJoinResponseDto* = object permissions*: Table[string, CheckPermissionsResultDto] validCombinations*: seq[AccountChainIDsCombinationDto] +type MetricsIntervalDto* = object + startTimestamp*: uint64 + endTimestamp*: uint64 + timestamps*: seq[uint64] + count*: int + +type CommunityMetricsDto* = object + communityId*: string + metricsType*: CommunityMetricsType + intervals*: seq[MetricsIntervalDto] + type CommunityDto* = object id*: string memberRole*: MemberRole @@ -301,6 +319,34 @@ proc toCheckAllChannelsPermissionsResponseDto*(jsonObj: JsonNode): CheckAllChann for channelId, permissionResponse in channelsObj: result.channels[channelId] = permissionResponse.toCheckChannelPermissionsResponseDto() +proc toMetricsIntervalDto*(jsonObj: JsonNode): MetricsIntervalDto = + result = MetricsIntervalDto() + discard jsonObj.getProp("startTimestamp", result.startTimestamp) + discard jsonObj.getProp("endTimestamp", result.endTimestamp) + + var timestampsObj: JsonNode + if (jsonObj.getProp("timestamps", timestampsObj) and timestampsObj.kind == JArray): + for timestamp in timestampsObj: + result.timestamps.add(uint64(timestamp.getInt)) + + discard jsonObj.getProp("count", result.count) + +proc toCommunityMetricsDto*(jsonObj: JsonNode): CommunityMetricsDto = + result = CommunityMetricsDto() + + discard jsonObj.getProp("communityId", result.communityId) + + result.metricsType = CommunityMetricsType.MessagesTimestamps + var metricsTypeInt: int + if (jsonObj.getProp("metricsType", metricsTypeInt) and (metricsTypeInt >= ord(low(CommunityMetricsType)) and + metricsTypeInt <= ord(high(CommunityMetricsType)))): + result.metricsType = CommunityMetricsType(metricsTypeInt) + + var intervalsObj: JsonNode + if (jsonObj.getProp("intervals", intervalsObj) and intervalsObj.kind == JArray): + for interval in intervalsObj: + result.intervals.add(interval.toMetricsIntervalDto) + proc toCommunityDto*(jsonObj: JsonNode): CommunityDto = result = CommunityDto() discard jsonObj.getProp("id", result.id) diff --git a/src/app_service/service/community/service.nim b/src/app_service/service/community/service.nim index 3ae8e26f26..4d5d5fae88 100644 --- a/src/app_service/service/community/service.nim +++ b/src/app_service/service/community/service.nim @@ -121,6 +121,10 @@ type communityId*: string checkPermissionsToJoinResponse*: CheckPermissionsToJoinResponseDto + CommunityMetricsArgs* = ref object of Args + communityId*: string + metricsType*: CommunityMetricsType + # Signals which may be emitted by this service: const SIGNAL_COMMUNITY_DATA_LOADED* = "communityDataLoaded" const SIGNAL_COMMUNITY_JOINED* = "communityJoined" @@ -189,6 +193,8 @@ const SIGNAL_CHECK_PERMISSIONS_TO_JOIN_RESPONSE* = "checkPermissionsToJoinRespon const SIGNAL_COMMUNITY_PRIVATE_KEY_REMOVED* = "communityPrivateKeyRemoved" +const SIGNAL_COMMUNITY_METRICS_UPDATED* = "communityMetricsUpdated" + QtObject: type Service* = ref object of QObject @@ -202,6 +208,7 @@ QtObject: myCommunityRequests*: seq[CommunityMembershipRequestDto] historyArchiveDownloadTaskCommunityIds*: HashSet[string] requestedCommunityIds*: HashSet[string] + communityMetrics: Table[string, CommunityMetricsDto] # Forward declaration proc asyncLoadCuratedCommunities*(self: Service) @@ -237,6 +244,7 @@ QtObject: result.myCommunityRequests = @[] result.historyArchiveDownloadTaskCommunityIds = initHashSet[string]() result.requestedCommunityIds = initHashSet[string]() + result.communityMetrics = initTable[string, CommunityMetricsDto]() proc getFilteredJoinedCommunities(self: Service): Table[string, CommunityDto] = result = initTable[string, CommunityDto]() @@ -1359,6 +1367,18 @@ QtObject: except Exception as e: error "Error reordering category channel", msg = e.msg, communityId, categoryId, position + proc asyncCommunityMetricsLoaded*(self: Service, rpcResponse: string) {.slot.} = + let rpcResponseObj = rpcResponse.parseJson + if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "": + error "Error collecting community metrics", msg = rpcResponseObj{"error"} + return + + let communityId = rpcResponseObj{"communityId"}.getStr() + let metricsType = rpcResponseObj{"metricsType"}.getInt() + + var metrics = rpcResponseObj{"response"}{"result"}.toCommunityMetricsDto() + self.communityMetrics[communityId] = metrics + self.events.emit(SIGNAL_COMMUNITY_METRICS_UPDATED, CommunityMetricsArgs(communityId: communityId, metricsType: metrics.metricsType)) proc asyncCommunityInfoLoaded*(self: Service, communityIdAndRpcResponse: string) {.slot.} = let rpcResponseObj = communityIdAndRpcResponse.parseJson @@ -1551,6 +1571,34 @@ QtObject: error "error loading curated communities: ", errMsg self.events.emit(SIGNAL_CURATED_COMMUNITIES_LOADING_FAILED, Args()) + proc getCommunityMetrics*(self: Service, communityId: string, metricsType: CommunityMetricsType): CommunityMetricsDto = + # NOTE: use metricsType when other metrics types added + if self.communityMetrics.hasKey(communityId): + return self.communityMetrics[communityId] + return CommunityMetricsDto() + + proc collectCommunityMetricsMessagesTimestamps*(self: Service, communityId: string, intervals: string) = + let arg = AsyncCollectCommunityMetricsTaskArg( + tptr: cast[ByteAddress](asyncCollectCommunityMetricsTask), + vptr: cast[ByteAddress](self.vptr), + slot: "asyncCommunityMetricsLoaded", + communityId: communityId, + metricsType: CommunityMetricsType.MessagesTimestamps, + intervals: parseJson(intervals) + ) + self.threadpool.start(arg) + + proc collectCommunityMetricsMessagesCount*(self: Service, communityId: string, intervals: string) = + let arg = AsyncCollectCommunityMetricsTaskArg( + tptr: cast[ByteAddress](asyncCollectCommunityMetricsTask), + vptr: cast[ByteAddress](self.vptr), + slot: "asyncCommunityMetricsLoaded", + communityId: communityId, + metricsType: CommunityMetricsType.MessagesCount, + intervals: parseJson(intervals) + ) + self.threadpool.start(arg) + proc requestCommunityInfo*(self: Service, communityId: string, importing = false) = if communityId in self.requestedCommunityIds: diff --git a/src/backend/communities.nim b/src/backend/communities.nim index 7ccd540bf2..3eae86cb9a 100644 --- a/src/backend/communities.nim +++ b/src/backend/communities.nim @@ -352,6 +352,15 @@ proc deleteCommunityCategory*( "categoryId": categoryId }]) +proc collectCommunityMetrics*(communityId: string, metricsType: int, intervals: JsonNode + ):RpcResponse[JsonNode] {.raises: [Exception].} = + result = callPrivateRPC("collectCommunityMetrics".prefix, %*[ + { + "communityId": communityId, + "type": metricsType, + "intervals": intervals + }]) + proc requestCommunityInfo*(communityId: string): RpcResponse[JsonNode] {.raises: [Exception].} = result = callPrivateRPC("requestCommunityInfoFromMailserver".prefix, %*[communityId]) diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml index e5188c4618..bcf093cdda 100644 --- a/ui/app/AppLayouts/Chat/stores/RootStore.qml +++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml @@ -56,6 +56,8 @@ QtObject { } } + readonly property string overviewChartData: chatCommunitySectionModule.overviewChartData + readonly property bool isUserAllowedToSendMessage: _d.isUserAllowedToSendMessage readonly property string chatInputPlaceHolderText: _d.chatInputPlaceHolderText readonly property var oneToOneChatContact: _d.oneToOneChatContact @@ -416,6 +418,11 @@ QtObject { return communitiesList.getSectionByIdJson(id) } + // intervals is a string containing json array [{startTimestamp: 1690548852, startTimestamp: 1690547684}, {...}] + function collectCommunityMetricsMessagesTimestamps(intervals) { + chatCommunitySectionModule.collectCommunityMetricsMessagesTimestamps(intervals) + } + function requestCommunityInfo(id, importing = false) { communitiesModuleInst.requestCommunityInfo(id, importing) } diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml index 3fdee7a843..bfe6878e61 100644 --- a/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml @@ -20,6 +20,20 @@ StatusChartPanel { */ property var model: [] + signal collectCommunityMetricsMessagesTimestamps(var intervals) + + function requestCommunityMetrics() { + let intervals = d.selectedTabInfo.modelItems.map(item => { + return { + startTimestamp: item.start, + endTimestamp: item.end + } + }) + collectCommunityMetricsMessagesTimestamps(JSON.stringify(intervals)) + } + + onVisibleChanged: if (visible) requestCommunityMetrics() + QtObject { id: d @@ -219,6 +233,8 @@ StatusChartPanel { return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5) : Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5) } + + onSelectedTabInfoChanged: root.requestCommunityMetrics() } headerLeftPadding: 0 headerBottomPadding: Style.current.bigPadding diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml index ecac138703..d74d2b43fd 100644 --- a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml @@ -40,6 +40,8 @@ StackLayout { property int loginType: Constants.LoginType.Password property bool communitySettingsDisabled + property string overviewChartData: "" + function navigateBack() { if (editSettingsPanelLoader.item.dirty) settingsDirtyToastMessage.notifyDirty() @@ -47,6 +49,8 @@ StackLayout { root.currentIndex = 0 } + signal collectCommunityMetricsMessagesTimestamps(var intervals) + signal edited(Item item) // item containing edited fields (name, description, logoImagePath, color, options, etc..) signal inviteNewPeopleClicked @@ -113,11 +117,21 @@ StackLayout { } OverviewSettingsChart { + model: JSON.parse(root.overviewChartData) + onCollectCommunityMetricsMessagesTimestamps: { + root.collectCommunityMetricsMessagesTimestamps(intervals) + } Layout.topMargin: 16 Layout.fillWidth: true Layout.fillHeight: true Layout.bottomMargin: 16 + + Connections { + target: root + onCommunityIdChanged: requestCommunityMetrics() + } } + Rectangle { Layout.fillWidth: true diff --git a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml index 9cd5caf905..a0030f2b5c 100644 --- a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml @@ -176,6 +176,11 @@ StatusSectionLayout { loginType: root.rootStore.loginType isControlNode: root.isControlNode communitySettingsDisabled: root.communitySettingsDisabled + overviewChartData: rootStore.overviewChartData + + onCollectCommunityMetricsMessagesTimestamps: { + rootStore.collectCommunityMetricsMessagesTimestamps(intervals) + } onEdited: { const error = root.chatCommunitySectionModule.editCommunity( diff --git a/vendor/status-go b/vendor/status-go index 0ae7aa44f0..4ad84d80cc 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 0ae7aa44f00bff345539c1e288705057e7e4574c +Subproject commit 4ad84d80cc7b0363f3c8da589d7bb8930fb8a629