From c2568f84d2513db4dd2181e1697f6605fdcfc2f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 May 2025 12:25:19 +0200 Subject: [PATCH] Feature : Report room (#4654) * feature (report room) : introduce all presentation classes. * feature (report room) : branch entry point in the room list * refactor (matrix ui) : move some code from appnav to matrix ui * feature (report room) : add api on room * feature (report room) : adjust ui * feature (report room) : branch api * feature (decline invite and block) : move things around and introduce presentation classes * feature (decline invite and block) : continue to move things * feature (report room) : remove reference to "conversation" for now * feature (report room) : add report room action to room detail screen * feature (report room) : enabled button state * feature (report room) : improve code and reuse * feature (report room) : add feature flag * feature (report room) : change feature flag to static bool * feature (report room) : add tests * feature (report room) : fix ui with new api on ListItem * feature (report room) : clean up and add more tests. * Update screenshots * feature (report room) : more test and fix issue * feature (report room) : update strings * feature (report room) : fix konsist preview * feature (report room) : disable feature * Update screenshots * var -> val * Improve preview of AcceptDeclineInviteView * Improve preview consistency * Add missing test on DismissErrorAndHideContent * Update screenshots * Add missing tests --------- Co-authored-by: ElementBot Co-authored-by: Benoit Marty --- .../android/appconfig/MatrixConfiguration.kt | 3 + .../android/appnav/room/RoomFlowNode.kt | 2 +- .../appnav/room/joined/JoinedRoomFlowNode.kt | 2 + .../appnav/room/joined/LoadingRoomNodeView.kt | 2 + .../LoadingBaseRoomStateFlowFactoryTest.kt | 4 +- features/invite/api/build.gradle.kts | 3 + .../android/features/invite/api/InviteData.kt | 38 ++ .../AcceptDeclineInviteEvents.kt | 15 + .../AcceptDeclineInviteState.kt | 2 +- .../AcceptDeclineInviteStateProvider.kt | 18 +- .../AcceptDeclineInviteView.kt | 6 +- .../ConfirmingDeclineInvite.kt | 8 +- .../DeclineInviteAndBlockEntryPoint.kt | 17 + .../api/response/AcceptDeclineInviteEvents.kt | 13 - .../invite/api/response/InviteData.kt | 18 - features/invite/impl/build.gradle.kts | 8 + .../features/invite/impl/AcceptInvite.kt | 42 ++ .../features/invite/impl/DeclineInvite.kt | 74 ++++ .../AcceptDeclineInvitePresenter.kt | 103 +++++ .../AcceptDeclineInviteView.kt | 62 ++- .../DefaultAcceptDeclineInviteView.kt | 14 +- .../InternalAcceptDeclineInviteEvents.kt | 5 +- .../declineandblock/DeclineAndBlockEvents.kt | 16 + .../declineandblock/DeclineAndBlockNode.kt | 43 ++ .../DeclineAndBlockPresenter.kt | 92 +++++ .../declineandblock/DeclineAndBlockState.kt | 20 + .../DeclineAndBlockStateProvider.kt | 45 +++ .../declineandblock/DeclineAndBlockView.kt | 159 ++++++++ .../DefaultDeclineAndBlockEntryPoint.kt | 25 ++ .../features/invite/impl/di/InviteModule.kt | 4 +- .../response/AcceptDeclineInvitePresenter.kt | 135 ------- .../impl/response/InvalidDataException.kt | 10 - .../src/main/res/values-cs/translations.xml | 5 + .../src/main/res/values-cy/translations.xml | 5 + .../src/main/res/values-de/translations.xml | 5 + .../src/main/res/values-el/translations.xml | 5 + .../src/main/res/values-et/translations.xml | 5 + .../src/main/res/values-fi/translations.xml | 5 + .../src/main/res/values-fr/translations.xml | 5 + .../src/main/res/values-hu/translations.xml | 5 + .../src/main/res/values-nb/translations.xml | 5 + .../src/main/res/values-pl/translations.xml | 5 + .../src/main/res/values-ru/translations.xml | 3 + .../src/main/res/values-sk/translations.xml | 5 + .../src/main/res/values-sv/translations.xml | 5 + .../src/main/res/values-uk/translations.xml | 5 + .../main/res/values-zh-rTW/translations.xml | 4 + .../impl/src/main/res/values/localazy.xml | 5 + .../invite/impl/DefaultAcceptInviteTest.kt | 94 +++++ .../invite/impl/DefaultDeclineInviteTest.kt | 178 +++++++++ .../AcceptDeclineInvitePresenterTest.kt | 217 +++++++++++ .../DeclineAndBlockPresenterTest.kt | 175 +++++++++ .../DeclineAndBlockViewTest.kt | 124 ++++++ .../invite/impl/fake/FakeAcceptInvite.kt | 21 + .../invite/impl/fake/FakeDeclineInvite.kt | 21 + .../AcceptDeclineInvitePresenterTest.kt | 368 ------------------ features/invite/test/build.gradle.kts | 1 + .../features/invite/test/InviteData.kt | 23 ++ features/joinroom/impl/build.gradle.kts | 1 + .../impl/DefaultJoinRoomEntryPoint.kt | 2 +- .../features/joinroom/impl/JoinRoomEvents.kt | 6 +- .../joinroom/impl/JoinRoomFlowNode.kt | 101 +++++ .../features/joinroom/impl/JoinRoomNode.kt | 58 --- .../joinroom/impl/JoinRoomPresenter.kt | 46 +-- .../features/joinroom/impl/JoinRoomState.kt | 6 +- .../joinroom/impl/JoinRoomStateProvider.kt | 31 +- .../features/joinroom/impl/JoinRoomView.kt | 25 +- .../joinroom/impl/di/JoinRoomModule.kt | 2 +- .../joinroom/impl/JoinRoomPresenterTest.kt | 240 +++++++++++- .../joinroom/impl/JoinRoomViewTest.kt | 55 ++- .../features/leaveroom/api/LeaveRoomView.kt | 4 +- features/reportroom/api/build.gradle.kts | 19 + .../reportroom/api/ReportRoomEntryPoint.kt | 17 + features/reportroom/impl/build.gradle.kts | 45 +++ .../impl/DefaultReportRoomEntryPoint.kt | 24 ++ .../features/reportroom/impl/ReportRoom.kt | 60 +++ .../reportroom/impl/ReportRoomEvents.kt | 15 + .../reportroom/impl/ReportRoomNode.kt | 43 ++ .../reportroom/impl/ReportRoomPresenter.kt | 83 ++++ .../reportroom/impl/ReportRoomState.kt | 19 + .../impl/ReportRoomStateProvider.kt | 38 ++ .../reportroom/impl/ReportRoomView.kt | 154 ++++++++ .../src/main/res/values-cs/translations.xml | 8 + .../src/main/res/values-cy/translations.xml | 8 + .../src/main/res/values-de/translations.xml | 8 + .../src/main/res/values-el/translations.xml | 8 + .../src/main/res/values-et/translations.xml | 8 + .../src/main/res/values-fi/translations.xml | 8 + .../src/main/res/values-fr/translations.xml | 8 + .../src/main/res/values-hu/translations.xml | 8 + .../src/main/res/values-nb/translations.xml | 8 + .../src/main/res/values-pl/translations.xml | 8 + .../src/main/res/values-ru/translations.xml | 6 + .../src/main/res/values-sk/translations.xml | 8 + .../src/main/res/values-sv/translations.xml | 8 + .../src/main/res/values-uk/translations.xml | 8 + .../main/res/values-zh-rTW/translations.xml | 7 + .../impl/src/main/res/values/localazy.xml | 8 + .../reportroom/impl/DefaultReportRoomTest.kt | 150 +++++++ .../impl/ReportRoomPresenterTest.kt | 151 +++++++ .../reportroom/impl/ReportRoomViewTest.kt | 101 +++++ .../reportroom/impl/fakes/FakeReportRoom.kt | 26 ++ features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 12 + .../roomdetails/impl/RoomDetailsNode.kt | 6 + .../roomdetails/impl/RoomDetailsPresenter.kt | 11 +- .../roomdetails/impl/RoomDetailsState.kt | 1 + .../impl/RoomDetailsStateProvider.kt | 2 + .../roomdetails/impl/RoomDetailsView.kt | 34 +- .../roomdetails/impl/RoomDetailsViewTest.kt | 16 + features/roomlist/impl/build.gradle.kts | 1 + .../impl/DefaultRoomListEntryPoint.kt | 2 +- .../roomlist/impl/RoomListContextMenu.kt | 35 +- .../impl/RoomListDeclineInviteMenu.kt | 125 ++++++ .../features/roomlist/impl/RoomListEvents.kt | 9 +- .../roomlist/impl/RoomListFlowNode.kt | 166 ++++++++ .../features/roomlist/impl/RoomListNode.kt | 111 ------ .../roomlist/impl/RoomListPresenter.kt | 45 +-- .../features/roomlist/impl/RoomListState.kt | 9 +- .../roomlist/impl/RoomListStateProvider.kt | 8 +- .../features/roomlist/impl/RoomListView.kt | 14 + .../impl/components/RoomSummaryRow.kt | 2 +- .../impl/model/RoomListRoomSummary.kt | 7 + .../roomlist/impl/RoomListContextMenuTest.kt | 49 ++- .../impl/RoomListDeclineInviteMenuTest.kt | 100 +++++ .../roomlist/impl/RoomListPresenterTest.kt | 16 +- .../roomlist/impl/RoomListViewTest.kt | 10 +- .../libraries/featureflag/api/FeatureFlags.kt | 2 +- .../libraries/matrix/api/room/BaseRoom.kt | 7 + .../matrix/impl/room/RustBaseRoom.kt | 7 + .../matrix/test/room/FakeBaseRoom.kt | 3 + .../matrix/ui/room}/LoadingRoomState.kt | 5 +- .../src/main/res/values-cs/translations.xml | 10 - .../src/main/res/values-cy/translations.xml | 10 - .../src/main/res/values-de/translations.xml | 10 - .../src/main/res/values-el/translations.xml | 10 - .../src/main/res/values-et/translations.xml | 10 - .../src/main/res/values-fi/translations.xml | 10 - .../src/main/res/values-fr/translations.xml | 10 - .../src/main/res/values-hu/translations.xml | 10 - .../src/main/res/values-nb/translations.xml | 10 - .../src/main/res/values-pl/translations.xml | 10 - .../src/main/res/values-ru/translations.xml | 6 - .../src/main/res/values-sk/translations.xml | 10 - .../src/main/res/values-sv/translations.xml | 10 - .../src/main/res/values-uk/translations.xml | 10 - .../main/res/values-zh-rTW/translations.xml | 8 - .../src/main/res/values/localazy.xml | 10 - ...line_AcceptDeclineInviteView_Day_0_en.png} | 0 ...cline_AcceptDeclineInviteView_Day_1_en.png | 3 + ...cline_AcceptDeclineInviteView_Day_2_en.png | 3 + ...cline_AcceptDeclineInviteView_Day_3_en.png | 3 + ...cline_AcceptDeclineInviteView_Day_4_en.png | 3 + ...ne_AcceptDeclineInviteView_Night_0_en.png} | 0 ...ine_AcceptDeclineInviteView_Night_1_en.png | 3 + ...ine_AcceptDeclineInviteView_Night_2_en.png | 3 + ...ine_AcceptDeclineInviteView_Night_3_en.png | 3 + ...ine_AcceptDeclineInviteView_Night_4_en.png | 3 + ...eandblock_DeclineAndBlockView_Day_0_en.png | 3 + ...eandblock_DeclineAndBlockView_Day_1_en.png | 3 + ...eandblock_DeclineAndBlockView_Day_2_en.png | 3 + ...eandblock_DeclineAndBlockView_Day_3_en.png | 3 + ...eandblock_DeclineAndBlockView_Day_4_en.png | 3 + ...ndblock_DeclineAndBlockView_Night_0_en.png | 3 + ...ndblock_DeclineAndBlockView_Night_1_en.png | 3 + ...ndblock_DeclineAndBlockView_Night_2_en.png | 3 + ...ndblock_DeclineAndBlockView_Night_3_en.png | 3 + ...ndblock_DeclineAndBlockView_Night_4_en.png | 3 + ...ponse_AcceptDeclineInviteView_Day_1_en.png | 3 - ...ponse_AcceptDeclineInviteView_Day_2_en.png | 3 - ...ponse_AcceptDeclineInviteView_Day_3_en.png | 3 - ...ponse_AcceptDeclineInviteView_Day_4_en.png | 3 - ...ponse_AcceptDeclineInviteView_Day_5_en.png | 3 - ...nse_AcceptDeclineInviteView_Night_1_en.png | 3 - ...nse_AcceptDeclineInviteView_Night_2_en.png | 3 - ...nse_AcceptDeclineInviteView_Night_3_en.png | 3 - ...nse_AcceptDeclineInviteView_Night_4_en.png | 3 - ...nse_AcceptDeclineInviteView_Night_5_en.png | 3 - ...s.leaveroom.api_LeaveRoomView_Day_6_en.png | 4 +- ...leaveroom.api_LeaveRoomView_Night_6_en.png | 4 +- ...eportroom.impl_ReportRoomView_Day_0_en.png | 3 + ...eportroom.impl_ReportRoomView_Day_1_en.png | 3 + ...eportroom.impl_ReportRoomView_Day_2_en.png | 3 + ...eportroom.impl_ReportRoomView_Day_3_en.png | 3 + ...eportroom.impl_ReportRoomView_Day_4_en.png | 3 + ...ortroom.impl_ReportRoomView_Night_0_en.png | 3 + ...ortroom.impl_ReportRoomView_Night_1_en.png | 3 + ...ortroom.impl_ReportRoomView_Night_2_en.png | 3 + ...ortroom.impl_ReportRoomView_Night_3_en.png | 3 + ...ortroom.impl_ReportRoomView_Night_4_en.png | 3 + ...roomdetails.impl_RoomDetailsDark_10_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_11_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_12_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_13_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_16_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_1_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_2_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_3_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_4_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_7_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_8_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_9_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_10_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_11_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_12_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_13_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_16_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_1_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_2_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_3_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_4_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_7_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_8_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_9_en.png | 4 +- ...mListDeclineInviteMenuContent_Day_0_en.png | 3 + ...istDeclineInviteMenuContent_Night_0_en.png | 3 + ...omListModalBottomSheetContent_Day_0_en.png | 4 +- ...omListModalBottomSheetContent_Day_1_en.png | 4 +- ...omListModalBottomSheetContent_Day_2_en.png | 4 +- ...ListModalBottomSheetContent_Night_0_en.png | 4 +- ...ListModalBottomSheetContent_Night_1_en.png | 4 +- ...ListModalBottomSheetContent_Night_2_en.png | 4 +- ...es.roomlist.impl_RoomListView_Day_3_en.png | 4 +- ...es.roomlist.impl_RoomListView_Day_4_en.png | 4 +- ...es.roomlist.impl_RoomListView_Day_5_en.png | 4 +- ....roomlist.impl_RoomListView_Night_3_en.png | 4 +- ....roomlist.impl_RoomListView_Night_4_en.png | 4 +- ....roomlist.impl_RoomListView_Night_5_en.png | 4 +- tools/localazy/config.json | 9 +- 229 files changed, 3995 insertions(+), 1210 deletions(-) create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt rename features/invite/api/src/main/kotlin/io/element/android/features/invite/api/{response => acceptdecline}/AcceptDeclineInviteState.kt (88%) rename features/invite/api/src/main/kotlin/io/element/android/features/invite/api/{response => acceptdecline}/AcceptDeclineInviteStateProvider.kt (67%) rename features/invite/api/src/main/kotlin/io/element/android/features/invite/api/{response => acceptdecline}/AcceptDeclineInviteView.kt (74%) rename features/invite/api/src/main/kotlin/io/element/android/features/invite/api/{response => acceptdecline}/ConfirmingDeclineInvite.kt (52%) create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt delete mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt delete mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{response => acceptdecline}/AcceptDeclineInviteView.kt (62%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{response => acceptdecline}/DefaultAcceptDeclineInviteView.kt (64%) rename features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/{response => acceptdecline}/InternalAcceptDeclineInviteEvents.kt (69%) create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt delete mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt delete mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt create mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt delete mode 100644 features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt create mode 100644 features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt create mode 100644 features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt delete mode 100644 features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt create mode 100644 features/reportroom/api/build.gradle.kts create mode 100644 features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt create mode 100644 features/reportroom/impl/build.gradle.kts create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt create mode 100644 features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt create mode 100644 features/reportroom/impl/src/main/res/values-cs/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-cy/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-de/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-el/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-et/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-fi/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-fr/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-hu/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-nb/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-pl/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-ru/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-sk/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-sv/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-uk/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml create mode 100644 features/reportroom/impl/src/main/res/values/localazy.xml create mode 100644 features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt create mode 100644 features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt create mode 100644 features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt create mode 100644 features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenu.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListFlowNode.kt delete mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt create mode 100644 features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt rename {appnav/src/main/kotlin/io/element/android/appnav/room/joined => libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room}/LoadingRoomState.kt (91%) rename tests/uitests/src/test/snapshots/images/{features.invite.impl.response_AcceptDeclineInviteView_Day_0_en.png => features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en.png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png rename tests/uitests/src/test/snapshots/images/{features.invite.impl.response_AcceptDeclineInviteView_Night_0_en.png => features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en.png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Night_0_en.png diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt index a8f424bd08..d5bc728bd9 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt @@ -10,4 +10,7 @@ package io.element.android.appconfig object MatrixConfiguration { const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/" val clientPermalinkBaseUrl: String? = null + + // TODO remove this when report is fixed + const val CAN_REPORT_ROOM = false } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index a2016a9171..0dca1d6221 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -28,7 +28,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.LoadingRoomNodeView -import io.element.android.appnav.room.joined.LoadingRoomState import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.features.roomdirectory.api.RoomDescription @@ -49,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.sync.SyncService +import io.element.android.libraries.matrix.ui.room.LoadingRoomState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt index 3c6f30af72..07580060c7 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.sync.SyncService +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt index 83c4e0e623..938e46f915 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt @@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt index d27cf5b567..91e0b9f968 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt @@ -9,8 +9,6 @@ package io.element.android.appnav.room import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.appnav.room.joined.LoadingRoomState -import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -18,6 +16,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/invite/api/build.gradle.kts b/features/invite/api/build.gradle.kts index c4039e2382..d17b392e59 100644 --- a/features/invite/api/build.gradle.kts +++ b/features/invite/api/build.gradle.kts @@ -7,6 +7,7 @@ plugins { id("io.element.android-compose-library") + id("kotlin-parcelize") } android { @@ -16,5 +17,7 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) implementation(projects.services.analytics.api) } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt new file mode 100644 index 0000000000..eb1dfd3df6 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.api + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomInfo +import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import kotlinx.parcelize.Parcelize + +@Parcelize +data class InviteData( + val roomId: RoomId, + val roomName: String, + val isDm: Boolean, +) : Parcelable + +fun RoomPreviewInfo.toInviteData(): InviteData { + return InviteData( + roomId = roomId, + roomName = name ?: roomId.value, + isDm = false, + ) +} + +fun RoomInfo.toInviteData(): InviteData { + return InviteData( + roomId = id, + roomName = name ?: id.value, + isDm = isDm, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt new file mode 100644 index 0000000000..015fc16182 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.api.acceptdecline + +import io.element.android.features.invite.api.InviteData + +interface AcceptDeclineInviteEvents { + data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents + data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt similarity index 88% rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt index ad883119cb..f4645383df 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.api.response +package io.element.android.features.invite.api.acceptdecline import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt similarity index 67% rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt index 4ad3ae4781..b0450975af 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -5,12 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.api.response +package io.element.android.features.invite.api.acceptdecline import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.InviteData import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.UserId open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { override val values: Sequence @@ -18,27 +18,21 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider Unit, - onDeclineInvite: (RoomId) -> Unit, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, modifier: Modifier, ) } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt similarity index 52% rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt index 0e428eda4a..f0c4366351 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt @@ -5,11 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.api.response +package io.element.android.features.invite.api.acceptdecline +import io.element.android.features.invite.api.InviteData import io.element.android.libraries.architecture.AsyncAction -data class ConfirmingDeclineInvite( - val inviteData: InviteData, - val blockUser: Boolean, -) : AsyncAction.Confirming +data class ConfirmingDeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : AsyncAction.Confirming diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt new file mode 100644 index 0000000000..27080ee354 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.api.declineandblock + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.architecture.FeatureEntryPoint + +interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt deleted file mode 100644 index 5734fcf152..0000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.invite.api.response - -interface AcceptDeclineInviteEvents { - data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents - data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents -} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt deleted file mode 100644 index be283e8dcd..0000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.invite.api.response - -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.UserId - -data class InviteData( - val senderId: UserId, - val roomId: RoomId, - val roomName: String, - val isDm: Boolean, -) diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index 7f052bea09..d6337f65c9 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -14,6 +14,11 @@ plugins { android { namespace = "io.element.android.features.invite.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -36,9 +41,12 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) testImplementation(projects.features.invite.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt new file mode 100644 index 0000000000..c4d520a6b8 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import com.squareup.anvil.annotations.ContributesBinding +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.push.api.notifications.NotificationCleaner +import javax.inject.Inject + +interface AcceptInvite { + suspend operator fun invoke(roomId: RoomId): Result +} + +@ContributesBinding(SessionScope::class) +class DefaultAcceptInvite @Inject constructor( + private val client: MatrixClient, + private val joinRoom: JoinRoom, + private val notificationCleaner: NotificationCleaner, + private val seenInvitesStore: SeenInvitesStore, +) : AcceptInvite { + override suspend fun invoke(roomId: RoomId): Result { + return joinRoom( + roomIdOrAlias = roomId.toRoomIdOrAlias(), + serverNames = emptyList(), + trigger = JoinedRoom.Trigger.Invite, + ).onSuccess { + notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) + seenInvitesStore.markAsUnSeen(roomId) + }.map { roomId } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt new file mode 100644 index 0000000000..9276f37365 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.api.notifications.NotificationCleaner +import javax.inject.Inject + +interface DeclineInvite { + suspend operator fun invoke( + roomId: RoomId, + blockUser: Boolean, + reportRoom: Boolean, + reportReason: String? + ): Result + + sealed class Exception : kotlin.Exception() { + data object RoomNotFound : Exception() + data object DeclineInviteFailed : Exception() + data object ReportRoomFailed : Exception() + data object BlockUserFailed : Exception() + } +} + +@ContributesBinding(SessionScope::class) +class DefaultDeclineInvite @Inject constructor( + private val client: MatrixClient, + private val notificationCleaner: NotificationCleaner, + private val seenInvitesStore: SeenInvitesStore, +) : DeclineInvite { + override suspend fun invoke( + roomId: RoomId, + blockUser: Boolean, + reportRoom: Boolean, + reportReason: String? + ): Result { + val room = client.getRoom(roomId) ?: return Result.failure(DeclineInvite.Exception.RoomNotFound) + room.use { + room.leave() + .onFailure { return Result.failure(DeclineInvite.Exception.DeclineInviteFailed) } + .onSuccess { + notificationCleaner.clearMembershipNotificationForRoom( + sessionId = client.sessionId, + roomId = roomId + ) + seenInvitesStore.markAsUnSeen(roomId) + } + + if (blockUser) { + val userIdToBlock = room.info().inviter?.userId + if (userIdToBlock != null) { + client + .ignoreUser(userIdToBlock) + .onFailure { return Result.failure(DeclineInvite.Exception.BlockUserFailed) } + } + } + if (reportRoom) { + room + .reportRoom(reportReason) + .onFailure { return Result.failure(DeclineInvite.Exception.ReportRoomFailed) } + } + } + return Result.success(roomId) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt new file mode 100644 index 0000000000..6dd75761d3 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AcceptDeclineInvitePresenter @Inject constructor( + private val acceptInvite: AcceptInvite, + private val declineInvite: DeclineInvite, +) : Presenter { + @Composable + override fun present(): AcceptDeclineInviteState { + val localCoroutineScope = rememberCoroutineScope() + val acceptedAction: MutableState> = + remember { mutableStateOf(AsyncAction.Uninitialized) } + val declinedAction: MutableState> = + remember { mutableStateOf(AsyncAction.Uninitialized) } + + fun handleEvents(event: AcceptDeclineInviteEvents) { + when (event) { + is AcceptDeclineInviteEvents.AcceptInvite -> { + localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + } + + is AcceptDeclineInviteEvents.DeclineInvite -> { + val inviteData = event.invite + if (event.shouldConfirm) { + declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser) + } else { + localCoroutineScope.declineInvite( + inviteData = inviteData, + blockUser = event.blockUser, + declinedAction = declinedAction, + ) + } + } + is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { + declinedAction.value = AsyncAction.Uninitialized + } + + is InternalAcceptDeclineInviteEvents.DismissAcceptError -> { + acceptedAction.value = AsyncAction.Uninitialized + } + + is InternalAcceptDeclineInviteEvents.DismissDeclineError -> { + declinedAction.value = AsyncAction.Uninitialized + } + } + } + + return AcceptDeclineInviteState( + acceptAction = acceptedAction.value, + declineAction = declinedAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.acceptInvite( + roomId: RoomId, + acceptedAction: MutableState>, + ) = launch { + acceptedAction.runUpdatingState { + acceptInvite(roomId) + } + } + + private fun CoroutineScope.declineInvite( + inviteData: InviteData, + blockUser: Boolean, + declinedAction: MutableState>, + ) = launch { + declinedAction.runUpdatingState { + declineInvite( + roomId = inviteData.roomId, + blockUser = blockUser, + reportRoom = false, + reportReason = null + ) + } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt similarity index 62% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt index 4039a0aefb..2585300431 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt @@ -5,17 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.impl.acceptdecline import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.features.invite.api.response.InviteData +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteStateProvider +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite import io.element.android.features.invite.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog @@ -27,21 +28,21 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun AcceptDeclineInviteView( state: AcceptDeclineInviteState, - onAcceptInvite: (RoomId) -> Unit, - onDeclineInvite: (RoomId) -> Unit, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { AsyncActionView( async = state.acceptAction, - onSuccess = onAcceptInvite, + onSuccess = onAcceptInviteSuccess, onErrorDismiss = { state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError) }, ) AsyncActionView( async = state.declineAction, - onSuccess = onDeclineInvite, + onSuccess = onDeclineInviteSuccess, onErrorDismiss = { state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) }, @@ -52,7 +53,13 @@ fun AcceptDeclineInviteView( invite = confirming.inviteData, blockUser = confirming.blockUser, onConfirmClick = { - state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite) + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite( + confirming.inviteData, + blockUser = confirming.blockUser, + shouldConfirm = false + ) + ) }, onDismissClick = { state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) @@ -72,30 +79,21 @@ private fun DeclineConfirmationDialog( onDismissClick: () -> Unit, modifier: Modifier = Modifier ) { - val senderId = invite.senderId.value - val content = when { - blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId) - invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName) - else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName) - } - val title = when { - blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title) - invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title) - else -> stringResource(R.string.screen_invites_decline_chat_title) - } - val submitText = if (blockUser) { - stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation) - } else { - stringResource(CommonStrings.action_decline) - } ConfirmationDialog( modifier = modifier, - content = content, - title = title, - submitText = submitText, + content = stringResource(R.string.screen_invites_decline_chat_message, invite.roomName), + title = if (blockUser) { + stringResource(R.string.screen_join_room_decline_and_block_alert_title) + } else { + stringResource(R.string.screen_invites_decline_chat_title) + }, + submitText = if (blockUser) { + stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation) + } else { + stringResource(CommonStrings.action_decline) + }, cancelText = stringResource(CommonStrings.action_cancel), onSubmitClick = onConfirmClick, - destructiveSubmit = blockUser, onDismiss = onDismissClick, ) } @@ -106,7 +104,7 @@ internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInvit ElementPreview { AcceptDeclineInviteView( state = state, - onAcceptInvite = {}, - onDeclineInvite = {}, + onAcceptInviteSuccess = {}, + onDeclineInviteSuccess = {}, ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt similarity index 64% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt index 8d52c26df7..e37582f63f 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt @@ -5,13 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.impl.acceptdecline import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.AcceptDeclineInviteView +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import javax.inject.Inject @@ -21,14 +21,14 @@ class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInvite @Composable override fun Render( state: AcceptDeclineInviteState, - onAcceptInvite: (RoomId) -> Unit, - onDeclineInvite: (RoomId) -> Unit, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, modifier: Modifier, ) { AcceptDeclineInviteView( state = state, - onAcceptInvite = onAcceptInvite, - onDeclineInvite = onDeclineInvite, + onAcceptInviteSuccess = onAcceptInviteSuccess, + onDeclineInviteSuccess = onDeclineInviteSuccess, modifier = modifier ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt similarity index 69% rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt index 8895c80328..4423430432 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt @@ -5,12 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.impl.response +package io.element.android.features.invite.impl.acceptdecline -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { - data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents data object DismissDeclineError : InternalAcceptDeclineInviteEvents diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt new file mode 100644 index 0000000000..033d2e58de --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +sealed interface DeclineAndBlockEvents { + data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents + data object ToggleReportRoom : DeclineAndBlockEvents + data object ToggleBlockUser : DeclineAndBlockEvents + data object Decline : DeclineAndBlockEvents + data object ClearDeclineAction : DeclineAndBlockEvents +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt new file mode 100644 index 0000000000..100a5de4b8 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class DeclineAndBlockNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: DeclineAndBlockPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + data class Inputs(val inviteData: InviteData) : NodeInputs + + private val inviteData = inputs().inviteData + private val presenter = presenterFactory.create(inviteData) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + DeclineAndBlockView( + state = state, + onBackClick = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt new file mode 100644 index 0000000000..9a18aa746b --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class DeclineAndBlockPresenter @AssistedInject constructor( + @Assisted private val inviteData: InviteData, + private val declineInvite: DeclineInvite, + private val snackbarDispatcher: SnackbarDispatcher, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(inviteData: InviteData): DeclineAndBlockPresenter + } + + @Composable + override fun present(): DeclineAndBlockState { + var reportReason by rememberSaveable { mutableStateOf("") } + var blockUser by rememberSaveable { mutableStateOf(true) } + var reportRoom by rememberSaveable { mutableStateOf(false) } + val declineAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: DeclineAndBlockEvents) { + when (event) { + DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized + DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction) + DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser + DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom + is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason + } + } + + return DeclineAndBlockState( + reportRoom = reportRoom, + reportReason = reportReason, + blockUser = blockUser, + declineAction = declineAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.decline( + reason: String, + blockUser: Boolean, + reportRoom: Boolean, + action: MutableState> + ) = launch { + action.value = AsyncAction.Loading + declineInvite( + roomId = inviteData.roomId, + blockUser = blockUser, + reportRoom = reportRoom, + reportReason = reason + ).onSuccess { + action.value = AsyncAction.Success(Unit) + }.onFailure { error -> + if (error is DeclineInvite.Exception.DeclineInviteFailed) { + action.value = AsyncAction.Failure(error) + } else { + action.value = AsyncAction.Uninitialized + snackbarDispatcher.post(SnackbarMessage(CommonStrings.error_unknown)) + } + } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt new file mode 100644 index 0000000000..0ee7444b2c --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import io.element.android.libraries.architecture.AsyncAction + +data class DeclineAndBlockState( + val reportRoom: Boolean, + val reportReason: String, + val blockUser: Boolean, + val declineAction: AsyncAction, + val eventSink: (DeclineAndBlockEvents) -> Unit +) { + val canDecline = blockUser || reportRoom && reportReason.isNotEmpty() +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt new file mode 100644 index 0000000000..87016d13cc --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class DeclineAndBlockStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aDeclineAndBlockState(), + aDeclineAndBlockState( + reportRoom = true, + reportReason = "Inappropriate content", + ), + aDeclineAndBlockState( + blockUser = true, + ), + aDeclineAndBlockState( + declineAction = AsyncAction.Loading, + ), + aDeclineAndBlockState( + declineAction = AsyncAction.Failure(Exception("Failed to decline")), + ), + ) +} + +fun aDeclineAndBlockState( + reportRoom: Boolean = false, + reportReason: String = "", + blockUser: Boolean = false, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (DeclineAndBlockEvents) -> Unit = {}, +) = DeclineAndBlockState( + reportRoom = reportRoom, + reportReason = reportReason, + blockUser = blockUser, + declineAction = declineAction, + eventSink = eventSink, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt new file mode 100644 index 0000000000..c313382b27 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.invite.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeclineAndBlockView( + state: DeclineAndBlockState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + + val isDeclining = state.declineAction is AsyncAction.Loading + AsyncActionView( + async = state.declineAction, + onSuccess = { onBackClick() }, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) }, + onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + stringResource(R.string.screen_decline_and_block_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = onBackClick) + } + ) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp) + ) { + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title)) + }, + supportingContent = { + Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description)) + }, + onClick = { + state.eventSink(DeclineAndBlockEvents.ToggleBlockUser) + }, + trailingContent = ListItemContent.Switch(checked = state.blockUser) + ) + + Spacer(modifier = Modifier.height(24.dp)) + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(CommonStrings.action_report_room)) + }, + supportingContent = { + Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description)) + }, + onClick = { + state.eventSink(DeclineAndBlockEvents.ToggleReportRoom) + }, + trailingContent = ListItemContent.Switch(checked = state.reportRoom) + ) + + if (state.reportRoom) { + Spacer(modifier = Modifier.height(24.dp)) + TextField( + value = state.reportReason, + onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) }, + placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder), + minLines = 3, + enabled = !isDeclining, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(min = 90.dp), + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + text = stringResource(CommonStrings.action_decline), + destructive = true, + showProgress = isDeclining, + enabled = !isDeclining && state.canDecline, + onClick = { + focusManager.clearFocus(force = true) + state.eventSink(DeclineAndBlockEvents.Decline) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun DeclineAndBlockViewPreview( + @PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState +) = ElementPreview { + DeclineAndBlockView( + state = state, + onBackClick = {}, + ) +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt new file mode 100644 index 0000000000..5eab22dd91 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node { + val inputs = DeclineAndBlockNode.Inputs(inviteData) + return parentNode.createNode(buildContext, plugins = listOf(inputs)) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt index a9308ca0fc..4f5da603a6 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt @@ -12,9 +12,9 @@ import dagger.Binds import dagger.Module import dagger.Provides import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.impl.SeenInvitesStoreFactory -import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter +import io.element.android.features.invite.impl.acceptdecline.AcceptDeclineInvitePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt deleted file mode 100644 index 1e5b20d034..0000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.invite.impl.response - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.features.invite.api.response.InviteData -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.architecture.runUpdatingState -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.api.room.join.JoinRoom -import io.element.android.libraries.push.api.notifications.NotificationCleaner -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class AcceptDeclineInvitePresenter @Inject constructor( - private val client: MatrixClient, - private val joinRoom: JoinRoom, - private val notificationCleaner: NotificationCleaner, - private val seenInvitesStore: SeenInvitesStore, -) : Presenter { - @Composable - override fun present(): AcceptDeclineInviteState { - val localCoroutineScope = rememberCoroutineScope() - val acceptedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - val declinedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - - fun handleEvents(event: AcceptDeclineInviteEvents) { - when (event) { - is AcceptDeclineInviteEvents.AcceptInvite -> { - val inviteData = event.invite - if (inviteData == null) { - acceptedAction.value = AsyncAction.Failure(InvalidDataException()) - } else { - localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction) - } - } - - is AcceptDeclineInviteEvents.DeclineInvite -> { - val inviteData = event.invite - if (inviteData == null) { - declinedAction.value = AsyncAction.Failure(InvalidDataException()) - } else { - declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser) - } - } - - is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { - when (val declinedActionValue = declinedAction.value) { - is ConfirmingDeclineInvite -> { - localCoroutineScope.declineInvite( - inviteData = declinedActionValue.inviteData, - declinedAction = declinedAction, - blockUser = declinedActionValue.blockUser, - ) - } - else -> Unit - } - } - - is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { - declinedAction.value = AsyncAction.Uninitialized - } - - is InternalAcceptDeclineInviteEvents.DismissAcceptError -> { - acceptedAction.value = AsyncAction.Uninitialized - } - - is InternalAcceptDeclineInviteEvents.DismissDeclineError -> { - declinedAction.value = AsyncAction.Uninitialized - } - } - } - - return AcceptDeclineInviteState( - acceptAction = acceptedAction.value, - declineAction = declinedAction.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.acceptInvite( - roomId: RoomId, - acceptedAction: MutableState>, - ) = launch { - acceptedAction.runUpdatingState { - joinRoom( - roomIdOrAlias = roomId.toRoomIdOrAlias(), - serverNames = emptyList(), - trigger = JoinedRoom.Trigger.Invite, - ) - .onSuccess { - notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) - seenInvitesStore.markAsUnSeen(roomId) - } - .map { roomId } - } - } - - private fun CoroutineScope.declineInvite( - inviteData: InviteData, - blockUser: Boolean, - declinedAction: MutableState>, - ) = launch { - suspend { - client.getRoom(inviteData.roomId)?.use { - it.leave().getOrThrow() - } - if (blockUser) { - client.ignoreUser(inviteData.senderId).getOrThrow() - } - notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId) - seenInvitesStore.markAsUnSeen(inviteData.roomId) - inviteData.roomId - }.runCatchingUpdatingState(declinedAction) - } -} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt deleted file mode 100644 index dff7b904db..0000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.invite.impl.response - -class InvalidDataException : Exception() diff --git a/features/invite/impl/src/main/res/values-cs/translations.xml b/features/invite/impl/src/main/res/values-cs/translations.xml index 58876c3392..f576269343 100644 --- a/features/invite/impl/src/main/res/values-cs/translations.xml +++ b/features/invite/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,10 @@ + "Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti" + "Zablokovat uživatele" + "Nahlaste tuto místnost svému poskytovateli účtu." + "Popište důvod nahlášení…" + "Odmítnout a zablokovat" "Opravdu chcete odmítnout pozvánku do %1$s?" "Odmítnout pozvání" "Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?" diff --git a/features/invite/impl/src/main/res/values-cy/translations.xml b/features/invite/impl/src/main/res/values-cy/translations.xml index 27f355a18e..a84fa2397a 100644 --- a/features/invite/impl/src/main/res/values-cy/translations.xml +++ b/features/invite/impl/src/main/res/values-cy/translations.xml @@ -1,5 +1,10 @@ + "Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn" + "Rhwystro defnyddiwr" + "Adrodd am yr ystafell hon i ddarparwr eich cyfrif." + "Disgrifiwch y rheswm dros adrodd…" + "Gwrthod a rhwystro" "Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?" "Gwrthod y gwahoddiad" "Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?" diff --git a/features/invite/impl/src/main/res/values-de/translations.xml b/features/invite/impl/src/main/res/values-de/translations.xml index fb390c4354..13fae055e7 100644 --- a/features/invite/impl/src/main/res/values-de/translations.xml +++ b/features/invite/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,10 @@ + "Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen." + "Benutzer blockieren" + "Melden Sie diesen Raum Ihrem Kontoanbieter." + "Beschreiben Sie den Grund für die Meldung…" + "Ablehnen und blockieren" "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" "Einladung ablehnen" "Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?" diff --git a/features/invite/impl/src/main/res/values-el/translations.xml b/features/invite/impl/src/main/res/values-el/translations.xml index bc5949704d..e7e55379f6 100644 --- a/features/invite/impl/src/main/res/values-el/translations.xml +++ b/features/invite/impl/src/main/res/values-el/translations.xml @@ -1,5 +1,10 @@ + "Δε θα δείτε μηνύματα ή προσκλήσεις δωματίου από αυτόν τον χρήστη" + "Αποκλεισμός χρήστη" + "Αναφέρετε αυτό το δωμάτιο στον πάροχο του λογαριασμού σας." + "Περιγράψτε τον λόγο αναφοράς…" + "Απόρριψη και αποκλεισμός" "Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;" "Απόρριψη πρόσκλησης" "Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;" diff --git a/features/invite/impl/src/main/res/values-et/translations.xml b/features/invite/impl/src/main/res/values-et/translations.xml index 59a6b112c6..91906fa877 100644 --- a/features/invite/impl/src/main/res/values-et/translations.xml +++ b/features/invite/impl/src/main/res/values-et/translations.xml @@ -1,5 +1,10 @@ + "Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid" + "Blokeeri kasutaja" + "Teata sellest jututoast oma teenusepakkujale." + "Kirjelda teatamise põhjust…" + "Keeldu ja blokeeri" "Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?" "Lükka kutse tagasi" "Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?" diff --git a/features/invite/impl/src/main/res/values-fi/translations.xml b/features/invite/impl/src/main/res/values-fi/translations.xml index c97a8c709e..47ad368d73 100644 --- a/features/invite/impl/src/main/res/values-fi/translations.xml +++ b/features/invite/impl/src/main/res/values-fi/translations.xml @@ -1,5 +1,10 @@ + "Et tule näkemään viestejä tai kutsuja tältä käyttäjältä" + "Estä käyttäjä" + "Ilmoita tästä huoneesta palveluntarjoajallesi." + "Kerro syy ilmoittamiseen…" + "Hylkää ja estä" "Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?" "Hylkää kutsu" "Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?" diff --git a/features/invite/impl/src/main/res/values-fr/translations.xml b/features/invite/impl/src/main/res/values-fr/translations.xml index eae8484897..1775da0538 100644 --- a/features/invite/impl/src/main/res/values-fr/translations.xml +++ b/features/invite/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,10 @@ + "Vous ne verrez aucun messages ou invitation à un salon de la part de cet utilisateur" + "Bloquer l’utilisateur" + "Signalez ce salon à votre fournisseur de compte." + "Décrivez la raison du signalement…" + "Refuser et bloquer" "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?" "Refuser l’invitation" "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?" diff --git a/features/invite/impl/src/main/res/values-hu/translations.xml b/features/invite/impl/src/main/res/values-hu/translations.xml index 1ba1b208b8..97595ed421 100644 --- a/features/invite/impl/src/main/res/values-hu/translations.xml +++ b/features/invite/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,10 @@ + "Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni." + "Felhasználó letiltása" + "A szoba jelentése a fiókszolgáltatójának." + "Írja le a jelentés okát…" + "Elutasítás és blokkolás" "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?" "Meghívás elutasítása" "Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?" diff --git a/features/invite/impl/src/main/res/values-nb/translations.xml b/features/invite/impl/src/main/res/values-nb/translations.xml index 224ad72689..2523466348 100644 --- a/features/invite/impl/src/main/res/values-nb/translations.xml +++ b/features/invite/impl/src/main/res/values-nb/translations.xml @@ -1,5 +1,10 @@ + "Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren" + "Blokker bruker" + "Rapporter dette rommet til din kontoleverandør." + "Beskriv årsaken for å rapportere…" + "Avslå og blokker" "Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?" "Avvis invitasjon" "Er du sikker på at du vil avslå denne private chatten med %1$s?" diff --git a/features/invite/impl/src/main/res/values-pl/translations.xml b/features/invite/impl/src/main/res/values-pl/translations.xml index b507ee1e2c..e8636fc3dc 100644 --- a/features/invite/impl/src/main/res/values-pl/translations.xml +++ b/features/invite/impl/src/main/res/values-pl/translations.xml @@ -1,5 +1,10 @@ + "Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika" + "Zablokuj użytkownika" + "Zgłoś pokój dostawcy swojego konta." + "Opisz powód zgłoszenia…" + "Odrzuć i zablokuj" "Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?" "Odrzuć zaproszenie" "Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?" diff --git a/features/invite/impl/src/main/res/values-ru/translations.xml b/features/invite/impl/src/main/res/values-ru/translations.xml index 2a38a96e1b..da7bb1c449 100644 --- a/features/invite/impl/src/main/res/values-ru/translations.xml +++ b/features/invite/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,8 @@ + "Заблокировать пользователя" + "Опишите причину жалобы…" + "Отклонить и заблокировать" "Вы уверены, что хотите отклонить приглашение в %1$s?" "Отклонить приглашение" "Вы уверены, что хотите отказаться от личного общения с %1$s?" diff --git a/features/invite/impl/src/main/res/values-sk/translations.xml b/features/invite/impl/src/main/res/values-sk/translations.xml index 5ad69a009c..a14b5b0dae 100644 --- a/features/invite/impl/src/main/res/values-sk/translations.xml +++ b/features/invite/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,10 @@ + "Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti" + "Zablokovať používateľa" + "Nahlásiť túto miestnosť poskytovateľovi účtu." + "Opíšte dôvod nahlásenia…" + "Odmietnuť a zablokovať" "Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?" "Odmietnuť pozvanie" "Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?" diff --git a/features/invite/impl/src/main/res/values-sv/translations.xml b/features/invite/impl/src/main/res/values-sv/translations.xml index 1145e80df0..a57336b50e 100644 --- a/features/invite/impl/src/main/res/values-sv/translations.xml +++ b/features/invite/impl/src/main/res/values-sv/translations.xml @@ -1,5 +1,10 @@ + "Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren" + "Blockera användare" + "Rapportera det här rummet till din kontoleverantör." + "Beskriv skälet för anmälan …" + "Avvisa och blockera" "Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?" "Avböj inbjudan" "Är du säker på att du vill avböja denna privata chatt med %1$s?" diff --git a/features/invite/impl/src/main/res/values-uk/translations.xml b/features/invite/impl/src/main/res/values-uk/translations.xml index 2f09bf4ea8..9a5aef7298 100644 --- a/features/invite/impl/src/main/res/values-uk/translations.xml +++ b/features/invite/impl/src/main/res/values-uk/translations.xml @@ -1,5 +1,10 @@ + "Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача" + "Заблокувати користувача" + "Поскаржитися на цю кімнату постачальнику облікового запису." + "Опишіть причину скарги…" + "Відхилити та заблокувати" "Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?" "Відхилити запрошення" "Ви дійсно хочете відмовитися від приватної бесіди з %1$s?" diff --git a/features/invite/impl/src/main/res/values-zh-rTW/translations.xml b/features/invite/impl/src/main/res/values-zh-rTW/translations.xml index 90e2a840df..63cac65d3d 100644 --- a/features/invite/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/invite/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,9 @@ + "您將不會看到來自此使用者的任何訊息或聊天室邀請" + "封鎖使用者" + "向您的帳號提供者回報此聊天室。" + "拒絕並封鎖" "您確定您想要拒絕加入 %1$s 的邀請嗎?" "拒絕邀請" "您確定您要拒絕此與 %1$s 的私人聊天嗎?" diff --git a/features/invite/impl/src/main/res/values/localazy.xml b/features/invite/impl/src/main/res/values/localazy.xml index 7800bdae07..425602595f 100644 --- a/features/invite/impl/src/main/res/values/localazy.xml +++ b/features/invite/impl/src/main/res/values/localazy.xml @@ -1,5 +1,10 @@ + "You will not see any messages or room invites from this user" + "Block user" + "Report this room to your account provider." + "Describe the reason to report…" + "Decline and block" "Are you sure you want to decline the invitation to join %1$s?" "Decline invite" "Are you sure you want to decline this private chat with %1$s?" diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt new file mode 100644 index 0000000000..e2a9a65f8a --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom +import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultAcceptInviteTest { + private val roomId = A_ROOM_ID + private val client = FakeMatrixClient() + private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId)) + + private val clearMembershipNotificationForRoomLambda = + lambdaRecorder { _, _ -> } + private val notificationCleaner = + FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda) + + @Test + fun `accept invite success scenario`() = runTest { + val joinRoomLambda = + lambdaRecorder, JoinedRoom.Trigger, Result> { _, _, _ -> + Result.success(Unit) + } + + val acceptInvite = DefaultAcceptInvite( + client = client, + notificationCleaner = notificationCleaner, + joinRoom = FakeJoinRoom(lambda = joinRoomLambda), + seenInvitesStore = seenInvitesStore + ) + + val result = acceptInvite(roomId) + + assertThat(result.isSuccess).isTrue() + + assert(joinRoomLambda) + .isCalledOnce() + .with(value(roomId.toRoomIdOrAlias()), any(), any()) + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `accept invite failure scenario`() = runTest { + val joinRoomLambda = + lambdaRecorder, JoinedRoom.Trigger, Result> { _, _, _ -> + Result.failure(Throwable("Join room failed")) + } + + val acceptInvite = DefaultAcceptInvite( + client = client, + notificationCleaner = notificationCleaner, + joinRoom = FakeJoinRoom(lambda = joinRoomLambda), + seenInvitesStore = seenInvitesStore + ) + + val result = acceptInvite(roomId) + + assertThat(result.isFailure).isTrue() + + assert(joinRoomLambda) + .isCalledOnce() + .with(value(roomId.toRoomIdOrAlias()), any(), any()) + + assert(clearMembershipNotificationForRoomLambda).isNeverCalled() + + assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomId) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt new file mode 100644 index 0000000000..cb07bd5191 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultDeclineInviteTest { + private val roomId = A_ROOM_ID + private val inviter = aRoomMember() + private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId)) + private val clearMembershipNotificationForRoomLambda = + lambdaRecorder { _, _ -> } + private val notificationCleaner = + FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda) + + private val successLeaveRoomLambda = lambdaRecorder> { -> Result.success(Unit) } + private val successIgnoreUserLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + private val successReportRoomLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + + private val failureLeaveRoomLambda = + lambdaRecorder> { -> Result.failure(Exception("Leave room error")) } + private val failureIgnoreUserLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Ignore user error")) } + private val failureReportRoomLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Report room error")) } + + @Test + fun `decline invite, block=false, report=false, all success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + + val result = + declineInvite(roomId, blockUser = false, reportRoom = false, reportReason = null) + + assertThat(result.isSuccess).isTrue() + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, all success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.isSuccess).isTrue() + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, decline invite failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = failureLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.DeclineInviteFailed) + + assert(clearMembershipNotificationForRoomLambda) + .isNeverCalled() + + assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() + } + + @Test + fun `decline invite, block=true, report=true, ignore user failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = failureIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.BlockUserFailed) + + assert(clearMembershipNotificationForRoomLambda).isCalledOnce() + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, report room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = failureReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.ReportRoomFailed) + + assert(clearMembershipNotificationForRoomLambda).isCalledOnce() + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt new file mode 100644 index 0000000000..c8a427bea6 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.features.invite.impl.fake.FakeAcceptInvite +import io.element.android.features.invite.impl.fake.FakeDeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AcceptDeclineInvitePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + } + + @Test + fun `present - declining invite cancel flow`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) + state.eventSink( + InternalAcceptDeclineInviteEvents.CancelDeclineInvite + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + } + + @Test + fun `present - declining invite error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.DeclineInviteFailed) + } + val presenter = createAcceptDeclineInvitePresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false) + ) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissDeclineError + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(false), value(false), value(null)) + } + + @Test + fun `present - declining invite success flow`() = runTest { + val declineInviteSuccess = lambdaRecorder> { roomId, _, _, _ -> Result.success(roomId) } + val presenter = createAcceptDeclineInvitePresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false)) + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false) + ) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID), value(false), value(false), value(null)) + } + + @Test + fun `present - accepting invite error flow`() = runTest { + val acceptInviteFailure = lambdaRecorder> { roomId: RoomId -> + Result.failure(RuntimeException("Failed to accept invite")) + } + val presenter = createAcceptDeclineInvitePresenter( + acceptInvite = FakeAcceptInvite(lambda = acceptInviteFailure), + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissAcceptError + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(acceptInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + @Test + fun `present - accepting invite success flow`() = runTest { + val acceptInviteSuccess = lambdaRecorder> { roomId: RoomId -> Result.success(roomId) } + val presenter = createAcceptDeclineInvitePresenter( + acceptInvite = FakeAcceptInvite(lambda = acceptInviteSuccess) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + acceptInviteSuccess.assertions() + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + private fun anInviteData( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDm: Boolean = false, + ): InviteData { + return InviteData( + roomId = roomId, + roomName = name, + isDm = isDm, + ) + } + + private fun createAcceptDeclineInvitePresenter( + acceptInvite: AcceptInvite = FakeAcceptInvite(), + declineInvite: DeclineInvite = FakeDeclineInvite(), + ): AcceptDeclineInvitePresenter { + return AcceptDeclineInvitePresenter( + acceptInvite = acceptInvite, + declineInvite = declineInvite, + ) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt new file mode 100644 index 0000000000..c5d2f53c92 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.features.invite.impl.fake.FakeDeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DeclineAndBlockPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createDeclineAndBlockPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.blockUser).isTrue() + assertThat(state.reportRoom).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.canDecline).isTrue() + } + } + } + + @Test + fun `present - update form values`() = runTest { + val presenter = createDeclineAndBlockPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reportRoom).isFalse() + assertThat(state.blockUser).isTrue() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isTrue() + state.eventSink(DeclineAndBlockEvents.ToggleBlockUser) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isFalse() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isFalse() + state.eventSink(DeclineAndBlockEvents.ToggleReportRoom) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isTrue() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isFalse() + state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam")) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isTrue() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEqualTo("Spam") + assertThat(state.canDecline).isTrue() + } + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun `present - declining invite success flow`() = runTest { + val declineInviteSuccess = lambdaRecorder> { roomId, _, _, _ -> Result.success(roomId) } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + @Test + fun `present - declining invite error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.DeclineInviteFailed) + } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + @Test + fun `present - block user error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.BlockUserFailed) + } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + private fun anInviteData( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDm: Boolean = false, + ): InviteData { + return InviteData( + roomId = roomId, + roomName = name, + isDm = isDm, + ) + } + + private fun createDeclineAndBlockPresenter( + inviteData: InviteData = anInviteData(), + declineInvite: DeclineInvite = FakeDeclineInvite(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + ): DeclineAndBlockPresenter { + return DeclineAndBlockPresenter( + inviteData = inviteData, + declineInvite = declineInvite, + snackbarDispatcher = snackbarDispatcher, + ) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt new file mode 100644 index 0000000000..60e00f8e35 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.declineandblock + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.invite.impl.R +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DeclineAndBlockViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on decline when enabled emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = true, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) + } + + @Test + fun `clicking on decline when disabled does not emit event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = false, + reportRoom = false, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertEmpty() + } + + @Test + fun `clicking on block option emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = true, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_decline_and_block_block_user_option_title) + eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser) + } + + @Test + fun `clicking on report room option emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + reportRoom = true, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_report_room) + eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) + } + + @Test + fun `typing text in the reason field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + reportRoom = true, + reportReason = "", + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithText("").performTextInput("Spam!") + eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) + } +} + +private fun AndroidComposeTestRule.setDeclineAndBlockView( + state: DeclineAndBlockState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + DeclineAndBlockView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt new file mode 100644 index 0000000000..1ba95bc76e --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.fake + +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeAcceptInvite( + private val lambda: (RoomId) -> Result = { lambdaError() }, +) : AcceptInvite { + override suspend fun invoke(roomId: RoomId) = simulateLongTask { + lambda(roomId) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt new file mode 100644 index 0000000000..18b46dbd5f --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl.fake + +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeDeclineInvite( + private val lambda: (RoomId, Boolean, Boolean, String?) -> Result = { _, _, _, _ -> lambdaError() }, +) : DeclineInvite { + override suspend fun invoke(roomId: RoomId, blockUser: Boolean, reportRoom: Boolean, reportReason: String?): Result = simulateLongTask { + lambda(roomId, blockUser, reportRoom, reportReason) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt deleted file mode 100644 index bb8dc09b79..0000000000 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.invite.impl.response - -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.features.invite.api.response.InviteData -import io.element.android.features.invite.test.InMemorySeenInvitesStore -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_ID_2 -import io.element.android.libraries.matrix.test.A_ROOM_ID_3 -import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeBaseRoom -import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom -import io.element.android.libraries.push.api.notifications.NotificationCleaner -import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.assert -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import io.element.android.tests.testutils.test -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class AcceptDeclineInvitePresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = createAcceptDeclineInvitePresenter() - presenter.test { - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - } - } - - @Test - fun `present - declining invite cancel flow`() = runTest { - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) - state.eventSink( - InternalAcceptDeclineInviteEvents.CancelDeclineInvite - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - } - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - declining invite error flow`() = runTest { - val declineInviteFailure = lambdaRecorder { -> - Result.failure(RuntimeException("Failed to leave room")) - } - val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure)) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - client = client, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) - state.eventSink( - InternalAcceptDeclineInviteEvents.DismissDeclineError - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(declineInviteFailure).isCalledOnce() - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - declining invite success flow`() = runTest { - val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> - Result.success(Unit) - } - val fakeNotificationCleaner = FakeNotificationCleaner( - clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda - ) - val declineInviteSuccess = lambdaRecorder { -> - Result.success(Unit) - } - val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess)) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - client = client, - notificationCleaner = fakeNotificationCleaner, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) - } - cancelAndConsumeRemainingEvents() - } - declineInviteSuccess.assertions().isCalledOnce() - clearMembershipNotificationForRoomLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - declining invite with block success flow`() = runTest { - val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> - Result.success(Unit) - } - val fakeNotificationCleaner = FakeNotificationCleaner( - clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda - ) - val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) } - val ignoreUserSuccess = lambdaRecorder { _: UserId -> Result.success(Unit) } - val client = FakeMatrixClient( - ignoreUserResult = ignoreUserSuccess - ).apply { - givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess)) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - client = client, - notificationCleaner = fakeNotificationCleaner, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) - } - cancelAndConsumeRemainingEvents() - } - declineInviteSuccess.assertions().isCalledOnce() - ignoreUserSuccess.assertions().isCalledOnce().with(value(A_USER_ID)) - clearMembershipNotificationForRoomLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - declining invite with block error flow`() = runTest { - val declineInviteFailure = lambdaRecorder { -> - Result.failure(RuntimeException("Failed to leave room")) - } - val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure)) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - client = client, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - } - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - accepting invite error flow`() = runTest { - val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List, _: JoinedRoom.Trigger -> - Result.failure(RuntimeException("Failed to join room $roomIdOrAlias")) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - joinRoomLambda = joinRoomFailure, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) - state.eventSink( - InternalAcceptDeclineInviteEvents.DismissAcceptError - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(joinRoomFailure) - .isCalledOnce() - .with( - value(A_ROOM_ID.toRoomIdOrAlias()), - value(emptyList()), - value(JoinedRoom.Trigger.Invite) - ) - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) - } - - @Test - fun `present - accepting invite success flow`() = runTest { - val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> - Result.success(Unit) - } - val fakeNotificationCleaner = FakeNotificationCleaner( - clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda - ) - val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List, _: JoinedRoom.Trigger -> - Result.success(Unit) - } - val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)) - val presenter = createAcceptDeclineInvitePresenter( - joinRoomLambda = joinRoomSuccess, - notificationCleaner = fakeNotificationCleaner, - seenInvitesStore = seenInvitesStore, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(joinRoomSuccess) - .isCalledOnce() - .with( - value(A_ROOM_ID.toRoomIdOrAlias()), - value(emptyList()), - value(JoinedRoom.Trigger.Invite) - ) - clearMembershipNotificationForRoomLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3) - } - - private fun anInviteData( - roomId: RoomId = A_ROOM_ID, - name: String = A_ROOM_NAME, - isDm: Boolean = false, - senderId: UserId = A_USER_ID, - ): InviteData { - return InviteData( - roomId = roomId, - roomName = name, - isDm = isDm, - senderId = senderId, - ) - } - - private fun createAcceptDeclineInvitePresenter( - client: MatrixClient = FakeMatrixClient(), - joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> - Result.success(Unit) - }, - notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), - seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), - ): AcceptDeclineInvitePresenter { - return AcceptDeclineInvitePresenter( - client = client, - joinRoom = FakeJoinRoom(joinRoomLambda), - notificationCleaner = notificationCleaner, - seenInvitesStore = seenInvitesStore, - ) - } -} diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts index dc43ba00c3..e504873c3b 100644 --- a/features/invite/test/build.gradle.kts +++ b/features/invite/test/build.gradle.kts @@ -25,5 +25,6 @@ android { dependencies { implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.test) api(projects.features.invite.api) } diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt new file mode 100644 index 0000000000..62e6d8b322 --- /dev/null +++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.test + +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME + +fun anInviteData( + roomId: RoomId = A_ROOM_ID, + roomName: String = A_ROOM_NAME, + isDm: Boolean = false, +) = InviteData( + roomId = roomId, + roomName = roomName, + isDm = isDm, +) diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 983ae33ce3..e195efd1bc 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.features.roomdirectory.api) implementation(projects.services.analytics.api) implementation(projects.libraries.preferences.api) + implementation(projects.appconfig) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt index 224ea19291..e28b2affe8 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { - return parentNode.createNode( + return parentNode.createNode( buildContext = buildContext, plugins = listOf(inputs) ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt index 96c16e1f05..323b556c38 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt @@ -7,6 +7,8 @@ package io.element.android.features.joinroom.impl +import io.element.android.features.invite.api.InviteData + sealed interface JoinRoomEvents { data object RetryFetchingContent : JoinRoomEvents data object DismissErrorAndHideContent : JoinRoomEvents @@ -16,6 +18,6 @@ sealed interface JoinRoomEvents { data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents data class UpdateKnockMessage(val message: String) : JoinRoomEvents data object ClearActionStates : JoinRoomEvents - data object AcceptInvite : JoinRoomEvents - data class DeclineInvite(val blockUser: Boolean) : JoinRoomEvents + data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents + data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt new file mode 100644 index 0000000000..4d3d9be86c --- /dev/null +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.joinroom.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class JoinRoomFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: JoinRoomPresenter.Factory, + private val acceptDeclineInviteView: AcceptDeclineInviteView, + private val declineAndBlockEntryPoint: DeclineInviteAndBlockEntryPoint +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + private val inputs: JoinRoomEntryPoint.Inputs = inputs() + private val presenter = presenterFactory.create( + inputs.roomId, + inputs.roomIdOrAlias, + inputs.roomDescription, + inputs.serverNames, + inputs.trigger, + ) + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(this, buildContext, navTarget.inviteData) + NavTarget.Root -> rootNode(buildContext) + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView(modifier) + } + + private fun rootNode(buildContext: BuildContext): Node { + return node(buildContext) { modifier -> + val state = presenter.present() + JoinRoomView( + state = state, + onBackClick = ::navigateUp, + onJoinSuccess = ::navigateUp, + onForgetSuccess = ::navigateUp, + onCancelKnockSuccess = {}, + onKnockSuccess = {}, + onDeclineInviteAndBlockUser = { + backstack.push( + NavTarget.DeclineInviteAndBlockUser(it) + ) + }, + modifier = modifier + ) + acceptDeclineInviteView.Render( + state = state.acceptDeclineInviteState, + onAcceptInviteSuccess = {}, + onDeclineInviteSuccess = {}, + modifier = Modifier + ) + } + } +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt deleted file mode 100644 index dbf741f2d8..0000000000 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.joinroom.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.features.joinroom.api.JoinRoomEntryPoint -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.SessionScope - -@ContributesNode(SessionScope::class) -class JoinRoomNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: JoinRoomPresenter.Factory, - private val acceptDeclineInviteView: AcceptDeclineInviteView, -) : Node(buildContext, plugins = plugins) { - private val inputs: JoinRoomEntryPoint.Inputs = inputs() - private val presenter = presenterFactory.create( - inputs.roomId, - inputs.roomIdOrAlias, - inputs.roomDescription, - inputs.serverNames, - inputs.trigger, - ) - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - JoinRoomView( - state = state, - onBackClick = ::navigateUp, - onJoinSuccess = ::navigateUp, - onForgetSuccess = ::navigateUp, - onCancelKnockSuccess = {}, - onKnockSuccess = {}, - modifier = modifier - ) - acceptDeclineInviteView.Render( - state = state.acceptDeclineInviteState, - onAcceptInvite = {}, - onDeclineInvite = {}, - modifier = Modifier - ) - } -} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 3d99ce44ee..a234bb019b 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -23,10 +23,11 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.appconfig.MatrixConfiguration import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.toInviteData import io.element.android.features.joinroom.impl.di.CancelKnockRoom import io.element.android.features.joinroom.impl.di.ForgetRoom import io.element.android.features.joinroom.impl.di.KnockRoom @@ -170,16 +171,14 @@ class JoinRoomPresenter @AssistedInject constructor( when (event) { JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage) - JoinRoomEvents.AcceptInvite -> { - val inviteData = contentState.toInviteData() + is JoinRoomEvents.AcceptInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) + AcceptDeclineInviteEvents.AcceptInvite(event.inviteData) ) } is JoinRoomEvents.DeclineInvite -> { - val inviteData = contentState.toInviteData() acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(invite = inviteData, blockUser = event.blockUser) + AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true) ) } is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction) @@ -213,6 +212,7 @@ class JoinRoomPresenter @AssistedInject constructor( applicationName = buildMeta.applicationName, knockMessage = knockMessage, hideInviteAvatars = hideInviteAvatars, + canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM, eventSink = ::handleEvents ) } @@ -273,7 +273,12 @@ private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: St roomType = roomType, roomAvatarUrl = avatarUrl, joinAuthorisationStatus = when (membership) { - CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(senderMember?.toInviteSender()) + CurrentUserMembership.INVITED -> { + JoinAuthorisationStatus.IsInvited( + inviteData = toInviteData(), + inviteSender = senderMember?.toInviteSender() + ) + } CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason) CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked else -> joinRule.toJoinAuthorisationStatus() @@ -317,7 +322,8 @@ internal fun RoomInfo.toContentState( roomAvatarUrl = avatarUrl, joinAuthorisationStatus = when (currentUserMembership) { CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( - inviteSender = membershipSender?.toInviteSender() + inviteData = toInviteData(), + inviteSender = membershipSender?.toInviteSender(), ) CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned( banSender = membershipSender?.toInviteSender(), @@ -340,23 +346,3 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus { else -> JoinAuthorisationStatus.Unknown } } - -@VisibleForTesting -internal fun ContentState.toInviteData(): InviteData? { - return when (this) { - is ContentState.Loaded -> { - if (joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited && joinAuthorisationStatus.inviteSender != null) { - InviteData( - roomId = roomId, - // Note: name should not be null at this point, but use Id just in case... - roomName = name ?: roomId.value, - senderId = joinAuthorisationStatus.inviteSender.userId, - isDm = isDm - ) - } else { - null - } - } - else -> null - } -} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index df2a5fcf8f..e027b5ed9c 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -8,7 +8,8 @@ package io.element.android.features.joinroom.impl import androidx.compose.runtime.Immutable -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -32,6 +33,7 @@ data class JoinRoomState( private val applicationName: String, val knockMessage: String, val hideInviteAvatars: Boolean, + val canReportRoom: Boolean, val eventSink: (JoinRoomEvents) -> Unit ) { val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin @@ -95,7 +97,7 @@ sealed interface ContentState { sealed interface JoinAuthorisationStatus { data object None : JoinAuthorisationStatus data class IsSpace(val applicationName: String) : JoinAuthorisationStatus - data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus + data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus data object IsKnocked : JoinAuthorisationStatus data object CanKnock : JoinAuthorisationStatus diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index d191915972..4a5039e364 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -8,8 +8,9 @@ package io.element.android.features.joinroom.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -50,12 +51,20 @@ open class JoinRoomStateProvider : PreviewParameterProvider { joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong", null)) ), aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)) + contentState = aLoadedContentState( + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = anInviteData(), + inviteSender = null, + ) + ) ), aJoinRoomState( contentState = aLoadedContentState( numberOfMembers = 123, - joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()), + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = anInviteData(), + inviteSender = anInviteSender(), + ), ) ), aJoinRoomState( @@ -149,7 +158,7 @@ fun aLoadedContentState( isDm: Boolean = false, roomType: RoomType = RoomType.Room, roomAvatarUrl: String? = null, - joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown + joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, ) = ContentState.Loaded( roomId = roomId, name = name, @@ -172,6 +181,7 @@ fun aJoinRoomState( cancelKnockAction: AsyncAction = AsyncAction.Uninitialized, knockMessage: String = "", hideInviteAvatars: Boolean = false, + canReportRoom: Boolean = true, eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( roomIdOrAlias = roomIdOrAlias, @@ -184,6 +194,7 @@ fun aJoinRoomState( applicationName = "AppName", knockMessage = knockMessage, hideInviteAvatars = hideInviteAvatars, + canReportRoom = canReportRoom, eventSink = eventSink ) @@ -199,5 +210,15 @@ internal fun anInviteSender( membershipChangeReason = membershipChangeReason, ) +internal fun anInviteData( + roomId: RoomId = A_ROOM_ID, + roomName: String = "Room name", + isDm: Boolean = false, +) = InviteData( + roomId = roomId, + roomName = roomName, + isDm = isDm, +) + private val A_ROOM_ID = RoomId("!exa:matrix.org") private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org") diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 8aae4a029b..b63bdff347 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.invite.api.InviteData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom @@ -78,6 +79,7 @@ fun JoinRoomView( onKnockSuccess: () -> Unit, onForgetSuccess: () -> Unit, onCancelKnockSuccess: () -> Unit, + onDeclineInviteAndBlockUser: (InviteData) -> Unit, modifier: Modifier = Modifier, ) { Box( @@ -104,11 +106,15 @@ fun JoinRoomView( footer = { JoinRoomFooter( joinAuthorisationStatus = state.joinAuthorisationStatus, - onAcceptInvite = { - state.eventSink(JoinRoomEvents.AcceptInvite) + onAcceptInvite = { inviteData -> + state.eventSink(JoinRoomEvents.AcceptInvite(inviteData)) }, - onDeclineInvite = { blockUser -> - state.eventSink(JoinRoomEvents.DeclineInvite(blockUser)) + onDeclineInvite = { inviteData, blockUser -> + if (state.canReportRoom && blockUser) { + onDeclineInviteAndBlockUser(inviteData) + } else { + state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser)) + } }, onJoinRoom = { state.eventSink(JoinRoomEvents.JoinRoom) @@ -184,8 +190,8 @@ fun JoinRoomView( @Composable private fun JoinRoomFooter( joinAuthorisationStatus: JoinAuthorisationStatus, - onAcceptInvite: () -> Unit, - onDeclineInvite: (Boolean) -> Unit, + onAcceptInvite: (InviteData) -> Unit, + onDeclineInvite: (InviteData, Boolean) -> Unit, onJoinRoom: () -> Unit, onKnockRoom: () -> Unit, onCancelKnock: () -> Unit, @@ -204,13 +210,13 @@ private fun JoinRoomFooter( ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) { OutlinedButton( text = stringResource(CommonStrings.action_decline), - onClick = { onDeclineInvite(false) }, + onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, ) Button( text = stringResource(CommonStrings.action_accept), - onClick = onAcceptInvite, + onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, ) @@ -218,7 +224,7 @@ private fun JoinRoomFooter( Spacer(modifier = Modifier.height(24.dp)) TextButton( text = stringResource(R.string.screen_join_room_decline_and_block_button_title), - onClick = { onDeclineInvite(true) }, + onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, true) }, modifier = Modifier.fillMaxWidth(), destructive = true ) @@ -585,5 +591,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) onKnockSuccess = { }, onForgetSuccess = { }, onCancelKnockSuccess = { }, + onDeclineInviteAndBlockUser = { }, ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index 90e22b70bc..7142133eb1 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -12,7 +12,7 @@ import dagger.Module import dagger.Provides import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.Presenter diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 5ad14a9b94..006cc986ab 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -9,10 +9,12 @@ package io.element.android.features.joinroom.impl import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState +import io.element.android.features.invite.api.toInviteData import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.joinroom.impl.di.CancelKnockRoom import io.element.android.features.joinroom.impl.di.ForgetRoom @@ -21,6 +23,8 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -46,6 +50,7 @@ import io.element.android.libraries.matrix.test.room.aRoomPreview import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom +import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore @@ -124,11 +129,12 @@ class JoinRoomPresenterTest { matrixClient = matrixClient, seenInvitesStore = seenInvitesStore, ) + val inviteData = roomSummary.info.toInviteData() assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() presenter.test { skipItems(2) awaitItem().also { state -> - assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null)) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, null)) } // Check that the roomId is stored in the seen invites store assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomSummary.roomId) @@ -144,6 +150,7 @@ class JoinRoomPresenterTest { joinedMembersCount = 5, inviter = inviter, ) + val inviteData = roomSummary.info.toInviteData() val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, ).apply { @@ -157,7 +164,7 @@ class JoinRoomPresenterTest { presenter.test { skipItems(2) awaitItem().also { state -> - assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender)) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender)) assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5) } } @@ -208,6 +215,7 @@ class JoinRoomPresenterTest { flowOf(Optional.of(roomSummary)) } } + val inviteData = roomSummary.info.toInviteData() val presenter = createJoinRoomPresenter( matrixClient = matrixClient, acceptDeclineInvitePresenter = acceptDeclinePresenter @@ -216,16 +224,14 @@ class JoinRoomPresenterTest { skipItems(1) awaitItem().also { state -> - state.eventSink(JoinRoomEvents.AcceptInvite) - state.eventSink(JoinRoomEvents.DeclineInvite(false)) - - val inviteData = state.contentState.toInviteData() + state.eventSink(JoinRoomEvents.AcceptInvite(inviteData)) + state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false)) assert(eventSinkRecorder) .isCalledExactly(2) .withSequence( listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), - listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), + listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))), ) } } @@ -613,7 +619,7 @@ class JoinRoomPresenterTest { } @Test - fun `present - when room is not known RoomPreview is loaded`() = runTest { + fun `present - when room is not known RoomPreview is loaded - membership null`() = runTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( @@ -657,6 +663,193 @@ class JoinRoomPresenterTest { } } + @Test + fun `present - when room is not known RoomPreview is loaded - membership INVITED`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = "Room name", + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.INVITED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Loaded( + roomId = A_ROOM_ID, + name = "Room name", + topic = "Room topic", + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, + roomType = RoomType.Room, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = InviteData( + roomId = A_ROOM_ID, + roomName = "Room name", + isDm = false, + ), + inviteSender = InviteSender( + userId = A_USER_ID_2, + displayName = "Bob", + avatarData = AvatarData( + id = A_USER_ID_2.value, + name = "Bob", + size = AvatarSize.InviteSender, + ), + membershipChangeReason = null, + ), + ) + ) + ) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded - membership BANNED`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = null, + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.BANNED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Loaded( + roomId = A_ROOM_ID, + name = null, + topic = "Room topic", + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, + roomType = RoomType.Room, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( + banSender = InviteSender( + userId = A_USER_ID_2, + displayName = "Bob", + avatarData = AvatarData( + id = A_USER_ID_2.value, + name = "Bob", + size = AvatarSize.InviteSender, + ), + membershipChangeReason = null, + ), + reason = null, + ) + ) + ) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded - membership KNOCKED`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = "Room name", + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.KNOCKED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Loaded( + roomId = A_ROOM_ID, + name = "Room name", + topic = "Room topic", + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, + roomType = RoomType.Room, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked + ) + ) + } + } + } + @Test fun `present - when room is not known RoomPreview is loaded as Private`() = runTest { val client = FakeMatrixClient( @@ -807,6 +1000,31 @@ class JoinRoomPresenterTest { } } + @Test + fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.failure(AN_EXCEPTION) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Failure(error = AN_EXCEPTION) + ) + state.eventSink(JoinRoomEvents.DismissErrorAndHideContent) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo(ContentState.Dismissing) + } + } + } + @Test fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest { val client = FakeMatrixClient( diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt index e9ac23d76e..00d73db03b 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt @@ -11,13 +11,19 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.test.anInviteData import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack import org.junit.Rule import org.junit.Test @@ -141,40 +147,61 @@ class JoinRoomViewTest { @Test fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() rule.setJoinRoomView( aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)), + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_accept) - eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite) + eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) } @Test fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() rule.setJoinRoomView( aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)), + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_decline) - eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(false)) + eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false)) } @Test - fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited emits the expected Event`() { - val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( - aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)), - eventSink = eventsRecorder, - ), + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = true, + eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) - eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(true)) + ensureCalledOnceWithParam(inviteData) { + rule.setJoinRoomView( + state = joinRoomState, + onDeclineInviteAndBlockUser = it, + ) + rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + } + } + + @Test + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = false, + eventSink = eventsRecorder, + ) + rule.setJoinRoomView(state = joinRoomState,) + rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true)) } @Test @@ -242,6 +269,7 @@ private fun AndroidComposeTestRule.setJoinR onKnockSuccess: () -> Unit = EnsureNeverCalled(), onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(), onForgetSuccess: () -> Unit = EnsureNeverCalled(), + onDeclineInviteAndBlockUser: (InviteData) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { JoinRoomView( @@ -251,6 +279,7 @@ private fun AndroidComposeTestRule.setJoinR onKnockSuccess = onKnockSuccess, onForgetSuccess = onForgetSuccess, onCancelKnockSuccess = onCancelKnockSuccess, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, ) } } diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt index e1c5cde223..28321d1b99 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt @@ -40,9 +40,9 @@ private fun LeaveRoomConfirmationDialog( is LeaveRoomState.Confirmation.Hidden -> {} is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog( - text = R.string.leave_conversation_alert_subtitle, + text = R.string.leave_room_alert_private_subtitle, roomId = state.confirmation.roomId, - isDm = true, + isDm = false, eventSink = state.eventSink, ) diff --git a/features/reportroom/api/build.gradle.kts b/features/reportroom/api/build.gradle.kts new file mode 100644 index 0000000000..f5499b7a74 --- /dev/null +++ b/features/reportroom/api/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.reportroom.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt b/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt new file mode 100644 index 0000000000..7f531fb9e3 --- /dev/null +++ b/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId + +interface ReportRoomEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node +} diff --git a/features/reportroom/impl/build.gradle.kts b/features/reportroom/impl/build.gradle.kts new file mode 100644 index 0000000000..d60cc76f8d --- /dev/null +++ b/features/reportroom/impl/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import extension.setupAnvil + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.reportroom.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupAnvil() + +dependencies { + api(projects.features.reportroom.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(libs.test.robolectric) +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt new file mode 100644 index 0000000000..d3131040d9 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.reportroom.api.ReportRoomEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultReportRoomEntryPoint @Inject constructor() : ReportRoomEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node { + return parentNode.createNode(buildContext, plugins = listOf(ReportRoomNode.Inputs(roomId))) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt new file mode 100644 index 0000000000..55ccb25417 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +interface ReportRoom { + suspend operator fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean, + ): Result + + sealed class Exception : kotlin.Exception() { + data object RoomNotFound : Exception() + data object LeftRoomFailed : Exception() + data object ReportRoomFailed : Exception() + } +} + +@ContributesBinding(SessionScope::class) +class DefaultReportRoom @Inject constructor( + private val client: MatrixClient, +) : ReportRoom { + override suspend operator fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean + ): Result { + val room = client.getRoom(roomId) + ?: return Result.failure(ReportRoom.Exception.RoomNotFound) + + if (shouldReport) { + room + .reportRoom(reason.takeIf { it.isNotBlank() }) + .onFailure { + return Result.failure(ReportRoom.Exception.ReportRoomFailed) + } + } + if (shouldLeave) { + room + .leave() + .onFailure { + return Result.failure(ReportRoom.Exception.LeftRoomFailed) + } + } + return Result.success(Unit) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt new file mode 100644 index 0000000000..e72f00e2a9 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +sealed interface ReportRoomEvents { + data class UpdateReason(val reason: String) : ReportRoomEvents + data object ToggleLeaveRoom : ReportRoomEvents + data object Report : ReportRoomEvents + data object ClearReportAction : ReportRoomEvents +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt new file mode 100644 index 0000000000..0c24d6db74 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId + +@ContributesNode(SessionScope::class) +class ReportRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ReportRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + data class Inputs(val roomId: RoomId) : NodeInputs + + private val roomId = inputs().roomId + private val presenter: ReportRoomPresenter = presenterFactory.create(roomId = roomId) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ReportRoomView( + state = state, + modifier = modifier, + onBackClick = ::navigateUp, + ) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt new file mode 100644 index 0000000000..30ccb9e20c --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class ReportRoomPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, + private val reportRoom: ReportRoom, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(roomId: RoomId): ReportRoomPresenter + } + + @Composable + override fun present(): ReportRoomState { + var reason by rememberSaveable { mutableStateOf("") } + var leaveRoom by rememberSaveable { mutableStateOf(false) } + var reportAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: ReportRoomEvents) { + when (event) { + ReportRoomEvents.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction) + ReportRoomEvents.ToggleLeaveRoom -> { + leaveRoom = !leaveRoom + } + is ReportRoomEvents.UpdateReason -> { + reason = event.reason + } + ReportRoomEvents.ClearReportAction -> { + reportAction.value = AsyncAction.Uninitialized + } + } + } + return ReportRoomState( + reason = reason, + leaveRoom = leaveRoom, + reportAction = reportAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.reportRoom( + reason: String, + shouldLeave: Boolean, + action: MutableState> + ) = launch { + val previousFailure = action.value as? AsyncAction.Failure + val shouldReport = previousFailure?.error !is ReportRoom.Exception.LeftRoomFailed + runUpdatingState(action) { + reportRoom( + roomId = roomId, + shouldReport = shouldReport, + reason = reason, + shouldLeave = shouldLeave + ) + } + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt new file mode 100644 index 0000000000..a04f24123b --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import io.element.android.libraries.architecture.AsyncAction + +data class ReportRoomState( + val reason: String, + val leaveRoom: Boolean, + val reportAction: AsyncAction, + val eventSink: (ReportRoomEvents) -> Unit +) { + val canReport: Boolean = reason.isNotBlank() +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt new file mode 100644 index 0000000000..1e89a31adb --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class ReportRoomStateProvider : PreviewParameterProvider { + companion object { + private const val A_REPORT_ROOM_REASON = "Inappropriate content" + } + + override val values: Sequence + get() = sequenceOf( + aReportRoomState(), + aReportRoomState(reason = A_REPORT_ROOM_REASON), + aReportRoomState(leaveRoom = true), + aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Loading), + aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Failure(Exception("Failed to report"))), + ) +} + +fun aReportRoomState( + reason: String = "", + leaveRoom: Boolean = false, + reportAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (ReportRoomEvents) -> Unit = {} +) = ReportRoomState( + reason = reason, + leaveRoom = leaveRoom, + reportAction = reportAction, + eventSink = eventSink, +) diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt new file mode 100644 index 0000000000..6421b7d576 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReportRoomView( + state: ReportRoomState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + + val isReporting = state.reportAction is AsyncAction.Loading + AsyncActionView( + async = state.reportAction, + onSuccess = { onBackClick() }, + errorTitle = { failure -> + when (failure) { + is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_title) + else -> stringResource(CommonStrings.dialog_title_error) + } + }, + errorMessage = { failure -> + when (failure) { + is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_message) + else -> stringResource(CommonStrings.error_unknown) + } + }, + onRetry = { + state.eventSink(ReportRoomEvents.Report) + }, + onErrorDismiss = { state.eventSink(ReportRoomEvents.ClearReportAction) } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + stringResource(R.string.screen_report_room_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = onBackClick) + } + ) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp) + ) { + TextField( + value = state.reason, + onValueChange = { state.eventSink(ReportRoomEvents.UpdateReason(it)) }, + placeholder = stringResource(R.string.screen_report_room_reason_placeholder), + minLines = 3, + enabled = !isReporting, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(min = 90.dp), + supportingText = stringResource(R.string.screen_report_room_reason_footer), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(CommonStrings.action_leave_room)) + }, + onClick = { + state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + }, + trailingContent = ListItemContent.Switch(checked = state.leaveRoom) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + text = stringResource(CommonStrings.action_report), + enabled = state.canReport && !isReporting, + destructive = true, + showProgress = isReporting, + onClick = { + focusManager.clearFocus(force = true) + state.eventSink(ReportRoomEvents.Report) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ReportRoomViewPreview( + @PreviewParameter(ReportRoomStateProvider::class) state: ReportRoomState +) = ElementPreview { + ReportRoomView( + state = state, + onBackClick = {}, + ) +} diff --git a/features/reportroom/impl/src/main/res/values-cs/translations.xml b/features/reportroom/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..809cfb68a5 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,8 @@ + + + "Vaše hlášení bylo úspěšně odesláno, ale při pokusu o opuštění místnosti jsme narazili na problém. Zkuste to prosím znovu." + "Nelze opustit místnost" + "Nahlaste tuto místnost svému administrátorovi. Pokud jsou zprávy zašifrované, váš administrátor je nebude moci číst." + "Popište důvod…" + "Nahlásit místnost" + diff --git a/features/reportroom/impl/src/main/res/values-cy/translations.xml b/features/reportroom/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 0000000000..89c44c2d0d --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,8 @@ + + + "Cyflwynwyd eich adroddiad yn llwyddiannus, ond cododd problem wrth geisio gadael yr ystafell. Ceisiwch eto." + "Methu Gadael yr Ystafell" + "Adroddwch yr ystafell hon i\'ch gweinyddwr. Os yw\'r negeseuon wedi\'u hamgryptio, fydd eich gweinyddwr ddim yn gallu eu darllen." + "Disgrifiwch y rheswm…" + "Adrodd ar ystafell" + diff --git a/features/reportroom/impl/src/main/res/values-de/translations.xml b/features/reportroom/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..6d379818d6 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,8 @@ + + + "Ihr Bericht wurde erfolgreich übermittelt, aber beim Versuch, den Raum zu verlassen, ist ein Problem aufgetreten. Bitte versuchen Sie es erneut." + "Der Chatroom kann nicht verlassen werden" + "Melden Sie diesen Chatroom Ihrem Administrator. Wenn die Nachrichten verschlüsselt sind, kann Ihr Administrator sie nicht lesen." + "Beschreiben Sie den Grund…" + "Chatroom melden" + diff --git a/features/reportroom/impl/src/main/res/values-el/translations.xml b/features/reportroom/impl/src/main/res/values-el/translations.xml new file mode 100644 index 0000000000..d8789dcd31 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,8 @@ + + + "Η αναφορά σας υποβλήθηκε με επιτυχία, αλλά αντιμετωπίσαμε πρόβλημα κατά την προσπάθεια αποχώρησης από το δωμάτιο. Παρακαλώ προσπαθήστε ξανά." + "Δεν είναι δυνατή η αποχώρηση από το δωμάτιο" + "Αναφέρετε αυτό το δωμάτιο στον διαχειριστή σας. Εάν τα μηνύματα είναι κρυπτογραφημένα, ο διαχειριστής σας δε θα μπορεί να τα διαβάσει." + "Περιγράψτε τον λόγο αναφοράς…" + "Αναφορά δωματίου" + diff --git a/features/reportroom/impl/src/main/res/values-et/translations.xml b/features/reportroom/impl/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..1cc6e6b277 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,8 @@ + + + "Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda." + "Pole võimalik lahkuda jututoast" + "Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda." + "Kirjelda põhjust…" + "Teata jututoast" + diff --git a/features/reportroom/impl/src/main/res/values-fi/translations.xml b/features/reportroom/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 0000000000..cf23910f8b --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,8 @@ + + + "Ilmoituksesi lähetettiin onnistuneesti, mutta kohtasimme ongelman yrittäessämme poistua huoneesta. Yritä uudelleen." + "Huoneesta poistuminen epäonnistui" + "Ilmoita tästä huoneesta palvelimesi ylläpitäjälle. Jos viestit on salattu, ylläpitäjäsi ei voi lukea niitä." + "Kuvaile syytä…" + "Ilmoita huoneesta" + diff --git a/features/reportroom/impl/src/main/res/values-fr/translations.xml b/features/reportroom/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..51376e34ea --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,8 @@ + + + "Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer." + "Impossible de quitter le salon" + "Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire." + "Décrivez la raison…" + "Signaler le salon" + diff --git a/features/reportroom/impl/src/main/res/values-hu/translations.xml b/features/reportroom/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..dcf563eb8e --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,8 @@ + + + "A jelentése sikeresen el lett küldve, de hibát találtunk a szoba elhagyása során. Próbálja újra." + "Nem tudja elhagyni a szobát" + "A szoba jelentése a rendszergazdának. Ha az üzenetek titkosítva vannak, akkor a rendszergazda nem fogja tudni elolvasni őket." + "Írja le az okot…" + "Szoba jelentése" + diff --git a/features/reportroom/impl/src/main/res/values-nb/translations.xml b/features/reportroom/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 0000000000..bda1334210 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,8 @@ + + + "Rapporten din ble sendt inn, men vi oppdaget et problem da vi prøvde å forlate rommet. Prøv igjen." + "Kan ikke forlate rommet" + "Rapporter dette rommet til administratoren din. Hvis meldingene er kryptert, vil administratoren ikke kunne lese dem." + "Beskriv årsaken…" + "Rapporter rom" + diff --git a/features/reportroom/impl/src/main/res/values-pl/translations.xml b/features/reportroom/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 0000000000..dd1b928e81 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,8 @@ + + + "Twoje zgłoszenie zostało wysłane pomyślnie, ale napotkaliśmy problem podczas opuszczania pokoju. Spróbuj ponownie." + "Nie można wyjść z pokoju" + "Zgłoś ten pokój swojemu administratorowi. Jeśli wiadomości są zaszyfrowane, administrator nie będzie mógł ich odczytać." + "Opisz powód…" + "Zgłoś pokój" + diff --git a/features/reportroom/impl/src/main/res/values-ru/translations.xml b/features/reportroom/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..de5871f29f --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,6 @@ + + + "Невозможно покинуть комнату" + "Сообщите об этой комнате своему администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать." + "Опишите причину…" + diff --git a/features/reportroom/impl/src/main/res/values-sk/translations.xml b/features/reportroom/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..87abe2f4af --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,8 @@ + + + "Vaša správa bola úspešne odoslaná, ale pri pokuse o opustenie miestnosti sme narazili na problém. Skúste to prosím znova." + "Nie je možné opustiť miestnosť" + "Nahláste túto miestnosť svojmu správcovi. Ak sú správy zašifrované, váš správca ich nebude môcť prečítať." + "Popíšte dôvod…" + "Nahlásiť miestnosť" + diff --git a/features/reportroom/impl/src/main/res/values-sv/translations.xml b/features/reportroom/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..174949b87d --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,8 @@ + + + "Din anmälan skickades in framgångsrikt, men vi stötte på ett problem när vi försökte lämna rummet. Vänligen försök igen." + "Kunde inte lämna rummet" + "Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem." + "Beskriv anledningen …" + "Rapportera rum" + diff --git a/features/reportroom/impl/src/main/res/values-uk/translations.xml b/features/reportroom/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..e720852848 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,8 @@ + + + "Ваша скарга надіслана, але ми зіткнулися з проблемою під час спроби вийти з кімнати. Повторіть спробу." + "Не вдалося вийти з кімнати" + "Поскаржтеся на цю кімнату своєму адміністратору. Якщо повідомлення зашифровані, ваш адміністратор не зможе їх прочитати." + "Опишіть причину…" + "Поскаржитися на кімнату" + diff --git a/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..fa63e0fb1d --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,7 @@ + + + "您的回報已成功遞交,但我們嘗試離開聊天室時遇到了問題。請再試一次。" + "無法離開聊天室" + "將此聊天室回報給您的管理員。若訊息已加密,您的管理員將無法讀取它們。" + "回報聊天室" + diff --git a/features/reportroom/impl/src/main/res/values/localazy.xml b/features/reportroom/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..16183e6f1b --- /dev/null +++ b/features/reportroom/impl/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + "Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again." + "Unable to Leave Room" + "Report this room to your admin. If the messages are encrypted, your admin will not be able to read them." + "Describe the reason to report…" + "Report room" + diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt new file mode 100644 index 0000000000..fc410ab08f --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultReportRoomTest { + private val roomId = A_ROOM_ID + private val successLeaveRoomLambda = lambdaRecorder> { -> Result.success(Unit) } + private val successReportRoomLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + + private val failureLeaveRoomLambda = + lambdaRecorder> { -> Result.failure(Exception("Leave room error")) } + private val failureReportRoomLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Report room error")) } + + @Test + fun `report room, leave=false, report=false, nothing is called`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = false) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isNeverCalled() + assert(successReportRoomLambda).isNeverCalled() + } + + @Test + fun `report room, leave=false, report=true, report room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = false) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isNeverCalled() + assert(successReportRoomLambda) + .isCalledOnce() + .with(value("Spam")) + } + + @Test + fun `report room, leave=true, report=false, leave room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = true) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda).isNeverCalled() + } + + @Test + fun `report room, leave=true, report=true, leave room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda) + .isCalledOnce() + .with(value("Spam")) + } + + @Test + fun `report room, leave=true, report=true, leave room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = failureLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.LeftRoomFailed) + assert(failureLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda).isCalledOnce() + } + + @Test + fun `report room, leave=true, report=true, report room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = failureReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.ReportRoomFailed) + assert(successLeaveRoomLambda).isNeverCalled() + assert(failureReportRoomLambda).isCalledOnce() + } +} diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt new file mode 100644 index 0000000000..d85f2b86e8 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.reportroom.impl.fakes.FakeReportRoom +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ReportRoomPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createReportRoomPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reason).isEmpty() + assertThat(state.leaveRoom).isFalse() + assertThat(state.reportAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(state.canReport).isFalse() + } + } + } + + @Test + fun `present - update form values`() = runTest { + val presenter = createReportRoomPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reason).isEmpty() + assertThat(state.canReport).isFalse() + assertThat(state.leaveRoom).isFalse() + state.eventSink(ReportRoomEvents.UpdateReason("Spam")) + } + awaitItem().also { state -> + assertThat(state.reason).isEqualTo("Spam") + assertThat(state.canReport).isTrue() + assertThat(state.leaveRoom).isFalse() + state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + } + awaitItem().also { state -> + assertThat(state.leaveRoom).isTrue() + assertThat(state.canReport).isTrue() + assertThat(state.canReport).isTrue() + } + } + } + + @Test + fun `present - report room success`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> Result.success(Unit) } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(reportRoomLambda) + .isCalledOnce() + .with(value(roomId), value(true), any(), value(true)) + } + } + + @Test + fun `present - report failed`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> + Result.failure(ReportRoom.Exception.ReportRoomFailed) + } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(reportRoomLambda) + .isCalledOnce() + .with(value(roomId), value(true), any(), any()) + } + } + + @Test + fun `present - leave room failed after report room success`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> + Result.failure(ReportRoom.Exception.LeftRoomFailed) + } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(ReportRoomEvents.Report) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(reportRoomLambda) + .isCalledExactly(2) + .withSequence( + // The first call should report the room and try leaving it + listOf(value(roomId), value(true), any(), value(true)), + // The second call should not report the room again + listOf(value(roomId), value(false), any(), value(true)) + ) + } + } + + fun createReportRoomPresenter( + roomId: RoomId = A_ROOM_ID, + reportRoom: ReportRoom = FakeReportRoom() + ): ReportRoomPresenter { + return ReportRoomPresenter(roomId, reportRoom) + } +} diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt new file mode 100644 index 0000000000..13dc5eb204 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ReportRoomViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setReportRoomView( + aReportRoomState( + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on report when enabled emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState( + reason = "Spam", + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_report) + eventsRecorder.assertSingle(ReportRoomEvents.Report) + } + + @Test + fun `clicking on decline when disabled does not emit event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setReportRoomView( + aReportRoomState(eventSink = eventsRecorder), + ) + rule.clickOn(CommonStrings.action_report) + } + + @Test + fun `clicking on leave room option emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState(eventSink = eventsRecorder), + ) + rule.clickOn(CommonStrings.action_leave_room) + eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) + } + + @Test + fun `typing text in the reason field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState( + eventSink = eventsRecorder, + reason = "" + ), + ) + rule.onNodeWithText("").performTextInput("Spam!") + eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) + } +} + +private fun AndroidComposeTestRule.setReportRoomView( + state: ReportRoomState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ReportRoomView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt new file mode 100644 index 0000000000..35d2db3ad8 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.reportroom.impl.fakes + +import io.element.android.features.reportroom.impl.ReportRoom +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeReportRoom( + var lambda: (RoomId, Boolean, String, Boolean) -> Result = { _, _, _, _ -> lambdaError() } +) : ReportRoom { + override suspend fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean + ): Result = simulateLongTask { + lambda(roomId, shouldReport, reason, shouldLeave) + } +} diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 315b00b153..87ca1afe4d 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.features.roomcall.api) implementation(projects.features.knockrequests.api) implementation(projects.features.verifysession.api) + implementation(projects.features.reportroom.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index f736109363..e0ecd4ebba 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -28,6 +28,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.poll.api.history.PollHistoryEntryPoint +import io.element.android.features.reportroom.api.ReportRoomEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode @@ -71,6 +72,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val mediaGalleryEntryPoint: MediaGalleryEntryPoint, private val verifySessionEntryPoint: VerifySessionEntryPoint, + private val reportRoomEntryPoint: ReportRoomEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -127,6 +129,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data class VerifyUser(val userId: UserId) : NavTarget + + @Parcelize + data object ReportRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -189,6 +194,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) elementCallEntryPoint.startCall(inputs) } + + override fun openReportRoom() { + backstack.push(NavTarget.ReportRoom) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -340,6 +349,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( }) .build() } + is NavTarget.ReportRoom -> { + reportRoomEntryPoint.createNode(this, buildContext, room.roomId) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 7c1efbacd5..1c76f561e2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openSecurityAndPrivacy() fun openDmUserProfile(userId: UserId) fun onJoinCall() + fun openReportRoom() } private val callbacks = plugins() @@ -132,6 +133,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openDmUserProfile(userId) } } + private fun onReportRoomClick() { + callbacks.forEach { it.openReportRoom() } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -166,6 +171,7 @@ class RoomDetailsNode @AssistedInject constructor( onKnockRequestsClick = ::openKnockRequestsLists, onSecurityAndPrivacyClick = ::openSecurityAndPrivacy, onProfileClick = ::onProfileClick, + onReportRoomClick = ::onReportRoomClick, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index c7a38a4731..e70430c8bf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -16,8 +16,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.Interaction +import io.element.android.appconfig.MatrixConfiguration import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled @@ -88,12 +88,12 @@ class RoomDetailsPresenter @Inject constructor( val joinRule by remember { derivedStateOf { roomInfo.joinRule } } val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() - var canShowMediaGallery by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery) - } val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } } + val canShowMediaGallery by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaGallery) + }.collectAsState(false) + LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) if (canShowNotificationSettings.value) { @@ -208,6 +208,7 @@ class RoomDetailsPresenter @Inject constructor( knockRequestsCount = knockRequestsCount, canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, hasMemberVerificationViolations = hasMemberVerificationViolations, + canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 8a0439b15d..5addbf3db8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -48,6 +48,7 @@ data class RoomDetailsState( val knockRequestsCount: Int?, val canShowSecurityAndPrivacy: Boolean, val hasMemberVerificationViolations: Boolean, + val canReportRoom: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) { val roomBadges = buildList { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 4304f151a3..d2cb4c5944 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -117,6 +117,7 @@ fun aRoomDetailsState( knockRequestsCount: Int? = null, canShowSecurityAndPrivacy: Boolean = true, hasMemberVerificationViolations: Boolean = false, + canReportRoom: Boolean = true, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -146,6 +147,7 @@ fun aRoomDetailsState( knockRequestsCount = knockRequestsCount, canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, hasMemberVerificationViolations = hasMemberVerificationViolations, + canReportRoom = canReportRoom, eventSink = eventSink, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 381f94ad26..8f7502dee5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -107,6 +107,7 @@ fun RoomDetailsView( onKnockRequestsClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit, onProfileClick: (UserId) -> Unit, + onReportRoomClick: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -255,8 +256,9 @@ fun RoomDetailsView( } OtherActionsSection( - isDm = state.roomType is RoomDetailsType.Dm, - onLeaveRoom = { state.eventSink(RoomDetailsEvent.LeaveRoom) } + canReportRoom = state.canReportRoom, + onReportRoomClick = onReportRoomClick, + onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom) } ) } } @@ -669,22 +671,29 @@ private fun MediaGalleryItem( } @Composable -private fun OtherActionsSection(isDm: Boolean, onLeaveRoom: () -> Unit) { +private fun OtherActionsSection( + canReportRoom: Boolean, + onReportRoomClick: () -> Unit, + onLeaveRoomClick: () -> Unit, +) { PreferenceCategory(showTopDivider = true) { + if (canReportRoom) { + ListItem( + headlineContent = { + Text(stringResource(CommonStrings.action_report_room)) + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), + style = ListItemStyle.Destructive, + onClick = onReportRoomClick, + ) + } ListItem( headlineContent = { - val leaveText = stringResource( - id = if (isDm) { - R.string.screen_room_details_leave_conversation_title - } else { - R.string.screen_room_details_leave_room_title - } - ) - Text(leaveText) + Text(stringResource(CommonStrings.action_leave_room)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())), style = ListItemStyle.Destructive, - onClick = onLeaveRoom, + onClick = onLeaveRoomClick, ) } } @@ -719,5 +728,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onKnockRequestsClick = {}, onSecurityAndPrivacyClick = {}, onProfileClick = {}, + onReportRoomClick = {}, ) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index da7f92aed5..2f4eebcad1 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -283,6 +283,20 @@ class RoomDetailsViewTest { eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom) } + @Config(qualifiers = "h1500dp") + @Test + fun `click on report room invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + ), + onReportRoomClick = callback, + ) + rule.clickOn(CommonStrings.action_report_room) + } + } + @Config(qualifiers = "h1024dp") @Test fun `click on knock requests invokes expected callback`() { @@ -333,6 +347,7 @@ private fun AndroidComposeTestRule.setRoomD onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onReportRoomClick: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomDetailsView( @@ -352,6 +367,7 @@ private fun AndroidComposeTestRule.setRoomD onKnockRequestsClick = onKnockRequestsClick, onSecurityAndPrivacyClick = onSecurityAndPrivacyClick, onProfileClick = onProfileClick, + onReportRoomClick = onReportRoomClick, ) } } diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index a6f09ff4ed..847787d774 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.features.rageshake.api) implementation(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) + implementation(projects.features.reportroom.api) api(projects.features.roomlist.api) testImplementation(libs.androidx.compose.ui.test.junit) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt index 727a404602..cb97dda171 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt @@ -28,7 +28,7 @@ class DefaultRoomListEntryPoint @Inject constructor() : RoomListEntryPoint { } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return parentNode.createNode(buildContext, plugins) } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt index 5dfaf90eab..d95064708b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -34,14 +34,17 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean, eventSink: (RoomListEvents.ContextMenuEvents) -> Unit, onRoomSettingsClick: (roomId: RoomId) -> Unit, + onReportRoomClick: (roomId: RoomId) -> Unit ) { ModalBottomSheet( onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) }, ) { RoomListModalBottomSheetContent( contextMenu = contextMenu, + canReportRoom = canReportRoom, onRoomMarkReadClick = { eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId)) @@ -65,6 +68,10 @@ fun RoomListContextMenu( eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId)) }, + onReportRoomClick = { + eventSink(RoomListEvents.HideContextMenu) + onReportRoomClick(contextMenu.roomId) + }, ) } } @@ -72,12 +79,14 @@ fun RoomListContextMenu( @Composable private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean, onRoomSettingsClick: () -> Unit, onLeaveRoomClick: () -> Unit, onFavoriteChange: (isFavorite: Boolean) -> Unit, onRoomMarkReadClick: () -> Unit, onRoomMarkUnreadClick: () -> Unit, onClearCacheRoomClick: () -> Unit, + onReportRoomClick: () -> Unit, ) { Column( modifier = Modifier.fillMaxWidth() @@ -157,16 +166,24 @@ private fun RoomListModalBottomSheetContent( ), style = ListItemStyle.Primary, ) + if (canReportRoom) { + ListItem( + headlineContent = { + Text(text = stringResource(CommonStrings.action_report_room)) + }, + modifier = Modifier.clickable { onReportRoomClick() }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector( + CompoundIcons.ChatProblem(), + contentDescription = stringResource(CommonStrings.action_report_room), + ) + ), + style = ListItemStyle.Destructive, + ) + } ListItem( headlineContent = { - val leaveText = stringResource( - id = if (contextMenu.isDm) { - CommonStrings.action_leave_conversation - } else { - CommonStrings.action_leave_room - } - ) - Text(text = leaveText) + Text(text = stringResource(CommonStrings.action_leave_room)) }, modifier = Modifier.clickable { onLeaveRoomClick() }, leadingContent = ListItemContent.Icon( @@ -201,11 +218,13 @@ internal fun RoomListModalBottomSheetContentPreview( ) = ElementPreview { RoomListModalBottomSheetContent( contextMenu = contextMenu, + canReportRoom = true, onRoomMarkReadClick = {}, onRoomMarkUnreadClick = {}, onRoomSettingsClick = {}, onLeaveRoomClick = {}, onFavoriteChange = {}, onClearCacheRoomClick = {}, + onReportRoomClick = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenu.kt new file mode 100644 index 0000000000..0f986999f8 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenu.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomListDeclineInviteMenu( + menu: RoomListState.DeclineInviteMenu.Shown, + canReportRoom: Boolean, + onDeclineAndBlockClick: (RoomListRoomSummary) -> Unit, + eventSink: (RoomListEvents) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { eventSink(RoomListEvents.HideDeclineInviteMenu) }, + ) { + RoomListDeclineInviteMenuContent( + roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value, + onDeclineClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, false)) + }, + onDeclineAndBlockClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + if (canReportRoom) { + onDeclineAndBlockClick(menu.roomSummary) + } else { + eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, true)) + } + }, + onCancelClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + } + ) + } +} + +@Composable +private fun RoomListDeclineInviteMenuContent( + roomName: String, + onDeclineClick: () -> Unit, + onDeclineAndBlockClick: () -> Unit, + onCancelClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.screen_invites_decline_chat_title), + style = ElementTheme.typography.fontHeadingSmMedium, + color = ElementTheme.colors.textPrimary, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_invites_decline_chat_message, roomName), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(22.dp)) + Button( + text = stringResource(CommonStrings.action_decline), + modifier = Modifier.fillMaxWidth(), + onClick = onDeclineClick, + ) + Spacer(Modifier.height(16.dp)) + OutlinedButton( + text = stringResource(CommonStrings.action_decline_and_block), + modifier = Modifier.fillMaxWidth(), + destructive = true, + onClick = onDeclineAndBlockClick + ) + Spacer(Modifier.height(16.dp)) + TextButton( + text = stringResource(CommonStrings.action_cancel), + modifier = Modifier.fillMaxWidth(), + onClick = onCancelClick + ) + } +} + +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. +@PreviewsDayNight +@Composable +internal fun RoomListDeclineInviteMenuContentPreview() = ElementPreview { + RoomListDeclineInviteMenuContent( + roomName = "Room name", + onCancelClick = {}, + onDeclineClick = {}, + onDeclineAndBlockClick = {}, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 9e9f701380..b17b45b86b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -15,9 +15,12 @@ sealed interface RoomListEvents { data object DismissRequestVerificationPrompt : RoomListEvents data object DismissBanner : RoomListEvents data object ToggleSearchResults : RoomListEvents - data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents + data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents + + data class AcceptInvite(val roomSummary: RoomListRoomSummary) : RoomListEvents + data class DeclineInvite(val roomSummary: RoomListRoomSummary, val blockUser: Boolean) : RoomListEvents + data class ShowDeclineInviteMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents + data object HideDeclineInviteMenu : RoomListEvents sealed interface ContextMenuEvents : RoomListEvents data object HideContextMenu : ContextMenuEvents diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListFlowNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListFlowNode.kt new file mode 100644 index 0000000000..208a90e606 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListFlowNode.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl + +import android.app.Activity +import android.os.Parcelable +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.features.logout.api.direct.DirectLogoutView +import io.element.android.features.reportroom.api.ReportRoomEntryPoint +import io.element.android.features.roomlist.api.RoomListEntryPoint +import io.element.android.features.roomlist.impl.components.RoomListMenuAction +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class RoomListFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomListPresenter, + private val inviteFriendsUseCase: InviteFriendsUseCase, + private val analyticsService: AnalyticsService, + private val acceptDeclineInviteView: AcceptDeclineInviteView, + private val directLogoutView: DirectLogoutView, + private val reportRoomEntryPoint: ReportRoomEntryPoint, + private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + init { + lifecycle.subscribe( + onResume = { + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home)) + } + ) + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class ReportRoom(val roomId: RoomId) : NavTarget + + @Parcelize + data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget + } + + private fun onRoomClick(roomId: RoomId) { + plugins().forEach { it.onRoomClick(roomId) } + } + + private fun onOpenSettings() { + plugins().forEach { it.onSettingsClick() } + } + + private fun onCreateRoomClick() { + plugins().forEach { it.onCreateRoomClick() } + } + + private fun onSetUpRecoveryClick() { + plugins().forEach { it.onSetUpRecoveryClick() } + } + + private fun onSessionConfirmRecoveryKeyClick() { + plugins().forEach { it.onSessionConfirmRecoveryKeyClick() } + } + + private fun onRoomSettingsClick(roomId: RoomId) { + plugins().forEach { it.onRoomSettingsClick(roomId) } + } + + private fun onReportRoomClick(roomId: RoomId) { + backstack.push(NavTarget.ReportRoom(roomId)) + } + + private fun onDeclineInviteAndBlockUserClick(roomSummary: RoomListRoomSummary) { + backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData())) + } + + private fun onMenuActionClick(activity: Activity, roomListMenuAction: RoomListMenuAction) { + when (roomListMenuAction) { + RoomListMenuAction.InviteFriends -> { + inviteFriendsUseCase.execute(activity) + } + RoomListMenuAction.ReportBug -> { + plugins().forEach { it.onReportBugClick() } + } + } + } + + fun rootNode(buildContext: BuildContext): Node { + return node(buildContext) { modifier -> + val state = presenter.present() + val activity = requireNotNull(LocalActivity.current) + + RoomListView( + state = state, + onRoomClick = this::onRoomClick, + onSettingsClick = this::onOpenSettings, + onCreateRoomClick = this::onCreateRoomClick, + onSetUpRecoveryClick = this::onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick, + onRoomSettingsClick = this::onRoomSettingsClick, + onMenuActionClick = { onMenuActionClick(activity, it) }, + onReportRoomClick = this::onReportRoomClick, + onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick, + modifier = modifier, + ) { + acceptDeclineInviteView.Render( + state = state.acceptDeclineInviteState, + onAcceptInviteSuccess = this::onRoomClick, + onDeclineInviteSuccess = { }, + modifier = Modifier + ) + } + + directLogoutView.Render(state.directLogoutState) + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId) + is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData) + NavTarget.Root -> rootNode(buildContext) + } + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt deleted file mode 100644 index 9228e847b4..0000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomlist.impl - -import android.app.Activity -import androidx.activity.compose.LocalActivity -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.lifecycle.subscribe -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.core.plugin.plugins -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.features.logout.api.direct.DirectLogoutView -import io.element.android.features.roomlist.api.RoomListEntryPoint -import io.element.android.features.roomlist.impl.components.RoomListMenuAction -import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.services.analytics.api.AnalyticsService - -@ContributesNode(SessionScope::class) -class RoomListNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: RoomListPresenter, - private val inviteFriendsUseCase: InviteFriendsUseCase, - private val analyticsService: AnalyticsService, - private val acceptDeclineInviteView: AcceptDeclineInviteView, - private val directLogoutView: DirectLogoutView, -) : Node(buildContext, plugins = plugins) { - init { - lifecycle.subscribe( - onResume = { - analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home)) - } - ) - } - - private fun onRoomClick(roomId: RoomId) { - plugins().forEach { it.onRoomClick(roomId) } - } - - private fun onOpenSettings() { - plugins().forEach { it.onSettingsClick() } - } - - private fun onCreateRoomClick() { - plugins().forEach { it.onCreateRoomClick() } - } - - private fun onSetUpRecoveryClick() { - plugins().forEach { it.onSetUpRecoveryClick() } - } - - private fun onSessionConfirmRecoveryKeyClick() { - plugins().forEach { it.onSessionConfirmRecoveryKeyClick() } - } - - private fun onRoomSettingsClick(roomId: RoomId) { - plugins().forEach { it.onRoomSettingsClick(roomId) } - } - - private fun onMenuActionClick(activity: Activity, roomListMenuAction: RoomListMenuAction) { - when (roomListMenuAction) { - RoomListMenuAction.InviteFriends -> { - inviteFriendsUseCase.execute(activity) - } - RoomListMenuAction.ReportBug -> { - plugins().forEach { it.onReportBugClick() } - } - } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val activity = requireNotNull(LocalActivity.current) - - RoomListView( - state = state, - onRoomClick = this::onRoomClick, - onSettingsClick = this::onOpenSettings, - onCreateRoomClick = this::onCreateRoomClick, - onSetUpRecoveryClick = this::onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick, - onRoomSettingsClick = this::onRoomSettingsClick, - onMenuActionClick = { onMenuActionClick(activity, it) }, - modifier = modifier, - ) { - acceptDeclineInviteView.Render( - state = state.acceptDeclineInviteState, - onAcceptInvite = this::onRoomClick, - onDeclineInvite = { }, - modifier = Modifier - ) - } - - directLogoutView.Render(state.directLogoutState) - } -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index e2d1ffd698..07029b7026 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -7,7 +7,6 @@ package io.element.android.features.roomlist.impl -import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -24,17 +23,17 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import im.vector.app.features.analytics.plan.Interaction +import io.element.android.appconfig.MatrixConfiguration import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.InviteData -import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.leaveroom.api.LeaveRoomEvent.ShowConfirmation import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchEvents import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.libraries.architecture.AsyncData @@ -124,6 +123,7 @@ class RoomListPresenter @Inject constructor( }.collectAsState(initial = false) val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) } val directLogoutState = logoutPresenter.present() @@ -141,20 +141,22 @@ class RoomListPresenter @Inject constructor( is RoomListEvents.HideContextMenu -> { contextMenu.value = RoomListState.ContextMenu.Hidden } - is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) + is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(ShowConfirmation(event.roomId)) is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite) is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId) is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId) is RoomListEvents.AcceptInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(event.roomListRoomSummary.toInviteData()) + AcceptInvite(event.roomSummary.toInviteData()) ) } is RoomListEvents.DeclineInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData()) + DeclineInvite(event.roomSummary.toInviteData(), blockUser = event.blockUser, shouldConfirm = false) ) } + is RoomListEvents.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary) + RoomListEvents.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId) } } @@ -169,6 +171,7 @@ class RoomListPresenter @Inject constructor( snackbarMessage = snackbarMessage, hasNetworkConnection = isOnline, contextMenu = contextMenu.value, + declineInviteMenu = declineInviteMenu.value, leaveRoomState = leaveRoomState, filtersState = filtersState, canReportBug = canReportBug, @@ -177,6 +180,7 @@ class RoomListPresenter @Inject constructor( acceptDeclineInviteState = acceptDeclineInviteState, directLogoutState = directLogoutState, hideInvitesAvatars = hideInvitesAvatar, + canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM, eventSink = ::handleEvents, ) } @@ -253,18 +257,18 @@ class RoomListPresenter @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState) = launch { val initialState = RoomListState.ContextMenu.Shown( - roomId = event.roomListRoomSummary.roomId, - roomName = event.roomListRoomSummary.name, - isDm = event.roomListRoomSummary.isDm, - isFavorite = event.roomListRoomSummary.isFavorite, + roomId = event.roomSummary.roomId, + roomName = event.roomSummary.name, + isDm = event.roomSummary.isDm, + isFavorite = event.roomSummary.isFavorite, markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread), - hasNewContent = event.roomListRoomSummary.hasNewContent, + hasNewContent = event.roomSummary.hasNewContent, eventCacheFeatureFlagEnabled = appPreferencesStore.isDeveloperModeEnabledFlow().first() && featureFlagService.isFeatureEnabled(FeatureFlags.EventCache), ) contextMenuState.value = initialState - client.getRoom(event.roomListRoomSummary.roomId)?.use { room -> + client.getRoom(event.roomSummary.roomId)?.use { room -> val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown } .distinctUntilChanged() @@ -342,14 +346,3 @@ class RoomListPresenter @Inject constructor( } } } - -@VisibleForTesting -internal fun RoomListRoomSummary.toInviteData(): InviteData? { - if (inviteSender == null) return null - return InviteData( - roomId = roomId, - roomName = name ?: roomId.value, - isDm = isDm, - senderId = inviteSender.userId, - ) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 307a2d1313..8d203498cb 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -8,7 +8,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState @@ -28,6 +28,7 @@ data class RoomListState( val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val contextMenu: ContextMenu, + val declineInviteMenu: DeclineInviteMenu, val leaveRoomState: LeaveRoomState, val filtersState: RoomListFiltersState, val canReportBug: Boolean, @@ -36,6 +37,7 @@ data class RoomListState( val acceptDeclineInviteState: AcceptDeclineInviteState, val directLogoutState: DirectLogoutState, val hideInvitesAvatars: Boolean, + val canReportRoom: Boolean, val eventSink: (RoomListEvents) -> Unit, ) { val displayFilters = contentState is RoomListContentState.Rooms @@ -53,6 +55,11 @@ data class RoomListState( val hasNewContent: Boolean, ) : ContextMenu } + + sealed interface DeclineInviteMenu { + data object Hidden : DeclineInviteMenu + data class Shown(val roomSummary: RoomListRoomSummary) : DeclineInviteMenu + } } enum class SecurityBannerState { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 1f93e81d67..4a221d3c97 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -8,8 +8,8 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.logout.api.direct.DirectLogoutState @@ -54,6 +54,7 @@ internal fun aRoomListState( hasNetworkConnection: Boolean = true, snackbarMessage: SnackbarMessage? = null, contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, + declineInviteMenu: RoomListState.DeclineInviteMenu = RoomListState.DeclineInviteMenu.Hidden, leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(), @@ -62,6 +63,7 @@ internal fun aRoomListState( acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), directLogoutState: DirectLogoutState = aDirectLogoutState(), hideInvitesAvatars: Boolean = false, + canReportRoom: Boolean = true, eventSink: (RoomListEvents) -> Unit = {} ) = RoomListState( matrixUser = matrixUser, @@ -69,6 +71,7 @@ internal fun aRoomListState( hasNetworkConnection = hasNetworkConnection, snackbarMessage = snackbarMessage, contextMenu = contextMenu, + declineInviteMenu = declineInviteMenu, leaveRoomState = leaveRoomState, filtersState = filtersState, canReportBug = canReportBug, @@ -77,6 +80,7 @@ internal fun aRoomListState( acceptDeclineInviteState = acceptDeclineInviteState, directLogoutState = directLogoutState, hideInvitesAvatars = hideInvitesAvatars, + canReportRoom = canReportRoom, eventSink = eventSink, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 090180a7b8..016cc4d116 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -49,6 +49,8 @@ fun RoomListView( onCreateRoomClick: () -> Unit, onRoomSettingsClick: (roomId: RoomId) -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, + onReportRoomClick: (roomId: RoomId) -> Unit, + onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, acceptDeclineInviteView: @Composable () -> Unit, ) { @@ -60,8 +62,18 @@ fun RoomListView( if (state.contextMenu is RoomListState.ContextMenu.Shown) { RoomListContextMenu( contextMenu = state.contextMenu, + canReportRoom = state.canReportRoom, eventSink = state.eventSink, onRoomSettingsClick = onRoomSettingsClick, + onReportRoomClick = onReportRoomClick, + ) + } + if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) { + RoomListDeclineInviteMenu( + menu = state.declineInviteMenu, + canReportRoom = state.canReportRoom, + eventSink = state.eventSink, + onDeclineAndBlockClick = onDeclineInviteAndBlockUser, ) } @@ -177,7 +189,9 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) onConfirmRecoveryKeyClick = {}, onCreateRoomClick = {}, onRoomSettingsClick = {}, + onReportRoomClick = {}, onMenuActionClick = {}, + onDeclineInviteAndBlockUser = {}, acceptDeclineInviteView = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index 9f7a67bd00..7e5265c965 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -104,7 +104,7 @@ internal fun RoomSummaryRow( eventSink(RoomListEvents.AcceptInvite(room)) }, onDeclineClick = { - eventSink(RoomListEvents.DeclineInvite(room)) + eventSink(RoomListEvents.ShowDeclineInviteMenu(room)) } ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index cb4c48d3f7..a1c1156c2a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomlist.impl.model import androidx.compose.runtime.Immutable +import io.element.android.features.invite.api.InviteData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -45,4 +46,10 @@ data class RoomListRoomSummary( numberOfUnreadMentions > 0 || numberOfUnreadNotifications > 0 || isMarkedUnread + + fun toInviteData() = InviteData( + roomId = roomId, + roomName = name ?: roomId.value, + isDm = isDm, + ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt index 25d57dab7a..80b5a87c10 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -31,8 +31,10 @@ class RoomListContextMenuTest { rule.setContent { RoomListContextMenu( contextMenu = contextMenu, + canReportRoom = false, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = EnsureNeverCalledWithParam(), ) } rule.clickOn(R.string.screen_roomlist_mark_as_read) @@ -51,8 +53,10 @@ class RoomListContextMenuTest { rule.setContent { RoomListContextMenu( contextMenu = contextMenu, + canReportRoom = false, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = EnsureNeverCalledWithParam(), ) } rule.clickOn(R.string.screen_roomlist_mark_as_unread) @@ -64,26 +68,6 @@ class RoomListContextMenuTest { ) } - @Test - fun `clicking on Leave dm generates expected Events`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(isDm = true) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - ) - } - rule.clickOn(CommonStrings.action_leave_conversation) - eventsRecorder.assertList( - listOf( - RoomListEvents.HideContextMenu, - RoomListEvents.LeaveRoom(contextMenu.roomId), - ) - ) - } - @Test fun `clicking on Leave room generates expected Events`() { val eventsRecorder = EventsRecorder() @@ -91,8 +75,10 @@ class RoomListContextMenuTest { rule.setContent { RoomListContextMenu( contextMenu = contextMenu, + canReportRoom = false, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = EnsureNeverCalledWithParam(), ) } rule.clickOn(CommonStrings.action_leave_room) @@ -104,6 +90,25 @@ class RoomListContextMenuTest { ) } + @Test + fun `clicking on Report room invokes the expected callback and generates expected Event`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown() + val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = true, + eventSink = eventsRecorder, + onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = callback, + ) + } + rule.clickOn(CommonStrings.action_report_room) + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + callback.assertSuccess() + } + @Test fun `clicking on Settings invokes the expected callback and generates expected Event`() { val eventsRecorder = EventsRecorder() @@ -112,8 +117,10 @@ class RoomListContextMenuTest { rule.setContent { RoomListContextMenu( contextMenu = contextMenu, + canReportRoom = false, eventSink = eventsRecorder, onRoomSettingsClick = callback, + onReportRoomClick = EnsureNeverCalledWithParam(), ) } rule.clickOn(CommonStrings.common_settings) @@ -129,8 +136,10 @@ class RoomListContextMenuTest { rule.setContent { RoomListContextMenu( contextMenu = contextMenu, + canReportRoom = false, eventSink = eventsRecorder, onRoomSettingsClick = callback, + onReportRoomClick = EnsureNeverCalledWithParam(), ) } rule.clickOn(CommonStrings.common_favourite) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt new file mode 100644 index 0000000000..0fdb5d7639 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomListDeclineInviteMenuTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on decline emits the expected Events`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideDeclineInviteMenu, + RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = false), + ) + ) + } + + @Test + fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = true, + onDeclineAndBlockClick = EnsureCalledOnceWithParam(menu.roomSummary, Unit), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline_and_block) + val expectedEvents = listOf(RoomListEvents.HideDeclineInviteMenu) + eventsRecorder.assertList(expectedEvents) + } + + @Test + fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline_and_block) + val expectedEvents = listOf( + RoomListEvents.HideDeclineInviteMenu, + RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = true), + ) + eventsRecorder.assertList(expectedEvents) + } + + @Test + fun `clicking on cancel emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertList(listOf(RoomListEvents.HideDeclineInviteMenu)) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index c5e42bc96c..073e163c16 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -13,9 +13,9 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.invite.api.SeenInvitesStore -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState @@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService @@ -577,7 +578,8 @@ class RoomListPresenterTest { roomListService = roomListService, ) val roomSummary = aRoomSummary( - currentUserMembership = CurrentUserMembership.INVITED + currentUserMembership = CurrentUserMembership.INVITED, + inviter = aRoomMember(), ) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(roomSummary)) @@ -593,16 +595,16 @@ class RoomListPresenterTest { val roomListRoomSummary = state.contentAsRooms().summaries.first { it.id == roomSummary.roomId.value } + state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary)) - state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary)) + state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary, blockUser = false)) val inviteData = roomListRoomSummary.toInviteData() - assert(eventSinkRecorder) .isCalledExactly(2) .withSequence( listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), - listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), + listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))), ) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 780af7c98c..ee4cc0b564 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomlist.impl.components.RoomListMenuAction +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings @@ -226,7 +227,10 @@ class RoomListViewTest { rule.clickOn(CommonStrings.action_accept) rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertList( - listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)), + listOf( + RoomListEvents.AcceptInvite(invitedRoom), + RoomListEvents.ShowDeclineInviteMenu(invitedRoom), + ) ) } } @@ -240,6 +244,8 @@ private fun AndroidComposeTestRule.setRoomL onCreateRoomClick: () -> Unit = EnsureNeverCalled(), onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), + onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { RoomListView( @@ -251,6 +257,8 @@ private fun AndroidComposeTestRule.setRoomL onCreateRoomClick = onCreateRoomClick, onRoomSettingsClick = onRoomSettingsClick, onMenuActionClick = onMenuActionClick, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, + onReportRoomClick = onReportRoomClick, acceptDeclineInviteView = { }, ) } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 6e8fba8bd1..2a167ad774 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -163,5 +163,5 @@ enum class FeatureFlags( defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE }, // False so it's displayed in the developer options screen isFinished = false, - ) + ), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 020dc7e449..8df4cb5671 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -222,6 +222,13 @@ interface BaseRoom : Closeable { */ suspend fun clearComposerDraft(): Result + /** + * Reports a room as inappropriate to the server. + * The caller is not required to be joined to the room to report it. + * @param reason - The reason the room is being reported. + */ + suspend fun reportRoom(reason: String?): Result + /** * Destroy the room and release all resources associated to it. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index b2582f1612..0be2f49ce6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -264,4 +264,11 @@ class RustBaseRoom( innerRoom.clearComposerDraft() } } + + override suspend fun reportRoom(reason: String?): Result = withContext(roomDispatcher) { + runCatching { + Timber.d("reportRoom $roomId") + innerRoom.reportRoom(reason) + } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index e260dbfd52..76e5403133 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -64,6 +64,7 @@ class FakeBaseRoom( private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, private val getRoomVisibilityResult: () -> Result = { lambdaError() }, private val forgetResult: () -> Result = { lambdaError() }, + private val reportRoomResult: (String?) -> Result = { lambdaError() }, ) : BaseRoom { private val _roomInfoFlow: MutableStateFlow = MutableStateFlow(initialRoomInfo) override val roomInfoFlow: StateFlow = _roomInfoFlow @@ -206,6 +207,8 @@ class FakeBaseRoom( override suspend fun clearEventCacheStorage(): Result { return Result.success(Unit) } + + override suspend fun reportRoom(reason: String?) = reportRoomResult(reason) } fun defaultRoomPowerLevels() = RoomPowerLevels( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt similarity index 91% rename from appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt index 14ead1be9a..7fc95035c6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt @@ -5,12 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.appnav.room.joined +package io.element.android.libraries.matrix.ui.room import androidx.compose.runtime.Immutable import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -38,7 +36,6 @@ open class LoadingRoomStateProvider : PreviewParameterProvider ) } -@SingleIn(SessionScope::class) class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) { fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow = getJoinedRoomFlow(roomId) diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index dba34acbc2..4d7409e7c1 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -355,11 +355,6 @@ Opravdu chcete pokračovat?" "Odebrat z místnosti" "Odebrat člena a zakázat mu připojení v budoucnu?" "Odstraňování %1$s…" - "Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti" - "Zablokovat uživatele" - "Nahlaste tuto místnost svému poskytovateli účtu." - "Popište důvod nahlášení…" - "Odmítnout a zablokovat" "Výběr média se nezdařil, zkuste to prosím znovu." "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace." "Nahrání média se nezdařilo, zkuste to prosím znovu." @@ -372,11 +367,6 @@ Opravdu chcete pokračovat?" "%1$d Připnutých zpráv" "Připnuté zprávy" - "Vaše hlášení bylo úspěšně odesláno, ale při pokusu o opuštění místnosti jsme narazili na problém. Zkuste to prosím znovu." - "Nelze opustit místnost" - "Nahlaste tuto místnost svému administrátorovi. Pokud jsou zprávy zašifrované, váš administrátor je nebude moci číst." - "Popište důvod…" - "Nahlásit místnost" "Chystáte se přejít na svůj %1$s účet a obnovit svou identitu. Poté budete přesměrováni zpět do aplikace." "Nemůžete to potvrdit? Přejděte na svůj účet a resetujte svou identitu." "Zrušit ověření a odeslat" diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index 888bcab034..e9ba8c094f 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -335,11 +335,6 @@ Ydych chi\'n siŵr eich bod am barhau?" "Tynnu o\'r ystafell" "Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?" "Wrthi\'n dileu %1$s…" - "Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn" - "Rhwystro defnyddiwr" - "Adrodd am yr ystafell hon i ddarparwr eich cyfrif." - "Disgrifiwch y rheswm dros adrodd…" - "Gwrthod a rhwystro" "Wedi methu dewis cyfrwng, ceisiwch eto." "Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn." "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." @@ -347,11 +342,6 @@ Ydych chi\'n siŵr eich bod am barhau?" "Pwyswch ar neges a dewis “%1$s” i\'w cynnwys yma." "Pinio negeseuon pwysig fel y mae modd eu darganfod yn hawdd" "Negeseuon wedi\'u pinio" - "Cyflwynwyd eich adroddiad yn llwyddiannus, ond cododd problem wrth geisio gadael yr ystafell. Ceisiwch eto." - "Methu Gadael yr Ystafell" - "Adroddwch yr ystafell hon i\'ch gweinyddwr. Os yw\'r negeseuon wedi\'u hamgryptio, fydd eich gweinyddwr ddim yn gallu eu darllen." - "Disgrifiwch y rheswm…" - "Adrodd ar ystafell" "Rydych chi ar fin mynd i\'ch cyfrif %1$s i ailosod eich hunaniaeth. Wedi hynny byddwch yn cael eich tywys yn ôl i\'r ap." "Methu cadarnhau? Ewch i\'ch cyfrif i ailosod eich hunaniaeth." "Tynnu\'r dilysiad yn ôl a\'i anfon" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 91c505c3c0..8945b8cd6b 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -351,11 +351,6 @@ Möchten Sie wirklich fortfahren?" "Mitglied entfernen" "Mitglied entfernen und den erneuten Beitritt sperren?" "%1$s wird entfernt." - "Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen." - "Benutzer blockieren" - "Melden Sie diesen Raum Ihrem Kontoanbieter." - "Beschreiben Sie den Grund für die Meldung…" - "Ablehnen und blockieren" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." @@ -367,11 +362,6 @@ Möchten Sie wirklich fortfahren?" "%1$d fixierte Nachrichten" "Fixierte Nachrichten" - "Ihr Bericht wurde erfolgreich übermittelt, aber beim Versuch, den Raum zu verlassen, ist ein Problem aufgetreten. Bitte versuchen Sie es erneut." - "Der Chatroom kann nicht verlassen werden" - "Melden Sie diesen Chatroom Ihrem Administrator. Wenn die Nachrichten verschlüsselt sind, kann Ihr Administrator sie nicht lesen." - "Beschreiben Sie den Grund…" - "Chatroom melden" "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen." "Verifizierung zurückziehen und senden" diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml index da79a04f1b..24a92cad85 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -351,11 +351,6 @@ "Αφαίρεση από το δωμάτιο" "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;" "Αφαίρεση %1$s…" - "Δε θα δείτε μηνύματα ή προσκλήσεις δωματίου από αυτόν τον χρήστη" - "Αποκλεισμός χρήστη" - "Αναφέρετε αυτό το δωμάτιο στον πάροχο του λογαριασμού σας." - "Περιγράψτε τον λόγο αναφοράς…" - "Απόρριψη και αποκλεισμός" "Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά." "Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές." "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." @@ -367,11 +362,6 @@ "%1$d Καρφιτσωμένα μηνύματα" "Καρφιτσωμένα μηνύματα" - "Η αναφορά σας υποβλήθηκε με επιτυχία, αλλά αντιμετωπίσαμε πρόβλημα κατά την προσπάθεια αποχώρησης από το δωμάτιο. Παρακαλώ προσπαθήστε ξανά." - "Δεν είναι δυνατή η αποχώρηση από το δωμάτιο" - "Αναφέρετε αυτό το δωμάτιο στον διαχειριστή σας. Εάν τα μηνύματα είναι κρυπτογραφημένα, ο διαχειριστής σας δε θα μπορεί να τα διαβάσει." - "Περιγράψτε τον λόγο αναφοράς…" - "Αναφορά δωματίου" "Πρόκειται να μεταβείς στον λογαριασμό σου %1$s για να επαναφέρεις την ταυτότητά σου. Στη συνέχεια, θα επιστρέψεις στην εφαρμογή." "Δεν μπορείς να επιβεβαιώσεις; Πήγαινε στον λογαριασμό σου για να επαναφέρεις την ταυτότητά σου." "Ανάκληση επαλήθευσης και αποστολή" diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index 5caf236e22..d0053d470d 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -351,11 +351,6 @@ Kas sa oled kindel, et soovid jätkata?" "Eemalda kasutaja jututoast" "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?" "Eemaldame kasutajat %1$s…" - "Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid" - "Blokeeri kasutaja" - "Teata sellest jututoast oma teenusepakkujale." - "Kirjelda teatamise põhjust…" - "Keeldu ja blokeeri" "Meediafaili valimine ei õnnestunud. Palun proovi uuesti." "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele." "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti." @@ -367,11 +362,6 @@ Kas sa oled kindel, et soovid jätkata?" "%1$d esiletõstetud sõnumit" "Esiletõstetud sõnumid" - "Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda." - "Pole võimalik lahkuda jututoast" - "Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda." - "Kirjelda põhjust…" - "Teata jututoast" "Oma võrguidentiteedi lähtestamiseks suuname sind %1$s kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde." "Sa ei saa seda kinnitada? Ava oma kasutajakonto haldus ja lähtesta oma võrguidentiteet." "Unusta verifitseerimine ja saada ikkagi" diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index e3316e1f8f..293b3a49e2 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -351,11 +351,6 @@ Haluatko varmasti jatkaa?" "Poista huoneesta" "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?" "Poistetaan käyttäjää %1$s huoneesta…" - "Et tule näkemään viestejä tai kutsuja tältä käyttäjältä" - "Estä käyttäjä" - "Ilmoita tästä huoneesta palveluntarjoajallesi." - "Kerro syy ilmoittamiseen…" - "Hylkää ja estä" "Median valinta epäonnistui, yritä uudelleen." "Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia." "Median käsittely epäonnistui, yritä uudelleen." @@ -367,11 +362,6 @@ Haluatko varmasti jatkaa?" "%1$d kiinnitettyä viestiä" "Kiinnitetyt viestit" - "Ilmoituksesi lähetettiin onnistuneesti, mutta kohtasimme ongelman yrittäessämme poistua huoneesta. Yritä uudelleen." - "Huoneesta poistuminen epäonnistui" - "Ilmoita tästä huoneesta palvelimesi ylläpitäjälle. Jos viestit on salattu, ylläpitäjäsi ei voi lukea niitä." - "Kuvaile syytä…" - "Ilmoita huoneesta" "Olet siirtymässä %1$s -tilillesi nollaamaan identiteettisi. Tämän jälkeen sinut ohjataan takaisin sovellukseen." "Etkö voi vahvistaa? Siirry tilillesi ja nollaa identiteettisi." "Peruuta vahvistus ja lähetä" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 9ef6ad1e0a..27cac31aa0 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -351,11 +351,6 @@ Raison : %1$s." "Retirer le membre du salon" "Retirer le membre et interdire l’adhésion à l’avenir ?" "Enlever %1$s…" - "Vous ne verrez aucun messages ou invitation à un salon de la part de cet utilisateur" - "Bloquer l’utilisateur" - "Signalez ce salon à votre fournisseur de compte." - "Décrivez la raison du signalement…" - "Refuser et bloquer" "Échec de la sélection du média, veuillez réessayer." "Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications." "Échec du traitement des médias à télécharger, veuillez réessayer." @@ -367,11 +362,6 @@ Raison : %1$s." "%1$d messages épinglés" "Messages épinglés" - "Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer." - "Impossible de quitter le salon" - "Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire." - "Décrivez la raison…" - "Signaler le salon" "Vous êtes sur le point d’accéder à votre compte %1$s pour réinitialiser votre identité. Vous serez ensuite redirigé vers l’application." "Vous ne pouvez pas confirmer ? Accédez à votre compte pour réinitialiser votre identité." "Révoquer la verification et envoyer" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index 7545dcf87e..d304030e00 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -351,11 +351,6 @@ Biztos, hogy folytatja?" "Eltávolítás a szobából" "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?" "%1$s eltávolítása…" - "Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni." - "Felhasználó letiltása" - "A szoba jelentése a fiókszolgáltatójának." - "Írja le a jelentés okát…" - "Elutasítás és blokkolás" "Nem sikerült kiválasztani a médiát, próbálja újra." "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára." "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." @@ -367,11 +362,6 @@ Biztos, hogy folytatja?" "%1$d kitűzött üzenet" "Kitűzött üzenetek" - "A jelentése sikeresen el lett küldve, de hibát találtunk a szoba elhagyása során. Próbálja újra." - "Nem tudja elhagyni a szobát" - "A szoba jelentése a rendszergazdának. Ha az üzenetek titkosítva vannak, akkor a rendszergazda nem fogja tudni elolvasni őket." - "Írja le az okot…" - "Szoba jelentése" "Arra készül, hogy belépjen a(z) %1$s fiókjába, hogy visszaállítsa a személyazonosságát. Ezután vissza fog térni az alkalmazásba." "Nem tudja megerősíteni? Ugorjon a fiókjához, és állítsa vissza a személyazonosságát." "Ellenőrzés visszavonása és elküldés" diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index 2c6f2a5055..ce8d655b6a 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -351,11 +351,6 @@ Er du sikker på at du vil fortsette?" "Fjern fra rommet" "Fjerne medlem og utestenge fra å bli med i fremtiden?" "Fjerner %1$s…" - "Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren" - "Blokker bruker" - "Rapporter dette rommet til din kontoleverandør." - "Beskriv årsaken for å rapportere…" - "Avslå og blokker" "Kunne ikke velge medium, prøv igjen." "Teksting er kanskje ikke synlig for personer som bruker eldre apper." "Kunne ikke behandle medier for opplasting, vennligst prøv igjen." @@ -367,11 +362,6 @@ Er du sikker på at du vil fortsette?" "%1$d Festede meldinger" "Festede meldinger" - "Rapporten din ble sendt inn, men vi oppdaget et problem da vi prøvde å forlate rommet. Prøv igjen." - "Kan ikke forlate rommet" - "Rapporter dette rommet til administratoren din. Hvis meldingene er kryptert, vil administratoren ikke kunne lese dem." - "Beskriv årsaken…" - "Rapporter rom" "Du er i ferd med å gå til %1$s kontoen din for å tilbakestille identiteten din. Etterpå blir du tatt tilbake til appen." "Kan du ikke bekrefte? Gå til kontoen din for å tilbakestille identiteten din." "Trekk tilbake verifikasjon og send" diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index a92a6f82ff..e3e742072a 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -355,11 +355,6 @@ Czy na pewno chcesz kontynuować?" "Usuń z pokoju" "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" "Usuwanie %1$s…" - "Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika" - "Zablokuj użytkownika" - "Zgłoś pokój dostawcy swojego konta." - "Opisz powód zgłoszenia…" - "Odrzuć i zablokuj" "Nie udało się wybrać multimediów. Spróbuj ponownie." "Opis może być niedostępny dla osób korzystających ze starszej wersji aplikacji." "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie." @@ -372,11 +367,6 @@ Czy na pewno chcesz kontynuować?" "%1$d przypiętych wiadomości" "Przypięte wiadomości" - "Twoje zgłoszenie zostało wysłane pomyślnie, ale napotkaliśmy problem podczas opuszczania pokoju. Spróbuj ponownie." - "Nie można wyjść z pokoju" - "Zgłoś ten pokój swojemu administratorowi. Jeśli wiadomości są zaszyfrowane, administrator nie będzie mógł ich odczytać." - "Opisz powód…" - "Zgłoś pokój" "Zostaniesz przeniesiony na swoje konto %1$s, aby zresetować tożsamość. Wrócisz do aplikacji po zakończeniu." "Nie możesz potwierdzić? Przejdź do swojego konta i zresetuj swoją tożsamość." "Wycofaj weryfikację i wyślij" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index fac746f8b7..0ce75822d2 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -352,9 +352,6 @@ "Удалить участника из комнаты" "Удалить участника и запретить присоединяться в будущем?" "Удаление %1$s…" - "Заблокировать пользователя" - "Опишите причину жалобы…" - "Отклонить и заблокировать" "Не удалось выбрать носитель, попробуйте еще раз." "Подпись может быть не видна пользователям старых приложений." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." @@ -367,9 +364,6 @@ "%1$d закреплённых сообщений" "Закрепленные сообщения" - "Невозможно покинуть комнату" - "Сообщите об этой комнате своему администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать." - "Опишите причину…" "Вы собираетесь перейти в свою учетную запись %1$s, чтобы сбросить идентификацию. После этого вы вернетесь в приложение." "Не можете подтвердить? Перейдите в свою учетную запись, чтобы сбросить свою идентификацию." "Отозвать статус и отправить" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 027cea8488..7c4302db9a 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -355,11 +355,6 @@ Naozaj chcete pokračovať?" "Odstrániť z miestnosti" "Odstrániť člena a zakázať vstup v budúcnosti?" "Odstraňuje sa %1$s…" - "Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti" - "Zablokovať používateľa" - "Nahlásiť túto miestnosť poskytovateľovi účtu." - "Opíšte dôvod nahlásenia…" - "Odmietnuť a zablokovať" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." @@ -372,11 +367,6 @@ Naozaj chcete pokračovať?" "%1$d pripnutých správ" "Pripnuté správy" - "Vaša správa bola úspešne odoslaná, ale pri pokuse o opustenie miestnosti sme narazili na problém. Skúste to prosím znova." - "Nie je možné opustiť miestnosť" - "Nahláste túto miestnosť svojmu správcovi. Ak sú správy zašifrované, váš správca ich nebude môcť prečítať." - "Popíšte dôvod…" - "Nahlásiť miestnosť" "Chystáte sa prejsť na svoj %1$s účet, aby ste obnovili svoju identitu. Potom budete vrátení späť do aplikácie." "Neviete potvrdiť? Prejdite do svojho účtu a obnovte svoju identitu." "Odvolať overenie a odoslať" diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index b8ea986a02..0fda3671c3 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -351,11 +351,6 @@ Anledning:%1$s." "Ta bort från rummet" "Ta bort medlem och banna från att gå med i framtiden?" "Tar bort %1$s …" - "Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren" - "Blockera användare" - "Rapportera det här rummet till din kontoleverantör." - "Beskriv skälet för anmälan …" - "Avvisa och blockera" "Misslyckades att välja media, vänligen pröva igen." "Bildtexter kanske inte är synliga för personer som använder äldre appar." "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." @@ -367,11 +362,6 @@ Anledning:%1$s." "%1$d Fästa meddelanden" "Fästa meddelanden" - "Din anmälan skickades in framgångsrikt, men vi stötte på ett problem när vi försökte lämna rummet. Vänligen försök igen." - "Kunde inte lämna rummet" - "Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem." - "Beskriv anledningen …" - "Rapportera rum" "Du är på väg att gå till ditt %1$s-konto för att återställa din identitet. Därefter kommer du att tas tillbaka till appen." "Kan du inte bekräfta? Gå till ditt konto för att återställa din identitet." "Dra tillbaka verifieringen och skicka" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index e6902179d4..f8a233d183 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -355,11 +355,6 @@ "Вилучити з кімнати" "Вилучити учасника та заборонити приєднання в майбутньому?" "Вилучення %1$s…" - "Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача" - "Заблокувати користувача" - "Поскаржитися на цю кімнату постачальнику облікового запису." - "Опишіть причину скарги…" - "Відхилити та заблокувати" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Користувачі старих застосунків можуть не бачити підписи." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." @@ -372,11 +367,6 @@ "%1$d Закріплених повідомлення" "Закріплені повідомлення" - "Ваша скарга надіслана, але ми зіткнулися з проблемою під час спроби вийти з кімнати. Повторіть спробу." - "Не вдалося вийти з кімнати" - "Поскаржтеся на цю кімнату своєму адміністратору. Якщо повідомлення зашифровані, ваш адміністратор не зможе їх прочитати." - "Опишіть причину…" - "Поскаржитися на кімнату" "Ви збираєтеся перейти до свого облікового запису %1$s, щоб скинути свій обліковий запис. Після цього ви повернетесь до програми." "Не можете підтвердити? Перейдіть до свого облікового запису, щоб скинути облікові дані." "Відкликати верифікацію та відправити" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index c589b4200d..ec0c7be7b6 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -345,10 +345,6 @@ "踢出聊天室" "移除成員並禁止未來再度加入?" "正在踢出 %1$s…" - "您將不會看到來自此使用者的任何訊息或聊天室邀請" - "封鎖使用者" - "向您的帳號提供者回報此聊天室。" - "拒絕並封鎖" "選取媒體失敗,請再試一次。" "使用舊應用程式的使用者可能看不到標題。" "無法處理要上傳的媒體,請再試一次。" @@ -359,10 +355,6 @@ "%1$d 則釘選的訊息" "釘選訊息" - "您的回報已成功遞交,但我們嘗試離開聊天室時遇到了問題。請再試一次。" - "無法離開聊天室" - "將此聊天室回報給您的管理員。若訊息已加密,您的管理員將無法讀取它們。" - "回報聊天室" "您將要前往您的 %1$s 帳號重設身份。然後您將會被帶回應用程式。" "無法確認?前往您的帳號以重設您的身份。" "撤回驗證並傳送" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 9fe4895181..1506b12e3e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -351,11 +351,6 @@ Are you sure you want to continue?" "Remove from room" "Remove member and ban from joining in the future?" "Removing %1$s…" - "You will not see any messages or room invites from this user" - "Block user" - "Report this room to your account provider." - "Describe the reason to report…" - "Decline and block" "Failed selecting media, please try again." "Captions might not be visible to people using older apps." "Failed processing media to upload, please try again." @@ -367,11 +362,6 @@ Are you sure you want to continue?" "%1$d Pinned messages" "Pinned messages" - "Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again." - "Unable to Leave Room" - "Report this room to your admin. If the messages are encrypted, your admin will not be able to read them." - "Describe the reason to report…" - "Report room" "You\'re about to go to your %1$s account to reset your identity. Afterwards you\'ll be taken back to the app." "Can\'t confirm? Go to your account to reset your identity." "Withdraw verification and send" diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png new file mode 100644 index 0000000000..934dc43d9d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db72f73fd89896d575cea546e3cf382c7be3736af6c3db74fc2301434939d99e +size 21960 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png new file mode 100644 index 0000000000..b23b2c98d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:828b5234e2bfb40965b7b17362402b8e77332d11f0338443866153d6a9ab5437 +size 25963 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png new file mode 100644 index 0000000000..8c50af672e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e7a67dfcec946de1e355afed430198c6c89f1938755907db1e83ab1bb68488 +size 12177 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png new file mode 100644 index 0000000000..e73f998cc0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96e2d9fd6b20b70dd3bb9b3caaea2cc3eaed1644c3371889cbc56d608bd4b49b +size 11826 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png new file mode 100644 index 0000000000..c299337e54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52ee0fa663944b2972f16675c337f52e9bbb3912aea1df86200965fae2cd9849 +size 20040 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png new file mode 100644 index 0000000000..64da9e64bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a51609d679d9ce799a98b7215e99cfe7553c1a154358f6f9ff7d5e0048e031a +size 24105 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png new file mode 100644 index 0000000000..f9d4df3145 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24adccc976a5611f31d0caaa614212da841a5a9abd5e9ebb706fe1f9cbfb6408 +size 10729 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png new file mode 100644 index 0000000000..cf81e64432 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f41edac4c092e5999950e97b0352ffa46c083ba2cd50c8b5b408eff8a85df06 +size 10376 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png new file mode 100644 index 0000000000..9b7e2b3ce9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d7f72a828564c9e798c7b207befbf585753951b04a1c32debc2e10b25bced6c +size 33179 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png new file mode 100644 index 0000000000..92a82fefb1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f8c6170e259cd1cc8579d750ca4dfb68d40e1d2994757c7e3d57a0b1931a593 +size 38072 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png new file mode 100644 index 0000000000..23f6835b3b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dce84fcbb387139cbcbd5907bafb4954b6b8d52209c45e251eeb715973232fa +size 33366 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png new file mode 100644 index 0000000000..d23ab2365e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4777b45b61243ce502af6fc69d9b262bede2362c9f556bbbfd66d2391d4980d9 +size 30172 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png new file mode 100644 index 0000000000..0a543ce441 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a1fe6b47d0651ffbd7587fc56f1be0fc8771dc31648050eb98d097198ae4d53 +size 34583 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png new file mode 100644 index 0000000000..386d1ee720 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e9c0bdaeabc62c5a66649bcd37f207dccca268ea8bd2d118dc151d6a2d55601 +size 31817 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png new file mode 100644 index 0000000000..ba9f9bdf0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c69cd79f91cbba2a72682d229fbbdaed566302067ce6c6910a613e15cc67767 +size 36455 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png new file mode 100644 index 0000000000..3350b2f936 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:056cedc5a79d692c1cd81eb163e22a809ea52053cef85b8a6ab08cfabc316474 +size 31965 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png new file mode 100644 index 0000000000..5cf3f2d357 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6f494a918082962ff121a809a54a5cd00911af35c85c0a2f4a49cc8475ba210 +size 28300 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png new file mode 100644 index 0000000000..14687f0531 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86cf5163cdf20bde56e6a0d858d69ead4fc4275c38c595c0debbd743ea0e9770 +size 32107 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png deleted file mode 100644 index 001133641e..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:293b11b5ffbef9253c51922e84c707d4404eb6945d6f99d64ff9abf7c1f632e8 -size 22186 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png deleted file mode 100644 index 0af5940238..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e36bd9c0358f68a2b6a833677775b8db9b93c4d1851e9adf8221fb8b5a005cd4 -size 22767 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png deleted file mode 100644 index 454d34361b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:970efb76758b76905d04d159e123201bbcf34e25ca92ba49956f5ed4beb68d1e -size 37097 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png deleted file mode 100644 index bb2c035f90..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff -size 9962 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png deleted file mode 100644 index bb2c035f90..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff -size 9962 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png deleted file mode 100644 index 76355d9a9d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84adf676f352a072e79269d53b5b572aa3ea12978d0c6076bd442d83db6aeeb3 -size 20110 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png deleted file mode 100644 index 2978d98e7e..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9a7acf590703274148b0887d0bf71934ad9ba0bfce6c86c8675f2277c49a0bc -size 20851 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png deleted file mode 100644 index 7c7e5a736f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b07a822f818e4709be8afe2e3a79a8aa03b42bb67ec3206cc310b5386037ab3 -size 34758 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png deleted file mode 100644 index c35c13d07f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 -size 8596 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png deleted file mode 100644 index c35c13d07f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 -size 8596 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png index 49004b08fa..3ca3f74e6d 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9772de041aa3819dc1f322000fc49a0f2c48c5b978181406da9e64b6ac083f08 -size 32821 +oid sha256:a723573878b837d6774d6fb5bdd20d661468c991e1f34054b6551e5c969e5b11 +size 28385 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png index c88ef3b46a..243070e00f 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7bed5155e5d41abe69b8a24905a5aa2b4935f56c7b04040206dfdecd59951b0 -size 30817 +oid sha256:1288017d71303ce29628cae7837968b4ec12576fff096ea4c4f9b19bbaf5a2ec +size 26547 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png new file mode 100644 index 0000000000..c0c23f4b39 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93b26b97920577fb83bd01e31e2fe9b03de7eafd02edf3946fc59d7d5ae847ba +size 30476 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png new file mode 100644 index 0000000000..7b035db776 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1c42f96f448cde729b0a28ff068439b629b53e64f29a2f722cbba752c2864f2 +size 29951 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png new file mode 100644 index 0000000000..f564ba51ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3add6d1c206f509459fd64d717b6f1457863e333f2c0181982ac27f47f1b6b22 +size 30129 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png new file mode 100644 index 0000000000..ac89c1f290 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2f1c39e1e23e98d0f80ea492d68993c1d4ff7cabfa5ebcd96414742f2844752 +size 27434 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png new file mode 100644 index 0000000000..1c10a34c53 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74403b531a8c2f563e6cf08f0de06392b6c608add907d4d7fee03d01bccb1f7d +size 33236 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png new file mode 100644 index 0000000000..af97e0e7fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3eb1d959794baf1a25ffc0cef77a496c627cdb32879984b5c7e18692f0b072b +size 29618 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png new file mode 100644 index 0000000000..282ea14c1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d7f1374c8b8e4ae2a045bcd7966378183de1a2b5830cd7cb82ef11bc44a6d1f +size 29175 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png new file mode 100644 index 0000000000..6349b50c22 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1a5b992070e12ecda9f9d26c8460898a1e2b19bab604d34cd45042b561fbaf +size 29171 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png new file mode 100644 index 0000000000..f86157cd28 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbcd3623d21cd587912cd2444ef0f04ff6fd35bec811f4bc794654f7809584eb +size 25681 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png new file mode 100644 index 0000000000..ce98d696ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1230423b1bed62d800d545855b59d48df3083741d821c5caf1e1fc221b561fc3 +size 30788 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png index 9b16567953..4c47f3d623 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6b667d57c0cf7147a8b713535738487f88ace48d7deb9ba338f7e26eea0b643 -size 41233 +oid sha256:11af1897c0ebc05108efb6d79156af7a549788141afabe60e6b3a721e73462b8 +size 41450 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png index 7159edfbf0..5d0e028c03 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96bda111da271692bb83a6049a65ab62ee706282806026882757b63c12a5629f -size 40143 +oid sha256:44575c8ffe72df361eab4e6e7775b150cf901b309e7be59d0ceeddc27f9800b1 +size 40360 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png index 772b2f04a5..6d90061fd0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcb0a066e02486d81040d7a1089ae2e4baf20e483e661fb513579494769f1461 -size 43381 +oid sha256:022987a206a0277729d65129a6efad6dfb0cdc8f06dc6e811f90307cba9f78be +size 43596 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png index ffa68efd51..a2551c3c09 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d3ea582081948929abb7229b5d2e7fd87c631963340a506072bcc7be4843e49 -size 41493 +oid sha256:dbf72b0000c25d5a8bc46e187b410b5e1d4dcdf49672b7e238221fd22af014ce +size 41705 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png index fd91a368d0..1f0daa85dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92e08b157e0b3091e136160cdb399926cf96ba48bf1de40ea84e57c5f9f2f0f7 -size 41804 +oid sha256:e610606229c8843fecfc699d798e530277ca7fbfb870198809a95417053fd594 +size 42018 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png index 943d018b45..d57c2018ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59c09f612209f3550e3b3a7488347b139422c821a3c47723e44d18b8d2d59593 -size 32366 +oid sha256:efe7ab9fb0d5a0d8e14039ae73bb947558ae8c9ac0be1b146d4fba78c7f96f84 +size 34280 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png index 506a73e964..7a25cb4dce 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa41d32745e495b9b9bb18ee9aa089674ddb5263da29feeb7362fea4e17f8617 -size 34524 +oid sha256:1cbbcbeb51688c957181c186319c8ae3843368ea5e684f407bec67ec16f90371 +size 36219 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png index 2c8e4e9e81..a80e7db43c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a334db0766538dbfbfda746b84af6e81b0cc3beee927c45d0e72368d25167ade -size 41722 +oid sha256:7f7ff91e97c256c96e261048d2f5c1fe50e7c1b6b752ec830ad63fe51cac44f5 +size 41939 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png index fd822b4361..6941fb2722 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbd0ba4fc6221bf4048bc355fbb7ec2bd4059c8baa8596ebec5f83388e1f8e8a -size 40592 +oid sha256:ed84b2185eeecdcaf5d846b386f8d3b37f6ae6892d099950d8ec2c988aaf7ae8 +size 41111 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png index 1dfa6994cc..0746cb3861 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47431534c2a84619c74fae1b2c4acda8c7d3310ca31e002ad55b89b5c54ca7ec -size 42567 +oid sha256:bf0c4686f5c3b8dc33a65eda243bab68a01fce9e5bb747957a421028f67c2f51 +size 42783 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png index e563f552b8..440eb609ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47f8d0b06c9aaea4ef5c45bb38614c174870ab0c2f22dc03e54c24f1f5ba2952 -size 41507 +oid sha256:b1c43dcb8fa94250922a5c55da1480295c843881f5f325583061c3a4efcc1c8d +size 41721 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png index cb57d31474..33d3574fd3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d50b32ab9b3fd6d7145a04676fb491c8ce31af655c54b9a369c3a946036ae12 -size 41552 +oid sha256:a19942e696011410011085fdb05573d5a9924c26a7cd5eb14d8ef47357520228 +size 41771 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png index 92299d3e4d..dd60c1ceac 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e86fbb58436a583df74d60e410e625865036d908d792eb9fb1fd506c875c3067 -size 41995 +oid sha256:2989b72b62c6454e2770c24f488be188db70743fc98ec382f96f67a506fda909 +size 42207 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png index bfb8b3ff0b..a6b369d6d3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64cd6474f7fdca9dec9d4a0c9c82089e1445c49835afaf1d33469f325174098c -size 41000 +oid sha256:941447581bdae57977bc6021e586618765756b88fdfc0d5ce5f52aaefa7f6e71 +size 41209 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png index b767349cd8..63bf4ceab4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:202cd1054d5177cc8e7fa0ce00c3338cbed0c7a1eda693589bd3d537bfa6a044 -size 43838 +oid sha256:42795ec59657adcab57a18c989be8a84434561448c5b242e655ea2568c3f536a +size 44046 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png index 5d1f0c5cc5..a348a4ffb7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0369d8fda8eea52848f520751a295fccaf9d7bf9d9e65161f5e1d10df03347d0 -size 42321 +oid sha256:713573ab48dc25c11ae0e5bfbe1e9909af6a1ba1115ffcfc37394171669c73f3 +size 42535 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png index ace46aa6ee..184d471cc7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:433e2d955156019aa6ef906418fcb23ed26f7c357ca91b526f941a8fdd9c7161 -size 42664 +oid sha256:c1e43739e24423815b90dc6e5249400fd1049985811763422e9fc2aa9a74e610 +size 42875 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png index 84360a0028..4d9769f32d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43c94f11c9275eca4386cb5771d62808e7de53d4fe8f0923f73474bd97e933c8 -size 33100 +oid sha256:b1835abce4bbf94590dd7e53387987bfee770bca173639c42b4b2b2587719198 +size 35113 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png index 05eabd23a4..d703fd166b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe5aade89a3afa13f2d9e0eea8a94c133e1e50f91a35d28d42a92c4e4d3baaf0 -size 35285 +oid sha256:1d1f4f05037d4ad192067d189dc9fddc860d55dceed95a98ae70b57ef3b74a48 +size 37097 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png index af030ef6ef..483f10f66f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eef5ef526837a00762336f1df6d900500b7956dc70d95b4619f80c4f158b0ee -size 42459 +oid sha256:3b63477c6dcd324babb989a5bfdccdec99dde345a134e267c13127869748accc +size 42670 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png index ad155142c2..d661dc86fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f038763501f54386b7751684d0aa1402320e9d913bb231cb422da06a02fce300 -size 41427 +oid sha256:596e2613127ccb7ae3f2362727082a473ba617e8ad05a61fece2ada05b0b8c14 +size 42006 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png index 0c8f52659d..bd8f4f8830 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc543c855b813e48a2e0140555ab9d34ca3c30aa5f9afdc56bafdaae4eb2b476 -size 43529 +oid sha256:aceffc8c16afc95f26a0aabb861aaba630ea0a9365ca1da3ad7a483ab6451282 +size 43740 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png index cb41bda82b..c26265b201 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1370d804e4f71194ce4a07f449fbe1c21bb6e3f4de7d42f92ff17b653d1f554 -size 42409 +oid sha256:011c18ce9dd7bc99bc9f09ce4f296aafc619d83ddc4f94f1465b49925591705a +size 42625 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png index 8418b73ae1..0b06aa53b8 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cab876c15147e3745112d63154d8097b685cc5d4abb76c2b6600f9334bd449f -size 42395 +oid sha256:b3971573f164e80b6e59bca91e32fdb1b5336086561e69148c22b1e7bb396935 +size 42601 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Day_0_en.png new file mode 100644 index 0000000000..33e4177147 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da5423fd025457b1465b9c3073a8ffa29e24f7eec379bbdb43473b2ab4e57257 +size 25968 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Night_0_en.png new file mode 100644 index 0000000000..3e218b0b78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListDeclineInviteMenuContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5830c192e065e7e59c7995d1079fd2abd08d3aa5cc41fea41505daf6c64e17a8 +size 25245 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png index f329086404..41b808d546 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e12a4d0611f4cd286afe1fecba2e8998995fbad463caaa05dce9475b298d040e -size 18973 +oid sha256:49e9723d3b41096d7d3ddd29601e1bf510d2a1f92e1ef0d5ada2e0d84ceb19bc +size 22189 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png index 9c33ba1ce0..939137422f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:902990e5b54b9e53dd1b8ebf8a3739e0772411c34027b0297c8e363e75dce138 -size 20772 +oid sha256:174037d1706a96148acba807bcb89b3a4e840a3ceb5f6c56550e4f0296f42741 +size 22452 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png index c65254de68..c30a6637e6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6361b17cacbb8e41211a385cbb3e66b5bfa7ba8b5710b76b21eb679357cf8a08 -size 21029 +oid sha256:93a9294ae555735ef7fa238f277ec694d8cde7207a270c417714a514eed0e7ba +size 24028 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png index 4be348ef79..c2aa4ab638 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:749b47983c45b07ad9a1ce72ae31371d39c66435cb1168d3e1ea4c551f9516fa -size 18258 +oid sha256:6435bc1f5e506257f3a0e6cfd4fc5e500c143d7f396a44e5f74df63ca37e230d +size 21459 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png index c51578a597..c42cf1e345 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b922fea887898cb032ee881bb397b0897a8e5618eedfc5263bb8c6df5abf290 -size 19975 +oid sha256:40269776883b9c524236936dcc2bb4110a96d381efb68c46a32ff7739220996d +size 21738 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png index 29e86e956e..800611b721 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cea937451ef247ba0e2bcd4bde84bff258f620682df315a6f1c2c4bb975ccd3a -size 20175 +oid sha256:f7638ef3bdead48705bac34df1450614d696a9e7ccbfdb70ed68b10bb0fbabf3 +size 23231 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png index a4e5a18c5c..20806bc42c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e5a824bd3d89bb853630704d5033404a093c830c3230a918ecbb13ca8c3bc8c -size 62048 +oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367 +size 60691 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png index e7a86d5b09..1b34b511fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1ded2f89fe73b0f695b097b47a2461dbaaa76ad15f4d359a8c2f47173913af7 -size 61795 +oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41 +size 60495 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png index 43b441d3f8..13db73a34e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a0eb6c87e9d565c7a21d6a9ec136274f5e75dfc467010935a8a5cc63933c672 -size 60038 +oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2 +size 58734 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png index c685c4fc18..611da1630c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:874fab965a108478a8049c93d3ade9d7eac5d2fff44adc91f06eebffd99d7e7e -size 70380 +oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3 +size 69463 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png index f3274bfcaa..642eca994d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:182892a050673f4cdc4be1562d2b05ace0c9d6fe6f4598942e218879b667d9dd -size 70137 +oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426 +size 69240 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png index c05ce5235e..0604b0bc73 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de8dd98f70665a8e556e4403df748e52518091bf08b9b18e5f65b2d7926f3d05 -size 68343 +oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348 +size 67496 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 6fbe996585..e5d35def0c 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -57,7 +57,8 @@ "name" : ":features:invite:impl", "includeRegex" : [ "screen_invites_.*", - "screen\\.join_room\\.decline_and_block_.*" + "screen\\.join_room\\.decline_and_block_.*", + "screen\\.decline_and_block\\..*" ] }, { @@ -318,6 +319,12 @@ "screen\\.room\\.single_knock_request.*", "screen\\.room\\.multiple_knock_requests.*" ] + }, + { + "name" : ":features:reportroom:impl", + "includeRegex" : [ + "screen\\.report_room\\..*" + ] } ] }