From c68ec2c38272a3a178463ccdbd4c96b8f49708d5 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:55:37 +0000 Subject: [PATCH] Add analytics for Room Moderation. (#2571) * Add an option to use analytics locally. * Add analytics for Room Moderation. * Update tests. * Include the role in the event where appropriate. * Update the AnalyticsEvents package. --- ElementX.xcodeproj/project.pbxproj | 6 +++- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../RoomFlowCoordinator.swift | 13 ++++---- ...omRolesAndPermissionsFlowCoordinator.swift | 13 ++++++-- ...omChangePermissionsScreenCoordinator.swift | 4 ++- ...RoomChangePermissionsScreenViewModel.swift | 29 ++++++++++++++++-- .../View/RoomChangePermissionsScreen.swift | 3 +- .../RoomChangeRolesScreenCoordinator.swift | 4 ++- .../RoomChangeRolesScreenViewModel.swift | 22 +++++++++++++- .../View/RoomChangeRolesScreen.swift | 3 +- .../RoomMembersListScreenCoordinator.swift | 7 +++-- .../RoomMembersListScreenViewModel.swift | 8 ++++- .../RoomMembersListManageMemberSheet.swift | 3 +- .../View/RoomMembersListScreen.swift | 3 +- .../RoomMembersListScreenMemberCell.swift | 3 +- ...RolesAndPermissionsScreenCoordinator.swift | 4 ++- ...omRolesAndPermissionsScreenViewModel.swift | 7 ++++- .../View/RoomRolesAndPermissionsScreen.swift | 3 +- .../Services/Analytics/AnalyticsService.swift | 6 ++++ .../Helpers/RoomModerationRole.swift | 30 +++++++++++++++++++ .../UITests/UITestsAppCoordinator.swift | 11 +++++-- ...hangePermissionsScreenViewModelTests.swift | 8 +++-- .../RoomChangeRolesScreenViewModelTests.swift | 18 +++++------ .../RoomMembersListViewModelTests.swift | 3 +- ...esAndPermissionsScreenViewModelTests.swift | 27 ++++++++--------- project.yml | 3 +- 26 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1c6ba8ce5..5d1577890 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -524,6 +524,7 @@ 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; }; 835B7AD20407F766C747BEC5 /* RoomPollsHistoryScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D147EB979902DBBE452EADC /* RoomPollsHistoryScreenUITests.swift */; }; 83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; }; + 83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; }; @@ -1141,6 +1142,7 @@ 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; 0A634D8DD1E10D858CF7995D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = ""; }; 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingToolbar.swift; sourceTree = ""; }; + 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = ""; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = ""; }; 0BB05221D7D941CC82DC8480 /* LogViewerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModel.swift; sourceTree = ""; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; @@ -2789,6 +2791,7 @@ children = ( B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */, CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */, + 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */, ); path = Helpers; sourceTree = ""; @@ -6060,6 +6063,7 @@ F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */, C4078364FD9FA00EA9D00A15 /* RoomMembersListScreenViewModelProtocol.swift in Sources */, D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */, + 83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */, C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */, 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */, CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */, @@ -7124,7 +7128,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-analytics-events"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.11.0; + minimumVersion = 0.13.0; }; }; C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ef743e424..d20db5262 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -121,8 +121,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { - "revision" : "354562b5cabf2b9aec6cbb12e3a614490b3cc6e8", - "version" : "0.11.0" + "revision" : "ccc4af6aa00987abe7135fa0b7cea97c8cfb3d26", + "version" : "0.13.0" } }, { diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 985186968..3b692a2ba 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -592,10 +592,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { fatalError() } - let params = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider, - roomProxy: roomProxy, - appSettings: appSettings) - let coordinator = RoomMembersListScreenCoordinator(parameters: params) + let parameters = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider, + roomProxy: roomProxy, + userIndicatorController: userIndicatorController, + appSettings: appSettings, + analytics: analytics) + let coordinator = RoomMembersListScreenCoordinator(parameters: parameters) coordinator.actions .sink { [weak self] action in @@ -1176,7 +1178,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomRolesAndPermissionsFlowCoordinatorParameters(roomProxy: roomProxy, navigationStackCoordinator: navigationStackCoordinator, - userIndicatorController: userIndicatorController) + userIndicatorController: userIndicatorController, + analytics: analytics) let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [weak self] action in switch action { diff --git a/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift index a14dd3fe8..d30e93782 100644 --- a/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift @@ -27,12 +27,14 @@ struct RoomRolesAndPermissionsFlowCoordinatorParameters { let roomProxy: RoomProxyProtocol let navigationStackCoordinator: NavigationStackCoordinator let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { private let roomProxy: RoomProxyProtocol private let navigationStackCoordinator: NavigationStackCoordinator private let userIndicatorController: UserIndicatorControllerProtocol + private let analytics: AnalyticsService enum State: StateType { /// The state machine hasn't started. @@ -74,6 +76,7 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { roomProxy = parameters.roomProxy navigationStackCoordinator = parameters.navigationStackCoordinator userIndicatorController = parameters.userIndicatorController + analytics = parameters.analytics stateMachine = .init(state: .initial) configureStateMachine() @@ -132,7 +135,9 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { } private func presentRolesAndPermissionsScreen() { - let parameters = RoomRolesAndPermissionsScreenCoordinatorParameters(roomProxy: roomProxy, userIndicatorController: userIndicatorController) + let parameters = RoomRolesAndPermissionsScreenCoordinatorParameters(roomProxy: roomProxy, + userIndicatorController: userIndicatorController, + analytics: analytics) let coordinator = RoomRolesAndPermissionsScreenCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [stateMachine] action in switch action { @@ -159,7 +164,8 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: mode, roomProxy: roomProxy, - userIndicatorController: userIndicatorController) + userIndicatorController: userIndicatorController, + analytics: analytics) let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } @@ -180,7 +186,8 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomChangePermissionsScreenCoordinatorParameters(permissions: permissions, permissionsGroup: group, roomProxy: roomProxy, - userIndicatorController: userIndicatorController) + userIndicatorController: userIndicatorController, + analytics: analytics) let coordinator = RoomChangePermissionsScreenCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift index 8fe839674..29a1ef449 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift @@ -24,6 +24,7 @@ struct RoomChangePermissionsScreenCoordinatorParameters { let permissionsGroup: RoomRolesAndPermissionsScreenPermissionsGroup let roomProxy: RoomProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } enum RoomChangePermissionsScreenCoordinatorAction { @@ -46,7 +47,8 @@ final class RoomChangePermissionsScreenCoordinator: CoordinatorProtocol { viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: parameters.permissions, group: parameters.permissionsGroup, roomProxy: parameters.roomProxy, - userIndicatorController: parameters.userIndicatorController) + userIndicatorController: parameters.userIndicatorController, + analytics: parameters.analytics) } func start() { diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift index 6aafcfff9..f5751d01c 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift @@ -23,8 +23,9 @@ typealias RoomChangePermissionsScreenViewModelType = StateStoreViewModel = .init() + private let analytics: AnalyticsService + private var actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -32,9 +33,11 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel init(currentPermissions: RoomPermissions, group: RoomRolesAndPermissionsScreenPermissionsGroup, roomProxy: RoomProxyProtocol, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + analytics: AnalyticsService) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController + self.analytics = analytics super.init(initialViewState: .init(currentPermissions: currentPermissions, group: group)) } @@ -64,13 +67,15 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel } var changes = RoomPowerLevelChanges() - for setting in state.bindings.settings { + let changedSettings = state.bindings.settings.filter { state.currentPermissions[keyPath: $0.keyPath] != $0.value } + for setting in changedSettings { changes[keyPath: setting.rustKeyPath] = setting.value.rustPowerLevel } switch await roomProxy.applyPowerLevelChanges(changes) { case .success: MXLog.info("Success") + trackChanges(changedSettings) actionsSubject.send(.complete) case .failure: context.alertInfo = AlertInfo(id: .generic) @@ -92,4 +97,22 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel private func hideLoadingIndicator() { userIndicatorController.retractIndicatorWithId(Self.indicatorID) } + + // MARK: Analytics + + private func trackChanges(_ settings: [RoomPermissionsSetting]) { + for setting in settings { + switch setting.keyPath { + case \.ban: analytics.trackRoomModeration(action: .ChangePermissionsBanMembers, role: setting.value) + case \.invite: analytics.trackRoomModeration(action: .ChangePermissionsInviteUsers, role: setting.value) + case \.kick: analytics.trackRoomModeration(action: .ChangePermissionsKickMembers, role: setting.value) + case \.redact: analytics.trackRoomModeration(action: .ChangePermissionsRedactMessages, role: setting.value) + case \.eventsDefault: analytics.trackRoomModeration(action: .ChangePermissionsSendMessages, role: setting.value) + case \.roomName: analytics.trackRoomModeration(action: .ChangePermissionsRoomName, role: setting.value) + case \.roomAvatar: analytics.trackRoomModeration(action: .ChangePermissionsRoomAvatar, role: setting.value) + case \.roomTopic: analytics.trackRoomModeration(action: .ChangePermissionsRoomTopic, role: setting.value) + default: MXLog.warning("Unexpected change: \(setting.keyPath).") + } + } + } } diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift index 11222d740..a3a0203b3 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift @@ -78,6 +78,7 @@ struct RoomChangePermissionsScreen_Previews: PreviewProvider, TestablePreview { RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock), group: group, roomProxy: RoomProxyMock(with: .init()), - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift index da8454b60..bfa83d1fb 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift @@ -23,6 +23,7 @@ struct RoomChangeRolesScreenCoordinatorParameters { let mode: RoomMemberDetails.Role let roomProxy: RoomProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } enum RoomChangeRolesScreenCoordinatorAction { @@ -45,7 +46,8 @@ final class RoomChangeRolesScreenCoordinator: CoordinatorProtocol { viewModel = RoomChangeRolesScreenViewModel(mode: parameters.mode, roomProxy: parameters.roomProxy, - userIndicatorController: parameters.userIndicatorController) + userIndicatorController: parameters.userIndicatorController, + analytics: parameters.analytics) } func start() { diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index 3a8f76980..d35ffdb1f 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -22,17 +22,22 @@ typealias RoomChangeRolesScreenViewModelType = StateStoreViewModel = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(mode: RoomMemberDetails.Role, roomProxy: RoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + init(mode: RoomMemberDetails.Role, + roomProxy: RoomProxyProtocol, + userIndicatorController: UserIndicatorControllerProtocol, + analytics: AnalyticsService) { guard mode != .user else { fatalError("Invalid screen configuration: \(mode)") } self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController + self.analytics = analytics super.init(initialViewState: RoomChangeRolesScreenViewState(mode: mode, members: [], @@ -134,6 +139,9 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh _ = await infoTask.value await roomProxy.updateMembers() + trackChanges(promotionCount: promotingUpdates.count, + demotionCount: demotingUpdates.count) + actionsSubject.send(.complete) case .failure: context.alertInfo = AlertInfo(id: .error) @@ -154,4 +162,16 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh private func hideSavingIndicator() { userIndicatorController.retractIndicatorWithId(Self.indicatorID) } + + // MARK: Analytics + + private func trackChanges(promotionCount: Int, demotionCount: Int) { + for _ in 0.. RoomChangeRolesScreenViewModel { RoomChangeRolesScreenViewModel(mode: mode, roomProxy: RoomProxyMock(with: .init(members: .allMembersAsAdmin)), - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 1b7968ea8..58052d813 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -20,7 +20,9 @@ import SwiftUI struct RoomMembersListScreenCoordinatorParameters { let mediaProvider: MediaProviderProtocol let roomProxy: RoomProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol let appSettings: AppSettings + let analytics: AnalyticsService } enum RoomMembersListScreenCoordinatorAction { @@ -41,8 +43,9 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { init(parameters: RoomMembersListScreenCoordinatorParameters) { viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy, mediaProvider: parameters.mediaProvider, - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appSettings: parameters.appSettings) + userIndicatorController: parameters.userIndicatorController, + appSettings: parameters.appSettings, + analytics: parameters.analytics) } func start() { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index b70ab92d2..ef8409944 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -23,6 +23,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private let roomProxy: RoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let appSettings: AppSettings + private let analytics: AnalyticsService private var members: [RoomMemberProxyProtocol] = [] @@ -36,10 +37,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol, - appSettings: AppSettings) { + appSettings: AppSettings, + analytics: AnalyticsService) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.appSettings = appSettings + self.analytics = analytics super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, bindings: .init(mode: initialMode)), @@ -182,6 +185,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe case .success: state.bindings.memberToManage = nil hideManageMemberIndicator(title: indicatorTitle) + analytics.trackRoomModeration(action: .KickMember, role: nil) case .failure: showManageMemberFailure(title: indicatorTitle) } @@ -195,6 +199,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe case .success: state.bindings.memberToManage = nil hideManageMemberIndicator(title: indicatorTitle) + analytics.trackRoomModeration(action: .BanMember, role: nil) case .failure: showManageMemberFailure(title: indicatorTitle) } @@ -208,6 +213,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe case .success: state.bindings.memberToManage = nil hideManageMemberIndicator(title: indicatorTitle) + analytics.trackRoomModeration(action: .UnbanMember, role: nil) case .failure: showManageMemberFailure(title: indicatorTitle) } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift index aef6a5cdc..154dd2e17 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift @@ -106,6 +106,7 @@ private extension RoomMembersListScreenViewModel { roomProxy: RoomProxyMock(with: .init(members: .allMembersAsAdmin)), mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index 729a6b6ad..8256a2c19 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -183,6 +183,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview { canUserInvite: false)), mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index bafa190f3..7f034a06b 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -110,7 +110,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { members: members)), mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics) static var previews: some View { VStack(spacing: 12) { Section("Invited/Joined") { diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenCoordinator.swift index 87fd9f9c9..07e5156a2 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenCoordinator.swift @@ -22,6 +22,7 @@ import SwiftUI struct RoomRolesAndPermissionsScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } enum RoomRolesAndPermissionsScreenCoordinatorAction { @@ -41,7 +42,8 @@ final class RoomRolesAndPermissionsScreenCoordinator: CoordinatorProtocol { init(parameters: RoomRolesAndPermissionsScreenCoordinatorParameters) { viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: parameters.roomProxy, - userIndicatorController: parameters.userIndicatorController) + userIndicatorController: parameters.userIndicatorController, + analytics: parameters.analytics) } func start() { diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index e47c5853e..a4723f6dd 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -22,15 +22,17 @@ typealias RoomRolesAndPermissionsScreenViewModelType = StateStoreViewModel = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(initialPermissions: RoomPermissions? = nil, roomProxy: RoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + init(initialPermissions: RoomPermissions? = nil, roomProxy: RoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol, analytics: AnalyticsService) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController + self.analytics = analytics super.init(initialViewState: RoomRolesAndPermissionsScreenViewState(permissions: initialPermissions)) // Automatically update the admin/moderator counts. @@ -107,6 +109,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM _ = await infoTask.value await roomProxy.updateMembers() + analytics.trackRoomModeration(action: .ChangeMemberRole, role: role) + actionsSubject.send(.demotedOwnUser) showSuccessIndicator() case .failure: @@ -141,6 +145,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM switch await roomProxy.resetPowerLevels() { case .success: + analytics.trackRoomModeration(action: .ResetPermissions, role: nil) showSuccessIndicator() case .failure: state.bindings.alertInfo = AlertInfo(id: .error) diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift index 3bf4a9855..8c29a9711 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift @@ -128,7 +128,8 @@ struct RoomRolesAndPermissionsScreen: View { struct RoomRolesAndPermissionsScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomRolesAndPermissionsScreenViewModel(initialPermissions: RoomPermissions(powerLevels: .mock), roomProxy: RoomProxyMock(with: .init(members: .allMembersAsAdmin)), - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { RoomRolesAndPermissionsScreen(context: viewModel.context) diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift index 31fc21459..bd5700094 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsService.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsService.swift @@ -196,4 +196,10 @@ extension AnalyticsService { func trackPollEnd() { capture(event: AnalyticsEvent.PollEnd(doNotUse: nil)) } + + /// Track a room moderation action. + func trackRoomModeration(action: AnalyticsEvent.RoomModeration.Action, role: RoomMemberDetails.Role?) { + let role = role.map(AnalyticsEvent.RoomModeration.Role.init) + capture(event: AnalyticsEvent.RoomModeration(action: action, role: role)) + } } diff --git a/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift b/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift new file mode 100644 index 000000000..a0a858681 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift @@ -0,0 +1,30 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AnalyticsEvents + +extension AnalyticsEvent.RoomModeration.Role { + init(role: RoomMemberDetails.Role) { + switch role { + case .administrator: + self = .Administrator + case .moderator: + self = .Moderator + case .user: + self = .User + } + } +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 3f26b466a..a20632214 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -718,7 +718,9 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie] let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(), roomProxy: RoomProxyMock(with: .init(name: "test", members: members)), - appSettings: ServiceLocator.shared.settings)) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMembersListScreenPendingInvites: @@ -726,7 +728,9 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(), roomProxy: RoomProxyMock(with: .init(name: "test", members: members)), - appSettings: ServiceLocator.shared.settings)) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomNotificationSettingsDefaultSetting: @@ -752,7 +756,8 @@ class MockScreen: Identifiable { navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(members: .allMembersAsAdmin)), navigationStackCoordinator: navigationStackCoordinator, - userIndicatorController: ServiceLocator.shared.userIndicatorController)) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + analytics: ServiceLocator.shared.analytics)) retainedState.append(coordinator) coordinator.start() return navigationStackCoordinator diff --git a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift index 5f833fb18..2d2843b9d 100644 --- a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift @@ -32,7 +32,8 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase { viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock), group: .roomDetails, roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) } func testChangeSetting() { @@ -62,6 +63,7 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase { context.settings[index] = RoomPermissionsSetting(title: "", value: .user, keyPath: \.roomAvatar) XCTAssertEqual(context.settings[index].value, .user) XCTAssertTrue(context.viewState.hasChanges) + XCTAssertEqual(context.settings.count, 3) // When saving changes. context.send(viewAction: .save) @@ -70,8 +72,8 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase { // Then the changes should be applied. XCTAssertTrue(roomProxy.applyPowerLevelChangesCalled) - XCTAssertEqual(roomProxy.applyPowerLevelChangesReceivedChanges, .init(roomName: 50, roomAvatar: 0, roomTopic: 50), - "Only the changes for this screen should be applied, the others should be nil to remain unchanged.") + XCTAssertEqual(roomProxy.applyPowerLevelChangesReceivedChanges, .init(roomAvatar: 0), + "Only the avatar setting should be applied. No other settings were changed so they should be nil to remain left alone.") } func testSaveNoChanges() async throws { diff --git a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift index 4c79ad2df..a1cf3d58e 100644 --- a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift @@ -28,8 +28,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { } func testInitialStateAdministrators() { - setupRoomProxy() - viewModel = RoomChangeRolesScreenViewModel(mode: .administrator, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(mode: .administrator) XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, []) XCTAssertEqual(context.viewState.members, context.viewState.visibleMembers) @@ -40,8 +39,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { } func testInitialStateModerators() { - setupRoomProxy() - viewModel = RoomChangeRolesScreenViewModel(mode: .moderator, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(mode: .moderator) XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, []) XCTAssertEqual(context.viewState.members, context.viewState.visibleMembers) @@ -150,8 +148,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { func testSaveModeratorChanges() async throws { // Given the change roles view model for moderators. - setupRoomProxy() - viewModel = RoomChangeRolesScreenViewModel(mode: .moderator, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(mode: .moderator) guard let firstUser = context.viewState.members.first(where: { !context.viewState.isMemberSelected($0) }), let existingModerator = context.viewState.membersWithRole.first else { @@ -175,8 +172,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { func testSavePromotedAdministrator() async throws { // Given the change roles view model for administrators. - setupRoomProxy() - viewModel = RoomChangeRolesScreenViewModel(mode: .administrator, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(mode: .administrator) XCTAssertNil(context.alertInfo) guard let firstUser = context.viewState.members.first(where: { !context.viewState.isMemberSelected($0) }) else { @@ -201,7 +197,11 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == firstUser.id && $0.powerLevel == 100 }), true) } - private func setupRoomProxy() { + private func setupViewModel(mode: RoomMemberDetails.Role) { roomProxy = RoomProxyMock(with: .init(members: .allMembersAsAdmin)) + viewModel = RoomChangeRolesScreenViewModel(mode: mode, + roomProxy: roomProxy, + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) } } diff --git a/UnitTests/Sources/RoomMembersListViewModelTests.swift b/UnitTests/Sources/RoomMembersListViewModelTests.swift index 7eca3ce0c..cc91de610 100644 --- a/UnitTests/Sources/RoomMembersListViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListViewModelTests.swift @@ -170,6 +170,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { viewModel = .init(roomProxy: roomProxy, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics) } } diff --git a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift index 4e26d705a..356c1d326 100644 --- a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift @@ -28,25 +28,19 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { } func testEmptyCounters() { - roomProxy = RoomProxyMock(with: .init()) - viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(members: .allMembers) XCTAssertEqual(context.viewState.administratorCount, 0) XCTAssertEqual(context.viewState.moderatorCount, 0) } func testFilledCounters() { - roomProxy = RoomProxyMock(with: .init(members: .allMembersAsAdmin)) - viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(members: .allMembersAsAdmin) XCTAssertEqual(context.viewState.administratorCount, 2) XCTAssertEqual(context.viewState.moderatorCount, 1) } func testResetPermissions() async throws { - roomProxy = RoomProxyMock(with: .init(members: .allMembersAsAdmin)) - viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(members: .allMembersAsAdmin) context.send(viewAction: .reset) XCTAssertNotNil(context.alertInfo) @@ -59,9 +53,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { } func testDemoteToModerator() async throws { - roomProxy = RoomProxyMock(with: .init(members: .allMembersAsAdmin)) - viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(members: .allMembersAsAdmin) context.send(viewAction: .editOwnUserRole) XCTAssertNotNil(context.alertInfo) @@ -76,9 +68,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { } func testDemoteToMember() async throws { - roomProxy = RoomProxyMock(with: .init(members: .allMembersAsAdmin)) - viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, - userIndicatorController: UserIndicatorControllerMock()) + setupViewModel(members: .allMembersAsAdmin) context.send(viewAction: .editOwnUserRole) XCTAssertNotNil(context.alertInfo) @@ -91,4 +81,11 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel, RoomMemberDetails.Role.user.rustPowerLevel) } + + private func setupViewModel(members: [RoomMemberProxyMock]) { + roomProxy = RoomProxyMock(with: .init(members: members)) + viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) + } } diff --git a/project.yml b/project.yml index 0c5249be4..7594a6cb0 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,8 @@ packages: # path: ../compound-ios AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events - minorVersion: 0.11.0 + minorVersion: 0.13.0 + # path: ../matrix-analytics-events Emojibase: url: https://github.com/matrix-org/emojibase-bindings minorVersion: 1.0.0