Room's details edit screen (#956)

* Add RoomDetailsEditScreen template

* Add navigation to the edit screen

* Delete template code

* Start RoomDetailsEditScreen UI

* Manage power levels in RoomDetailsScreen

* Start RoomDetailsEditScreenViewModel logic

* Inject initial room state

* Add cancel action

* Expose set name/topic APIs in RoomProxy

* Refine RoomDetailsEditScreen UI

* Add save logic

* Add localisations

* Fix avatar image

* Update localisations

* Add “Add topic” button

* Add feature flag

* Add dismiss on save logic

* Reduce throttling

* Improve form logic

* Fix UT build errors

* Add media sheet

* Add media preprocessing

* Add LoadableEditableAvatarImage

* Add condition on delete image

* Add avatar save button logic

* Add remove avatar logic

* Cleanup

* Fix edit bug in DM

* Add upload avatar

* Add focus

* Add RoomDetailsViewModel UTs

* Fix button style

* Add UTs

* Add empty topic ui test

* Fix iPad sheet presentation

* Revert topic appearance in room’s details

* Address PR comments

* Add UI tests
This commit is contained in:
Alfonso Grillo
2023-05-26 15:47:12 +02:00
committed by GitHub
parent 524997438f
commit f2b7faa183
47 changed files with 1281 additions and 54 deletions
@@ -65,9 +65,8 @@ class InviteUsersScreenViewModelTests: XCTestCase {
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
context.send(viewAction: .toggleUser(.mockAlice))
Task {
await Task.yield()
context.send(viewAction: .proceed)
Task.detached(priority: .low) {
await self.context.send(viewAction: .proceed)
}
let action = await viewModel.actions.values.first()
@@ -0,0 +1,135 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import MatrixRustSDK
import XCTest
@testable import ElementX
@MainActor
class RoomDetailsEditScreenViewModelTests: XCTestCase {
var viewModel: RoomDetailsEditScreenViewModel!
var userIndicatorController: MockUserIndicatorController!
var context: RoomDetailsEditScreenViewModelType.Context {
viewModel.context
}
func testCannotSaveOnLanding() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertFalse(context.viewState.canSave)
}
func testCanEdit() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertTrue(context.viewState.canEditAvatar)
XCTAssertTrue(context.viewState.canEditName)
XCTAssertTrue(context.viewState.canEditTopic)
}
func testCannotEdit() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: []),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertFalse(context.viewState.canEditAvatar)
XCTAssertFalse(context.viewState.canEditName)
XCTAssertFalse(context.viewState.canEditTopic)
}
func testNameDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
XCTAssertTrue(context.viewState.nameDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testTopicDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.topic = "topic"
XCTAssertTrue(context.viewState.topicDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testAvatarDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room", avatarURL: .picturesDirectory))
context.send(viewAction: .removeImage)
XCTAssertTrue(context.viewState.avatarDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testEmptyNameCannotBeSaved() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = ""
XCTAssertFalse(context.viewState.canSave)
}
func testSaveShowsSheet() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
XCTAssertFalse(context.showMediaSheet)
context.send(viewAction: .presentMediaSource)
XCTAssertTrue(context.showMediaSheet)
}
func testSaveTriggersViewModelAction() async {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
context.send(viewAction: .save)
let action = await viewModel.actions.values.first()
XCTAssertEqual(action, .saveFinished)
}
func testErrorShownOnFailedFetchOfMedia() async throws {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
viewModel.didSelectMediaUrl(url: .picturesDirectory)
let alertInfo = await userIndicatorController.alertInfoPublisher
.compactMap { $0 }
.values
.first()
XCTAssertNotNil(alertInfo)
}
// MARK: - Private
private func setupViewModel(accountOwner: RoomMemberProxyMock, roomProxyConfiguration: RoomProxyMockConfiguration) {
userIndicatorController = MockUserIndicatorController()
viewModel = .init(accountOwner: accountOwner,
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: roomProxyConfiguration),
userIndicatorController: userIndicatorController)
}
}
private extension ImageInfo {
static let mock: ImageInfo = .init(height: nil,
width: nil,
mimetype: nil,
size: nil,
thumbnailInfo: nil,
thumbnailSource: nil,
blurhash: nil)
}
@@ -14,6 +14,7 @@
// limitations under the License.
//
import MatrixRustSDK
import XCTest
@testable import ElementX
@@ -27,6 +28,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
override func setUp() {
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test"))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
AppSettings.reset()
ServiceLocator.shared.settings.editRoomDetailsFlowEnabled = true
}
func testLeaveRoomTappedWhenPublic() async {
@@ -219,4 +223,66 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
await context.nextViewState()
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
}
func testCanEditAvatar() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertTrue(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCanEditName() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertTrue(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCanEditTopic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertTrue(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCannotEditRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertFalse(context.viewState.canEdit)
}
func testCannotEditDirectRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEdit)
}
}