Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow display of user avatar in tab bar #593

Merged
merged 5 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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