Skip to content

Commit

Permalink
Allow display of user avatar in tab bar (mlemgroup#593)
Browse files Browse the repository at this point in the history
  • Loading branch information
mormaer authored Sep 11, 2023
1 parent 3eac027 commit e0baae4
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 48 deletions.
36 changes: 32 additions & 4 deletions Mlem/App State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,58 @@ import Foundation
import SwiftUI

class AppState: ObservableObject {
@Dependency(\.accountsTracker) var accountsTracker
@Dependency(\.apiClient) var apiClient

@AppStorage("defaultAccountId") var defaultAccountId: Int?
@AppStorage("profileTabLabel") var profileTabLabel: ProfileTabLabel = .username
@AppStorage("showUserAvatarOnProfileTab") var showUserAvatar: Bool = true

@Published private(set) var currentActiveAccount: SavedAccount?
@Published private(set) var currentNickname: String?

/// A method to set the current active account
/// A variable representing how the current account should be displayed in the tab bar
var tabDisplayName: String {
guard let currentActiveAccount else {
return "Profile"
}

switch profileTabLabel {
case .username:
return currentActiveAccount.username
case .instance:
return currentActiveAccount.hostName ?? "Instance"
case .nickname:
return currentActiveAccount.nickname
case .anonymous:
return "Profile"
}
}

/// A variable representing the remote location to load the user profile avatar from if applicable
var profileTabRemoteSymbolUrl: URL? {
guard profileTabLabel != .anonymous, showUserAvatar else {
return nil
}

return currentActiveAccount?.avatarUrl
}

/// A method to set the current active account, any changes to the account will be propogated to the persistence layer
/// - Important: If you wish to _clear_ the current active account please use the `\.setAppFlow` method available via the environment to reset to our `.onboarding` flow
/// - Parameter account: The `SavedAccount` which should become the active account
func setActiveAccount(_ account: SavedAccount) {
AppConstants.keychain["\(account.id)_accessToken"] = account.accessToken
// we configure the client here to ensure any updated session tokens are updated
apiClient.configure(for: .account(account))
currentActiveAccount = account
currentNickname = account.nickname
defaultAccountId = account.id
accountsTracker.update(with: account)
}

/// A method to clear the currentlly active account
/// - Important: It is unlikely you will want to call this method directly but instead use the `\.setAppFlow` method available via the environment
func clearActiveAccount() {
currentActiveAccount = nil
currentNickname = nil
}

func isCurrentAccountId(_ id: Int) -> Bool {
Expand Down
30 changes: 10 additions & 20 deletions Mlem/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ struct ContentView: View {

@AppStorage("showInboxUnreadBadge") var showInboxUnreadBadge: Bool = true
@AppStorage("homeButtonExists") var homeButtonExists: Bool = false
@AppStorage("profileTabLabel") var profileTabLabel: ProfileTabLabel = .username

var accessibilityFont: Bool { UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory }

Expand All @@ -43,8 +42,7 @@ struct ContentView: View {
.fancyTabItem(tag: TabSelection.feeds) {
FancyTabBarLabel(
tag: TabSelection.feeds,
symbolName: "scroll",
activeSymbolName: "scroll.fill"
symbolConfiguration: .feed
)
}

Expand All @@ -56,8 +54,7 @@ struct ContentView: View {
.fancyTabItem(tag: TabSelection.inbox) {
FancyTabBarLabel(
tag: TabSelection.inbox,
symbolName: "mail.stack",
activeSymbolName: "mail.stack.fill",
symbolConfiguration: .inbox,
badgeCount: showInboxUnreadBadge ? unreadTracker.total : 0
)
}
Expand All @@ -66,9 +63,12 @@ struct ContentView: View {
.fancyTabItem(tag: TabSelection.profile) {
FancyTabBarLabel(
tag: TabSelection.profile,
customText: computeUsername(account: account),
symbolName: "person.circle",
activeSymbolName: "person.circle.fill"
customText: appState.tabDisplayName,
symbolConfiguration: .init(
symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol,
activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol,
remoteSymbolUrl: appState.profileTabRemoteSymbolUrl
)
)
.simultaneousGesture(accountSwitchLongPress)
}
Expand All @@ -78,16 +78,15 @@ struct ContentView: View {
.fancyTabItem(tag: TabSelection.search) {
FancyTabBarLabel(
tag: TabSelection.search,
symbolName: "magnifyingglass",
activeSymbolName: "text.magnifyingglass"
symbolConfiguration: .search
)
}

SettingsView()
.fancyTabItem(tag: TabSelection.settings) {
FancyTabBarLabel(
tag: TabSelection.settings,
symbolName: "gear"
symbolConfiguration: .settings
)
}
}
Expand Down Expand Up @@ -152,15 +151,6 @@ struct ContentView: View {
}
}

func computeUsername(account: SavedAccount) -> String {
switch profileTabLabel {
case .username: return account.username
case .instance: return account.hostName ?? account.username
case .nickname: return appState.currentNickname ?? account.username
case .anonymous: return "Profile"
}
}

func showAccountSwitcherDragCallback() {
if !homeButtonExists {
isPresentingAccountSwitcher = true
Expand Down
49 changes: 41 additions & 8 deletions Mlem/Custom Tab Bar/FancyTabBarLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,25 @@ import Foundation
import SwiftUI

struct FancyTabBarLabel: View {
struct SymbolConfiguration {
let symbol: String
let activeSymbol: String
let remoteSymbolUrl: URL?

static var feed: Self { .init(symbol: "scroll", activeSymbol: "scroll.fill", remoteSymbolUrl: nil) }
static var inbox: Self { .init(symbol: "mail.stack", activeSymbol: "mail.stack.fill", remoteSymbolUrl: nil) }
static var profile: Self { .init(symbol: "person.circle", activeSymbol: "person.circle.fill", remoteSymbolUrl: nil) }
static var search: Self { .init(symbol: "magnifyingglass", activeSymbol: "text.magnifyingglass", remoteSymbolUrl: nil) }
static var settings: Self { .init(symbol: "gear", activeSymbol: "gear", remoteSymbolUrl: nil) }
}

@Environment(\.tabSelectionHashValue) private var selectedTagHashValue
@AppStorage("showTabNames") var showTabNames: Bool = true

let tabIconSize: CGFloat = 24

let tagHash: Int
let symbolName: String?
let activeSymbolName: String?
let symbolConfiguration: SymbolConfiguration
let labelText: String?
let color: Color
let activeColor: Color
Expand All @@ -37,15 +48,13 @@ struct FancyTabBarLabel: View {
init(
tag: any FancyTabBarSelection,
customText: String? = nil,
symbolName: String? = nil,
activeSymbolName: String? = nil,
symbolConfiguration: SymbolConfiguration,
customColor: Color = Color.primary,
activeColor: Color = .accentColor,
badgeCount: Int? = nil
) {
self.tagHash = tag.hashValue
self.symbolName = symbolName
self.activeSymbolName = activeSymbolName
self.symbolConfiguration = symbolConfiguration
self.labelText = customText ?? tag.labelText
self.color = customColor
self.activeColor = activeColor
Expand All @@ -66,10 +75,26 @@ struct FancyTabBarLabel: View {
.animation(.linear(duration: 0.1), value: active)
}

@ViewBuilder
var labelDisplay: some View {
VStack(spacing: 4) {
if let symbolName {
Image(systemName: active ? activeSymbolName ?? symbolName : symbolName)
if let remoteSymbolUrl = symbolConfiguration.remoteSymbolUrl {
CachedImage(
url: remoteSymbolUrl,
shouldExpand: false,
fixedSize: .init(width: tabIconSize, height: tabIconSize),
imageNotFound: { defaultTabIcon(for: symbolConfiguration) },
errorBackgroundColor: .clear,
contentMode: .fill
)
.frame(width: tabIconSize, height: tabIconSize)
.clipShape(Circle())
.overlay(Circle()
.stroke(.gray.opacity(0.3), lineWidth: 1))
.opacity(active ? 1 : 0.7)
.accessibilityHidden(true)
} else {
Image(systemName: active ? symbolConfiguration.activeSymbol : symbolConfiguration.symbol)
.resizable()
.scaledToFit()
.frame(width: tabIconSize, height: tabIconSize)
Expand All @@ -81,4 +106,12 @@ struct FancyTabBarLabel: View {
}
}
}

private func defaultTabIcon(for configuration: SymbolConfiguration) -> AnyView {
AnyView(Image(systemName: active ? configuration.activeSymbol : configuration.symbol)
.resizable()
.scaledToFill()
.frame(width: tabIconSize, height: tabIconSize)
)
}
}
10 changes: 8 additions & 2 deletions Mlem/Models/Saved Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,37 @@ struct SavedAccount: Identifiable, Codable, Equatable, Hashable {
let accessToken: String
let username: String
let storedNickname: String?
let avatarUrl: URL?

init(
id: Int,
instanceLink: URL,
accessToken: String,
username: String,
storedNickname: String? = nil
storedNickname: String? = nil,
avatarUrl: URL? = nil
) {
self.id = id
self.instanceLink = instanceLink
self.accessToken = accessToken
self.username = username
self.storedNickname = storedNickname
self.avatarUrl = avatarUrl
}

// Convenience initializer to create an equal copy with different non-identifying properties.
init(
from account: SavedAccount,
accessToken: String? = nil,
storedNickname: String? = nil
storedNickname: String? = nil,
avatarUrl: URL?
) {
self.id = account.id
self.instanceLink = account.instanceLink
self.accessToken = accessToken ?? account.accessToken
self.username = account.username
self.storedNickname = storedNickname ?? account.storedNickname
self.avatarUrl = avatarUrl
}

// convenience
Expand All @@ -55,6 +60,7 @@ struct SavedAccount: Identifiable, Codable, Equatable, Hashable {
try container.encode("redacted", forKey: .accessToken)
try container.encode(username, forKey: .username)
try container.encode(storedNickname, forKey: .storedNickname)
try container.encode(avatarUrl, forKey: .avatarUrl)
}

static func == (lhs: SavedAccount, rhs: SavedAccount) -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Repositories/PersistenceRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class PersistenceRepository {
return nil
}

return SavedAccount(from: account, accessToken: token)
return SavedAccount(from: account, accessToken: token, avatarUrl: account.avatarUrl)
}
}

Expand Down
11 changes: 6 additions & 5 deletions Mlem/Views/Shared/Accounts/Add Account View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,13 @@ struct AddSavedInstanceView: View {
viewState = .success
}

let newAccount = try await SavedAccount(
id: getUserID(authToken: response.jwt, instanceURL: instanceURL),
let user = try await loadUser(authToken: response.jwt, instanceURL: instanceURL)
let newAccount = SavedAccount(
id: user.id,
instanceLink: instanceURL,
accessToken: response.jwt,
username: username
username: username,
avatarUrl: user.avatar
)

// MARK: - Save the account's credentials into the keychain
Expand All @@ -336,14 +338,13 @@ struct AddSavedInstanceView: View {
}
}

private func getUserID(authToken: String, instanceURL: URL) async throws -> Int {
private func loadUser(authToken: String, instanceURL: URL) async throws -> APIPerson {
// create a session to use for this request, since we're in the process of creating the account...
let session = APISession.authenticated(instanceURL, authToken)
do {
return try await apiClient.getPersonDetails(session: session, username: username)
.personView
.person
.id
} catch {
print("getUserId Error info: \(error)")
throw UserIDRetrievalError.couldNotFetchUserInformation
Expand Down
5 changes: 4 additions & 1 deletion Mlem/Views/Shared/Cached Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct CachedImage: View {
let screenWidth: CGFloat
let contentMode: ContentMode
let cornerRadius: CGFloat
let errorBackgroundColor: Color

// Optional callback triggered when the quicklook preview is dismissed
let dismissCallback: (() -> Void)?
Expand All @@ -37,6 +38,7 @@ struct CachedImage: View {
maxHeight: CGFloat = .infinity,
fixedSize: CGSize? = nil,
imageNotFound: @escaping () -> AnyView = imageNotFoundDefault,
errorBackgroundColor: Color = Color(uiColor: .systemGray4),
contentMode: ContentMode = .fit,
dismissCallback: (() -> Void)? = nil,
cornerRadius: CGFloat? = nil
Expand All @@ -45,6 +47,7 @@ struct CachedImage: View {
self.shouldExpand = shouldExpand
self.maxHeight = maxHeight
self.imageNotFound = imageNotFound
self.errorBackgroundColor = errorBackgroundColor
self.contentMode = contentMode
self.dismissCallback = dismissCallback
self.cornerRadius = cornerRadius ?? 0
Expand Down Expand Up @@ -140,7 +143,7 @@ struct CachedImage: View {
// Indicates an error
imageNotFound()
.frame(idealWidth: size.width, maxHeight: size.height)
.background(Color(uiColor: .systemGray4))
.background(errorBackgroundColor)
} else {
ProgressView() // Acts as a placeholder
.frame(idealWidth: size.width, maxHeight: size.height)
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Views/Shared/TokenRefreshView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ struct TokenRefreshView: View {
try? await Task.sleep(for: .seconds(0.5))

await MainActor.run {
refreshedAccount(.init(from: account, accessToken: newToken))
refreshedAccount(.init(from: account, accessToken: newToken, avatarUrl: account.avatarUrl))
dismiss()
}
}
Expand Down
Loading

0 comments on commit e0baae4

Please sign in to comment.