Room screen header (#86)
* #35 Create `ElementNavigationController` subclass * #35 Add encryption icons * #35 Add avatar and encryption badge image to the room screen view model * #35 Create `RoomHeaderView` class * #35 Replace room title with a RoomHeaderView instance in the toolbar * #35 Add changelog * #35 Introduce `UITestScreenIdentifier` and refactor ui tests * #35 Fix old tests * #35 add some tests for room screen * #35 Use svgs instead of pngs * #35 Fix PR remarks
This commit is contained in:
@@ -44,7 +44,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
}
|
||||
|
||||
splashViewController = SplashViewController()
|
||||
mainNavigationController = UINavigationController(rootViewController: splashViewController)
|
||||
mainNavigationController = ElementNavigationController(rootViewController: splashViewController)
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = mainNavigationController
|
||||
window.tintColor = .element.accent
|
||||
@@ -251,7 +251,9 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
memberDetailProvider: memberDetailProvider)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name)
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL),
|
||||
roomEncryptionBadge: roomProxy.encryptionBadgeImage)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
@@ -333,7 +335,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
let navController = UINavigationController(rootViewController: coordinator.toPresentable())
|
||||
let navController = ElementNavigationController(rootViewController: coordinator.toPresentable())
|
||||
navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(dismissBugReportScreen))
|
||||
|
||||
@@ -20,6 +20,9 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
|
||||
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||
internal enum Asset {
|
||||
internal enum Images {
|
||||
internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal")
|
||||
internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted")
|
||||
internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning")
|
||||
internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1")
|
||||
internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2")
|
||||
internal static let splashScreenPage3 = ImageAsset(name: "Images/Splash Screen Page 3")
|
||||
|
||||
18
ElementX/Sources/Other/ElementNavigationController.swift
Normal file
18
ElementX/Sources/Other/ElementNavigationController.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// ElementNavigationController.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 20.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ElementNavigationController: UINavigationController {
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
navigationBar.topItem?.backButtonDisplayMode = .minimal
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class NavigationRouterStore: NavigationRouterStoreProtocol {
|
||||
return existingNavigationRouter
|
||||
}
|
||||
|
||||
let navigationRouter = NavigationRouter(navigationController: UINavigationController())
|
||||
let navigationRouter = NavigationRouter(navigationController: ElementNavigationController())
|
||||
return navigationRouter
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import SwiftUI
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let roomName: String?
|
||||
let roomAvatar: UIImage?
|
||||
let roomEncryptionBadge: UIImage?
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
@@ -43,7 +45,9 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
roomName: parameters.roomName)
|
||||
roomName: parameters.roomName,
|
||||
roomAvatar: parameters.roomAvatar,
|
||||
roomEncryptionBadge: parameters.roomEncryptionBadge)
|
||||
|
||||
let view = RoomScreen(context: viewModel.context)
|
||||
roomScreenViewModel = viewModel
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum RoomScreenViewModelAction {
|
||||
|
||||
@@ -35,6 +36,8 @@ enum RoomScreenViewAction {
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomTitle: String = ""
|
||||
var roomAvatar: UIImage?
|
||||
var roomEncryptionBadge: UIImage?
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var isBackPaginating = false
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
|
||||
@@ -31,11 +31,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
init(timelineController: RoomTimelineControllerProtocol,
|
||||
timelineViewFactory: RoomTimelineViewFactoryProtocol,
|
||||
roomName: String?) {
|
||||
roomName: String?,
|
||||
roomAvatar: UIImage? = nil,
|
||||
roomEncryptionBadge: UIImage? = nil) {
|
||||
self.timelineController = timelineController
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥", bindings: RoomScreenViewStateBindings(composerText: "")))
|
||||
super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥",
|
||||
roomAvatar: roomAvatar,
|
||||
roomEncryptionBadge: roomEncryptionBadge,
|
||||
bindings: .init(composerText: "")))
|
||||
|
||||
timelineController.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// RoomHeaderView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 21.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
import Introspect
|
||||
|
||||
struct RoomHeaderView: View {
|
||||
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
roomAvatar
|
||||
Text(context.viewState.roomTitle)
|
||||
.font(.element.headline)
|
||||
.accessibilityIdentifier("roomNameLabel")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatar: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
roomAvatarImage
|
||||
.clipShape(Circle())
|
||||
|
||||
if let encryptionBadge = context.viewState.roomEncryptionBadge {
|
||||
Image(uiImage: encryptionBadge)
|
||||
.accessibilityIdentifier("encryptionBadgeIcon")
|
||||
}
|
||||
}
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatarImage: some View {
|
||||
if let avatar = context.viewState.roomAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(firstCharacter: String(context.viewState.roomTitle.first ?? Character("")))
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct RoomHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
bodyPlain.preferredColorScheme(.light)
|
||||
bodyPlain.preferredColorScheme(.dark)
|
||||
bodyEncrypted.preferredColorScheme(.light)
|
||||
bodyEncrypted.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var bodyPlain: some View {
|
||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
roomName: "Some Room name",
|
||||
roomAvatar: Asset.Images.appLogo.image
|
||||
)
|
||||
|
||||
RoomHeaderView(context: viewModel.context)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.padding()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var bodyEncrypted: some View {
|
||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
roomName: "Some Room name",
|
||||
roomAvatar: Asset.Images.appLogo.image,
|
||||
roomEncryptionBadge: Asset.Images.encryptionTrusted.image
|
||||
)
|
||||
|
||||
RoomHeaderView(context: viewModel.context)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,12 @@ struct RoomScreen: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(context.viewState.roomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
@@ -83,6 +83,15 @@ class RoomProxy: RoomProxyProtocol {
|
||||
var avatarURL: String? {
|
||||
room.avatarUrl()
|
||||
}
|
||||
|
||||
var encryptionBadgeImage: UIImage? {
|
||||
guard isEncrypted else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return trusted image for now, should be updated after verification status known
|
||||
return Asset.Images.encryptionTrusted.image
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String?, RoomProxyError> in
|
||||
|
||||
40
ElementX/Sources/UITestScreenIdentifier.swift
Normal file
40
ElementX/Sources/UITestScreenIdentifier.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ScreenIdentifier.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 21.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum UITestScreenIdentifier: String {
|
||||
case login
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
case settings
|
||||
case bugReport
|
||||
case bugReportWithScreenshot
|
||||
case splash
|
||||
case roomPlainNoAvatar
|
||||
case roomEncryptedWithAvatar
|
||||
}
|
||||
|
||||
extension UITestScreenIdentifier: CustomStringConvertible {
|
||||
var description: String {
|
||||
return rawValue.titlecased()
|
||||
}
|
||||
}
|
||||
|
||||
extension UITestScreenIdentifier: CaseIterable { }
|
||||
|
||||
private extension String {
|
||||
func titlecased() -> String {
|
||||
replacingOccurrences(of: "([A-Z])",
|
||||
with: " $1",
|
||||
options: .regularExpression,
|
||||
range: range(of: self))
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.capitalized
|
||||
}
|
||||
}
|
||||
@@ -40,19 +40,45 @@ class UITestsAppCoordinator: Coordinator {
|
||||
}
|
||||
|
||||
private func mockScreens() -> [MockScreen] {
|
||||
[
|
||||
MockScreen(id: "Login screen", coordinator: LoginScreenCoordinator(parameters: .init())),
|
||||
MockScreen(id: "Simple Screen - Regular", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .regular))),
|
||||
MockScreen(id: "Simple Screen - Upgrade", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .upgrade))),
|
||||
MockScreen(id: "Settings screen", coordinator: SettingsCoordinator(parameters: .init(navigationRouter: NavigationRouter(navigationController: UINavigationController()), bugReportService: MockBugReportService()))),
|
||||
MockScreen(id: "Bug report screen", coordinator: BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), screenshot: nil))),
|
||||
MockScreen(id: "Bug report screen with screenshot", coordinator: BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image))),
|
||||
MockScreen(id: "Splash Screen", coordinator: SplashScreenCoordinator())
|
||||
]
|
||||
UITestScreenIdentifier.allCases.map { MockScreen(id: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct MockScreen: Identifiable {
|
||||
let id: String
|
||||
let coordinator: Coordinator & Presentable
|
||||
let id: UITestScreenIdentifier
|
||||
var coordinator: Coordinator & Presentable {
|
||||
switch id {
|
||||
case .login:
|
||||
return LoginScreenCoordinator(parameters: .init())
|
||||
case .simpleRegular:
|
||||
return TemplateSimpleScreenCoordinator(parameters: .init(promptType: .regular))
|
||||
case .simpleUpgrade:
|
||||
return TemplateSimpleScreenCoordinator(parameters: .init(promptType: .upgrade))
|
||||
case .settings:
|
||||
let router = NavigationRouter(navigationController: ElementNavigationController())
|
||||
return SettingsCoordinator(parameters: .init(navigationRouter: router,
|
||||
bugReportService: MockBugReportService()))
|
||||
case .bugReport:
|
||||
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
|
||||
screenshot: nil))
|
||||
case .bugReportWithScreenshot:
|
||||
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
|
||||
screenshot: Asset.Images.appLogo.image))
|
||||
case .splash:
|
||||
return SplashScreenCoordinator()
|
||||
case .roomPlainNoAvatar:
|
||||
let params = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
roomName: "Some room name",
|
||||
roomAvatar: nil,
|
||||
roomEncryptionBadge: nil)
|
||||
return RoomScreenCoordinator(parameters: params)
|
||||
case .roomEncryptedWithAvatar:
|
||||
let params = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
roomName: "Some room name",
|
||||
roomAvatar: Asset.Images.appLogo.image,
|
||||
roomEncryptionBadge: Asset.Images.encryptionTrusted.image)
|
||||
return RoomScreenCoordinator(parameters: params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,15 @@ import SwiftUI
|
||||
struct UITestsRootView: View {
|
||||
|
||||
let mockScreens: [MockScreen]
|
||||
var selectionCallback: ((String) -> Void)?
|
||||
var selectionCallback: ((UITestScreenIdentifier) -> Void)?
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(mockScreens) { coordinator in
|
||||
Button(coordinator.id) {
|
||||
Button(coordinator.id.description) {
|
||||
selectionCallback?(coordinator.id)
|
||||
}
|
||||
.accessibilityIdentifier(coordinator.id.rawValue)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user