diff --git a/Mlem/API/Models/Person/APIPerson.swift b/Mlem/API/Models/Person/APIPerson.swift index b719d34b6..278f2c02c 100644 --- a/Mlem/API/Models/Person/APIPerson.swift +++ b/Mlem/API/Models/Person/APIPerson.swift @@ -8,7 +8,7 @@ import Foundation // lemmy_db_schema::source::person::PersonSafe -struct APIPerson: Decodable, Identifiable, Hashable { +struct APIPerson: Decodable, Identifiable, Hashable, Equatable { let id: Int let name: String var displayName: String? @@ -29,12 +29,6 @@ struct APIPerson: Decodable, Identifiable, Hashable { let instanceId: Int } -extension APIPerson: Equatable { - static func == (lhs: APIPerson, rhs: APIPerson) -> Bool { - lhs.actorId == rhs.actorId - } -} - extension APIPerson { var avatarUrl: URL? { LemmyURL(string: avatar)?.url } var bannerUrl: URL? { LemmyURL(string: banner)?.url } diff --git a/Mlem/ContentView.swift b/Mlem/ContentView.swift index 44426dff5..8e0326a1d 100644 --- a/Mlem/ContentView.swift +++ b/Mlem/ContentView.swift @@ -42,14 +42,6 @@ struct ContentView: View { var accessibilityFont: Bool { UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory } - var myUser: UserModel? { - if let person = siteInformation.myUserInfo?.localUserView.person { - return UserModel(from: person) - } else { - return nil - } - } - var body: some View { FancyTabBar(selection: $tabSelection, navigationSelection: $tabNavigation, dragUpGestureCallback: showAccountSwitcherDragCallback) { Group { @@ -75,19 +67,19 @@ struct ContentView: View { } } - ProfileView(user: myUser) - .fancyTabItem(tag: TabSelection.profile) { - FancyTabBarLabel( - tag: TabSelection.profile, - customText: appState.tabDisplayName, - symbolConfiguration: .init( - symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol, - activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol, - remoteSymbolUrl: appState.profileTabRemoteSymbolUrl - ) + ProfileView() + .fancyTabItem(tag: TabSelection.profile) { + FancyTabBarLabel( + tag: TabSelection.profile, + customText: appState.tabDisplayName, + symbolConfiguration: .init( + symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol, + activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol, + remoteSymbolUrl: appState.profileTabRemoteSymbolUrl ) - .simultaneousGesture(accountSwitchLongPress) - } + ) + .simultaneousGesture(accountSwitchLongPress) + } SearchRoot() .fancyTabItem(tag: TabSelection.search) { diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index 10ca52888..ca396483d 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -27,7 +27,8 @@ struct InstanceModel { mutating func update(with response: SiteResponse) { self.administrators = response.admins.map { - var user = UserModel(from: $0, usesExternalData: true) + var user = UserModel(from: $0) + user.usesExternalData = true user.isAdmin = true return user } diff --git a/Mlem/Models/Content/User/UserModel.swift b/Mlem/Models/Content/User/UserModel.swift index 3227664e7..142dfd4f3 100644 --- a/Mlem/Models/Content/User/UserModel.swift +++ b/Mlem/Models/Content/User/UserModel.swift @@ -17,37 +17,37 @@ struct UserModel { @Dependency(\.notifier) var notifier @available(*, deprecated, message: "Use attributes of the UserModel directly instead.") - var person: APIPerson + var person: APIPerson! // Ids - let userId: Int - let instanceId: Int - let matrixUserId: String? + var userId: Int! + var instanceId: Int! + var matrixUserId: String? // Text - let name: String - let displayName: String - let bio: String? + var name: String! + var displayName: String! + var bio: String? // Images - let avatar: URL? - let banner: URL? + var avatar: URL? + var banner: URL? // State - let banned: Bool - let local: Bool - let deleted: Bool - let isBot: Bool - var blocked: Bool + var banned: Bool! + var local: Bool! + var deleted: Bool! + var isBot: Bool! + var blocked: Bool! // Dates - let creationDate: Date - let updatedDate: Date? - let banExpirationDate: Date? + var creationDate: Date! + var updatedDate: Date? + var banExpirationDate: Date? // URLs - let profileUrl: URL - let sharedInboxUrl: URL? + var profileUrl: URL! + var sharedInboxUrl: URL? // From APIPersonView var isAdmin: Bool? @@ -73,32 +73,41 @@ struct UserModel { /// Creates a UserModel from an GetPersonDetailsResponse /// - Parameter response: GetPersonDetailsResponse to create a UserModel representation of init(from response: GetPersonDetailsResponse) { - self.init(from: response.personView) - self.site = response.site - self.moderatedCommunities = response.moderates.map { CommunityModel(from: $0.community) } + self.update(with: response) } /// Creates a UserModel from an APIPersonView /// - Parameter apiPersonView: APIPersonView to create a UserModel representation of - init(from personView: APIPersonView, usesExternalData: Bool = false) { - self.init(from: personView.person) - + init(from personView: APIPersonView) { + self.update(with: personView) + } + + /// Creates a UserModel from an APIPerson. Note that using this initialiser nullifies count values, since + /// those are only accessable from APIPersonView. + /// - Parameter apiPerson: APIPerson to create a UserModel representation of + init(from person: APIPerson) { + update(with: person) + } + + mutating func update(with response: GetPersonDetailsResponse) { + self.moderatedCommunities = response.moderates.map { CommunityModel(from: $0.community) } + self.update(with: response.personView) + } + + mutating func update(with personView: APIPersonView) { self.postCount = personView.counts.postCount self.commentCount = personView.counts.commentCount - - self.usesExternalData = usesExternalData // TODO: 0.18 Deprecation @Dependency(\.siteInformation) var siteInformation if (siteInformation.version ?? .infinity) > .init("0.19.0") { self.isAdmin = personView.isAdmin } + + self.update(with: personView.person) } - /// Creates a UserModel from an APIPerson. Note that using this initialiser nullifies count values, since - /// those are only accessable from APIPersonView. - /// - Parameter apiPerson: APIPerson to create a UserModel representation of - init(from person: APIPerson) { + mutating func update(with person: APIPerson) { self.person = person self.userId = person.id @@ -128,7 +137,9 @@ struct UserModel { // Annoyingly, PersonView doesn't include whether the user is blocked so we can't // actually determine this without making extra requests... - self.blocked = false + if self.blocked == nil { + self.blocked = false + } } // Once we've done other model types we should stop this from relying on API types @@ -186,7 +197,7 @@ struct UserModel { var fullyQualifiedUsername: String? { if let host = self.profileUrl.host() { - return "\(name)@\(host)" + return "\(name!)@\(host)" } return nil } @@ -221,5 +232,10 @@ extension UserModel: Hashable { hasher.combine(blocked) hasher.combine(postCount) hasher.combine(commentCount) + hasher.combine(displayName) + hasher.combine(bio) + hasher.combine(avatar) + hasher.combine(banner) + hasher.combine(matrixUserId) } } diff --git a/Mlem/Views/Tabs/Profile/Profile View.swift b/Mlem/Views/Tabs/Profile/Profile View.swift index ff33b1924..49385d6b9 100644 --- a/Mlem/Views/Tabs/Profile/Profile View.swift +++ b/Mlem/Views/Tabs/Profile/Profile View.swift @@ -6,22 +6,60 @@ // import SwiftUI +import Dependencies struct ProfileView: View { + // appstorage + @Dependency(\.siteInformation) var siteInformation + @AppStorage("shouldShowUserHeaders") var shouldShowUserHeaders: Bool = true - let user: UserModel? - @StateObject private var profileTabNavigation: AnyNavigationPath = .init() + @StateObject private var editorSheetNavigation: AnyNavigationPath = .init() + @StateObject private var navigation: Navigation = .init() + @StateObject private var sheetNavigation: Navigation = .init() + + @State var isPresentingAccountSwitcher: Bool = false + @State var isPresentingProfileEditor: Bool = false var body: some View { ScrollViewReader { proxy in NavigationStack(path: $profileTabNavigation.path) { - if let user { - UserView(user: user) + if let person = siteInformation.myUserInfo?.localUserView.person { + UserView(user: UserModel(from: person)) .handleLemmyViews() .environmentObject(profileTabNavigation) .tabBarNavigationEnabled(.profile, navigation) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Switch Account", systemImage: Icons.switchUser) { + isPresentingAccountSwitcher = true + } + } + // TODO: 0.17 deprecation + if (siteInformation.version ?? .infinity) >= .init("0.18.0") { + ToolbarItem(placement: .secondaryAction) { + Button("Edit", systemImage: Icons.edit) { + isPresentingProfileEditor = true + } + } + } + } + .sheet(isPresented: $isPresentingAccountSwitcher) { + Form { + AccountListView() + } + } + .sheet(isPresented: $isPresentingProfileEditor) { + NavigationStack(path: $editorSheetNavigation.path) { + ProfileSettingsView(showCloseButton: true) + .handleLemmyViews() + .environmentObject(editorSheetNavigation) + } + .handleLemmyLinkResolution(navigationPath: .constant(editorSheetNavigation)) + .environment(\.navigationPathWithRoutes, $editorSheetNavigation.path) + .environment(\.navigation, sheetNavigation) + } } else { LoadingView(whatIsLoading: .profile) .fancyTabScrollCompatible() diff --git a/Mlem/Views/Tabs/Profile/UserFeedView.swift b/Mlem/Views/Tabs/Profile/UserFeedView.swift index 60f397636..c27b3c872 100644 --- a/Mlem/Views/Tabs/Profile/UserFeedView.swift +++ b/Mlem/Views/Tabs/Profile/UserFeedView.swift @@ -43,7 +43,7 @@ struct UserFeedView: View { switch selectedTab { case .communities: Label( - "\(user.displayName) moderates \(communityTracker.items.count) communities.", + "\(user.displayName) moderates ^[\(communityTracker.items.count) communities](inflect: true).", systemImage: Icons.moderationFill ) .foregroundStyle(.secondary) diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index 65d061b2a..55351abbd 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -8,7 +8,6 @@ import Dependencies import SwiftUI -// swiftlint:disable type_body_length struct UserView: View { @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler @@ -151,25 +150,10 @@ struct UserView: View { confirmationMenuFunction: confirmationMenuFunction ) .toolbar { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItemGroup(placement: .secondaryAction) { let functions = user.menuFunctions { user = $0 } - if functions.count == 1, let first = functions.first { - MenuButton(menuFunction: first, confirmDestructive: confirmDestructive) - } else { - Menu { - ForEach(functions) { item in - MenuButton(menuFunction: item, confirmDestructive: confirmDestructive) - } - } label: { - Label("Menu", systemImage: Icons.menuCircle) - } - } - } - if isOwnProfile { - ToolbarItem(placement: .topBarLeading) { - Button("Switch Account", systemImage: Icons.switchUser) { - isPresentingAccountSwitcher = true - } + ForEach(functions) { item in + MenuButton(menuFunction: item, confirmDestructive: confirmDestructive) } } } @@ -186,9 +170,16 @@ struct UserView: View { } } .refreshable { - await Task { + Task { await tryReloadUser() - }.value + } + } + .onChange(of: siteInformation.myUserInfo?.localUserView.person) { newValue in + if isOwnProfile { + if let newValue { + self.user.update(with: newValue) + } + } } .hoistNavigation { if navigationPath.isEmpty { @@ -211,11 +202,6 @@ struct UserView: View { .navigationBarColor() .navigationTitle(user.displayName) .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: $isPresentingAccountSwitcher) { - Form { - AccountListView() - } - } } var flairs: some View { @@ -287,5 +273,3 @@ struct UserView: View { .padding(.horizontal, AppConstants.postAndCommentSpacing) } } - -// swiftlint:enable type_body_length diff --git a/Mlem/Views/Tabs/Search/Results/UserResultView.swift b/Mlem/Views/Tabs/Search/Results/UserResultView.swift index cdbd0b05d..f1b47c00a 100644 --- a/Mlem/Views/Tabs/Search/Results/UserResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/UserResultView.swift @@ -52,7 +52,7 @@ struct UserResultView: View { var title: String { if user.blocked { - return "\(user.displayName) ∙ Blocked" + return "\(user.displayName!) ∙ Blocked" } else { return user.displayName } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Account/MatrixLinkView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Account/MatrixLinkView.swift index ec69e578c..5d620a310 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Account/MatrixLinkView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Account/MatrixLinkView.swift @@ -115,5 +115,6 @@ struct MatrixLinkView: View { } } .hoistNavigation() + .interactiveDismissDisabled(hasEdited != .unedited) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Account/ProfileSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Account/ProfileSettingsView.swift index d31ad8d5a..03a60bfd6 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Account/ProfileSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Account/ProfileSettingsView.swift @@ -17,6 +17,8 @@ struct ProfileSettingsView: View { @Dependency(\.apiClient) var apiClient: APIClient @Dependency(\.errorHandler) var errorHandler: ErrorHandler + @Environment(\.dismiss) var dismiss + @State var displayName: String @State var bio: String @@ -25,13 +27,16 @@ struct ProfileSettingsView: View { @State var hasEdited: UserSettingsEditState = .unedited - init() { + let showCloseButton: Bool + + init(showCloseButton: Bool = false) { @Dependency(\.siteInformation) var siteInformation: SiteInformationTracker let user = siteInformation.myUserInfo?.localUserView _displayName = State(wrappedValue: user?.person.displayName ?? "") _bio = State(wrappedValue: user?.person.bio ?? "") _avatarAttachmentModel = StateObject(wrappedValue: .init(url: user?.person.avatar ?? "")) _bannerAttachmentModel = StateObject(wrappedValue: .init(url: user?.person.banner ?? "")) + self.showCloseButton = showCloseButton } @ViewBuilder @@ -172,6 +177,7 @@ struct ProfileSettingsView: View { .scrollDismissesKeyboard(.interactively) .navigationTitle("My Profile") .navigationBarBackButtonHidden(hasEdited != .unedited) + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { if hasEdited == .edited { @@ -184,6 +190,10 @@ struct ProfileSettingsView: View { bannerAttachmentModel.url = user.person.banner ?? "" } } + } else if showCloseButton { + Button("Close", systemImage: Icons.close) { + dismiss() + } } } ToolbarItem(placement: .topBarTrailing) { @@ -222,5 +232,6 @@ struct ProfileSettingsView: View { } .fancyTabScrollCompatible() .hoistNavigation() + .interactiveDismissDisabled(hasEdited != .unedited) } }