Rename screens (#826)

* Rename: AnalyticsPrompt -> AnalyticsPromptScreen

* Rename: AnalyticsSettings -> AnalyticsSettingsScreen

* Rename: Settings -> SettingsScreen

* Rename: SessionVerification -> SessionVerificationScreen

* Rename: BugReport -> BugReportScreen

* Rename: FilePreview -> FilePreviewScreen

* Rename: AnalyticsPrompt -> AnalyticsPromptScreen

* Rename: Invites -> InvitesScreen

* Rename: InviteUsers -> InviteUsersScreen

* Rename: MediaPicker -> MediaPickerScreen

* Delete unused UIActivityViewControllerWrapper

* Rename: ReportContent -> ReportContentScreen

* Rename: RoomDetails -> RoomDetailsScreen

* Rename: RoomMemberDetails -> RoomMemberDetailsScreen

* Rename: RoomMemberList -> RoomMemberListScreen

* Rename: Cleanup

* Renaming missed items

* Low effort attempt at fixing the flakey unit tests
This commit is contained in:
Stefan Ceriu
2023-04-24 18:48:07 +03:00
committed by GitHub
parent 9911ad2b9b
commit 24572c6582
88 changed files with 492 additions and 520 deletions
@@ -0,0 +1,73 @@
//
// 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 SwiftUI
struct RoomDetailsScreenCoordinatorParameters {
let navigationStackCoordinator: NavigationStackCoordinator
let roomProxy: RoomProxyProtocol
let mediaProvider: MediaProviderProtocol
}
enum RoomDetailsScreenCoordinatorAction {
case cancel
case leftRoom
}
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomDetailsScreenCoordinatorParameters
private var viewModel: RoomDetailsScreenViewModelProtocol
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
var callback: ((RoomDetailsScreenCoordinatorAction) -> Void)?
init(parameters: RoomDetailsScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider)
}
// MARK: - Public
func start() {
viewModel.callback = { [weak self] action in
guard let self else { return }
switch action {
case .requestMemberDetailsPresentation(let members):
self.presentRoomMembersList(members)
case .cancel:
self.callback?(.cancel)
case .leftRoom:
self.callback?(.leftRoom)
}
}
}
func toPresentable() -> AnyView {
AnyView(RoomDetailsScreen(context: viewModel.context))
}
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
mediaProvider: parameters.mediaProvider,
members: members)
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
navigationStackCoordinator.push(coordinator)
}
}
@@ -0,0 +1,138 @@
//
// 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 Foundation
import UIKit
// MARK: - Coordinator
// MARK: View model
enum RoomDetailsScreenViewModelAction {
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
case leftRoom
case cancel
}
// MARK: View
struct RoomDetailsScreenViewState: BindableState {
let roomId: String
let canonicalAlias: String?
let isEncrypted: Bool
let isDirect: Bool
var title = ""
var topic: String?
var avatarURL: URL?
let permalink: URL?
var members: [RoomMemberDetails] = []
var isProcessingIgnoreRequest = false
var isLoadingMembers: Bool {
members.isEmpty
}
var bindings: RoomDetailsScreenViewStateBindings
var dmRecipient: RoomMemberDetails?
private var isDMRoom: Bool {
isEncrypted && isDirect && members.count == 2
}
}
struct RoomDetailsScreenViewStateBindings {
struct IgnoreUserAlertItem: AlertItem, Equatable {
enum Action {
case ignore
case unignore
}
let action: Action
let cancelTitle = L10n.actionCancel
var title: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockUser
case .unignore: return L10n.screenDmDetailsUnblockUser
}
}
var confirmationTitle: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockAlertAction
case .unignore: return L10n.screenDmDetailsUnblockAlertAction
}
}
var description: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockAlertDescription
case .unignore: return L10n.screenDmDetailsUnblockAlertDescription
}
}
var viewAction: RoomDetailsScreenViewAction {
switch action {
case .ignore: return .ignoreConfirmed
case .unignore: return .unignoreConfirmed
}
}
}
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomDetailsScreenErrorType>?
var leaveRoomAlertItem: LeaveRoomAlertItem?
var ignoreUserRoomAlertItem: IgnoreUserAlertItem?
}
struct LeaveRoomAlertItem: AlertItem {
enum RoomState {
case empty
case `public`
case `private`
}
let state: RoomState
let title = L10n.actionLeaveRoom
let confirmationTitle = L10n.actionLeave
let cancelTitle = L10n.actionCancel
var subtitle: String {
switch state {
case .empty: return L10n.leaveRoomAlertEmptySubtitle
case .private: return L10n.leaveRoomAlertPrivateSubtitle
case .public: return L10n.leaveRoomAlertSubtitle
}
}
}
enum RoomDetailsScreenViewAction {
case processTapPeople
case processTapLeave
case processTapIgnore
case processTapUnignore
case confirmLeave
case ignoreConfirmed
case unignoreConfirmed
}
enum RoomDetailsScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
/// Leaving room has failed..
case unknown
}
@@ -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 Combine
import SwiftUI
typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction>
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = [] {
didSet {
state.members = members.map { RoomMemberDetails(withProxy: $0) }
if roomProxy.isDirect, roomProxy.isEncrypted, members.count == 2 {
dmRecipient = members.first(where: { !$0.isAccountOwner })
} else {
dmRecipient = nil
}
}
}
private var dmRecipient: RoomMemberProxyProtocol? {
didSet {
if let dmRecipient {
state.dmRecipient = RoomMemberDetails(withProxy: dmRecipient)
} else {
state.dmRecipient = nil
}
}
}
var callback: ((RoomDetailsScreenViewModelAction) -> Void)?
init(roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol) {
self.roomProxy = roomProxy
super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias,
isEncrypted: roomProxy.isEncrypted,
isDirect: roomProxy.isDirect,
title: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room",
topic: roomProxy.topic,
avatarURL: roomProxy.avatarURL,
permalink: roomProxy.permalink,
bindings: .init()),
imageProvider: mediaProvider)
roomProxy.membersPublisher.sink { [weak self] members in
self?.members = members
}
.store(in: &cancellables)
Task {
await roomProxy.updateMembers()
}
}
// MARK: - Public
override func process(viewAction: RoomDetailsScreenViewAction) {
switch viewAction {
case .processTapPeople:
callback?(.requestMemberDetailsPresentation(members))
case .processTapLeave:
guard members.count > 1 else {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: .empty)
return
}
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: roomProxy.isPublic ? .public : .private)
case .confirmLeave:
Task { await leaveRoom() }
case .processTapIgnore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore)
case .processTapUnignore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore)
case .ignoreConfirmed:
Task { await ignore() }
case .unignoreConfirmed:
Task { await unignore() }
}
}
// MARK: - Private
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func leaveRoom() async {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLoading, persistent: true))
let result = await roomProxy.leaveRoom()
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
switch result {
case .failure:
state.bindings.alertInfo = AlertInfo(id: .unknown)
case .success:
callback?(.leftRoom)
}
}
private func ignore() async {
state.isProcessingIgnoreRequest = true
let result = await dmRecipient?.ignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.dmRecipient?.isIgnored = true
case .failure, .none:
state.bindings.alertInfo = .init(id: .unknown)
}
}
private func unignore() async {
state.isProcessingIgnoreRequest = true
let result = await dmRecipient?.unignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.dmRecipient?.isIgnored = false
case .failure, .none:
state.bindings.alertInfo = .init(id: .unknown)
}
}
}
@@ -0,0 +1,23 @@
//
// 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 Foundation
@MainActor
protocol RoomDetailsScreenViewModelProtocol {
var callback: ((RoomDetailsScreenViewModelAction) -> Void)? { get set }
var context: RoomDetailsScreenViewModelType.Context { get }
}
@@ -0,0 +1,258 @@
//
// 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 SwiftUI
struct RoomDetailsScreen: View {
@ObservedObject var context: RoomDetailsScreenViewModel.Context
var body: some View {
Form {
if let recipient = context.viewState.dmRecipient {
dmHeaderSection(recipient: recipient)
} else {
normalRoomHeaderSection
}
topicSection
if context.viewState.dmRecipient == nil {
aboutSection
}
securitySection
if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient)
} else {
leaveRoomSection
}
}
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
.alert(item: $context.alertInfo) { $0.alert }
.alert(item: $context.leaveRoomAlertItem,
actions: leaveRoomAlertActions,
message: leaveRoomAlertMessage)
.alert(item: $context.ignoreUserRoomAlertItem,
actions: blockUserAlertActions,
message: blockUserAlertMessage)
}
// MARK: - Private
@ViewBuilder
private var normalRoomHeaderSection: some View {
AvatarHeaderView(avatarUrl: context.viewState.avatarURL,
name: context.viewState.title,
id: context.viewState.roomId,
avatarSize: .room(on: .details),
imageProvider: context.imageProvider,
subtitle: context.viewState.canonicalAlias) {
if let permalink = context.viewState.permalink {
HStack(spacing: 32) {
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
}
.padding(.top, 32)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.avatar)
}
@ViewBuilder
private func dmHeaderSection(recipient: RoomMemberDetails) -> some View {
AvatarHeaderView(avatarUrl: recipient.avatarURL,
name: recipient.name,
id: recipient.id,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider,
subtitle: recipient.id) {
if let permalink = recipient.permalink {
HStack(spacing: 32) {
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
}
.padding(.top, 32)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.dmAvatar)
}
@ViewBuilder
private var topicSection: some View {
if let topic = context.viewState.topic {
Section {
Text(topic)
.foregroundColor(.element.secondaryContent)
.font(.element.footnote)
} header: {
Text(L10n.commonTopic)
.formSectionHeader()
}
.formSectionStyle()
}
}
private var aboutSection: some View {
Section {
Button {
context.send(viewAction: .processTapPeople)
} label: {
LabeledContent {
if context.viewState.isLoadingMembers {
ProgressView()
} else {
Text(String(context.viewState.members.count))
.foregroundColor(.element.tertiaryContent)
.font(.element.body)
}
} label: {
Label(L10n.commonPeople, systemImage: "person")
}
}
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
.disabled(context.viewState.isLoadingMembers)
}
.formSectionStyle()
}
@ViewBuilder
private var securitySection: some View {
if context.viewState.isEncrypted {
Section {
Label {
VStack(alignment: .leading, spacing: 2) {
Text(L10n.screenRoomDetailsEncryptionEnabledTitle)
Text(L10n.screenRoomDetailsEncryptionEnabledSubtitle)
.foregroundColor(.element.secondaryContent)
.font(.element.footnote)
}
} icon: {
Image(systemName: "lock.shield")
}
.labelStyle(FormRowLabelStyle(alignment: .top))
} header: {
Text(L10n.commonSecurity)
.formSectionHeader()
}
.formSectionStyle()
}
}
private var leaveRoomSection: some View {
Section {
Button(role: .destructive) {
context.send(viewAction: .processTapLeave)
} label: {
Label(L10n.actionLeaveRoom, systemImage: "door.right.hand.open")
}
.buttonStyle(FormButtonStyle(accessory: nil))
}
.formSectionStyle()
}
@ViewBuilder
private func ignoreUserSection(user: RoomMemberDetails) -> some View {
Section {
Button(role: user.isIgnored ? nil : .destructive) {
context.send(viewAction: user.isIgnored ? .processTapUnignore : .processTapIgnore)
} label: {
Label(user.isIgnored ? L10n.screenDmDetailsUnblockUser : L10n.screenDmDetailsBlockUser,
systemImage: "slash.circle")
}
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
.disabled(context.viewState.isProcessingIgnoreRequest)
}
.formSectionStyle()
}
@ViewBuilder
private func leaveRoomAlertActions(_ item: LeaveRoomAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
Button(item.confirmationTitle, role: .destructive) {
context.send(viewAction: .confirmLeave)
}
}
private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View {
Text(item.subtitle)
}
@ViewBuilder
private func blockUserAlertActions(_ item: RoomDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
Button(item.confirmationTitle,
role: item.action == .ignore ? .destructive : nil) {
context.send(viewAction: item.viewAction)
}
}
private func blockUserAlertMessage(_ item: RoomDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View {
Text(item.description)
}
}
// MARK: - Previews
struct RoomDetailsScreen_Previews: PreviewProvider {
static let genericRoomViewModel = {
let members: [RoomMemberProxyMock] = [
.mockAlice,
.mockBob,
.mockCharlie
]
let roomProxy = RoomProxyMock(with: .init(displayName: "Room A",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
isDirect: false,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members))
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
mediaProvider: MockMediaProvider())
}()
static let dmRoomViewModel = {
let members: [RoomMemberProxyMock] = [
.mockMe,
.mockDan
]
let roomProxy = RoomProxyMock(with: .init(displayName: "DM Room",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
isDirect: true,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members))
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
mediaProvider: MockMediaProvider())
}()
static var previews: some View {
RoomDetailsScreen(context: genericRoomViewModel.context)
.previewDisplayName("Generic Room")
RoomDetailsScreen(context: dmRoomViewModel.context)
.previewDisplayName("DM Room")
}
}