From 3c2ee8c5928d3fb2df3044e9c2ac02d268199407 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Wed, 4 Jan 2023 20:56:26 +0100 Subject: [PATCH] Add user profile --- Vernissage.xcodeproj/project.pbxproj | 12 ++ Vernissage/CoreData/CoreDataHandler.swift | 32 ++++- .../MastodonClientAuthenticated+Account.swift | 21 +++ Vernissage/Services/AccountService.swift | 21 +++ Vernissage/Views/DetailsView.swift | 8 +- Vernissage/Views/UserProfileView.swift | 125 ++++++++++++++++++ Vernissage/Widgets/CommentsSection.swift | 40 +++--- Vernissage/Widgets/InteractionRow.swift | 5 +- 8 files changed, 237 insertions(+), 27 deletions(-) create mode 100644 Vernissage/Extensions/MastodonClientAuthenticated+Account.swift create mode 100644 Vernissage/Services/AccountService.swift create mode 100644 Vernissage/Views/UserProfileView.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 10ad477..816027e 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -52,6 +52,9 @@ F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; }; F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */; }; F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD31295F5029009B20C9 /* RemoteFileService.swift */; }; + F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; + F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; }; + F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -101,6 +104,9 @@ F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = ""; }; F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = ""; }; F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = ""; }; + F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; + F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; + F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -125,6 +131,7 @@ F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */, F88FAD26295F400E009B20C9 /* NotificationsView.swift */, F866F6A629604629002E8F88 /* SignInView.swift */, + F8A93D7D2965FD89001D8331 /* UserProfileView.swift */, ); path = Views; sourceTree = ""; @@ -134,6 +141,7 @@ children = ( F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */, F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */, + F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */, F85D49862964334100751DF7 /* String+Date.swift */, ); path = Extensions; @@ -243,6 +251,7 @@ F88FAD31295F5029009B20C9 /* RemoteFileService.swift */, F85D4970296402DC00751DF7 /* AuthorizationService.swift */, F85D4974296407F100751DF7 /* TimelineService.swift */, + F8A93D7F2965FED4001D8331 /* AccountService.swift */, ); path = Services; sourceTree = ""; @@ -333,6 +342,7 @@ F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */, F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */, F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, + F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */, F85D49872964334100751DF7 /* String+Date.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */, @@ -344,6 +354,7 @@ F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */, F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */, + F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */, F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */, F88C246E295C37B80006098B /* MainView.swift in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, @@ -363,6 +374,7 @@ F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */, F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */, F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */, + F8A93D802965FED4001D8331 /* AccountService.swift in Sources */, F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */, F85D4973296406E700751DF7 /* BottomRight.swift in Sources */, ); diff --git a/Vernissage/CoreData/CoreDataHandler.swift b/Vernissage/CoreData/CoreDataHandler.swift index b1528d8..12577b0 100644 --- a/Vernissage/CoreData/CoreDataHandler.swift +++ b/Vernissage/CoreData/CoreDataHandler.swift @@ -60,10 +60,17 @@ extension CoreDataHandler { public static var preview: CoreDataHandler = { let result = CoreDataHandler(inMemory: true) let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = AccountData(context: viewContext) - newItem.id = "123" - } + + let statusData = StatusData(context: viewContext) + statusData.id = "516272295308651148" + statusData.uri = "https://pixelfed.social/p/z428/516272295308651148" + statusData.url = URL(string: "https://pixelfed.social/p/z428/516272295308651148") + statusData.content = "4: Along the way.
\n#outerworld #pixelfed365 #dresden #photography #smartphonephotography #afternoons #grey" + statusData.reblogsCount = 12 + statusData.createdAt = "2023-01-04T15:21:47.000Z" + statusData.visibility = "public" + statusData.applicationName = "web" + do { try viewContext.save() } catch { @@ -75,3 +82,20 @@ extension CoreDataHandler { return result }() } + +public struct PreviewData { + static func getStatus() -> StatusData { + let statusData = StatusData() + statusData.id = "516272295308651148" + statusData.uri = "https://pixelfed.social/p/z428/516272295308651148" + statusData.url = URL(string: "https://pixelfed.social/p/z428/516272295308651148") + statusData.content = "4: Along the way.
\n#outerworld #pixelfed365 #dresden #photography #smartphonephotography #afternoons #grey" + statusData.reblogsCount = 12 + statusData.createdAt = "2023-01-04T15:21:47.000Z" + statusData.visibility = "public" + statusData.applicationName = "web" + + return statusData + } +} + diff --git a/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift new file mode 100644 index 0000000..0f42715 --- /dev/null +++ b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift @@ -0,0 +1,21 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonSwift + +extension MastodonClientAuthenticated { + func getAccount(for accountId: String) async throws -> Account { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.account(accountId), + withBearerToken: token + ) + + let (data, _) = try await urlSession.data(for: request) + return try JSONDecoder().decode(Account.self, from: data) + } +} diff --git a/Vernissage/Services/AccountService.swift b/Vernissage/Services/AccountService.swift new file mode 100644 index 0000000..867913f --- /dev/null +++ b/Vernissage/Services/AccountService.swift @@ -0,0 +1,21 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonSwift + +public class AccountService { + public static let shared = AccountService() + + public func getAccount(withId accountId: String, and accountData: AccountData?) async throws -> Account? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.getAccount(for: accountId) + } +} diff --git a/Vernissage/Views/DetailsView.swift b/Vernissage/Views/DetailsView.swift index 608e5d5..5a48260 100644 --- a/Vernissage/Views/DetailsView.swift +++ b/Vernissage/Views/DetailsView.swift @@ -18,7 +18,13 @@ struct DetailsView: View { ImagesCarousel(attachments: statusData.attachments()) VStack(alignment: .leading) { - UsernameRow(statusData: statusData) + NavigationLink(destination: UserProfileView( + accountId: statusData.accountId, + accountDisplayName: statusData.accountDisplayName, + accountUserName: statusData.accountUsername) + .environmentObject(applicationState)) { + UsernameRow(statusData: statusData) + } HTMLFormattedText(statusData.content) .padding(.leading, -4) diff --git a/Vernissage/Views/UserProfileView.swift b/Vernissage/Views/UserProfileView.swift new file mode 100644 index 0000000..513aeec --- /dev/null +++ b/Vernissage/Views/UserProfileView.swift @@ -0,0 +1,125 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonSwift + +struct UserProfileView: View { + @EnvironmentObject var applicationState: ApplicationState + @State public var accountId: String + @State public var accountDisplayName: String? + @State public var accountUserName: String + @State private var account: Account? = nil + + var body: some View { + VStack(alignment: .leading) { + if let account = self.account { + + HStack(alignment: .center) { + AsyncImage(url: account.avatar) { image in + image + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fit) + } placeholder: { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(Color("MainTextColor")) + } + .frame(width: 96.0, height: 96.0) + + Spacer() + + VStack(alignment: .center) { + Text("\(account.statusesCount)") + .font(.title3) + Text("Posts") + .font(.subheadline) + .opacity(0.6) + } + + Spacer() + + VStack(alignment: .center) { + Text("\(account.followersCount)") + .font(.title3) + Text("Followers") + .font(.subheadline) + .opacity(0.6) + } + + Spacer() + + VStack(alignment: .center) { + Text("\(account.followingCount)") + .font(.title3) + Text("Following") + .font(.subheadline) + .opacity(0.6) + } + } + + HStack (alignment: .center) { + Text(account.displayName ?? account.username) + .foregroundColor(Color("DisplayNameColor")) + .font(.footnote) + .fontWeight(.bold) + Text("@\(account.username)") + .foregroundColor(Color("LightGrayColor")) + .font(.footnote) + + Spacer() + + Button { + // Folllow/Unfollow + } label: { + Text("Follow") + } + .buttonStyle(.borderedProminent) + .tint(.accentColor) + + } + + if let note = account.note { + HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16) + .padding(.top, -10) + .padding(.leading, -4) + } + + Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") + .foregroundColor(Color("LightGrayColor").opacity(0.5)) + .font(.footnote) + + Spacer() + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + .padding() + .navigationBarTitle(self.accountDisplayName ?? self.accountUserName) + .onAppear { + Task { + do { + if let account = try await AccountService.shared.getAccount( + withId: self.accountId, + and: self.applicationState.accountData + ) { + self.account = account + } + } catch { + print("Error \(error.localizedDescription)") + } + } + } + } +} + +struct UserProfileView_Previews: PreviewProvider { + static var previews: some View { + UserProfileView(accountId: "", accountDisplayName: "", accountUserName: "") + } +} diff --git a/Vernissage/Widgets/CommentsSection.swift b/Vernissage/Widgets/CommentsSection.swift index adc0729..cd17e3b 100644 --- a/Vernissage/Widgets/CommentsSection.swift +++ b/Vernissage/Widgets/CommentsSection.swift @@ -21,17 +21,26 @@ struct CommentsSection: View { if let context = context { ForEach(context.descendants, id: \.id) { status in HStack (alignment: .top) { - AsyncImage(url: status.account?.avatar) { image in - image - .resizable() - .clipShape(Circle()) - .aspectRatio(contentMode: .fit) - } placeholder: { - Image(systemName: "person.circle") - .resizable() - .foregroundColor(Color("MainTextColor")) + + if let account = status.account { + NavigationLink(destination: UserProfileView( + accountId: account.id, + accountDisplayName: account.displayName, + accountUserName: account.username) + .environmentObject(applicationState)) { + AsyncImage(url: account.avatar) { image in + image + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fit) + } placeholder: { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(Color("MainTextColor")) + } + .frame(width: 32.0, height: 32.0) + } } - .frame(width: 32.0, height: 32.0) VStack (alignment: .leading) { HStack (alignment: .top) { @@ -48,19 +57,10 @@ struct CommentsSection: View { Text(status.createdAt.toRelative(.isoDateTimeMilliSec)) .foregroundColor(Color("LightGrayColor").opacity(0.5)) .font(.footnote) - - /* - Image(systemName: "message") - .foregroundColor(Color.accentColor) - Image(systemName: "hand.thumbsup") - .foregroundColor(Color.accentColor) - */ } - .padding(.bottom, -10) - - HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth) + .padding(.top, -10) .padding(.leading, -4) if status.mediaAttachments.count > 0 { diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index eb33033..57046dc 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -27,7 +27,7 @@ struct InteractionRow: View { // Reboost } label: { HStack(alignment: .center) { - Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward") + Image(systemName: statusData.reblogged ? "paperplane.fill" : "paperplane") Text("\(statusData.reblogsCount)") .font(.caption) } @@ -69,6 +69,7 @@ struct InteractionRow: View { struct InteractionRow_Previews: PreviewProvider { static var previews: some View { - InteractionRow(statusData: StatusData()) + InteractionRow(statusData: PreviewData.getStatus()) + .previewLayout(.fixed(width: 300, height: 70)) } }