mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Migration Screen (#1224)
- Hide bottom bar when in skeletons. - Use the new design for the Waitlist screen too.
This commit is contained in:
@@ -194,6 +194,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
// MARK: - Private
|
||||
|
||||
private static func setupServiceLocator(navigationRootCoordinator: NavigationRootCoordinator) {
|
||||
if ProcessInfo.processInfo.environment["RESET_APP_SETTINGS"].map(Bool.init) == true {
|
||||
AppSettings.reset()
|
||||
}
|
||||
|
||||
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController(rootCoordinator: navigationRootCoordinator))
|
||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
|
||||
@@ -211,10 +215,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
guard oldVersion != newVersion else { return }
|
||||
|
||||
if oldVersion < Version(1, 1, 0) {
|
||||
MXLog.info("Migrating to v1.1.0, signing out the user.")
|
||||
// Version 1.1.0 switched the Rust crypto store to SQLite
|
||||
// There are no migrations in place so we need to reset everything
|
||||
wipeUserData()
|
||||
}
|
||||
|
||||
if oldVersion < Version(1, 1, 7) {
|
||||
MXLog.info("Migrating to v1.1.0, marking accounts as migrated.")
|
||||
for userID in userSessionStore.userIDs {
|
||||
ServiceLocator.shared.settings.migratedAccounts[userID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the keychain, app support directory etc ready for a fresh use.
|
||||
@@ -338,7 +350,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactory())
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
userSessionFlowCoordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
|
||||
@@ -22,6 +22,7 @@ final class AppSettings {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
case migratedAccounts
|
||||
case timelineStyle
|
||||
case analyticsConsentState
|
||||
case enableInAppNotifications
|
||||
@@ -104,6 +105,13 @@ final class AppSettings {
|
||||
/// Any pre-defined static client registrations for OIDC issuers.
|
||||
let oidcStaticRegistrations: [URL: String] = ["https://id.thirdroom.io/realms/thirdroom": "elementx"]
|
||||
|
||||
/// A dictionary of accounts that have performed an initial sync through their proxy.
|
||||
///
|
||||
/// This is a temporary workaround. In the future we should be able to receive a signal from the
|
||||
/// proxy that it is the first sync (or that an upgrade on the backend will involve a slower sync).
|
||||
@UserPreference(key: UserDefaultsKeys.migratedAccounts, defaultValue: [:], storageType: .userDefaults(store))
|
||||
var migratedAccounts: [String: Bool]
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
var pusherAppId: String {
|
||||
|
||||
@@ -27,11 +27,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
||||
private let roomFlowCoordinator: RoomFlowCoordinator
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
private var migrationCancellable: AnyCancellable?
|
||||
|
||||
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
||||
private let detailNavigationStackCoordinator: NavigationStackCoordinator
|
||||
@@ -43,12 +45,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
init(userSession: UserSessionProtocol,
|
||||
navigationSplitCoordinator: NavigationSplitCoordinator,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol) {
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||
appSettings: AppSettings) {
|
||||
stateMachine = UserSessionFlowCoordinatorStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
self.bugReportService = bugReportService
|
||||
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||
self.appSettings = appSettings
|
||||
|
||||
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
@@ -78,7 +82,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
func start() {
|
||||
stateMachine.processEvent(.start)
|
||||
if appSettings.migratedAccounts[userSession.userID] != true {
|
||||
// Show the migration screen for a new account.
|
||||
stateMachine.processEvent(.startWithMigration)
|
||||
} else {
|
||||
// Otherwise go straight to the home screen.
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
@@ -90,13 +100,17 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// MARK: - FlowCoordinatorProtocol
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
// Tidy up any state before applying the new route.
|
||||
switch stateMachine.state {
|
||||
case .initial, .migration:
|
||||
return // Not ready to handle a route.
|
||||
case .roomList:
|
||||
break // Nothing to tidy up on the home screen.
|
||||
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||
case .roomList, .initial:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Apply the route.
|
||||
switch appRoute {
|
||||
case .room, .roomDetails, .roomList:
|
||||
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
@@ -122,6 +136,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .roomList):
|
||||
self.presentHomeScreen()
|
||||
|
||||
case (.initial, .startWithMigration, .migration):
|
||||
self.presentMigrationScreen() // Full screen cover
|
||||
self.presentHomeScreen() // Have the home screen ready to show underneath
|
||||
case (.migration, .completeMigration, .roomList):
|
||||
self.dismissMigrationScreen()
|
||||
|
||||
case(.roomList, .selectRoom, .roomList):
|
||||
break
|
||||
@@ -179,6 +199,25 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMigrationScreen() {
|
||||
// Listen for the first sync to finish.
|
||||
migrationCancellable = userSession.clientProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self, stateMachine.state == .migration, case .receivedSyncUpdate = callback else { return }
|
||||
migrationCancellable = nil
|
||||
appSettings.migratedAccounts[userSession.userID] = true
|
||||
stateMachine.processEvent(.completeMigration)
|
||||
}
|
||||
|
||||
let coordinator = MigrationScreenCoordinator()
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func dismissMigrationScreen() {
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func presentHomeScreen() {
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
|
||||
@@ -23,6 +23,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
/// The initial state, used before the coordinator starts
|
||||
case initial
|
||||
|
||||
/// Showing the migration screen whilst the proxy performs an initial sync.
|
||||
case migration
|
||||
|
||||
/// Showing the home screen. The `selectedRoomId` represents the timeline shown on the detail panel (if any)
|
||||
case roomList(selectedRoomId: String?)
|
||||
|
||||
@@ -51,6 +54,11 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
/// Start the user session flows
|
||||
case start
|
||||
|
||||
/// Start the user session flows with a migration screen.
|
||||
case startWithMigration
|
||||
/// Request to transition from the migration state to the home screen.
|
||||
case completeMigration
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
case selectRoom(roomId: String)
|
||||
@@ -97,7 +105,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
|
||||
|
||||
stateMachine.addRoutes(event: .startWithMigration, transitions: [.initial => .migration])
|
||||
stateMachine.addRoutes(event: .completeMigration, transitions: [.migration => .roomList(selectedRoomId: nil)])
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (event, fromState) {
|
||||
case (.selectRoom(let roomId), .roomList):
|
||||
|
||||
@@ -40,6 +40,7 @@ internal enum Asset {
|
||||
internal static let locationMarker = ImageAsset(name: "images/location-marker")
|
||||
internal static let locationPin = ImageAsset(name: "images/location-pin")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
|
||||
internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient")
|
||||
}
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||
|
||||
@@ -644,6 +644,10 @@ public enum L10n {
|
||||
public static var screenMediaUploadPreviewErrorFailedProcessing: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_processing") }
|
||||
/// Failed uploading media, please try again.
|
||||
public static var screenMediaUploadPreviewErrorFailedSending: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_sending") }
|
||||
/// This is a one time process, thanks for waiting.
|
||||
public static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") }
|
||||
/// Setting up your account.
|
||||
public static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") }
|
||||
/// Sign in manually
|
||||
public static var screenOnboardingSignInManually: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_manually") }
|
||||
/// Sign in with QR code
|
||||
@@ -832,13 +836,13 @@ public enum L10n {
|
||||
public static func screenWaitlistMessage(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_waitlist_message", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// Welcome to %1$@
|
||||
/// Welcome to %1$@!
|
||||
public static func screenWaitlistMessageSuccess(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_waitlist_message_success", String(describing: p1))
|
||||
}
|
||||
/// You're on the waitlist!
|
||||
/// You’re almost there.
|
||||
public static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") }
|
||||
/// You're in!
|
||||
/// You're in.
|
||||
public static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") }
|
||||
/// Looks like you’re using a new device. Verify it’s you to access your encrypted messages.
|
||||
public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") }
|
||||
|
||||
@@ -84,9 +84,9 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
let velocity = scrollView.panGestureRecognizer.velocity(in: nil)
|
||||
|
||||
if velocity.y > Constant.scrollDirectionThreshold {
|
||||
scrollDirectionSubject.send(.down)
|
||||
} else if velocity.y < -Constant.scrollDirectionThreshold {
|
||||
scrollDirectionSubject.send(.up)
|
||||
} else if velocity.y < -Constant.scrollDirectionThreshold {
|
||||
scrollDirectionSubject.send(.down)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A standardised layout for waiting dialogs shown during the onboarding flow.
|
||||
struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
/// Not using the default to allow the gradient to fill edge to edge.
|
||||
private let horizontalPadding = 16.0
|
||||
/// The top padding of the content, used to keep the text below the image.
|
||||
private var contentTopPadding: CGFloat {
|
||||
if verticalSizeClass == .compact {
|
||||
// Reduced value for iPhones in landscape.
|
||||
return UIConstants.onboardingBreakerScreenTopPadding
|
||||
} else if horizontalSizeClass == .compact {
|
||||
// The default value for portrait iPhones.
|
||||
return 2 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
} else {
|
||||
// Larger on iPad specifically for 11" in Landscape.
|
||||
return 2.7 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
}
|
||||
}
|
||||
|
||||
/// The main content shown at the top of the layout.
|
||||
@ViewBuilder var content: () -> Content
|
||||
/// The content shown at the bottom of the layout.
|
||||
@ViewBuilder var bottomContent: () -> BottomContent
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.onboardingBreakerScreenTopPadding, horizontalPadding: 0) {
|
||||
content()
|
||||
.padding(.top, contentTopPadding)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
} bottomContent: {
|
||||
bottomContent()
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
.background {
|
||||
background
|
||||
.ignoresSafeArea(edges: [.horizontal, .bottom])
|
||||
}
|
||||
.ignoresSafeArea(edges: [.horizontal, .bottom])
|
||||
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
|
||||
.environment(\.colorScheme, .dark)
|
||||
.toolbar(.visible, for: .navigationBar) // Layout consistency in all states.
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.preferredColorScheme(.light) // Has no effect on Previews, works fine in-app.
|
||||
}
|
||||
|
||||
var background: some View {
|
||||
// Negative spacing to hide a rendering bug that shows the white background.
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: -5) {
|
||||
Color.white
|
||||
.frame(maxHeight: UIConstants.onboardingBreakerScreenTopPadding)
|
||||
|
||||
Image(asset: Asset.Images.waitingGradient)
|
||||
.resizable()
|
||||
.frame(width: horizontalSizeClass == .compact ? nil : geometry.size.width) // Forced for landscape iPhone and nicer iPad.
|
||||
.scaledToFit()
|
||||
.layoutPriority(1) // Force for all landscape layouts.
|
||||
|
||||
Color.compound.bgCanvasDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WaitingDialog_Previews: PreviewProvider {
|
||||
static let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
WaitlistScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,42 +20,29 @@ struct WaitlistScreen: View {
|
||||
@ObservedObject var context: WaitlistScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.iconTopPaddingToNavigationBar) {
|
||||
header
|
||||
WaitingDialog {
|
||||
content
|
||||
} bottomContent: {
|
||||
buttons
|
||||
}
|
||||
.background()
|
||||
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
|
||||
.navigationBarBackButtonHidden()
|
||||
.toolbar { toolbar }
|
||||
.toolbar(.visible, for: .navigationBar) // Layout consistency in all states.
|
||||
.overlay {
|
||||
EffectsView(effect: context.viewState.isWaiting ? .none : .confetti)
|
||||
.ignoresSafeArea()
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The main content of the view to be shown in a scroll view.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
AuthenticationIconImage(image: Image(systemName: context.viewState.iconSymbolName))
|
||||
.fontWeight(.semibold)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(context.viewState.title)
|
||||
.font(.compound.headingMDBold)
|
||||
var content: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text(context.viewState.title.tinting(".", color: .element.brand))
|
||||
.font(.compound.headingXLBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(context.viewState.message)
|
||||
.font(.compound.bodyMD)
|
||||
.font(.compound.bodyLG)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
/// The action buttons shown at the bottom of the view.
|
||||
|
||||
@@ -26,9 +26,17 @@ struct HomeScreen: View {
|
||||
@ObservedObject var context: HomeScreenViewModel.Context
|
||||
|
||||
@State private var scrollViewAdapter = ScrollViewAdapter()
|
||||
@State private var showingBottomToolbar = true
|
||||
@State private var lastScrollDirection: ScrollViewAdapter.ScrollDirection = .up
|
||||
@State private var isSearching = false
|
||||
|
||||
var bottomBarVisibility: Visibility {
|
||||
if lastScrollDirection == .up, context.viewState.roomListMode == .rooms {
|
||||
return .automatic
|
||||
} else {
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
@@ -81,7 +89,7 @@ struct HomeScreen: View {
|
||||
}
|
||||
.onReceive(scrollViewAdapter.scrollDirection) { direction in
|
||||
withAnimation(.elementDefault) {
|
||||
showingBottomToolbar = (direction == .down)
|
||||
lastScrollDirection = direction
|
||||
}
|
||||
}
|
||||
.onChange(of: context.viewState.visibleRooms) { _ in
|
||||
@@ -100,7 +108,7 @@ struct HomeScreen: View {
|
||||
actions: leaveRoomAlertActions,
|
||||
message: leaveRoomAlertMessage)
|
||||
.navigationTitle(L10n.screenRoomlistMainSpaceTitle)
|
||||
.toolbar(showingBottomToolbar ? .automatic : .hidden, for: .bottomBar)
|
||||
.toolbar(bottomBarVisibility, for: .bottomBar)
|
||||
.toolbar { toolbar }
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.track(screen: .home)
|
||||
@@ -117,6 +125,8 @@ struct HomeScreen: View {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Spacer()
|
||||
newRoomButton
|
||||
// Fix position animating on loop by getting caught up in the shimmer effect somehow.
|
||||
.animation(.noAnimation, value: context.viewState.roomListMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
final class MigrationScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: MigrationScreenViewModelProtocol
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
|
||||
init() {
|
||||
viewModel = MigrationScreenViewModel()
|
||||
}
|
||||
|
||||
func start() { }
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(MigrationScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MigrationScreenViewModelAction { }
|
||||
|
||||
struct MigrationScreenViewState: BindableState { }
|
||||
|
||||
enum MigrationScreenViewAction { }
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias MigrationScreenViewModelType = StateStoreViewModel<MigrationScreenViewState, MigrationScreenViewAction>
|
||||
|
||||
class MigrationScreenViewModel: MigrationScreenViewModelType, MigrationScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<MigrationScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<MigrationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: MigrationScreenViewState())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol MigrationScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<MigrationScreenViewModelAction, Never> { get }
|
||||
var context: MigrationScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MigrationScreen: View {
|
||||
@ObservedObject var context: MigrationScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
WaitingDialog {
|
||||
content
|
||||
} bottomContent: {
|
||||
EmptyView()
|
||||
}
|
||||
.navigationBarBackButtonHidden()
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.tint(.compound.iconPrimary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(L10n.screenMigrationTitle.tinting(".", color: .element.brand))
|
||||
.font(.compound.headingXLBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
|
||||
Text(L10n.screenMigrationMessage)
|
||||
.font(.compound.bodyLG)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct MigrationScreen_Previews: PreviewProvider {
|
||||
static let viewModel = MigrationScreenViewModel()
|
||||
static var previews: some View {
|
||||
MigrationScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
/// Whether or not there are sessions in the store.
|
||||
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
|
||||
/// All the user IDs managed by the store.
|
||||
var userIDs: [String] { keychainController.restorationTokens().map(\.userID) }
|
||||
|
||||
/// The base directory where all session data is stored.
|
||||
let baseDirectory: URL
|
||||
|
||||
@@ -31,6 +31,9 @@ protocol UserSessionStoreProtocol {
|
||||
/// Whether or not there are sessions in the store.
|
||||
var hasSessions: Bool { get }
|
||||
|
||||
/// All the user IDs managed by the store.
|
||||
var userIDs: [String] { get }
|
||||
|
||||
/// Returns the location to store user data for a particular username.
|
||||
var baseDirectory: URL { get }
|
||||
|
||||
|
||||
@@ -113,6 +113,9 @@ class MockScreen: Identifiable {
|
||||
analytics: ServiceLocator.shared.analytics))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .migration:
|
||||
let coordinator = MigrationScreenCoordinator()
|
||||
return coordinator
|
||||
case .authenticationFlow:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
@@ -326,7 +329,8 @@ class MockScreen: Identifiable {
|
||||
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
bugReportService: BugReportServiceMock(),
|
||||
roomTimelineControllerFactory: MockRoomTimelineControllerFactory())
|
||||
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
coordinator.start()
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case waitlist
|
||||
case analyticsPrompt
|
||||
case analyticsSettingsScreen
|
||||
case migration
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
case home
|
||||
|
||||
Reference in New Issue
Block a user