Some Post-demo Cleanups (#200)

* Remove redundant string

* Use placeholder avatar on home screen

* Add initial home screen ui test

* Fix settings screen PR remarks

* Remove UIKit alert from home screen sign out

* Remove UIKit alert from soft logout clear all data

* Add reference screenshots for home screen UI tests

* Formatting fixes

* Add clearing room method to client proxy

* Clear room proxies on screen dismiss

* Fix retain cycle in room view model

* Do not go into authentication state immediately

* Define sizes for user and room avatars on different screens

* Use defined avatar sizes everywhere

* Disable image disk caching

* Rename rounded corner shape

* Fix text color of placeholder avatars

* Fix PR reviews on formatted body text

* Fix merge conflict

* Remove shouldShowSenderDetails everywhere and just use it from inGroupState

* Remove redundant linter disablings

* Fix PR remarks

* Rename media provider size parameter
This commit is contained in:
ismailgulek 2022-09-23 12:21:41 +03:00 committed by GitHub
parent b9f8fb0b6f
commit bdc83dac27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 307 additions and 204 deletions

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@ -333,6 +333,8 @@
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; };
EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; };
ECA5A34628DC837E0024C8BE /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA5A34528DC837E0024C8BE /* AvatarSize.swift */; };
ECA5A34828DC959F0024C8BE /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA5A34728DC959F0024C8BE /* ImageCache.swift */; };
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; };
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
@ -346,7 +348,7 @@
F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
F764BE976EAB76D63E7C1678 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8023C7413A426FBF0A52B684 /* RoundedCorner.swift */; };
F764BE976EAB76D63E7C1678 /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */; };
F99FB21EFC6D99D247FE7CBE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D82E84F90358CC1118E6034B /* Introspect */; };
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */; };
@ -604,7 +606,7 @@
7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = "<group>"; };
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = "<group>"; };
7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = "<group>"; };
8023C7413A426FBF0A52B684 /* RoundedCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCorner.swift; sourceTree = "<group>"; };
8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = "<group>"; };
8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -629,7 +631,7 @@
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = "<group>"; };
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
@ -795,6 +797,8 @@
E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; };
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
ECA5A34528DC837E0024C8BE /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = "<group>"; };
ECA5A34728DC959F0024C8BE /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
@ -1009,7 +1013,7 @@
isa = PBXGroup;
children = (
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */,
8023C7413A426FBF0A52B684 /* RoundedCorner.swift */,
8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1097,6 +1101,7 @@
A40C19719687984FD9478FBE /* Task.swift */,
287FC98AF2664EAD79C0D902 /* UIDevice.swift */,
227AC5D71A4CE43512062243 /* URL.swift */,
ECA5A34728DC959F0024C8BE /* ImageCache.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1682,6 +1687,7 @@
6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */,
F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */,
BB3073CCD77D906B330BC1D6 /* Tests.swift */,
ECA5A34528DC837E0024C8BE /* AvatarSize.swift */,
44BBB96FAA2F0D53C507396B /* Extensions */,
8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */,
06501F0E978B2D5C92771DC7 /* Logging */,
@ -2419,6 +2425,7 @@
DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */,
BF35062D06888FA80BD139FF /* Presentable.swift in Sources */,
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */,
ECA5A34828DC959F0024C8BE /* ImageCache.swift in Sources */,
53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */,
00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */,
13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */,
@ -2449,7 +2456,7 @@
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */,
2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */,
F764BE976EAB76D63E7C1678 /* RoundedCorner.swift in Sources */,
F764BE976EAB76D63E7C1678 /* RoundedCornerShape.swift in Sources */,
462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */,
CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */,
1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */,
@ -2527,6 +2534,7 @@
15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */,
C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */,
80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */,
ECA5A34628DC837E0024C8BE /* AvatarSize.swift in Sources */,
9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */,
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */,
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,

View File

@ -21,8 +21,6 @@
"session_verification_screen_emojis_title" = "Lets check if these";
"session_verification_screen_emojis_message" = "Open Element on one of your other sessions to compare.";
"home_screen_all_chats" = "All Chats";
// MARK: - Authentication
"authentication_login_title" = "Welcome back!";

View File

@ -130,7 +130,7 @@ class AppCoordinator: Coordinator {
MXLog.configure(loggerConfiguration)
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
// swiftlint:disable:next cyclomatic_complexity
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self = self else { return }
@ -147,15 +147,14 @@ class AppCoordinator: Coordinator {
case (.restoringSession, .failedRestoringSession, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
self.startAuthentication()
case (.restoringSession, .succeededRestoringSession, .homeScreen):
self.hideLoadingIndicator()
self.presentHomeScreen()
case(_, _, .roomScreen(let roomId)):
self.presentRoomWithIdentifier(roomId)
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
self.tearDownDismissedRoomScreen()
case(.roomScreen(let roomId), .dismissedRoomScreen, .homeScreen):
self.tearDownDismissedRoomScreen(roomId)
case (_, .signOut, .signingOut):
self.showLoadingIndicator()
@ -239,13 +238,11 @@ class AppCoordinator: Coordinator {
self.remove(childCoordinator: coordinator)
self.stateMachine.processEvent(.succeededSigningIn)
case .clearAllData:
self.confirmClearAllData {
// clear user data
self.userSessionStore.logout(userSession: self.userSession)
self.userSession = nil
self.remove(childCoordinator: coordinator)
self.startAuthentication()
}
// clear user data
self.userSessionStore.logout(userSession: self.userSession)
self.userSession = nil
self.remove(childCoordinator: coordinator)
self.startAuthentication()
}
}
@ -310,7 +307,7 @@ class AppCoordinator: Coordinator {
case .verifySession:
self.stateMachine.processEvent(.showSessionVerificationScreen)
case .signOut:
self.confirmSignOut()
self.stateMachine.processEvent(.signOut)
}
}
@ -368,7 +365,7 @@ class AppCoordinator: Coordinator {
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
roomName: roomProxy.displayName ?? roomProxy.name,
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL, size: MediaProviderDefaultAvatarSize))
roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL, avatarSize: .room(on: .timeline)))
let coordinator = RoomScreenCoordinator(parameters: parameters)
add(childCoordinator: coordinator)
@ -378,7 +375,7 @@ class AppCoordinator: Coordinator {
}
}
private func tearDownDismissedRoomScreen() {
private func tearDownDismissedRoomScreen(_ roomId: String) {
guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
}
@ -438,32 +435,6 @@ class AppCoordinator: Coordinator {
navigationRouter.present(alert, animated: true)
}
private func confirmSignOut() {
let alert = UIAlertController(title: ElementL10n.actionSignOut,
message: ElementL10n.actionSignOutConfirmationSimple,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
self?.stateMachine.processEvent(.signOut)
})
navigationRouter.present(alert, animated: true)
}
/// Shows a confirmation to clear all data, and proceeds to do so if the user confirms.
private func confirmClearAllData(_ confirmed: @escaping () -> Void) {
let alert = UIAlertController(title: ElementL10n.softLogoutClearDataDialogTitle,
message: ElementL10n.softLogoutClearDataDialogContent,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { _ in
confirmed()
})
navigationRouter.present(alert, animated: true)
}
private func processScreenshotDetection(image: UIImage?, error: Error?) {
MXLog.debug("Detected screenshot: \(String(describing: image)), error: \(String(describing: error))")

View File

@ -22,8 +22,6 @@ extension ElementL10n {
public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description")
/// Choose your server to store your data
public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title")
/// All Chats
public static let homeScreenAllChats = ElementL10n.tr("Untranslated", "home_screen_all_chats")
/// Mobile
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
/// Tablet

View File

@ -0,0 +1,83 @@
//
// 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
enum AvatarSize {
case user(on: UserAvatarSizeOnScreen)
case room(on: RoomAvatarSizeOnScreen)
// custom
case custom(CGFloat)
/// Value in UIKit points
var value: CGFloat {
switch self {
case .user(let screen):
return screen.value
case .room(let screen):
return screen.value
case .custom(let val):
return val
}
}
/// Value in pixels by using the scale of the main screen
var scaledValue: CGFloat {
value * UIScreen.main.scale
}
}
enum UserAvatarSizeOnScreen {
case timeline
case home
case settings
var value: CGFloat {
switch self {
case .timeline:
return 32
case .home:
return 32
case .settings:
return 60
}
}
}
enum RoomAvatarSizeOnScreen {
case timeline
case home
var value: CGFloat {
switch self {
case .timeline:
return 32
case .home:
return 44
}
}
}
extension AvatarSize {
var size: CGSize {
CGSize(width: value, height: value)
}
var scaledSize: CGSize {
CGSize(width: scaledValue, height: scaledValue)
}
}

View File

@ -0,0 +1,26 @@
//
// 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 Kingfisher
extension ImageCache {
static var onlyInMemory: ImageCache {
let result = ImageCache.default
result.diskStorage.config.sizeLimit = 1
return result
}
}

View File

@ -16,7 +16,7 @@
import SwiftUI
struct RoundedCorner: Shape {
struct RoundedCornerShape: Shape {
let radius: CGFloat
let corners: UIRectCorner
@ -39,10 +39,10 @@ struct RoundedCorner: Shape {
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
clipShape(RoundedCornerShape(radius: radius, corners: corners))
}
func cornerRadius(_ radius: CGFloat, inGroupState: TimelineItemInGroupState) -> some View {
clipShape(RoundedCorner(radius: radius, inGroupState: inGroupState))
clipShape(RoundedCornerShape(radius: radius, inGroupState: inGroupState))
}
}

View File

@ -21,6 +21,8 @@ struct SoftLogoutScreen: View {
// MARK: Private
@State private var showingClearDataConfirmation = false
/// The focus state of the password text field.
@FocusState private var isPasswordFocused: Bool
@ -150,6 +152,14 @@ struct SoftLogoutScreen: View {
}
.buttonStyle(.elementAction(.xLarge, color: .element.alert))
.accessibilityIdentifier("clearDataButton")
.alert(ElementL10n.softLogoutClearDataDialogTitle,
isPresented: $showingClearDataConfirmation) {
Button(ElementL10n.actionSignOut,
role: .destructive,
action: clearData)
} message: {
Text(ElementL10n.softLogoutClearDataDialogContent)
}
}
}

View File

@ -39,6 +39,8 @@ enum HomeScreenViewAction {
}
struct HomeScreenViewState: BindableState {
var userID: String
var userDisplayName: String?
var userAvatar: UIImage?
var showSessionVerificationBanner = false

View File

@ -33,7 +33,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
roomSummaryProvider = userSession.clientProxy.roomSummaryProvider
self.attributedStringBuilder = attributedStringBuilder
super.init(initialViewState: HomeScreenViewState())
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID))
userSession.callbacks
.receive(on: DispatchQueue.main)
@ -61,13 +61,19 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
Task {
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, size: MediaProviderDefaultAvatarSize) {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, avatarSize: .user(on: .home)) {
state.userAvatar = avatar
}
}
await updateRooms()
}
Task {
if case let .success(userDisplayName) = await userSession.clientProxy.loadUserDisplayName() {
state.userDisplayName = userDisplayName
}
}
}
// MARK: - Public
@ -101,7 +107,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
if let avatarURLString = summary.avatarURLString {
Task {
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, size: MediaProviderDefaultAvatarSize) {
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
if let index = state.rooms.firstIndex(of: room) {
room.avatar = image
state.rooms[index] = room
@ -116,7 +122,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
var rooms = [HomeScreenRoom]()
for summary in self.roomSummaryProvider.roomSummaries {
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, size: MediaProviderDefaultAvatarSize)
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
var timestamp: String?
if let lastMessageTimestamp = summary.lastMessageTimestamp {

View File

@ -17,6 +17,7 @@
import SwiftUI
struct HomeScreen: View {
@State private var showingLogoutConfirmation = false
@ObservedObject var context: HomeScreenViewModel.Context
// MARK: Views
@ -50,7 +51,7 @@ struct HomeScreen: View {
.transition(.slide)
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
.ignoresSafeArea(.all, edges: .bottom)
.navigationTitle(ElementL10n.homeScreenAllChats)
.navigationTitle(ElementL10n.allChats)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
userMenuButton
@ -75,7 +76,9 @@ struct HomeScreen: View {
}
}
Section {
Button(role: .destructive, action: signOut) {
Button(role: .destructive) {
showingLogoutConfirmation = true
} label: {
Label(ElementL10n.actionSignOut, systemImage: "rectangle.portrait.and.arrow.right")
}
}
@ -84,23 +87,33 @@ struct HomeScreen: View {
.animation(.elementDefault, value: context.viewState.userAvatar)
.transition(.opacity)
}
.alert(ElementL10n.actionSignOut,
isPresented: $showingLogoutConfirmation) {
Button(ElementL10n.actionSignOut,
role: .destructive,
action: signOut)
} message: {
Text(ElementL10n.actionSignOutConfirmationSimple)
}
}
@ViewBuilder
private var userAvatarImageView: some View {
userAvatarImage
.resizable()
.scaledToFill()
.frame(width: 32, height: 32, alignment: .center)
.frame(width: AvatarSize.user(on: .home).value, height: AvatarSize.user(on: .home).value, alignment: .center)
.clipShape(Circle())
.accessibilityIdentifier("userAvatarImage")
}
private var userAvatarImage: Image {
@ViewBuilder
private var userAvatarImage: some View {
if let avatar = context.viewState.userAvatar {
return Image(uiImage: avatar)
Image(uiImage: avatar)
.resizable()
.scaledToFill()
} else {
return .empty
PlaceholderAvatarImage(text: context.viewState.userDisplayName ?? context.viewState.userID,
contentId: context.viewState.userID)
}
}

View File

@ -17,7 +17,7 @@
import SwiftUI
struct HomeScreenRoomCell: View {
@ScaledMetric private var avatarSize = 44.0
@ScaledMetric private var avatarSize = AvatarSize.room(on: .home).value
let room: HomeScreenRoom
let context: HomeScreenViewModel.Context

View File

@ -40,6 +40,7 @@ enum RoomScreenViewAction {
case sendMessage
case sendReaction(key: String, eventID: String)
case cancelReply
case viewDisappeared
}
struct RoomScreenViewState: BindableState {

View File

@ -89,6 +89,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
MXLog.warning("React with \(key) failed. Not implemented.")
case .cancelReply:
state.composerMode = .default
case .viewDisappeared:
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
state.contextMenuBuilder = nil
}
}

View File

@ -42,7 +42,7 @@ struct RoomHeaderView: View {
.accessibilityIdentifier("encryptionBadgeIcon")
}
}
.frame(width: 32.0, height: 32.0)
.frame(width: AvatarSize.room(on: .timeline).value, height: AvatarSize.room(on: .timeline).value)
}
@ViewBuilder private var roomAvatarImage: some View {

View File

@ -41,6 +41,9 @@ struct RoomScreen: View {
}
}
.alert(item: $context.alertInfo) { $0.alert }
.onDisappear {
context.send(viewAction: .viewDisappeared)
}
}
private func sendMessage() {

View File

@ -144,7 +144,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.background(Color.element.systemGray6) // Demo time!
.cornerRadius(12, inGroupState: timelineItem.inGroupState) // Demo time!
// .overlay(
// RoundedCorner(radius: 18, inGroupState: timelineItem.inGroupState)
// RoundedCornerShape(radius: 18, inGroupState: timelineItem.inGroupState)
// .stroke(Color.element.systemGray5)
// )
}

View File

@ -58,7 +58,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
EmoteRoomTimelineItem(id: UUID().uuidString,
text: text,
timestamp: timestamp,
shouldShowSenderDetails: true,
inGroupState: .single,
isOutgoing: false,
senderId: senderId)

View File

@ -26,25 +26,13 @@ struct FormattedBodyText: View {
VStack(alignment: .leading, spacing: 8.0) {
ForEach(attributedComponents, id: \.self) { component in
if component.isBlockquote {
if isOutgoing {
Text(component.attributedString)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.element.primaryContent)
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
.clipped()
.background(Color.element.systemGray4)
.cornerRadius(13)
} else {
Text(component.attributedString)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.element.primaryContent)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12))
.clipped()
.overlay(
RoundedRectangle(cornerRadius: 13)
.stroke(Color.element.systemGray5)
)
}
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.element.primaryContent)
.padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
.clipped()
.background(Color.element.systemGray4)
.cornerRadius(13)
} else {
Text(component.attributedString)
.fixedSize(horizontal: false, vertical: true)
@ -54,6 +42,12 @@ struct FormattedBodyText: View {
}
.tint(.element.accent)
}
private var blockquoteAttributes: AttributeContainer {
var container = AttributeContainer()
container.font = .element.caption1
return container
}
}
extension FormattedBodyText {

View File

@ -62,7 +62,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Some image",
timestamp: "Now",
shouldShowSenderDetails: false,
inGroupState: .single,
isOutgoing: false,
senderId: "Bob",
@ -72,7 +71,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Some other image",
timestamp: "Now",
shouldShowSenderDetails: false,
inGroupState: .single,
isOutgoing: false,
senderId: "Bob",
@ -82,7 +80,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Blurhashed image",
timestamp: "Now",
shouldShowSenderDetails: false,
inGroupState: .single,
isOutgoing: false,
senderId: "Bob",

View File

@ -59,7 +59,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
NoticeRoomTimelineItem(id: UUID().uuidString,
text: text,
timestamp: timestamp,
shouldShowSenderDetails: true,
inGroupState: .single,
isOutgoing: false,
senderId: senderId)

View File

@ -25,7 +25,7 @@ struct PlaceholderAvatarImage: View {
bgColor
Text(textForImage)
.padding(4)
.foregroundColor(.element.background)
.foregroundColor(.white)
.font(.title2.bold())
}
.aspectRatio(1, contentMode: .fill)

View File

@ -51,7 +51,6 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
RedactedRoomTimelineItem(id: UUID().uuidString,
text: text,
timestamp: timestamp,
shouldShowSenderDetails: true,
inGroupState: .single,
isOutgoing: false,
senderId: senderId)

View File

@ -43,26 +43,22 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
VStack(alignment: .leading, spacing: 20.0) {
TextRoomTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
timestamp: "Now",
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: "Bob"))
TextRoomTimelineView(timelineItem: itemWith(text: "Some other text",
timestamp: "Later",
shouldShowSenderDetails: true,
isOutgoing: true,
senderId: "Anne"))
TextRoomTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
timestamp: "Now",
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: "Bob"))
.timelineStyle(.plain)
TextRoomTimelineView(timelineItem: itemWith(text: "Some other text",
timestamp: "Later",
shouldShowSenderDetails: true,
isOutgoing: true,
senderId: "Anne"))
.timelineStyle(.plain)
@ -70,11 +66,10 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
.padding(.horizontal, 8)
}
private static func itemWith(text: String, timestamp: String, shouldShowSenderDetails: Bool, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString,
text: text,
timestamp: timestamp,
shouldShowSenderDetails: shouldShowSenderDetails,
inGroupState: .single,
isOutgoing: isOutgoing,
senderId: senderId)

View File

@ -20,7 +20,7 @@ import SwiftUI
struct TimelineSenderAvatarView: View {
let timelineItem: EventBasedTimelineItemProtocol
@ScaledMetric private var avatarSize = 32
@ScaledMetric private var avatarSize = AvatarSize.user(on: .timeline).value
var body: some View {
ZStack(alignment: .center) {

View File

@ -73,7 +73,7 @@ final class SettingsCoordinator: Coordinator, Presentable {
case .crash:
self.parameters.bugReportService.crash()
case .logout:
self.confirmSignOut()
self.callback?(.logout)
}
}
}
@ -118,19 +118,6 @@ final class SettingsCoordinator: Coordinator, Presentable {
}
}
private func confirmSignOut() {
let alert = UIAlertController(title: ElementL10n.actionSignOut,
message: ElementL10n.actionSignOutConfirmationSimple,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
self?.callback?(.logout)
})
navigationRouter.present(alert, animated: true)
}
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.

View File

@ -40,11 +40,13 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
Task {
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, size: MediaProviderDefaultAvatarSize) {
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURLString(userAvatarURLString, avatarSize: .user(on: .settings)) {
state.userAvatar = avatar
}
}
}
Task {
if case let .success(userDisplayName) = await self.userSession.clientProxy.loadUserDisplayName() {
state.userDisplayName = userDisplayName
}

View File

@ -19,10 +19,11 @@ import SwiftUI
struct SettingsScreen: View {
// MARK: Private
@State private var showingLogoutConfirmation = false
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var settings = ElementSettings.shared
@ScaledMetric private var avatarSize = 60.0
@ScaledMetric private var avatarSize = AvatarSize.user(on: .settings).value
@ScaledMetric private var menuIconSize = 30.0
private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
@ -169,7 +170,7 @@ struct SettingsScreen: View {
private var logoutSection: some View {
Section {
Button(action: logout) {
Button { showingLogoutConfirmation = true } label: {
HStack {
Image(systemName: "rectangle.portrait.and.arrow.right")
.foregroundColor(.element.systemGray)
@ -186,6 +187,14 @@ struct SettingsScreen: View {
.listRowInsets(listRowInsets)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("logoutButton")
.alert(ElementL10n.actionSignOut,
isPresented: $showingLogoutConfirmation) {
Button(ElementL10n.actionSignOut,
role: .destructive,
action: logout)
} message: {
Text(ElementL10n.actionSignOutConfirmationSimple)
}
} footer: {
versionText
.frame(maxWidth: .infinity)
@ -194,12 +203,10 @@ struct SettingsScreen: View {
private var closeButton: some View {
Button(action: close) {
HStack {
Image(systemName: "xmark")
.font(.title3.bold())
.foregroundColor(.element.secondaryContent)
.padding(4)
}
Image(systemName: "xmark")
.font(.title3.bold())
.foregroundColor(.element.secondaryContent)
.padding(4)
}
.accessibilityIdentifier("closeButton")
}

View File

@ -62,8 +62,6 @@ class ClientProxy: ClientProxyProtocol {
private var slidingSyncObserverToken: StoppableSpawn?
private let slidingSync: SlidingSync
private var roomProxies = [String: RoomProxyProtocol]()
let roomSummaryProvider: RoomSummaryProviderProtocol
deinit {
@ -158,10 +156,6 @@ class ClientProxy: ClientProxyProtocol {
}
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
if let roomProxy = roomProxies[identifier] {
return roomProxy
}
do {
guard let slidingSyncRoom = try slidingSync.getRoom(roomId: identifier),
let room = slidingSyncRoom.fullRoom() else {
@ -172,7 +166,6 @@ class ClientProxy: ClientProxyProtocol {
let roomProxy = RoomProxy(slidingSyncRoom: slidingSyncRoom,
room: room,
backgroundTaskService: backgroundTaskService)
roomProxies[identifier] = roomProxy
return roomProxy
} catch {
@ -180,7 +173,7 @@ class ClientProxy: ClientProxyProtocol {
return nil
}
}
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
await Task.dispatch(on: .global()) {
do {

View File

@ -30,28 +30,28 @@ struct MediaProvider: MediaProviderProtocol {
self.backgroundTaskService = backgroundTaskService
}
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
guard let source = source else {
return nil
}
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
}
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage? {
guard let urlString = urlString else {
return nil
}
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
}
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
}
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
if let image = imageFromSource(source, size: size) {
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
if let image = imageFromSource(source, avatarSize: avatarSize) {
return .success(image)
}
@ -60,7 +60,7 @@ struct MediaProvider: MediaProviderProtocol {
loadImageBgTask?.stop()
}
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
return await Task.detached { () -> Result<UIImage, MediaProviderError> in
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
@ -70,8 +70,8 @@ struct MediaProvider: MediaProviderProtocol {
do {
let imageData = try await Task.detached { () -> Data in
if let size = size {
return try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(size.width), height: UInt(size.height))
if let avatarSize = avatarSize {
return try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
} else {
return try await clientProxy.loadMediaContentForSource(source.underlyingSource)
}
@ -96,9 +96,9 @@ struct MediaProvider: MediaProviderProtocol {
// MARK: - Private
private func cacheKeyForURLString(_ urlString: String, size: CGSize?) -> String {
if let size = size {
return "\(urlString){\(size.width),\(size.height)}"
private func cacheKeyForURLString(_ urlString: String, avatarSize: AvatarSize?) -> String {
if let avatarSize = avatarSize {
return "\(urlString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
} else {
return urlString
}

View File

@ -22,33 +22,31 @@ enum MediaProviderError: Error {
case invalidImageData
}
let MediaProviderDefaultAvatarSize = CGSize(width: 44.0, height: 44.0)
@MainActor
protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage?
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError>
@discardableResult func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage?
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError>
@discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
}
extension MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?) -> UIImage? {
imageFromSource(source, size: nil)
imageFromSource(source, avatarSize: nil)
}
@discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(source, size: nil)
await loadImageFromSource(source, avatarSize: nil)
}
func imageFromURLString(_ urlString: String?) -> UIImage? {
imageFromURLString(urlString, size: nil)
imageFromURLString(urlString, avatarSize: nil)
}
@discardableResult func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
await loadImageFromURLString(urlString, size: nil)
await loadImageFromURLString(urlString, avatarSize: nil)
}
}

View File

@ -18,15 +18,15 @@ import Foundation
import UIKit
struct MockMediaProvider: MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
nil
}
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
.failure(.failedRetrievingImage)
}
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage? {
if urlString != nil {
return UIImage(systemName: "photo")
}
@ -34,7 +34,7 @@ struct MockMediaProvider: MediaProviderProtocol {
return nil
}
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
.failure(.failedRetrievingImage)
}
}

View File

@ -42,7 +42,7 @@ class RoomProxy: RoomProxyProtocol {
}()
deinit {
#warning("Should any timeline listeners be removed??")
room.removeTimeline()
}
init(slidingSyncRoom: SlidingSyncRoomProtocol,

View File

@ -32,7 +32,7 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
RoomSummary(id: "2",
name: "Second room",
isDirect: true,
avatarURLString: "mockImageURLString",
avatarURLString: nil,
lastMessage: nil,
lastMessageTimestamp: nil,
unreadNotificationCount: 1),

View File

@ -28,7 +28,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "That looks so good!",
timestamp: "10:10 AM",
shouldShowSenderDetails: true,
inGroupState: .single,
isOutgoing: false,
senderId: "",
@ -37,7 +36,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "Lets get lunch soon! New salad place opened up 🥗. When are yall free? 🤗",
timestamp: "10:11 AM",
shouldShowSenderDetails: true,
inGroupState: .beginning,
isOutgoing: false,
senderId: "",
@ -48,7 +46,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
timestamp: "10:11 AM",
shouldShowSenderDetails: false,
inGroupState: .middle,
isOutgoing: false,
senderId: "",
@ -62,7 +59,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Heres the menu, let me know what you want its on me!",
timestamp: "5 PM",
shouldShowSenderDetails: false,
inGroupState: .end,
isOutgoing: false,
senderId: "",
@ -71,7 +67,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "And John's speech was amazing!",
timestamp: "5 PM",
shouldShowSenderDetails: false,
inGroupState: .single,
isOutgoing: true,
senderId: "",
@ -80,7 +75,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
TextRoomTimelineItem(id: UUID().uuidString,
text: "New home office set up!",
timestamp: "5 PM",
shouldShowSenderDetails: false,
inGroupState: .single,
isOutgoing: true,
senderId: "",

View File

@ -149,7 +149,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
guard eventItem.isMessage || eventItem.isRedacted else { break } // To be handled in the future
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem,
showSenderDetails: inGroupState.shouldShowSenderDetails,
inGroupState: inGroupState))
case .virtual:
// case .virtual(let virtualItem):
@ -252,7 +251,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return
}
switch await mediaProvider.loadImageFromURLString(avatarURLString, size: MediaProviderDefaultAvatarSize) {
switch await mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .user(on: .timeline)) {
case .success(let avatar):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {

View File

@ -59,3 +59,9 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
var properties: RoomTimelineItemProperties { get }
}
extension EventBasedTimelineItemProtocol {
var shouldShowSenderDetails: Bool {
inGroupState.shouldShowSenderDetails
}
}

View File

@ -22,7 +22,6 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let timestamp: String
let shouldShowSenderDetails: Bool
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool

View File

@ -21,7 +21,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let id: String
let text: String
let timestamp: String
let shouldShowSenderDetails: Bool
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool

View File

@ -22,7 +22,6 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let timestamp: String
let shouldShowSenderDetails: Bool
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool

View File

@ -21,7 +21,6 @@ struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, E
let id: String
let text: String
let timestamp: String
let shouldShowSenderDetails: Bool
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool

View File

@ -22,7 +22,6 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let timestamp: String
let shouldShowSenderDetails: Bool
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool

View File

@ -36,15 +36,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
showSenderDetails: Bool,
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol {
let displayName = roomProxy.displayNameForUserId(eventItemProxy.sender)
let avatarURL = roomProxy.avatarURLStringForUserId(eventItemProxy.sender)
let avatarImage = mediaProvider.imageFromURLString(avatarURL, size: MediaProviderDefaultAvatarSize)
let avatarImage = mediaProvider.imageFromURLString(avatarURL, avatarSize: .user(on: .timeline))
let isOutgoing = eventItemProxy.isOwn
if eventItemProxy.isRedacted {
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, inGroupState, displayName, avatarImage)
}
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Must be a message for now.") }
@ -52,35 +51,31 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
switch messageContent.msgtype() {
case .text(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return await buildTextTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
case .image(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return await buildImageTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
case .notice(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
case .emote(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
case .none:
return await buildFallbackTimelineItem(eventItemProxy, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
return await buildFallbackTimelineItem(eventItemProxy, isOutgoing, inGroupState, displayName, avatarImage)
}
}
// MARK: - Private
// swiftformat:disable function_parameter_count
// swiftlint:disable function_parameter_count
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
RedactedRoomTimelineItem(id: eventItemProxy.id,
text: ElementL10n.eventRedacted,
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: eventItemProxy.sender,
@ -91,7 +86,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
@ -102,7 +96,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
text: eventItemProxy.body ?? "",
attributedComponents: attributedComponents,
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: eventItemProxy.sender,
@ -114,7 +107,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildTextTimelineItemFromMessage(_ message: MessageTimelineItem<TextMessageContent>,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
@ -125,7 +117,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
text: message.body,
attributedComponents: attributedComponents,
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: message.sender,
@ -137,7 +128,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildImageTimelineItemFromMessage(_ message: MessageTimelineItem<ImageMessageContent>,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
@ -150,7 +140,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return ImageRoomTimelineItem(id: message.id,
text: message.body,
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: message.sender,
@ -168,7 +157,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem<NoticeMessageContent>,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
@ -179,7 +167,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
text: message.body,
attributedComponents: attributedComponents,
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: message.sender,
@ -191,7 +178,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildEmoteTimelineItemFromMessage(_ message: MessageTimelineItem<EmoteMessageContent>,
_ isOutgoing: Bool,
_ showSenderDetails: Bool,
_ inGroupState: TimelineItemInGroupState,
_ displayName: String?,
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
@ -202,7 +188,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
text: message.body,
attributedComponents: attributedComponents,
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
shouldShowSenderDetails: showSenderDetails,
inGroupState: inGroupState,
isOutgoing: isOutgoing,
senderId: message.sender,
@ -212,9 +197,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
reactions: aggregateReactions(message.reactions)))
}
// swiftlint:enable function_parameter_count
// swiftformat:enable function_parameter_count
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in
let isHighlighted = false // reaction.details.contains(where: { $0.sender == userID })

View File

@ -19,6 +19,5 @@ import Foundation
@MainActor
protocol RoomTimelineItemFactoryProtocol {
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
showSenderDetails: Bool,
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol
}

View File

@ -59,7 +59,7 @@ class UserSessionStore: UserSessionStoreProtocol {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
imageCache: ImageCache.default,
imageCache: .onlyInMemory,
backgroundTaskService: backgroundTaskService)))
case .failure(let error):
MXLog.error("Failed restoring login with error: \(error)")
@ -77,7 +77,7 @@ class UserSessionStore: UserSessionStoreProtocol {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
imageCache: ImageCache.default,
imageCache: .onlyInMemory,
backgroundTaskService: backgroundTaskService)))
case .failure(let error):
MXLog.error("Failed creating user session with error: \(error)")

View File

@ -25,6 +25,7 @@ enum UITestScreenIdentifier: String {
case analyticsPrompt
case simpleRegular
case simpleUpgrade
case home
case settings
case bugReport
case bugReportWithScreenshot

View File

@ -27,6 +27,7 @@ class UITestsAppCoordinator: Coordinator {
init() {
mainNavigationController = ElementNavigationController()
mainNavigationController.navigationBar.prefersLargeTitles = true
navigationRouter = NavigationRouter(navigationController: mainNavigationController)
window = UIWindow(frame: UIScreen.main.bounds)
@ -94,6 +95,10 @@ class MockScreen: Identifiable {
return TemplateCoordinator(parameters: .init(promptType: .regular))
case .simpleUpgrade:
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
case .home:
let session = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:matrix.org"),
mediaProvider: MockMediaProvider())
return HomeScreenCoordinator(parameters: .init(userSession: session, attributedStringBuilder: AttributedStringBuilder()))
case .settings:
return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),

View File

@ -15,3 +15,14 @@
//
import XCTest
class HomeScreenUITests: XCTestCase {
func testInitialStateComponents() {
let app = Application.launch()
app.goToScreenWithIdentifier(.home)
XCTAssert(app.navigationBars[ElementL10n.allChats].exists)
app.assertScreenshot(.home)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.