Update login and server selection screens (#651)

* Update login and server selection screens.

* Replace all NavigationViews with NavigationStacks
This commit is contained in:
Doug 2023-03-02 18:05:18 +00:00 committed by GitHub
parent e7a8dc8cc2
commit 1c09a7eace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 287 additions and 205 deletions

View File

@ -23,7 +23,21 @@ public extension TextFieldStyle where Self == ElementTextFieldStyle {
footerText: String? = nil,
isError: Bool = false,
accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle {
ElementTextFieldStyle(labelText: labelText, footerText: footerText, isError: isError, accessibilityIdentifier: accessibilityIdentifier)
ElementTextFieldStyle(labelText: labelText.map(Text.init),
footerText: footerText.map(Text.init),
isError: isError,
accessibilityIdentifier: accessibilityIdentifier)
}
@_disfavoredOverload
static func elementInput(labelText: Text? = nil,
footerText: Text? = nil,
isError: Bool = false,
accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle {
ElementTextFieldStyle(labelText: labelText,
footerText: footerText,
isError: isError,
accessibilityIdentifier: accessibilityIdentifier)
}
}
@ -36,8 +50,8 @@ public struct ElementTextFieldStyle: TextFieldStyle {
@Environment(\.colorScheme) private var colorScheme
@FocusState private var isFocused: Bool
public let labelText: String?
public let footerText: String?
public let labelText: Text?
public let footerText: Text?
public let isError: Bool
public let accessibilityIdentifier: String?
@ -93,7 +107,7 @@ public struct ElementTextFieldStyle: TextFieldStyle {
/// - labelText: The text shown in the label above the field.
/// - footerText: The text shown in the footer label below the field.
/// - isError: Whether or not the text field is currently in the error state.
public init(labelText: String? = nil, footerText: String? = nil, isError: Bool = false, accessibilityIdentifier: String? = nil) {
public init(labelText: Text? = nil, footerText: Text? = nil, isError: Bool = false, accessibilityIdentifier: String? = nil) {
self.labelText = labelText
self.footerText = footerText
self.isError = isError
@ -104,12 +118,10 @@ public struct ElementTextFieldStyle: TextFieldStyle {
let rectangle = RoundedRectangle(cornerRadius: 14.0)
return VStack(alignment: .leading, spacing: 8) {
if let labelText {
Text(labelText)
.font(.element.footnote)
.foregroundColor(labelColor)
.padding(.horizontal, 16)
}
labelText
.font(.element.footnote)
.foregroundColor(labelColor)
.padding(.horizontal, 16)
configuration
.focused($isFocused)
@ -134,12 +146,11 @@ public struct ElementTextFieldStyle: TextFieldStyle {
textField.accessibilityIdentifier = accessibilityIdentifier
}
if let footerText {
Text(footerText)
.font(.element.caption1)
.foregroundColor(footerColor)
.padding(.horizontal, 16)
}
footerText
.tint(.element.links)
.font(.element.caption1)
.foregroundColor(footerColor)
.padding(.horizontal, 16)
}
}
}

View File

@ -53,7 +53,7 @@
"session_verification_banner_message" = "Looks like youre using a new device. Verify its you to access your encrypted messages.";
"session_verification_start" = "Start";
"server_selection_server_footer" = "You can only connect to an existing server";
"server_selection_server_footer" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %@";
"server_selection_sliding_sync_alert_title" = "Server not supported";
"server_selection_sliding_sync_alert_message" = "This server currently doesnt support sliding sync.";

View File

@ -136,8 +136,10 @@ extension ElementL10n {
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
/// You took a screenshot
public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title")
/// You can only connect to an existing server
public static let serverSelectionServerFooter = ElementL10n.tr("Untranslated", "server_selection_server_footer")
/// You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %@
public static func serverSelectionServerFooter(_ p1: Any) -> String {
return ElementL10n.tr("Untranslated", "server_selection_server_footer", String(describing: p1))
}
/// This server currently doesnt support sliding sync.
public static let serverSelectionSlidingSyncAlertMessage = ElementL10n.tr("Untranslated", "server_selection_sliding_sync_alert_message")
/// Server not supported

View File

@ -34,4 +34,21 @@ extension AttributedString {
return AttributedStringBuilderComponent(attributedString: attributedString, isBlockquote: isBlockquote, isReply: isReply)
}
}
/// Replaces the specified placeholder with the 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
replaceSubrange(range, with: replacement)
}
}

View File

@ -45,19 +45,9 @@ struct AnalyticsPromptStrings {
// Create the opt in content with a placeholder.
let linkPlaceholder = "{link}"
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.main.bundleDisplayName, linkPlaceholder))
guard let range = optInContent.range(of: linkPlaceholder) else {
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.main.bundleDisplayName,
ElementL10n.analyticsOptInContentLink))
MXLog.failure("Failed to add a link attribute to the opt in content.")
return
}
// Replace the placeholder with a link.
var link = AttributedString(ElementL10n.analyticsOptInContentLink)
link.link = ServiceLocator.shared.settings.analyticsConfiguration.termsURL
optInContent.replaceSubrange(range, with: link)
optInContent.replace(linkPlaceholder,
with: ElementL10n.analyticsOptInContentLink,
asLinkTo: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
self.optInContent = optInContent
}
}

View File

@ -18,7 +18,10 @@ import SwiftUI
/// An image that is styled for use as the screen icon in the onboarding flow.
struct AuthenticationIconImage: View {
/// The icon that is shown.
let image: Image
/// The amount of padding between the icon and the borders. Defaults to 16.
var insets: CGFloat = 16
var body: some View {
image
@ -27,9 +30,12 @@ struct AuthenticationIconImage: View {
.foregroundColor(.element.secondaryContent)
.aspectRatio(contentMode: .fit)
.accessibilityHidden(true)
.padding(16)
.frame(width: 72, height: 72)
.background(RoundedRectangle(cornerRadius: 14).fill(Color.element.quinaryContent))
.padding(insets)
.frame(width: 70, height: 70)
.background {
RoundedRectangle(cornerRadius: 14)
.fill(Color.element.quinaryContent)
}
}
}
@ -37,6 +43,9 @@ struct AuthenticationIconImage: View {
struct AuthenticationIconImage_Previews: PreviewProvider {
static var previews: some View {
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon))
HStack(spacing: 20) {
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19)
AuthenticationIconImage(image: Image(systemName: "hourglass"))
}
}
}

View File

@ -28,7 +28,7 @@ struct LoginScreen: View {
ScrollView {
VStack(spacing: 0) {
header
.padding(.top, UIConstants.topPaddingToNavigationBar)
.padding(.top, UIConstants.titleTopPaddingToNavigationBar)
.padding(.bottom, 32)
serverInfo
@ -70,15 +70,15 @@ struct LoginScreen: View {
var loginForm: some View {
VStack(alignment: .leading, spacing: 0) {
Text(ElementL10n.ftueAuthSignInEnterDetails)
.font(.element.subheadline)
.font(.element.footnote)
.foregroundColor(.element.primaryContent)
.padding(.horizontal, 16)
.padding(.bottom, 8)
TextField(ElementL10n.loginSigninUsernameHint,
TextField(ElementL10n.username,
text: $context.username,
// Prompt colour fixes a flicker that occurs before the text field style introspects the field.
prompt: Text(ElementL10n.loginSigninUsernameHint).foregroundColor(.element.tertiaryContent))
prompt: Text(ElementL10n.username).foregroundColor(.element.tertiaryContent))
.focused($isUsernameFocused)
.textFieldStyle(.elementInput(accessibilityIdentifier: A11yIdentifiers.loginScreen.emailUsername))
.disableAutocorrection(true)
@ -162,11 +162,18 @@ struct Login_Previews: PreviewProvider {
}
static func screen(for viewModel: LoginViewModel) -> some View {
NavigationView {
NavigationStack {
LoginScreen(context: viewModel.context)
.navigationBarTitleDisplayMode(.inline)
.tint(.element.accent)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button { } label: {
Text("\(Image(systemName: "chevron.backward")) Back")
}
}
}
}
.navigationViewStyle(.stack)
.tint(.element.accent)
}
}

View File

@ -31,7 +31,7 @@ struct LoginServerInfoSection: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(ElementL10n.ftueAuthSignInChooseServerHeader)
.font(.element.subheadline)
.font(.element.footnote)
.foregroundColor(.element.primaryContent)
.padding(.horizontal, 16)

View File

@ -24,6 +24,16 @@ enum ServerSelectionViewModelAction {
}
struct ServerSelectionViewState: BindableState {
/// The message to be shown in the text field footer when no error has occurred.
private let regularFooterMessage = {
let linkPlaceholder = "{link}"
var message = AttributedString(ElementL10n.serverSelectionServerFooter(linkPlaceholder))
message.replace(linkPlaceholder,
with: ElementL10n.actionLearnMore,
asLinkTo: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
return message
}()
/// View state that can be bound to from SwiftUI.
var bindings: ServerSelectionBindings
/// An error message to be shown in the text field footer.
@ -32,8 +42,8 @@ struct ServerSelectionViewState: BindableState {
var isModallyPresented: Bool
/// The message to show in the text field footer.
var footerMessage: String {
footerErrorMessage ?? ElementL10n.serverSelectionServerFooter
var footerMessage: AttributedString {
footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage
}
/// The title shown on the confirm button.

View File

@ -23,7 +23,7 @@ struct ServerSelectionScreen: View {
ScrollView {
VStack(spacing: 0) {
header
.padding(.top, UIConstants.topPaddingToNavigationBar)
.padding(.top, UIConstants.iconTopPaddingToNavigationBar)
.padding(.bottom, 36)
serverForm
@ -40,7 +40,7 @@ struct ServerSelectionScreen: View {
/// The title, message and icon at the top of the screen.
var header: some View {
VStack(spacing: 8) {
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon))
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19)
.padding(.bottom, 8)
Text(ElementL10n.ftueAuthChooseServerTitle)
@ -48,8 +48,8 @@ struct ServerSelectionScreen: View {
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
Text(ElementL10n.ftueAuthChooseServerSubtitle)
.font(.element.body)
Text(ElementL10n.ftueAuthChooseServerSignInSubtitle)
.font(.element.subheadline)
.multilineTextAlignment(.center)
.foregroundColor(.element.tertiaryContent)
}
@ -60,8 +60,8 @@ struct ServerSelectionScreen: View {
var serverForm: some View {
VStack(alignment: .leading, spacing: 24) {
TextField(ElementL10n.ftueAuthChooseServerEntryHint, text: $context.homeserverAddress)
.textFieldStyle(.elementInput(labelText: ElementL10n.hsUrl,
footerText: context.viewState.footerMessage,
.textFieldStyle(.elementInput(labelText: Text(ElementL10n.hsUrl),
footerText: Text(context.viewState.footerMessage),
isError: context.viewState.isShowingFooterError,
accessibilityIdentifier: A11yIdentifiers.changeServerScreen.server))
.keyboardType(.URL)
@ -104,11 +104,10 @@ struct ServerSelectionScreen: View {
struct ServerSelection_Previews: PreviewProvider {
static var previews: some View {
ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in
NavigationView {
NavigationStack {
ServerSelectionScreen(context: state.viewModel.context)
.tint(.element.accent)
}
.navigationViewStyle(.stack)
}
}
}

View File

@ -28,7 +28,7 @@ struct SoftLogoutScreen: View {
ScrollView {
VStack(spacing: 0) {
header
.padding(.top, UIConstants.topPaddingToNavigationBar)
.padding(.top, UIConstants.titleTopPaddingToNavigationBar)
.padding(.bottom, 36)
switch context.viewState.loginMode {
@ -179,11 +179,18 @@ struct SoftLogout_Previews: PreviewProvider {
}
static func screen(for viewModel: SoftLogoutViewModel) -> some View {
NavigationView {
NavigationStack {
SoftLogoutScreen(context: viewModel.context)
.navigationBarTitleDisplayMode(.inline)
.tint(.element.accent)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button { } label: {
Text("\(Image(systemName: "chevron.backward")) Back")
}
}
}
}
.navigationViewStyle(.stack)
.tint(.element.accent)
}
}

View File

@ -20,8 +20,10 @@ import SwiftUI
struct UIConstants {
static let maxContentHeight: CGFloat = 750
/// The padding used between the top of the main content and the navigation bar.
static let topPaddingToNavigationBar: CGFloat = 16
/// The padding used between the top of the main content's icon and the navigation bar.
static let iconTopPaddingToNavigationBar: CGFloat = 43
/// The padding used between the top of the main content's title and the navigation bar.
static let titleTopPaddingToNavigationBar: CGFloat = 32
/// The padding used between the footer and the bottom of the view.
static let actionButtonBottomPadding: CGFloat = 24

View File

@ -237,7 +237,7 @@ struct HomeScreen_Previews: PreviewProvider {
let viewModel = HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder())
return NavigationView {
return NavigationStack {
HomeScreen(context: viewModel.context)
}
}

View File

@ -123,12 +123,12 @@ struct RoomScreen: View {
// MARK: - Previews
struct RoomScreen_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomName: "Preview room")
static var previews: some View {
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomName: "Preview room")
NavigationView {
NavigationStack {
RoomScreen(context: viewModel.context)
}
}

View File

@ -23,23 +23,25 @@ struct TimelineItemDebugView: View {
var content: String
}
@Environment(\.presentationMode) private var presentationMode
@Environment(\.dismiss) private var dismiss
let info: DebugInfo
var body: some View {
NavigationView {
NavigationStack {
ScrollView {
Text(info.content)
.padding()
.font(.element.footnote)
.foregroundColor(.element.primaryContent)
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(info.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(ElementL10n.actionCancel) {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
ToolbarItem(placement: .secondaryAction) {
@ -49,5 +51,27 @@ struct TimelineItemDebugView: View {
}
}
}
.tint(.element.accent)
}
}
struct TimelineItemDebugView_Previews: PreviewProvider {
static let smallContent = """
{
SomeItem(
event_id: "$1234546634535",
sender: "@user:server.com",
timestamp: 42354534534
content: Message(
Message {
}
)
)
}
"""
static var previews: some View {
TimelineItemDebugView(info: .init(title: "Timeline item", content: smallContent))
}
}

View File

@ -87,7 +87,7 @@ struct TimelineTableView_Previews: PreviewProvider {
roomName: "Preview room")
static var previews: some View {
NavigationView {
NavigationStack {
RoomScreen(context: viewModel.context)
}
}

View File

@ -20,7 +20,7 @@ struct SessionVerificationScreen: View {
@ObservedObject var context: SessionVerificationViewModel.Context
var body: some View {
NavigationView {
NavigationStack {
ScrollView {
VStack(spacing: 32) {
screenHeader
@ -36,7 +36,6 @@ struct SessionVerificationScreen: View {
.background(Color.element.background.ignoresSafeArea())
.safeAreaInset(edge: .bottom) { actionButtons.padding() }
}
.navigationViewStyle(.stack)
.interactiveDismissDisabled() // Make sure dismissal goes through the state machine(s).
}

View File

@ -183,12 +183,14 @@ private extension TimelineStyle {
// MARK: - Previews
struct SettingsScreen_Previews: PreviewProvider {
static var previews: some View {
static let viewModel = {
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
mediaProvider: MockMediaProvider())
let viewModel = SettingsScreenViewModel(withUserSession: userSession)
NavigationView {
return SettingsScreenViewModel(withUserSession: userSession)
}()
static var previews: some View {
NavigationStack {
SettingsScreen(context: viewModel.context)
.tint(.element.accent)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -35,7 +35,8 @@ class ServerSelectionViewModelTests: XCTestCase {
func testErrorMessage() async throws {
// Given a new instance of the view model.
XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.")
XCTAssertEqual(context.viewState.footerMessage, ElementL10n.serverSelectionServerFooter, "The standard footer message should be shown.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), ElementL10n.serverSelectionServerFooter(ElementL10n.actionLearnMore),
"The standard footer message should be shown.")
// When an error occurs.
let message = "Unable to contact server."
@ -43,7 +44,7 @@ class ServerSelectionViewModelTests: XCTestCase {
// Then the footer should now be showing an error.
XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.")
XCTAssertEqual(context.viewState.footerMessage, message, "The error message should be shown.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), message, "The error message should be shown.")
// And when clearing the error.
context.send(viewAction: .clearFooterError)
@ -53,6 +54,7 @@ class ServerSelectionViewModelTests: XCTestCase {
// Then the error message should now be removed.
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
XCTAssertEqual(context.viewState.footerMessage, ElementL10n.serverSelectionServerFooter, "The standard footer message should be shown again.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), ElementL10n.serverSelectionServerFooter(ElementL10n.actionLearnMore),
"The standard footer message should be shown again.")
}
}

1
changelog.d/632.bugfix Normal file
View File

@ -0,0 +1 @@
Update top padding and a string in Login and Server Selection screens.