mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ enum HomeScreenViewAction {
|
||||
}
|
||||
|
||||
struct HomeScreenViewState: BindableState {
|
||||
var userID: String
|
||||
var userDisplayName: String?
|
||||
var userAvatar: UIImage?
|
||||
|
||||
var showSessionVerificationBanner = false
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,7 @@ enum RoomScreenViewAction {
|
||||
case sendMessage
|
||||
case sendReaction(key: String, eventID: String)
|
||||
case cancelReply
|
||||
case viewDisappeared
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -41,6 +41,9 @@ struct RoomScreen: View {
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.onDisappear {
|
||||
context.send(viewAction: .viewDisappeared)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
@@ -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)
|
||||
// )
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
EmoteRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -59,7 +59,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
NoticeRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
|
||||
@@ -25,7 +25,7 @@ struct PlaceholderAvatarImage: View {
|
||||
bgColor
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.element.background)
|
||||
.foregroundColor(.white)
|
||||
.font(.title2.bold())
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
|
||||
@@ -51,7 +51,6 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
||||
RedactedRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user