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
84 changed files with 287 additions and 205 deletions
@@ -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 doesn’t support sliding sync.
public static let serverSelectionSlidingSyncAlertMessage = ElementL10n.tr("Untranslated", "server_selection_sliding_sync_alert_message")
/// Server not supported
@@ -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)
}
}
@@ -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
}
}
@@ -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"))
}
}
}
@@ -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)
}
}
@@ -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)
@@ -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.
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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
@@ -237,7 +237,7 @@ struct HomeScreen_Previews: PreviewProvider {
let viewModel = HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder())
return NavigationView {
return NavigationStack {
HomeScreen(context: viewModel.context)
}
}
@@ -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)
}
}
@@ -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))
}
}
@@ -87,7 +87,7 @@ struct TimelineTableView_Previews: PreviewProvider {
roomName: "Preview room")
static var previews: some View {
NavigationView {
NavigationStack {
RoomScreen(context: viewModel.context)
}
}
@@ -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).
}
@@ -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)
}