Skip to content

Commit

Permalink
Show transition on account switch (#1086)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjmarf committed Jun 9, 2024
1 parent 6cf19de commit be28ef2
Show file tree
Hide file tree
Showing 17 changed files with 249 additions and 73 deletions.
8 changes: 8 additions & 0 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
036CC3AF2B8145C30098B6A1 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CC3AE2B8145C30098B6A1 /* AppState.swift */; };
037386472BDAFE81007492B5 /* LemmyMarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 037386462BDAFE81007492B5 /* LemmyMarkdownUI */; };
037658DF2BE7D9EF00F4DD4D /* Community1Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037658DE2BE7D9EF00F4DD4D /* Community1Providing+Extensions.swift */; };
0380965F2C10AA80003ED1D8 /* AppState+Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */; };
038096612C10AAD8003ED1D8 /* TransitionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038096602C10AAD8003ED1D8 /* TransitionView.swift */; };
0382A7F02C09F0F800C79DDA /* PersonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7EF2C09F0F800C79DDA /* PersonView.swift */; };
0382A7F22C0A758E00C79DDA /* ProfileDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */; };
0382A7F42C0A76A900C79DDA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7F32C0A76A900C79DDA /* Date+Extensions.swift */; };
Expand Down Expand Up @@ -203,6 +205,8 @@
0369B35A2BFB86E3001EFEDF /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
036CC3AE2B8145C30098B6A1 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
037658DE2BE7D9EF00F4DD4D /* Community1Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Community1Providing+Extensions.swift"; sourceTree = "<group>"; };
0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Transition.swift"; sourceTree = "<group>"; };
038096602C10AAD8003ED1D8 /* TransitionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionView.swift; sourceTree = "<group>"; };
0382A7EF2C09F0F800C79DDA /* PersonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonView.swift; sourceTree = "<group>"; };
0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDateView.swift; sourceTree = "<group>"; };
0382A7F32C0A76A900C79DDA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -633,6 +637,7 @@
6363D5C627EE196700E34822 /* ContentView.swift */,
6363D5C427EE196700E34822 /* MlemApp.swift */,
B157E0C32A507B8000B02C8B /* FlowRoot.swift */,
038096602C10AAD8003ED1D8 /* TransitionView.swift */,
CDA1E81A2B8FC39A007953EF /* Onboarding */,
6363D5F427EE1BAE00E34822 /* Tabs */,
);
Expand Down Expand Up @@ -781,6 +786,7 @@
children = (
CD4D58B22B86BFD400B82964 /* AccountsTracker.swift */,
036CC3AE2B8145C30098B6A1 /* AppState.swift */,
0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */,
CD317D4B2BE97FED008F63E2 /* Palette.swift */,
CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */,
CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */,
Expand Down Expand Up @@ -1079,6 +1085,7 @@
CD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */,
03FE140C2BF953B000A8377F /* HandleError.swift in Sources */,
CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */,
038096612C10AAD8003ED1D8 /* TransitionView.swift in Sources */,
0382A7F42C0A76A900C79DDA /* Date+Extensions.swift in Sources */,
03D2A63B2C010B7500ED4FF2 /* GuestAccount.swift in Sources */,
CD4ED84A2BF1113800EFA0A2 /* View+TabReselectConsumer.swift in Sources */,
Expand Down Expand Up @@ -1163,6 +1170,7 @@
CD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */,
0382A7F02C09F0F800C79DDA /* PersonView.swift in Sources */,
CDBFCB6C2C054AA7008CD468 /* TilePostView.swift in Sources */,
0380965F2C10AA80003ED1D8 /* AppState+Transition.swift in Sources */,
CD4D58932B86BA5C00B82964 /* StandardPalette.swift in Sources */,
03D2A63F2C010DBF00ED4FF2 /* GuestSession.swift in Sources */,
030FF6812BC859FD00F6BFAC /* CustomTabViewHostingController.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Mlem/App/Constants/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,6 @@ enum Icons {
static let noFile: String = "questionmark.folder"
static let forward: String = "chevron.right"
static let imageDetails: String = "doc.badge.ellipsis"
static let accountSwitchReload: String = "arrow.2.circlepath"
static let accountSwitchKeepPlace: String = "checkmark.diamond"
}
38 changes: 38 additions & 0 deletions Mlem/App/Globals/Definitions/AppState+transition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// AppState+Transition.swift
// Mlem
//
// Created by Sjmarf on 05/06/2024.
//

import SwiftUI

extension AppState {
func transition(_ account: any Account) {
Task { @MainActor in
let transition = TransitionView(account: account)
guard let transitionView = UIHostingController(rootView: transition).view,
let window = UIApplication.shared.firstKeyWindow else {
return
}

transitionView.alpha = 0
window.addSubview(transitionView)
UIView.animate(withDuration: 0.15) {
transitionView.alpha = 1
}

transitionView.translatesAutoresizingMaskIntoConstraints = false
transitionView.heightAnchor.constraint(equalTo: window.heightAnchor).isActive = true
transitionView.widthAnchor.constraint(equalTo: window.widthAnchor).isActive = true

DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
UIView.animate(withDuration: 0.3) {
transitionView.alpha = 0
} completion: { _ in
transitionView.removeFromSuperview()
}
}
}
}
}
59 changes: 45 additions & 14 deletions Mlem/App/Globals/Definitions/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,61 @@
import Dependencies
import Foundation
import MlemMiddleware
import Observation
import SwiftUI

@Observable
class AppState {
private(set) var guestSession: GuestSession!
private(set) var activeSessions: [UserSession] = []
private(set) var guestSession: GuestSession! {
didSet {
if oldValue != guestSession {
oldValue?.deactivate()
}
}
}

private(set) var activeSessions: [UserSession] = [] {
didSet {
if oldValue != activeSessions {
for session in Set(oldValue).subtracting(activeSessions) {
session.deactivate()
}
}
}
}

/// ``ContentView`` watches this for changes. When it is toggled, the app is refreshed.
var appRefreshToggle: Bool = true

private init() {
self.guestSession = .init(account: AccountsTracker.main.defaultGuestAccount)
changeAccount(to: AccountsTracker.main.defaultAccount, deactivateOldGuest: false)
setAccount(to: AccountsTracker.main.defaultAccount)
}

func changeAccount(to account: any Account) {
changeAccount(to: account, deactivateOldGuest: true)
/// If `keepPlace` is `nil`, use the value from `UserDefaults`.
func changeAccount(to account: any Account, keepPlace: Bool? = nil) {
@AppStorage("accounts.keepPlace") var keepPlaceSetting = false
let keepPlace = keepPlace ?? keepPlaceSetting
if keepPlace {
ToastModel.main.add(.account(account))
setAccount(to: account)
} else {
transition(account)
// The delays between these events are necessary to stop SwiftUIIntrospect from causing a lag spike.
// That library seems to not like us adding subviews to the window directly. For some reason adding
// these delays fixes that.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.appRefreshToggle = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
self.setAccount(to: account)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
self.appRefreshToggle = true
}
}
}

private func changeAccount(to account: any Account, deactivateOldGuest: Bool) {
ToastModel.main.add(.account(account))

activeSessions.forEach { $0.deactivate() }
if deactivateOldGuest {
guestSession?.deactivate()
}

private func setAccount(to account: any Account) {
// Save because we updated `lastUsed` in the above `deactivate()` calls
AccountsTracker.main.saveAccounts(ofType: .all)

Expand Down
41 changes: 25 additions & 16 deletions Mlem/App/Logic/HandleError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,18 @@ func handleError(
#if DEBUG
print("☠️ ERROR ☠️")
print("📝 -> \(error.localizedDescription)")
if let error = error as? ApiClientError {
print(" \(error.description)")
}
print("📂 -> \(file) | \(function) | line: \(line)")
#endif

switch error {
// TODO: Modify MlemMiddleware to attach the ApiClient throwing the error to ApiClientError.invalidSession, so that we can access the relevant UserStub in a multi-account context
case ApiClientError.invalidSession:
if let user = AppState.main.firstSession.account as? UserAccount {
for layer in NavigationModel.main.layers {
switch layer.path.first {
case let .login(page):
switch page {
case .reauth:
return
default:
break
}
default:
break
}
}
NavigationModel.main.openSheet(.login(.reauth(user)))
}
showReauthSheet()
case ApiClientError.cancelled:
print("Cancellation error")
default:
if let errorDetails {
errorDetails.wrappedValue = .init(error: error)
Expand All @@ -48,3 +38,22 @@ func handleError(
}
}
}

private func showReauthSheet() {
if let user = AppState.main.firstSession.account as? UserAccount {
for layer in NavigationModel.main.layers {
switch layer.path.first {
case let .login(page):
switch page {
case .reauth:
return
default:
break
}
default:
break
}
}
NavigationModel.main.openSheet(.login(.reauth(user)))
}
}
8 changes: 8 additions & 0 deletions Mlem/App/Models/ErrorDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Combine
import MlemMiddleware
import SwiftUI
import UniformTypeIdentifiers

Expand All @@ -31,4 +32,11 @@ struct ErrorDetails: Hashable {
static func == (lhs: ErrorDetails, rhs: ErrorDetails) -> Bool {
lhs.hashValue == rhs.hashValue
}

var errorText: String {
if let error = error as? ApiClientError {
return error.description
}
return error?.localizedDescription ?? ""
}
}
42 changes: 22 additions & 20 deletions Mlem/App/Views/Root/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,28 @@ struct ContentView: View {
var navigationModel: NavigationModel { .main }

var body: some View {
content
.onReceive(timer) { _ in
appState.cleanCaches()
}
.sheet(isPresented: Binding(
get: { !(navigationModel.layers.first?.isFullScreenCover ?? true) },
set: { if !$0 { navigationModel.layers.removeAll() } }
)) {
NavigationLayerView(layer: navigationModel.layers[0], hasSheetModifiers: true)
}
.fullScreenCover(isPresented: Binding(
get: { navigationModel.layers.first?.isFullScreenCover ?? false },
set: { if !$0 { navigationModel.layers.removeAll() } }
)) {
NavigationLayerView(layer: navigationModel.layers[0], hasSheetModifiers: true)
}
.tint(palette.accent)
.environment(palette)
.environment(tabReselectTracker)
.environment(appState)
if appState.appRefreshToggle {
content
.onReceive(timer) { _ in
appState.cleanCaches()
}
.sheet(isPresented: Binding(
get: { !(navigationModel.layers.first?.isFullScreenCover ?? true) },
set: { if !$0 { navigationModel.layers.removeAll() } }
)) {
NavigationLayerView(layer: navigationModel.layers[0], hasSheetModifiers: true)
}
.fullScreenCover(isPresented: Binding(
get: { navigationModel.layers.first?.isFullScreenCover ?? false },
set: { if !$0 { navigationModel.layers.removeAll() } }
)) {
NavigationLayerView(layer: navigationModel.layers[0], hasSheetModifiers: true)
}
.tint(palette.accent)
.environment(palette)
.environment(tabReselectTracker)
.environment(appState)
}
}

var shouldDisplayToasts: Bool {
Expand Down
4 changes: 3 additions & 1 deletion Mlem/App/Views/Root/Onboarding/LandingPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ struct LandingPage: View {
//
// let newAccount = try await authenticatedApiClient.loadUser()
//
// // MARK: Save the account's credentials into the keychain

// MARK: Save the account's credentials into the keychain

//
// AppConstants.keychain["\(newAccount.id)_accessToken"] = response.jwt
// AccountsTracker.main.addAccount(account: newAccount)
Expand Down
7 changes: 0 additions & 7 deletions Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ struct MinimalPostFeedView: View {
}
}
}
.task(id: appState.firstApi) {
do {
try await postTracker.loadMoreItems()
} catch {
handleError(error)
}
}
.task(id: appState.firstApi) {
do {
try await postTracker.changeFeedType(to: .aggregateFeed(appState.firstApi, type: .subscribed))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ import SwiftUI
struct AccountListSettingsView: View {
@Environment(AppState.self) var appState

@AppStorage("accounts.keepPlace") var keepPlace: Bool = false

var accounts: [UserAccount] { AccountsTracker.main.userAccounts }

var body: some View {
Form {
headerView
AccountListView()
Section {
Toggle(
"Reload on Switch",
isOn: Binding(get: { !keepPlace }, set: { keepPlace = !$0 })
)
}
}
}

Expand Down
30 changes: 30 additions & 0 deletions Mlem/App/Views/Root/TransitionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// AccountTransitionView.swift
// Mlem
//
// Created by Eric Andrews on 2023-10-02.
//

import Foundation
import SwiftUI

struct TransitionView: View {
let account: any Account
@State var accountNameOpacity: CGFloat = .zero

var body: some View {
VStack(spacing: 24) {
Text(account is UserAccount ? "Welcome" : "Welcome to")
.onAppear {
withAnimation(.easeIn(duration: 0.5)) {
accountNameOpacity = 1.0
}
}
Text(account.nickname)
.opacity(accountNameOpacity)
}
.font(.largeTitle)
.bold()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
2 changes: 1 addition & 1 deletion Mlem/App/Views/Shared/Avatar/AvatarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct AvatarView: View {
.aspectRatio(1, contentMode: .fill)
.clipShape(Circle())
.background {
if url != nil, loading {
if url != nil, loading, showLoadingPlaceholder {
ProgressView()
} else if url == nil {
DefaultAvatarView(avatarType: type)
Expand Down
Loading

0 comments on commit be28ef2

Please sign in to comment.