Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Community): Community messaging statistics chart #11696

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app/modules/main/chat_section/controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ proc init*(self: Controller) =
discard self.delegate.addOrUpdateChat(args.chat, belongsToCommunity, self.events, self.settingsService,
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):
MishkaRogachev marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down Expand Up @@ -652,3 +661,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)
6 changes: 6 additions & 0 deletions src/app/modules/main/chat_section/io_interface.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 6 additions & 0 deletions src/app/modules/main/chat_section/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
19 changes: 19 additions & 0 deletions src/app/modules/main/chat_section/view.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,6 +66,7 @@ QtObject:
result.amIMember = false
result.requiresTokenPermissionToJoin = false
result.chatsLoaded = false
result.communityMetrics = "[]"

proc load*(self: View) =
self.delegate.viewDidLoad()
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/app/modules/main/communities/controller.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import stint
import stint, std/strutils
import ./io_interface

import ../../../core/signals/types
Expand Down
23 changes: 23 additions & 0 deletions src/app_service/service/community/async_tasks.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions src/app_service/service/community/dto/community.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions src/app_service/service/community/service.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -202,6 +208,7 @@ QtObject:
myCommunityRequests*: seq[CommunityMembershipRequestDto]
historyArchiveDownloadTaskCommunityIds*: HashSet[string]
requestedCommunityIds*: HashSet[string]
communityMetrics: Table[string, CommunityMetricsDto]

# Forward declaration
proc asyncLoadCuratedCommunities*(self: Service)
Expand Down Expand Up @@ -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]()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions src/backend/communities.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
7 changes: 7 additions & 0 deletions ui/app/AppLayouts/Chat/stores/RootStore.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ StackLayout {
property int loginType: Constants.LoginType.Password
property bool communitySettingsDisabled

property string overviewChartData: ""

function navigateBack() {
if (editSettingsPanelLoader.item.dirty)
settingsDirtyToastMessage.notifyDirty()
else
root.currentIndex = 0
}

signal collectCommunityMetricsMessagesTimestamps(var intervals)

signal edited(Item item) // item containing edited fields (name, description, logoImagePath, color, options, etc..)

signal inviteNewPeopleClicked
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down