App Polish (#1098)

* App polish

- Skeleton avatar colour on app switching.
- Bug report icon and string.
- Semi-bold font on room list timestamps.
- Room attachments button spacing and alignment.
- Replace link tint with semi-bold font in AnalyticsSettingsScreen.
- Semi-bold font on Invites button.
- Add padding to media attachment sheet.
- Message composer header layout tweaks.
- Remove indicator after changing homeserver with an MXID.
- Change copy permalink string.
- Formatting of link in Analytics Prompt.
- Remove indicator after changing homeserver with an MXID.
- Change copy permalink string.
- Formatting of link in Analytics Prompt.
- Only semi-bold the room timestamp when unread.
- Tweak the colour of the date separator.
- Add activity indicator to toast style user indicator.
This commit is contained in:
Doug
2023-06-16 17:41:25 +01:00
committed by GitHub
parent 0d737d0dd8
commit 4de39201ed
33 changed files with 169 additions and 103 deletions
@@ -523,7 +523,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
let identifier = "StaleDataIndicator"
ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: identifier, type: .toast, title: L10n.commonLoading, persistent: true))
ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: identifier, type: .toast(progress: .indeterminate), title: L10n.commonSyncing, persistent: true))
initialSyncObserver = userSession.clientProxy
.callbacks
+4
View File
@@ -40,6 +40,8 @@ public enum L10n {
public static var actionCopy: String { return L10n.tr("Localizable", "action_copy") }
/// Copy link
public static var actionCopyLink: String { return L10n.tr("Localizable", "action_copy_link") }
/// Copy link to message
public static var actionCopyLinkToMessage: String { return L10n.tr("Localizable", "action_copy_link_to_message") }
/// Create
public static var actionCreate: String { return L10n.tr("Localizable", "action_create") }
/// Create a room
@@ -240,6 +242,8 @@ public enum L10n {
public static var commonSuccess: String { return L10n.tr("Localizable", "common_success") }
/// Suggestions
public static var commonSuggestions: String { return L10n.tr("Localizable", "common_suggestions") }
/// Syncing
public static var commonSyncing: String { return L10n.tr("Localizable", "common_syncing") }
/// Third-party notices
public static var commonThirdPartyNotices: String { return L10n.tr("Localizable", "common_third_party_notices") }
/// Topic
@@ -33,20 +33,29 @@ extension AttributedString {
}
}
/// Replaces the specified placeholder with the a string that links to the specified URL.
/// Replaces the specified placeholder with a string that links to the specified URL.
/// - Parameters:
/// - linkPlaceholder: The text in the string that will be replaced. Make sure this is unique within the string.
/// - string: The text for the link that will be substituted into the placeholder.
/// - url: The URL that the link should open.
mutating func replace(_ linkPlaceholder: String, with string: String, asLinkTo url: URL) {
guard let range = range(of: linkPlaceholder) else {
MXLog.failure("Failed to find the link placeholder to be replaced.")
return
}
// Replace the placeholder with a link.
var replacement = AttributedString(string)
replacement.link = url
replace(linkPlaceholder, with: replacement)
}
/// Replaces the specified placeholder with the supplied attributed string.
/// - Parameters:
/// - placeholder: The text in the string that will be replaced. Make sure this is unique within the string.
/// - attributedString: The text for the link that will be substituted into the placeholder.
mutating func replace(_ placeholder: String, with replacement: AttributedString) {
guard let range = range(of: placeholder) else {
MXLog.failure("Failed to find the placeholder to be replaced.")
return
}
// Replace the placeholder.
replaceSubrange(range, with: replacement)
}
@@ -63,4 +72,12 @@ extension AttributedString {
}
return newValue
}
/// Makes the entire string bold by setting the presentation intent to strongly emphasized.
///
/// In practice, this is rendered as semibold for smaller font sizes and just so happens to nicely
/// line up with the semibold → bold font switch used by compound.
mutating func bold() {
self[startIndex..<endIndex].inlinePresentationIntent = .stronglyEmphasized
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ protocol ProgressListener {
var progressSubject: CurrentValueSubject<Double, Never> { get }
}
protocol ProgressPublisher {
protocol ProgressPublisher: AnyObject {
var publisher: AnyPublisher<Double, Never> { get }
}
@@ -19,8 +19,10 @@ import SwiftUI
struct FixedIconSizeLabelStyle: LabelStyle {
@ScaledMetric private var iconSize = 24.0
var spacing: CGFloat = 16
func makeBody(configuration: Configuration) -> some View {
HStack {
HStack(spacing: spacing) {
configuration
.icon
.frame(width: iconSize, height: iconSize)
@@ -25,7 +25,7 @@ struct PlaceholderAvatarImage: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
bgColor
backgroundColor
// This text's frame doesn't look right when redacted
if redactionReasons != .placeholder {
@@ -47,8 +47,8 @@ struct PlaceholderAvatarImage: View {
self.contentID = contentID
}
private var bgColor: Color {
if redactionReasons == .placeholder {
private var backgroundColor: Color {
if redactionReasons.contains(.placeholder) {
return Color(.systemGray4) // matches the default text redaction
}
@@ -18,41 +18,51 @@ import Combine
import Foundation
enum UserIndicatorType: Equatable {
case toast
case modal(interactiveDismissDisabled: Bool)
case toast(progress: UserIndicator.Progress?)
case modal(progress: UserIndicator.Progress?, interactiveDismissDisabled: Bool)
static var modal: Self {
.modal(interactiveDismissDisabled: false)
}
static var toast: Self { .toast(progress: .none) }
static var modal: Self { .modal(progress: .indeterminate, interactiveDismissDisabled: false) }
}
struct UserIndicator: Equatable, Identifiable {
static func == (lhs: UserIndicator, rhs: UserIndicator) -> Bool {
lhs.id == rhs.id &&
lhs.type == rhs.type &&
lhs.title == rhs.title &&
lhs.iconName == rhs.iconName &&
lhs.persistent == rhs.persistent
}
enum LoaderType {
case unknownProgress
case progress(ProgressPublisher)
enum Progress: Equatable {
static func == (lhs: UserIndicator.Progress, rhs: UserIndicator.Progress) -> Bool {
switch (lhs, rhs) {
case (.indeterminate, .indeterminate): return true
case (.published(let lhsPublisher), .published(let rhsPublisher)): return lhsPublisher === rhsPublisher
default: return false
}
}
case indeterminate
case published(ProgressPublisher)
}
var id: String = UUID().uuidString
var type = UserIndicatorType.toast
var type: UserIndicatorType = .toast
var title: String
var iconName: String?
var persistent = false
var loaderType: LoaderType? = .unknownProgress
// MARK: - Associated values from the type
var progress: Progress? {
switch type {
case .toast(let progress): return progress
case .modal(let progress, _): return progress
}
}
var progressPublisher: AnyPublisher<Double, Never> {
switch loaderType {
case .none, .unknownProgress:
return Empty().eraseToAnyPublisher()
case .some(.progress(let progress)):
return progress.publisher.eraseToAnyPublisher()
switch type {
case .toast(let progress), .modal(let progress, _):
switch progress {
case .none, .indeterminate:
return Empty().eraseToAnyPublisher()
case .some(.published(let progress)):
return progress.publisher.eraseToAnyPublisher()
}
}
}
@@ -60,7 +70,7 @@ struct UserIndicator: Equatable, Identifiable {
switch type {
case .toast:
return false
case .modal(let interactiveDismissDisabled):
case .modal(_, let interactiveDismissDisabled):
return interactiveDismissDisabled
}
}
@@ -24,15 +24,17 @@ struct UserIndicatorModalView: View {
var body: some View {
ZStack {
VStack(spacing: 12.0) {
if case .unknownProgress = indicator.loaderType {
if case .indeterminate = indicator.progress {
ProgressView()
} else if case .progress = indicator.loaderType {
} else if case .published = indicator.progress {
ProgressView(value: progressFraction)
}
HStack {
if let iconName = indicator.iconName {
Image(systemName: iconName)
.font(.compound.bodyLG)
.foregroundColor(.compound.iconPrimary)
}
Text(indicator.title)
.font(.compound.bodyLG)
@@ -66,17 +68,17 @@ struct UserIndicatorModalView_Previews: PreviewProvider {
iconName: "checkmark")
)
.previewDisplayName("Spinner")
UserIndicatorModalView(indicator: UserIndicator(type: .modal,
UserIndicatorModalView(indicator: UserIndicator(type: .modal(progress: .published(ProgressTracker(initialValue: 0.5)),
interactiveDismissDisabled: false),
title: "Successfully logged in",
iconName: "checkmark",
loaderType: .progress(ProgressTracker(initialValue: 0.5)))
iconName: "checkmark")
)
.previewDisplayName("Progress Bar")
UserIndicatorModalView(indicator: UserIndicator(type: .modal,
UserIndicatorModalView(indicator: UserIndicator(type: .modal(progress: .none, interactiveDismissDisabled: false),
title: "Successfully logged in",
iconName: "checkmark",
loaderType: .none)
iconName: "checkmark")
)
.previewDisplayName("No progress")
}
@@ -20,21 +20,27 @@ struct UserIndicatorToastView: View {
let indicator: UserIndicator
var body: some View {
HStack {
HStack(spacing: 4) {
if case .indeterminate = indicator.progress {
ProgressView()
.controlSize(.small)
.tint(.compound.iconPrimary)
}
if let iconName = indicator.iconName {
Image(systemName: iconName)
.font(.compound.bodyMD)
.foregroundColor(.compound.iconPrimary)
}
Text(indicator.title)
.font(.compound.bodySM)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
}
.id(indicator.id)
.padding(.horizontal, 12.0)
.padding(.vertical, 10.0)
.frame(minWidth: 150.0)
.background(Color.compound.bgSubtlePrimary)
.clipShape(RoundedCornerShape(radius: 24.0, corners: .allCorners))
.shadow(color: .black.opacity(0.25), radius: 10.0, y: 4.0)
.shadow(color: .black.opacity(0.1), radius: 6.0, y: 4.0)
.transition(toastTransition)
}
@@ -48,11 +54,14 @@ struct UserIndicatorToastView: View {
struct UserIndicatorToastView_Previews: PreviewProvider {
static var previews: some View {
VStack {
VStack(spacing: 30) {
UserIndicatorToastView(indicator: UserIndicator(title: "Successfully logged in",
iconName: "checkmark"))
UserIndicatorToastView(indicator: UserIndicator(title: "Toast without icon"))
UserIndicatorToastView(indicator: UserIndicator(type: .toast(progress: .indeterminate),
title: "Syncing"))
}
}
}
@@ -44,12 +44,15 @@ struct AnalyticsPromptScreenStrings {
init(termsURL: URL) {
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
// Create the opt in content with a placeholder.
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsPromptReadTerms(linkPlaceholder))
readTerms.replace(linkPlaceholder,
with: L10n.screenAnalyticsPromptReadTermsContentLink,
asLinkTo: termsURL)
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
optInContent = content + "\n\n" + readTerms
}
}
@@ -49,7 +49,6 @@ struct AnalyticsPromptScreen: View {
.font(.compound.bodyLG)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textSecondary)
.tint(.compound.textLinkExternal)
Divider()
.overlay { Color.compound._borderRowSeparator }
@@ -92,14 +92,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
private func indicateSuccess() {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonSuccess))
}
private func indicateFailure() {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonError))
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
MXLog.info("Error occurred: \(error)")
@@ -163,7 +155,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
/// Updates the view model with a different homeserver.
private func updateViewModel() {
viewModel.update(homeserver: authenticationService.homeserver.value)
indicateSuccess()
}
/// Shows the forgot password screen.
@@ -88,10 +88,9 @@ final class BugReportScreenCoordinator: CoordinatorProtocol {
private func startLoading(label: String = L10n.commonLoading, progressPublisher: ProgressPublisher) {
parameters.userIndicatorController?.submitIndicator(
UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
type: .modal(progress: .published(progressPublisher), interactiveDismissDisabled: false),
title: label,
persistent: true,
loaderType: .progress(progressPublisher))
persistent: true)
)
}
@@ -168,7 +168,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
private func showLoadingIndicator() {
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal(interactiveDismissDisabled: true),
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true),
title: L10n.commonLoading,
persistent: true))
}
@@ -288,10 +288,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
state.bindings.alertInfo = AlertInfo(id: UUID(), title: L10n.errorUnknown)
case .some(.success):
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: UUID().uuidString,
type: .modal,
type: .modal(progress: .none, interactiveDismissDisabled: false),
title: L10n.commonCurrentUserLeftRoom,
iconName: "checkmark",
loaderType: .none))
iconName: "checkmark"))
callback?(.roomLeft(roomIdentifier: roomId))
}
}
@@ -154,7 +154,7 @@ struct HomeScreen: View {
Label(L10n.actionInvite, systemImage: "square.and.arrow.up")
}
Button(action: feedback) {
Label(L10n.commonReportABug, systemImage: "questionmark.circle")
Label(L10n.commonReportABug, systemImage: "ladybug")
}
}
Section {
@@ -28,7 +28,7 @@ struct HomeScreenInvitesButton: View {
HStack(spacing: 8) {
Text(title)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
.font(.compound.bodyMDSemibold)
if hasBadge {
badge
@@ -92,7 +92,7 @@ struct HomeScreenRoomCell: View {
if let timestamp = room.timestamp {
Text(timestamp)
.font(.compound.bodySM)
.font(room.hasUnreads ? .compound.bodySMSemibold : .compound.bodySM)
.foregroundColor(room.hasUnreads ? .compound.textActionAccent : .compound.textSecondary)
}
}
@@ -78,7 +78,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
}
userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
type: .modal(interactiveDismissDisabled: true),
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true),
title: L10n.commonLoading,
persistent: true))
@@ -102,7 +102,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
}
userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
type: .modal(interactiveDismissDisabled: true),
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true),
title: L10n.screenRoomDetailsUpdatingRoom,
persistent: true))
@@ -519,7 +519,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func showLoadingIndicator(with delay: Duration) -> Task<Void, Never> {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal(interactiveDismissDisabled: true),
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true),
title: L10n.commonLoading,
persistent: true),
delay: delay)
@@ -133,7 +133,7 @@ private struct MessageComposerReplyHeader: View {
.overlay(alignment: .topTrailing) {
Button(action: action) {
Image(systemName: "xmark")
.font(.caption2.weight(.medium))
.font(.compound.bodySM.weight(.medium))
.foregroundColor(.compound.iconTertiary)
.padding(16.0)
}
@@ -151,9 +151,9 @@ private struct MessageComposerEditHeader: View {
Spacer()
Button(action: action) {
Image(systemName: "xmark")
.font(.caption2.weight(.medium))
.font(.compound.bodySM.weight(.medium))
.foregroundColor(.compound.iconTertiary)
.padding(12.0)
.padding(EdgeInsets(top: 10, leading: 12, bottom: 12, trailing: 14))
}
}
}
@@ -220,6 +220,17 @@ struct MessageComposer_Previews: PreviewProvider {
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant(""),
focused: .constant(false),
sendingDisabled: false,
mode: .reply(itemID: UUID().uuidString,
replyDetails: .loaded(sender: .init(id: "Kirk"),
contentType: .text(.init(body: "Text: Where the wild things are")))),
sendAction: { },
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
}
.padding(.horizontal)
@@ -113,7 +113,7 @@ struct TimelineReplyView: View {
Spacer().frame(width: 4.0)
}
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 4) {
Text(sender.displayName ?? sender.id)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textPrimary)
@@ -54,6 +54,7 @@ struct RoomAttachmentPicker: View {
PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, systemImageName: "camera.fill")
}
}
.padding(.top, 20)
.background {
// This is done in the background otherwise GeometryReader tends to expand to
// all the space given to it like color or shape.
@@ -79,7 +80,16 @@ struct RoomAttachmentPicker: View {
.labelStyle(FixedIconSizeLabelStyle())
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.padding(16)
}
}
}
struct RoomAttachmentPicker_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(displayName: "")))
static var previews: some View {
RoomAttachmentPicker(context: viewModel.context)
}
}
@@ -21,17 +21,22 @@ struct RoomScreen: View {
@State private var showReactionsMenuForItemId = ""
@State private var dragOver = false
private let attachmentButtonPadding = 10.0
var body: some View {
timeline
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) // Kills the toolbar translucency.
.safeAreaInset(edge: .bottom, spacing: 0) {
HStack(spacing: 4.0) {
HStack(alignment: .bottom, spacing: attachmentButtonPadding) {
RoomAttachmentPicker(context: context)
.padding(.bottom, 5) // centre align with the send button
messageComposer
.environmentObject(context)
}
.padding([.horizontal, .bottom])
.padding(.leading, attachmentButtonPadding)
.padding(.trailing, 12)
.padding(.top, 8)
.padding(.bottom)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@@ -22,7 +22,7 @@ struct SeparatorRoomTimelineView: View {
var body: some View {
Text(timelineItem.text)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textSecondary)
.foregroundColor(.compound.textPrimary)
.frame(maxWidth: .infinity)
.padding(.horizontal, 36.0)
.padding(.vertical, 8.0)
@@ -187,7 +187,7 @@ public struct TimelineItemMenu: View {
}
case .copyPermalink:
Button { send(action) } label: {
MenuLabel(title: L10n.commonPermalink, systemImageName: "link")
MenuLabel(title: L10n.actionCopyLinkToMessage, systemImageName: "link")
}
case .reply:
Button { send(action) } label: {
@@ -35,12 +35,15 @@ struct AnalyticsSettingsScreenStrings {
init(termsURL: URL) {
let content = AttributedString(L10n.screenAnalyticsSettingsHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
readTerms.replace(linkPlaceholder,
with: L10n.screenAnalyticsSettingsReadTermsContentLink,
asLinkTo: termsURL)
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
sectionFooter = content + "\n\n" + readTerms
}
}
@@ -40,7 +40,6 @@ struct AnalyticsSettingsScreen: View {
} footer: {
Text(context.viewState.strings.sectionFooter)
.compoundFormSectionFooter()
.tint(.compound.textLinkExternal)
}
.compoundFormSection()
}
@@ -122,7 +122,7 @@ struct SettingsScreen: View {
// Report Bug
Button { context.send(viewAction: .reportBug) } label: {
Label(L10n.actionReportBug, systemImage: "ladybug")
Label(L10n.commonReportABug, systemImage: "ladybug")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("reportBugButton")
@@ -153,7 +153,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
private func showLoadingIndicator() {
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal(interactiveDismissDisabled: true),
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true),
title: L10n.commonLoading,
persistent: true))
}