Implement Knock Logic (#3573)

* WIP RequestToJoin struct

* implemented the logic to display the cells

* knock request banner accept flow

* mark all knocks as seen implemented

* details logic

* implemented accept, decline and ban in the list

* added a loader and modified the stacked view

of the banner

* pr suggestions

* updated naming and loading strings

* added the initial loading state

improved code and the tests

* updated a string that has changed

* code improvement

* tests for the room screen view model

* room details tests

and improved the knock requests tests for the room screen

* knock requests list tests

* added error state alerts with retry

* struct has been renamed on the sdk

so I renamed it also on the app side

* update SDK
This commit is contained in:
Mauro
2024-12-16 15:32:45 +01:00
committed by GitHub
parent 016cdc687a
commit 45a630dd85
54 changed files with 1700 additions and 150 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ class HomeScreenRoomTests: XCTestCase {
hasOngoingCall: Bool) {
roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
id: "Test room",
joinRequestType: nil,
knockRequestType: nil,
name: "Test room",
isDirect: false,
avatarURL: nil,
@@ -18,6 +18,209 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
}
override func setUpWithError() throws {
viewModel = KnockRequestsListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()), mediaProvider: MediaProviderMock())
AppSettings.resetAllSettings()
}
func testLoadingState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests &&
state.isKnockableRoom &&
state.canAccept &&
!state.canBan &&
!state.canDecline &&
state.isLoading &&
!state.shouldDisplayEmptyView
}
try await deferred.fulfill()
}
func testEmptyState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests &&
state.isKnockableRoom &&
state.canAccept &&
!state.canBan &&
!state.canDecline &&
!state.isLoading &&
state.shouldDisplayEmptyView
}
try await deferred.fulfill()
}
func testLoadedState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
var deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests &&
state.isKnockableRoom &&
state.canAccept &&
state.canBan &&
state.canDecline &&
!state.isLoading &&
!state.shouldDisplayEmptyView &&
state.displayedRequests.count == 4 &&
state.handledEventIDs.isEmpty &&
state.shouldDisplayAcceptAllButton
}
try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests &&
state.handledEventIDs == ["1"] &&
!state.shouldDisplayEmptyView &&
state.displayedRequests.count == 3 &&
state.shouldDisplayAcceptAllButton
}
context.send(viewAction: .acceptRequest(eventID: "1"))
try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState) { state in
state.bindings.alertInfo?.id == .declineRequest
}
context.send(viewAction: .declineRequest(eventID: "2"))
try await deferred.fulfill()
guard let declineAlertInfo = context.alertInfo else {
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests &&
state.handledEventIDs == ["1", "2"] &&
!state.shouldDisplayEmptyView &&
state.displayedRequests.count == 2 &&
state.shouldDisplayAcceptAllButton
}
declineAlertInfo.primaryButton.action?()
try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState) { state in
state.bindings.alertInfo?.id == .declineAndBan
}
context.send(viewAction: .ban(eventID: "3"))
try await deferred.fulfill()
guard let banAlertInfo = context.alertInfo else {
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests &&
state.handledEventIDs == ["1", "2", "3"] &&
!state.shouldDisplayEmptyView &&
state.displayedRequests.count == 1 &&
!state.shouldDisplayAcceptAllButton
}
banAlertInfo.primaryButton.action?()
try await deferred.fulfill()
}
func testAcceptAll() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
var deferred = deferFulfillment(context.$viewState) { state in
state.shouldDisplayRequests &&
state.isKnockableRoom &&
state.canAccept &&
!state.canBan &&
!state.canDecline &&
!state.isLoading &&
!state.shouldDisplayEmptyView &&
state.displayedRequests.count == 4 &&
state.handledEventIDs.isEmpty &&
state.shouldDisplayAcceptAllButton
}
try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState) { state in
state.bindings.alertInfo?.id == .acceptAllRequests
}
context.send(viewAction: .acceptAllRequests)
try await deferred.fulfill()
guard let alertInfo = context.alertInfo else {
XCTFail("Can't be nil")
return
}
deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests &&
state.handledEventIDs == ["1", "2", "3", "4"] &&
!state.isLoading &&
state.shouldDisplayEmptyView
}
alertInfo.primaryButton.action?()
try await deferred.fulfill()
}
func testLoadedStateBecomesEmptyIfTheJoinRuleIsNotKnocking() async throws {
// If there is a sudden change in the rule, but the requests are still published, we want to hide all of them and show the empty view
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .invite))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests &&
state.shouldDisplayEmptyView &&
!state.isLoading &&
!state.isKnockableRoom
}
try await deferred.fulfill()
}
func testLoadedStateBecomesEmptyIfPermissionsAreRemoved() async throws {
// If there is a sudden change in permissions, and the user can't do any other action, we hide all the requests and shoe the empty view
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
canUserInvite: false,
joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
!state.shouldDisplayRequests &&
state.shouldDisplayEmptyView &&
!state.canAccept &&
!state.canBan &&
!state.canDecline &&
!state.isLoading
}
try await deferred.fulfill()
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ class LoggingTests: XCTestCase {
let heroName = "Pseudonym"
let roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
id: "myroomid",
joinRequestType: nil,
knockRequestType: nil,
name: roomName,
isDirect: true,
avatarURL: nil,
@@ -21,6 +21,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
override func setUp() {
AppSettings.resetAllSettings()
cancellables.removeAll()
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
@@ -33,8 +34,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
AppSettings.resetAllSettings()
}
func testLeaveRoomTappedWhenPublic() async throws {
@@ -672,4 +671,99 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
XCTFail("invalid state")
}
}
// MARK: - Knock Requests
func testKnockRequestsCounter() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
clientProxy: ClientProxyMock(.init()),
mediaProvider: MediaProviderMock(configuration: .init()),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.$viewState) { state in
state.knockRequestsCount == 2 && state.canSeeKnockingRequests
}
try await deferred.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockingRequests }
context.send(viewAction: .processTapRequestsToJoin)
try await deferredAction.fulfill()
}
func testKnockRequestsCounterIsLoading() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loading, joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
clientProxy: ClientProxyMock(.init()),
mediaProvider: MediaProviderMock(configuration: .init()),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.$viewState) { state in
state.knockRequestsCount == 0 && state.canSeeKnockingRequests
}
try await deferred.fulfill()
}
func testKnockRequestsCounterIsNotShownIfNoPermissions() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), canUserInvite: false, joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
clientProxy: ClientProxyMock(.init()),
mediaProvider: MediaProviderMock(configuration: .init()),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.$viewState) { state in
state.knockRequestsCount == 2 &&
state.dmRecipient == nil &&
!state.canSeeKnockingRequests &&
!state.canInviteUsers
}
try await deferred.fulfill()
}
func testKnockRequestsCounterIsNotShownIfDM() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isPublic: false, members: mockedMembers, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
clientProxy: ClientProxyMock(.init()),
mediaProvider: MediaProviderMock(configuration: .init()),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.$viewState) { state in
state.knockRequestsCount == 2 &&
!state.canSeeKnockingRequests &&
state.dmRecipient != nil &&
state.canInviteUsers
}
try await deferred.fulfill()
}
}
@@ -239,4 +239,116 @@ class RoomScreenViewModelTests: XCTestCase {
// Then the call button should remain visible shown.
XCTAssertTrue(viewModel.state.shouldShowCallButton)
}
// MARK: - Knock Requests
func testKnockRequestBanner() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
// This one should be filtered
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org", isSeen: true))]),
joinRule: .knock))
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.shouldSeeKnockRequests &&
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1")]
}
try await deferred.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockRequests }
viewModel.context.send(viewAction: .viewKnockRequests)
try await deferredAction.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.handledEventIDs == ["1"] &&
!state.shouldSeeKnockRequests
}
viewModel.context.send(viewAction: .acceptKnock(eventID: "1"))
try await deferred.fulfill()
}
func testKnockRequestBannerMarkAsSeen() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
// This one should be filtered
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org"))]),
joinRule: .knock))
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.shouldSeeKnockRequests &&
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1"),
.init(displayName: nil, avatarURL: nil, userID: "@bob:matrix.org", reason: nil, eventID: "2")]
}
try await deferred.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.handledEventIDs == ["1", "2"] &&
!state.shouldSeeKnockRequests
}
viewModel.context.send(viewAction: .dismissKnockRequests)
try await deferred.fulfill()
}
func testLoadingKnockRequests() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading,
joinRule: .knock))
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
// Loading state just does not appear at all
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.shouldSeeKnockRequests }
try await deferred.fulfill()
}
func testKnockRequestsBannerDoesNotAppearIfUserHasNoPermission() async throws {
ServiceLocator.shared.settings.knockingEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!"))]),
canUserInvite: false,
joinRule: .knock))
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1")] &&
!state.shouldSeeKnockRequests
}
try await deferred.fulfill()
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ class RoomSummaryTests: XCTestCase {
func makeSummary(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummary {
RoomSummary(roomListItem: .init(noPointer: .init()),
id: roomDetails.id,
joinRequestType: nil,
knockRequestType: nil,
name: roomDetails.name,
isDirect: isDirect,
avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil,