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 <rainville.jonathan@gmail.com>

---------

Co-authored-by: Jonathan Rainville <rainville.jonathan@gmail.com>
This commit is contained in:
Mikhail Rogachev 2023-08-02 20:03:52 +04:00 committed by GitHub
parent dd346319ff
commit edba946a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 214 additions and 2 deletions

View File

@ -169,6 +169,16 @@ proc init*(self: Controller) =
self.nodeConfigurationService, self.contactService, self.chatService, self.communityService, self.nodeConfigurationService, self.contactService, self.chatService, self.communityService,
self.messageService, self.gifService, self.mailserversService, setChatAsActive = true) 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): self.events.on(SIGNAL_COMMUNITY_CHANNEL_DELETED) do(e:Args):
let args = CommunityChatIdArgs(e) let args = CommunityChatIdArgs(e)
if (args.communityId == self.sectionId): if (args.communityId == self.sectionId):
@ -652,3 +662,6 @@ proc getContractAddressesForToken*(self: Controller, symbol: string): Table[int,
proc getCommunityTokenList*(self: Controller): seq[CommunityTokenDto] = proc getCommunityTokenList*(self: Controller): seq[CommunityTokenDto] =
return self.communityTokensService.getCommunityTokens(self.getMySectionId()) return self.communityTokensService.getCommunityTokens(self.getMySectionId())
proc collectCommunityMetricsMessagesTimestamps*(self: Controller, intervals: string) =
self.communityService.collectCommunityMetricsMessagesTimestamps(self.getMySectionId(), intervals)

View File

@ -349,6 +349,12 @@ method createOrEditCommunityTokenPermission*(self: AccessInterface, communityId:
method deleteCommunityTokenPermission*(self: AccessInterface, communityId: string, permissionId: string) {.base.} = method deleteCommunityTokenPermission*(self: AccessInterface, communityId: string, permissionId: string) {.base.} =
raise newException(ValueError, "No implementation available") 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.} = method onCommunityTokenPermissionCreated*(self: AccessInterface, communityId: string, tokenPermission: CommunityTokenPermissionDto) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")

View File

@ -1322,3 +1322,9 @@ method deleteCommunityTokenPermission*(self: Module, communityId: string, permis
method onDeactivateChatLoader*(self: Module, chatId: string) = method onDeactivateChatLoader*(self: Module, chatId: string) =
self.view.chatsModel().disableChatLoader(chatId) 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)

View File

@ -27,6 +27,8 @@ QtObject:
requiresTokenPermissionToJoin: bool requiresTokenPermissionToJoin: bool
amIMember: bool amIMember: bool
chatsLoaded: bool chatsLoaded: bool
communityMetrics: string # NOTE: later this should be replaced with QAbstractListModel-based model
proc delete*(self: View) = proc delete*(self: View) =
self.model.delete self.model.delete
@ -64,6 +66,7 @@ QtObject:
result.amIMember = false result.amIMember = false
result.requiresTokenPermissionToJoin = false result.requiresTokenPermissionToJoin = false
result.chatsLoaded = false result.chatsLoaded = false
result.communityMetrics = "[]"
proc load*(self: View) = proc load*(self: View) =
self.delegate.viewDidLoad() self.delegate.viewDidLoad()
@ -409,3 +412,19 @@ QtObject:
QtProperty[bool] allTokenRequirementsMet: QtProperty[bool] allTokenRequirementsMet:
read = getAllTokenRequirementsMet read = getAllTokenRequirementsMet
notify = allTokenRequirementsMetChanged 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)

View File

@ -1,4 +1,4 @@
import stint import stint, std/strutils
import ./io_interface import ./io_interface
import ../../../core/signals/types import ../../../core/signals/types

View File

@ -24,6 +24,29 @@ const asyncLoadCommunitiesDataTask: Task = proc(argEncoded: string) {.gcsafe, ni
"error": e.msg, "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 type
AsyncRequestCommunityInfoTaskArg = ref object of QObjectTaskArg AsyncRequestCommunityInfoTaskArg = ref object of QObjectTaskArg
communityId: string communityId: string

View File

@ -25,6 +25,13 @@ type MutedType* {.pure.}= enum
For1min = 6, For1min = 6,
Unmuted = 7 Unmuted = 7
type
CommunityMetricsType* {.pure.} = enum
MessagesTimestamps = 0,
MessagesCount,
Members,
ControlNodeUptime
type CommunityMembershipRequestDto* = object type CommunityMembershipRequestDto* = object
id*: string id*: string
publicKey*: string publicKey*: string
@ -88,6 +95,17 @@ type CheckPermissionsToJoinResponseDto* = object
permissions*: Table[string, CheckPermissionsResultDto] permissions*: Table[string, CheckPermissionsResultDto]
validCombinations*: seq[AccountChainIDsCombinationDto] 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 type CommunityDto* = object
id*: string id*: string
memberRole*: MemberRole memberRole*: MemberRole
@ -301,6 +319,34 @@ proc toCheckAllChannelsPermissionsResponseDto*(jsonObj: JsonNode): CheckAllChann
for channelId, permissionResponse in channelsObj: for channelId, permissionResponse in channelsObj:
result.channels[channelId] = permissionResponse.toCheckChannelPermissionsResponseDto() 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 = proc toCommunityDto*(jsonObj: JsonNode): CommunityDto =
result = CommunityDto() result = CommunityDto()
discard jsonObj.getProp("id", result.id) discard jsonObj.getProp("id", result.id)

View File

@ -121,6 +121,10 @@ type
communityId*: string communityId*: string
checkPermissionsToJoinResponse*: CheckPermissionsToJoinResponseDto checkPermissionsToJoinResponse*: CheckPermissionsToJoinResponseDto
CommunityMetricsArgs* = ref object of Args
communityId*: string
metricsType*: CommunityMetricsType
# Signals which may be emitted by this service: # Signals which may be emitted by this service:
const SIGNAL_COMMUNITY_DATA_LOADED* = "communityDataLoaded" const SIGNAL_COMMUNITY_DATA_LOADED* = "communityDataLoaded"
const SIGNAL_COMMUNITY_JOINED* = "communityJoined" 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_PRIVATE_KEY_REMOVED* = "communityPrivateKeyRemoved"
const SIGNAL_COMMUNITY_METRICS_UPDATED* = "communityMetricsUpdated"
QtObject: QtObject:
type type
Service* = ref object of QObject Service* = ref object of QObject
@ -202,6 +208,7 @@ QtObject:
myCommunityRequests*: seq[CommunityMembershipRequestDto] myCommunityRequests*: seq[CommunityMembershipRequestDto]
historyArchiveDownloadTaskCommunityIds*: HashSet[string] historyArchiveDownloadTaskCommunityIds*: HashSet[string]
requestedCommunityIds*: HashSet[string] requestedCommunityIds*: HashSet[string]
communityMetrics: Table[string, CommunityMetricsDto]
# Forward declaration # Forward declaration
proc asyncLoadCuratedCommunities*(self: Service) proc asyncLoadCuratedCommunities*(self: Service)
@ -237,6 +244,7 @@ QtObject:
result.myCommunityRequests = @[] result.myCommunityRequests = @[]
result.historyArchiveDownloadTaskCommunityIds = initHashSet[string]() result.historyArchiveDownloadTaskCommunityIds = initHashSet[string]()
result.requestedCommunityIds = initHashSet[string]() result.requestedCommunityIds = initHashSet[string]()
result.communityMetrics = initTable[string, CommunityMetricsDto]()
proc getFilteredJoinedCommunities(self: Service): Table[string, CommunityDto] = proc getFilteredJoinedCommunities(self: Service): Table[string, CommunityDto] =
result = initTable[string, CommunityDto]() result = initTable[string, CommunityDto]()
@ -1359,6 +1367,18 @@ QtObject:
except Exception as e: except Exception as e:
error "Error reordering category channel", msg = e.msg, communityId, categoryId, position 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.} = proc asyncCommunityInfoLoaded*(self: Service, communityIdAndRpcResponse: string) {.slot.} =
let rpcResponseObj = communityIdAndRpcResponse.parseJson let rpcResponseObj = communityIdAndRpcResponse.parseJson
@ -1551,6 +1571,34 @@ QtObject:
error "error loading curated communities: ", errMsg error "error loading curated communities: ", errMsg
self.events.emit(SIGNAL_CURATED_COMMUNITIES_LOADING_FAILED, Args()) 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) = proc requestCommunityInfo*(self: Service, communityId: string, importing = false) =
if communityId in self.requestedCommunityIds: if communityId in self.requestedCommunityIds:

View File

@ -352,6 +352,15 @@ proc deleteCommunityCategory*(
"categoryId": categoryId "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].} = proc requestCommunityInfo*(communityId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
result = callPrivateRPC("requestCommunityInfoFromMailserver".prefix, %*[communityId]) result = callPrivateRPC("requestCommunityInfoFromMailserver".prefix, %*[communityId])

View File

@ -56,6 +56,8 @@ QtObject {
} }
} }
readonly property string overviewChartData: chatCommunitySectionModule.overviewChartData
readonly property bool isUserAllowedToSendMessage: _d.isUserAllowedToSendMessage readonly property bool isUserAllowedToSendMessage: _d.isUserAllowedToSendMessage
readonly property string chatInputPlaceHolderText: _d.chatInputPlaceHolderText readonly property string chatInputPlaceHolderText: _d.chatInputPlaceHolderText
readonly property var oneToOneChatContact: _d.oneToOneChatContact readonly property var oneToOneChatContact: _d.oneToOneChatContact
@ -416,6 +418,11 @@ QtObject {
return communitiesList.getSectionByIdJson(id) 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) { function requestCommunityInfo(id, importing = false) {
communitiesModuleInst.requestCommunityInfo(id, importing) communitiesModuleInst.requestCommunityInfo(id, importing)
} }

View File

@ -20,6 +20,20 @@ StatusChartPanel {
*/ */
property var model: [] 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 { QtObject {
id: d id: d
@ -219,6 +233,8 @@ StatusChartPanel {
return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5) return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5)
: Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5) : Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5)
} }
onSelectedTabInfoChanged: root.requestCommunityMetrics()
} }
headerLeftPadding: 0 headerLeftPadding: 0
headerBottomPadding: Style.current.bigPadding headerBottomPadding: Style.current.bigPadding

View File

@ -40,6 +40,8 @@ StackLayout {
property int loginType: Constants.LoginType.Password property int loginType: Constants.LoginType.Password
property bool communitySettingsDisabled property bool communitySettingsDisabled
property string overviewChartData: ""
function navigateBack() { function navigateBack() {
if (editSettingsPanelLoader.item.dirty) if (editSettingsPanelLoader.item.dirty)
settingsDirtyToastMessage.notifyDirty() settingsDirtyToastMessage.notifyDirty()
@ -47,6 +49,8 @@ StackLayout {
root.currentIndex = 0 root.currentIndex = 0
} }
signal collectCommunityMetricsMessagesTimestamps(var intervals)
signal edited(Item item) // item containing edited fields (name, description, logoImagePath, color, options, etc..) signal edited(Item item) // item containing edited fields (name, description, logoImagePath, color, options, etc..)
signal inviteNewPeopleClicked signal inviteNewPeopleClicked
@ -113,11 +117,21 @@ StackLayout {
} }
OverviewSettingsChart { OverviewSettingsChart {
model: JSON.parse(root.overviewChartData)
onCollectCommunityMetricsMessagesTimestamps: {
root.collectCommunityMetricsMessagesTimestamps(intervals)
}
Layout.topMargin: 16 Layout.topMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Connections {
target: root
onCommunityIdChanged: requestCommunityMetrics()
} }
}
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true

View File

@ -176,6 +176,11 @@ StatusSectionLayout {
loginType: root.rootStore.loginType loginType: root.rootStore.loginType
isControlNode: root.isControlNode isControlNode: root.isControlNode
communitySettingsDisabled: root.communitySettingsDisabled communitySettingsDisabled: root.communitySettingsDisabled
overviewChartData: rootStore.overviewChartData
onCollectCommunityMetricsMessagesTimestamps: {
rootStore.collectCommunityMetricsMessagesTimestamps(intervals)
}
onEdited: { onEdited: {
const error = root.chatCommunitySectionModule.editCommunity( const error = root.chatCommunitySectionModule.editCommunity(

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 0ae7aa44f00bff345539c1e288705057e7e4574c Subproject commit 4ad84d80cc7b0363f3c8da589d7bb8930fb8a629