mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Update login and server selection screens (#651)
* Update login and server selection screens. * Replace all NavigationViews with NavigationStacks
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+12
-2
@@ -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.
|
||||
|
||||
+7
-8
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user