From 4c8670dd7799c298c3ced675e17ac8e28da202df Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Mon, 9 Jan 2023 10:06:21 +0100 Subject: [PATCH] Move header/statuses from user profile --- Vernissage.xcodeproj/project.pbxproj | 8 + Vernissage/Views/UserProfileView.swift | 161 +------------------ Vernissage/Widgets/ImageRowAsync.swift | 6 +- Vernissage/Widgets/LoadingIndicator.swift | 10 +- Vernissage/Widgets/UserProfileHeader.swift | 139 ++++++++++++++++ Vernissage/Widgets/UserProfileStatuses.swift | 88 ++++++++++ 6 files changed, 249 insertions(+), 163 deletions(-) create mode 100644 Vernissage/Widgets/UserProfileHeader.swift create mode 100644 Vernissage/Widgets/UserProfileStatuses.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 72f3168..8e3c390 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; }; F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; }; F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F866F6B629608467002E8F88 /* MastodonSwift */; }; + F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */; }; + F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */; }; F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; }; F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; }; F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; }; @@ -123,6 +125,8 @@ F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = ""; }; + F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeader.swift; sourceTree = ""; }; + F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatuses.swift; sourceTree = ""; }; F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; F88ABD9529687D4D004EF61E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -273,6 +277,8 @@ F85D497E296416C800751DF7 /* CommentsSection.swift */, F897978729681B9C00B22335 /* UserAvatar.swift */, F89797892968314A00B22335 /* LoadingIndicator.swift */, + F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */, + F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */, ); path = Widgets; sourceTree = ""; @@ -493,6 +499,7 @@ F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */, F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */, F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */, + F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */, F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */, F866F6A729604629002E8F88 /* SignInView.swift in Sources */, F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */, @@ -501,6 +508,7 @@ F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */, F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */, F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */, + F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */, F897978F29684BCB00B22335 /* LoadingView.swift in Sources */, F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */, F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */, diff --git a/Vernissage/Views/UserProfileView.swift b/Vernissage/Views/UserProfileView.swift index 4186275..be928d4 100644 --- a/Vernissage/Views/UserProfileView.swift +++ b/Vernissage/Views/UserProfileView.swift @@ -15,145 +15,12 @@ struct UserProfileView: View { @State public var accountUserName: String @State private var account: Account? = nil @State private var relationship: Relationship? = nil - @State private var statuses: [Status] = [] - @State private var isDuringRelationshipAction = false - - @State private var allItemsLoaded = false - @State private var firstLoadFinished = false var body: some View { ScrollView { - if let account = self.account { - VStack(alignment: .leading) { - HStack(alignment: .center) { - UserAvatar(accountAvatar: account.avatar, width: 96, height: 96) - - Spacer() - - VStack(alignment: .center) { - Text("\(account.statusesCount)") - .font(.title3) - Text("Posts") - .font(.subheadline) - .opacity(0.6) - } - - Spacer() - - NavigationLink(destination: FollowersView(accountId: account.id) - .environmentObject(applicationState) - ) { - VStack(alignment: .center) { - Text("\(account.followersCount)") - .font(.title3) - Text("Followers") - .font(.subheadline) - .opacity(0.6) - } - }.foregroundColor(.mainTextColor) - - Spacer() - - NavigationLink(destination: FollowingView(accountId: account.id) - .environmentObject(applicationState) - ) { - VStack(alignment: .center) { - Text("\(account.followingCount)") - .font(.title3) - Text("Following") - .font(.subheadline) - .opacity(0.6) - } - }.foregroundColor(.mainTextColor) - } - - HStack (alignment: .center) { - VStack(alignment: .leading) { - Text(account.displayName ?? account.acct) - .foregroundColor(.mainTextColor) - .font(.footnote) - .fontWeight(.bold) - Text("@\(account.acct)") - .foregroundColor(.lightGrayColor) - .font(.footnote) - } - - Spacer() - - if self.applicationState.accountData?.id != self.accountId { - Button { - Task { - Task { @MainActor in - self.isDuringRelationshipAction = false - } - - HapticService.shared.touch() - self.isDuringRelationshipAction = true - do { - if let relationship = try await AccountService.shared.follow( - forAccountId: self.accountId, - andContext: self.applicationState.accountData - ) { - self.relationship = relationship - } - } catch { - print("Error \(error.localizedDescription)") - } - } - } label: { - if isDuringRelationshipAction { - LoadingIndicator() - } else { - HStack { - Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") - Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow")) - } - } - } - .disabled(isDuringRelationshipAction) - .buttonStyle(.borderedProminent) - .tint(relationship?.following == true ? .dangerColor : .accentColor) - } - } - - if let note = account.note, !note.isEmpty { - HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16) - .padding(.top, -10) - .padding(.leading, -4) - } - - Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") - .foregroundColor(.lightGrayColor.opacity(0.5)) - .font(.footnote) - - } - .padding() - - ForEach(self.statuses, id: \.id) { item in - VStack { - NavigationLink(destination: StatusView(statusId: item.id) - .environmentObject(applicationState)) { - ImageRowAsync(attachments: item.mediaAttachments) - } - } - } - - LazyVStack { - if allItemsLoaded == false && firstLoadFinished == true { - LoadingIndicator() - .onAppear { - Task { - do { - try await self.loadMoreStatuses() - } catch { - print("Error \(error.localizedDescription)") - } - } - } - .frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center) - } - } - + if let account = self.account, let relationship = self.relationship { + UserProfileHeader(account: account, relationship: relationship) + UserProfileStatuses(accountId: account.id) } else { LoadingIndicator() } @@ -176,28 +43,6 @@ struct UserProfileView: View { // Wait for download account and relationships. (self.relationship, self.account) = try await (relationshipTask, accountTask) - - self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData) - self.firstLoadFinished = true - - if self.statuses.count < 40 { - self.allItemsLoaded = true - } - } - - private func loadMoreStatuses() async throws { - if let lastStatusId = self.statuses.last?.id { - let previousStatuses = try await AccountService.shared.getStatuses( - forAccountId: self.accountId, - andContext: self.applicationState.accountData, - maxId: lastStatusId) - - if previousStatuses.count < 40 { - self.allItemsLoaded = true - } - - self.statuses.append(contentsOf: previousStatuses) - } } } diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index 1d646ef..049b1a1 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -25,7 +25,7 @@ struct ImageRowAsync: View { ZStack { Rectangle() .fill(Color.placeholderText) - .frame(width: self.imageWidth, height: self.imageHeight) + .scaledToFill() VStack(alignment: .center) { Spacer() @@ -34,6 +34,7 @@ struct ImageRowAsync: View { Spacer() } } + .frame(width: self.imageWidth, height: self.imageHeight) } else { VStack(alignment: .center) { if let blurhash = attachment.blurhash, @@ -44,9 +45,10 @@ struct ImageRowAsync: View { } else { Rectangle() .fill(Color.placeholderText) - .frame(width: self.imageWidth, height: self.imageHeight) + .scaledToFill() } } + .frame(width: self.imageWidth, height: self.imageHeight) } } .onSuccess { imageResponse in diff --git a/Vernissage/Widgets/LoadingIndicator.swift b/Vernissage/Widgets/LoadingIndicator.swift index 4dc492d..275c4fe 100644 --- a/Vernissage/Widgets/LoadingIndicator.swift +++ b/Vernissage/Widgets/LoadingIndicator.swift @@ -8,11 +8,15 @@ import SwiftUI struct LoadingIndicator: View { + @State var withText = true + var body: some View { ProgressView { - Text("Loading...") - .foregroundColor(.mainTextColor) - .font(.caption2) + if self.withText { + Text("Loading...") + .foregroundColor(.mainTextColor) + .font(.caption2) + } } .progressViewStyle(CircularProgressViewStyle()) .tint(.mainTextColor) diff --git a/Vernissage/Widgets/UserProfileHeader.swift b/Vernissage/Widgets/UserProfileHeader.swift new file mode 100644 index 0000000..4563245 --- /dev/null +++ b/Vernissage/Widgets/UserProfileHeader.swift @@ -0,0 +1,139 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonSwift + +struct UserProfileHeader: View { + @EnvironmentObject private var applicationState: ApplicationState + @State var account: Account + @State var relationship: Relationship + + @State private var isDuringRelationshipAction = false + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .center) { + UserAvatar(accountAvatar: account.avatar, width: 96, height: 96) + + Spacer() + + VStack(alignment: .center) { + Text("\(account.statusesCount)") + .font(.title3) + Text("Posts") + .font(.subheadline) + .opacity(0.6) + } + + Spacer() + + NavigationLink(destination: FollowersView(accountId: account.id) + .environmentObject(applicationState) + ) { + VStack(alignment: .center) { + Text("\(account.followersCount)") + .font(.title3) + Text("Followers") + .font(.subheadline) + .opacity(0.6) + } + }.foregroundColor(.mainTextColor) + + Spacer() + + NavigationLink(destination: FollowingView(accountId: account.id) + .environmentObject(applicationState) + ) { + VStack(alignment: .center) { + Text("\(account.followingCount)") + .font(.title3) + Text("Following") + .font(.subheadline) + .opacity(0.6) + } + }.foregroundColor(.mainTextColor) + } + + HStack (alignment: .center) { + VStack(alignment: .leading) { + Text(account.displayName ?? account.acct) + .foregroundColor(.mainTextColor) + .font(.footnote) + .fontWeight(.bold) + Text("@\(account.acct)") + .foregroundColor(.lightGrayColor) + .font(.footnote) + } + + Spacer() + + if self.applicationState.accountData?.id != self.account.id { + Button { + Task { + defer { + Task { @MainActor in + withAnimation { + self.isDuringRelationshipAction = false + } + } + } + + HapticService.shared.touch() + withAnimation { + self.isDuringRelationshipAction = true + } + + do { + if let relationship = try await AccountService.shared.follow( + forAccountId: self.account.id, + andContext: self.applicationState.accountData + ) { + self.relationship = relationship + } + } catch { + print("Error \(error.localizedDescription)") + } + } + } label: { + if isDuringRelationshipAction { + LoadingIndicator(withText: false) + .transition(.opacity) + } else { + HStack { + Image(systemName: relationship.following == true ? "person.badge.minus" : "person.badge.plus") + Text(relationship.following == true ? "Unfollow" : (relationship.followedBy == true ? "Follow back" : "Follow")) + } + .transition(.opacity) + } + } + .disabled(isDuringRelationshipAction) + .buttonStyle(.borderedProminent) + .tint(relationship.following == true ? .dangerColor : .accentColor) + } + } + + if let note = account.note, !note.isEmpty { + HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16) + .padding(.top, -10) + .padding(.leading, -4) + } + + Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") + .foregroundColor(.lightGrayColor.opacity(0.5)) + .font(.footnote) + + } + .padding() + } +} + +struct UserProfileHeader_Previews: PreviewProvider { + static var previews: some View { + Text("") + // UserProfileHeader(account: Account(), relationship: Relationship()) + } +} diff --git a/Vernissage/Widgets/UserProfileStatuses.swift b/Vernissage/Widgets/UserProfileStatuses.swift new file mode 100644 index 0000000..604ed8d --- /dev/null +++ b/Vernissage/Widgets/UserProfileStatuses.swift @@ -0,0 +1,88 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonSwift + +struct UserProfileStatuses: View { + @EnvironmentObject private var applicationState: ApplicationState + @State public var accountId: String + + @State private var allItemsLoaded = false + @State private var firstLoadFinished = false + + @State private var statuses: [Status] = [] + + var body: some View { + VStack { + if firstLoadFinished == true { + ForEach(self.statuses, id: \.id) { item in + NavigationLink(destination: StatusView(statusId: item.id) + .environmentObject(applicationState)) { + ImageRowAsync(attachments: item.mediaAttachments) + } + } + + LazyVStack { + if allItemsLoaded == false && firstLoadFinished == true { + LoadingIndicator() + .onAppear { + Task { + do { + try await self.loadMoreStatuses() + } catch { + print("Error \(error.localizedDescription)") + } + } + } + .frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center) + } + } + + } else { + LoadingIndicator() + } + }.onAppear { + Task { + do { + try await self.loadStatuses() + } catch { + print("Error \(error.localizedDescription)") + } + } + } + } + + private func loadStatuses() async throws { + self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData) + self.firstLoadFinished = true + + if self.statuses.count < 40 { + self.allItemsLoaded = true + } + } + + private func loadMoreStatuses() async throws { + if let lastStatusId = self.statuses.last?.id { + let previousStatuses = try await AccountService.shared.getStatuses( + forAccountId: self.accountId, + andContext: self.applicationState.accountData, + maxId: lastStatusId) + + if previousStatuses.count < 40 { + self.allItemsLoaded = true + } + + self.statuses.append(contentsOf: previousStatuses) + } + } +} + +struct UserProfileStatuses_Previews: PreviewProvider { + static var previews: some View { + UserProfileStatuses(accountId: "") + } +}