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:
ismailgulek
2022-06-21 20:28:42 +03:00
committed by GitHub
parent ffd3ffa369
commit 7dcd494de8
32 changed files with 347 additions and 51 deletions
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "encryption_normal.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.09949 11.4627C6.06108 11.4727 6.02775 11.481 6 11.4876C5.97225 11.481 5.93892 11.4727 5.90051 11.4627C5.76801 11.4282 5.57572 11.3731 5.34366 11.2926C4.87791 11.1309 4.2608 10.8694 3.64749 10.4708C2.42721 9.67759 1.25 8.3665 1.25 6.2025V1.91207L6 0.521L10.75 1.91207V6.2025C10.75 8.3665 9.57279 9.67759 8.35251 10.4708C7.7392 10.8694 7.12209 11.1309 6.65634 11.2926C6.42428 11.3731 6.23199 11.4282 6.09949 11.4627Z" fill="#17191C" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "encryption_trusted.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.09949 11.4627C6.06108 11.4727 6.02775 11.481 6 11.4876C5.97225 11.481 5.93892 11.4727 5.90051 11.4627C5.76801 11.4282 5.57572 11.3731 5.34366 11.2926C4.87791 11.1309 4.2608 10.8694 3.64749 10.4708C2.42721 9.67759 1.25 8.3665 1.25 6.2025V1.91207L6 0.521L10.75 1.91207V6.2025C10.75 8.3665 9.57279 9.67759 8.35251 10.4708C7.7392 10.8694 7.12209 11.1309 6.65634 11.2926C6.42428 11.3731 6.23199 11.4282 6.09949 11.4627Z" fill="#0DBD8B" stroke="white"/>
<path d="M8.70785 3.4047C8.57285 3.2622 8.34785 3.2547 8.20535 3.3897L5.00285 6.3897L3.82535 5.5647C3.66785 5.4597 3.45035 5.4597 3.30035 5.5947C3.12035 5.7447 3.10535 6.0147 3.25535 6.1947L4.61285 7.7547C4.63535 7.7772 4.65785 7.8072 4.68785 7.8222C4.94285 8.0322 5.32535 7.9947 5.53535 7.7397L5.55785 7.7097L8.72285 3.8772C8.82785 3.7422 8.82785 3.5397 8.70785 3.4047Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 950 B

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "encryption_warning.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.09949 11.4627C6.06108 11.4727 6.02775 11.481 6 11.4876C5.97225 11.481 5.93892 11.4727 5.90051 11.4627C5.76801 11.4282 5.57572 11.3731 5.34366 11.2926C4.87791 11.1309 4.2608 10.8694 3.64749 10.4708C2.42721 9.67759 1.25 8.3665 1.25 6.2025V1.91207L6 0.521L10.75 1.91207V6.2025C10.75 8.3665 9.57279 9.67759 8.35251 10.4708C7.7392 10.8694 7.12209 11.1309 6.65634 11.2926C6.42428 11.3731 6.23199 11.4282 6.09949 11.4627Z" fill="#FF4B55" stroke="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.42634 4.03626C5.39822 3.71563 5.63447 3.43438 5.95509 3.41188C6.27009 3.38938 6.55134 3.62563 6.58509 3.94626V4.03626L6.40509 6.28626C6.38822 6.49438 6.21384 6.65188 6.00572 6.65188H5.97197C5.77509 6.63501 5.62322 6.48313 5.60634 6.28626L5.42634 4.03626ZM6.49439 7.75379C6.49439 8.02717 6.27278 8.24879 5.99939 8.24879C5.72601 8.24879 5.50439 8.02717 5.50439 7.75379C5.50439 7.48041 5.72601 7.25879 5.99939 7.25879C6.27278 7.25879 6.49439 7.48041 6.49439 7.75379Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+5 -3
View File
@@ -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))
+3
View File
@@ -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")
@@ -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
@@ -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
}
}
+37 -11
View File
@@ -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)
}
}
}
+3 -2
View File
@@ -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)
}