From b5bed57d7a5533894c4e5748cc1ae850d34a5726 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Fri, 17 Jan 2025 07:15:44 -0600 Subject: [PATCH 1/2] added list detail view #155 --- CHANGELOG.md | 1 + Nos.xcodeproj/project.pbxproj | 8 ++ Nos/Views/Lists/AuthorListDetailView.swift | 89 ++++++++++++++++++++++ Nos/Views/Lists/AuthorListsView.swift | 70 +++++------------ Nos/Views/Lists/EditAuthorListView.swift | 7 +- Nos/Views/Lists/ListCircle.swift | 19 +++++ 6 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 Nos/Views/Lists/AuthorListDetailView.swift create mode 100644 Nos/Views/Lists/ListCircle.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7116440d5..07ce39173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed: adding/removing relays not reflected on feed filter. [#119](https://github.com/verse-pbc/issues/issues/119) - Added Lists view and two ways to navigate to it. [#133](https://github.com/verse-pbc/issues/issues/133) - Added view for editing a list's title and description. [#134](https://github.com/verse-pbc/issues/issues/134) +- Added List detail view. [#155](https://github.com/verse-pbc/issues/issues/155) ### Internal Changes - Added function for creating a new list and a test verifying list editing. [#112](https://github.com/verse-pbc/issues/issues/112) diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index fca3c931d..e71ba1114 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -220,6 +220,8 @@ 509533002C62535400E0BACA /* zap_request.json in Resources */ = {isa = PBXBuildFile; fileRef = 509532FF2C62535400E0BACA /* zap_request.json */; }; 5095330B2C625B5D00E0BACA /* zap_request_one_sat.json in Resources */ = {isa = PBXBuildFile; fileRef = 509533092C625B5D00E0BACA /* zap_request_one_sat.json */; }; 5095330C2C625B5D00E0BACA /* zap_request_no_amount.json in Resources */ = {isa = PBXBuildFile; fileRef = 5095330A2C625B5D00E0BACA /* zap_request_no_amount.json */; }; + 50CBD7AB2D39341B00BF8A0B /* AuthorListDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CBD7AA2D39341700BF8A0B /* AuthorListDetailView.swift */; }; + 50CBD8152D3A8FED00BF8A0B /* ListCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CBD80F2D3A8B6D00BF8A0B /* ListCircle.swift */; }; 50DE6B1B2C6B88FE0065665D /* View+StyledBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */; }; 50E2EB722C86175900D4B360 /* NSRegularExpression+Replacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */; }; 50E2EB7B2C8617C800D4B360 /* NSRegularExpression+Replacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */; }; @@ -792,6 +794,8 @@ 509532FF2C62535400E0BACA /* zap_request.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = zap_request.json; sourceTree = ""; }; 509533092C625B5D00E0BACA /* zap_request_one_sat.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zap_request_one_sat.json; sourceTree = ""; }; 5095330A2C625B5D00E0BACA /* zap_request_no_amount.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zap_request_no_amount.json; sourceTree = ""; }; + 50CBD7AA2D39341700BF8A0B /* AuthorListDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorListDetailView.swift; sourceTree = ""; }; + 50CBD80F2D3A8B6D00BF8A0B /* ListCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCircle.swift; sourceTree = ""; }; 50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+StyledBorder.swift"; sourceTree = ""; }; 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+Replacement.swift"; sourceTree = ""; }; 50EA86D32D28150D001E62CC /* FeedSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSource.swift; sourceTree = ""; }; @@ -1589,8 +1593,10 @@ 50EA886D2D2D5776001E62CC /* Lists */ = { isa = PBXGroup; children = ( + 50CBD7AA2D39341700BF8A0B /* AuthorListDetailView.swift */, 50EA886E2D2D5780001E62CC /* AuthorListsView.swift */, 50EA89A52D3010EA001E62CC /* EditAuthorListView.swift */, + 50CBD80F2D3A8B6D00BF8A0B /* ListCircle.swift */, ); path = Lists; sourceTree = ""; @@ -2512,6 +2518,7 @@ 3F30020D29C382EB003D4F8B /* OnboardingLoginView.swift in Sources */, C9A25B3D29F174D200B39534 /* ReadabilityPadding.swift in Sources */, 04C9D7952CC29E0A00EAAD4D /* FeaturedAuthor+Cohort3.swift in Sources */, + 50CBD8152D3A8FED00BF8A0B /* ListCircle.swift in Sources */, C9DFA972299BF9E8006929C1 /* CompactNoteView.swift in Sources */, C9AC31AD2A55E0BD00A94E5A /* NotificationViewModel.swift in Sources */, 0326347A2C10C57A00E489B5 /* FileStorageAPIClient.swift in Sources */, @@ -2580,6 +2587,7 @@ C92DF80529C25DE900400561 /* URL+Extensions.swift in Sources */, 3F60F42929B27D3E000D62C4 /* ThreadView.swift in Sources */, C9B678DB29EEBF3B00303F33 /* DependencyInjection.swift in Sources */, + 50CBD7AB2D39341B00BF8A0B /* AuthorListDetailView.swift in Sources */, 5B098DC62BDAF73500500A1B /* AttributedString+Links.swift in Sources */, 65BD8DC42BDAF2C300802039 /* DiscoverContentsView.swift in Sources */, C95D68A9299E709900429F86 /* LinearGradient+Planetary.swift in Sources */, diff --git a/Nos/Views/Lists/AuthorListDetailView.swift b/Nos/Views/Lists/AuthorListDetailView.swift new file mode 100644 index 000000000..95b18136b --- /dev/null +++ b/Nos/Views/Lists/AuthorListDetailView.swift @@ -0,0 +1,89 @@ +import Dependencies +import SwiftUI + +struct AuthorListDetailView: View { + + @Dependency(\.relayService) private var relayService + @EnvironmentObject private var router: Router + + @ObservedObject var list: AuthorList + + /// Subscriptions for metadata requests from the relay service, keyed by author ID. + @State private var subscriptions = [ObjectIdentifier: SubscriptionCancellable]() + + @State private var showingEditListInfo = false + + var body: some View { + ScrollView { + VStack(spacing: 16) { + ListCircle() + + VStack(spacing: 3) { + Text(list.title ?? "") + .font(.headline.weight(.bold)) + .multilineTextAlignment(.center) + + Text(String.localizedStringWithFormat(String(localized: "xUsers"), list.allAuthors.count)) + .foregroundStyle(Color.secondaryTxt) + .font(.footnote) + + if let description = list.listDescription, !description.isEmpty { + Text(description) + .foregroundStyle(Color.secondaryTxt) + .font(.footnote) + .padding(.top, 8) + } + } + } + .padding(.top, 24) + + LazyVStack { + ForEach(list.allAuthors.sorted(by: { ($0.displayName ?? "") < ($1.displayName ?? "") })) { author in + AuthorObservationView(authorID: author.hexadecimalPublicKey) { author in + AuthorCard(author: author) { + router.push(author) + } + .padding(.horizontal, 13) + .padding(.top, 5) + .readabilityPadding() + .task { + subscriptions[author.id] = + await relayService.requestMetadata( + for: author.hexadecimalPublicKey, + since: author.lastUpdatedMetadata + ) + } + } + } + } + .padding(.vertical, 12) + } + .nosNavigationBar("") + .background(Color.appBg) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Menu { + Button("editListInfo") { + showingEditListInfo = true + } + Button("manageUsers") { + // TODO: Manage Users + } + Button("deleteList", role: .destructive) { + // TODO: Delete List + } + } label: { + Image(systemName: "ellipsis") + .foregroundStyle(Color.secondaryTxt) + .fontWeight(.bold) + .padding(.vertical, 12) + } + } + } + .sheet(isPresented: $showingEditListInfo) { + NavigationStack { + EditAuthorListView(list: list) + } + } + } +} diff --git a/Nos/Views/Lists/AuthorListsView.swift b/Nos/Views/Lists/AuthorListsView.swift index 5e532d286..2d53e00cf 100644 --- a/Nos/Views/Lists/AuthorListsView.swift +++ b/Nos/Views/Lists/AuthorListsView.swift @@ -13,7 +13,6 @@ struct AuthorListsView: View { @FetchRequest var lists: FetchedResults - @State private var listToEditInfo: AuthorList? @State private var showingCreateList = false init(author: Author) { @@ -29,18 +28,7 @@ struct AuthorListsView: View { Group { if lists.isEmpty { VStack(spacing: 40) { - ZStack { - Circle() - .fill(Color.white) - .frame(width: 80, height: 80) - - Image(systemName: "person.2") - .resizable() - .fontWeight(.semibold) - .aspectRatio(contentMode: .fit) - .frame(width: 48) - .foregroundStyle(Color.black) - } + ListCircle() Text("listsDescription") .font(.subheadline.weight(.medium)) @@ -54,38 +42,25 @@ struct AuthorListsView: View { ScrollView { VStack(spacing: 0) { ForEach(lists) { list in - HStack { - VStack(alignment: .leading) { - Text(list.title ?? "") - .font(.body) - - Text(list.rowDescription) - .foregroundStyle(Color.secondaryTxt) - .font(.footnote) - } - - Spacer() - - Menu { - Button("editListInfo") { - listToEditInfo = list - } - Button("manageUsers") { - // TODO: Manage Users - } - Button("deleteList", role: .destructive) { - // TODO: Delete List + NavigationLink { + AuthorListDetailView(list: list) + } label: { + HStack { + VStack(alignment: .leading) { + Text(list.title ?? "") + .font(.body) + + Text(list.rowDescription) + .foregroundStyle(Color.secondaryTxt) + .font(.footnote) } - } label: { - Image(systemName: "ellipsis") - .foregroundStyle(Color.secondaryTxt) - .fontWeight(.bold) - .padding() + + Spacer() } + .padding(.leading, 16) + .padding(.vertical, 12) + .frame(minHeight: 50) } - .padding(.leading, 16) - .padding(.vertical, 12) - .frame(minHeight: 50) BeveledSeparator() } @@ -107,14 +82,6 @@ struct AuthorListsView: View { } } .nosNavigationBar("yourLists") - .sheet(item: $listToEditInfo) { list in - NavigationStack { - EditAuthorListView(list: list) - .onDisappear { - listToEditInfo = nil - } - } - } .sheet(isPresented: $showingCreateList) { NavigationStack { EditAuthorListView() @@ -135,7 +102,8 @@ extension AuthorList { let countString = String.localizedStringWithFormat(String(localized: "xUsers"), authorCount) descriptionComponents.append(countString) - descriptionComponents.append(listDescription ?? String(localized: "noDescription")) + let description = listDescription?.isEmpty == false ? listDescription! : String(localized: "noDescription") + descriptionComponents.append(description) return descriptionComponents.joined(separator: " • ") } } diff --git a/Nos/Views/Lists/EditAuthorListView.swift b/Nos/Views/Lists/EditAuthorListView.swift index f757bf3ed..f31a89529 100644 --- a/Nos/Views/Lists/EditAuthorListView.swift +++ b/Nos/Views/Lists/EditAuthorListView.swift @@ -26,6 +26,9 @@ struct EditAuthorListView: View { init(list: AuthorList? = nil) { self.list = list mode = list == nil ? .create : .update + + title = list?.title ?? "" + description = list?.listDescription ?? "" } var body: some View { @@ -61,12 +64,12 @@ struct EditAuthorListView: View { } .nosNavigationBar(mode == .create ? "newList" : "editListInfo") .toolbar { - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .cancellationAction) { Button("cancel") { dismiss() } } - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .primaryAction) { ActionButton(mode == .create ? "next" : "save", action: saveButtonPressed) .frame(height: 22) .padding(.bottom, 3) diff --git a/Nos/Views/Lists/ListCircle.swift b/Nos/Views/Lists/ListCircle.swift new file mode 100644 index 000000000..3d521ea32 --- /dev/null +++ b/Nos/Views/Lists/ListCircle.swift @@ -0,0 +1,19 @@ +import SwiftUI + +/// A white circle with a group icon in it for representing an ``AuthorList``. +struct ListCircle: View { + var body: some View { + ZStack { + Circle() + .fill(Color.white) + .frame(width: 80, height: 80) + + Image(systemName: "person.2") + .resizable() + .fontWeight(.semibold) + .aspectRatio(contentMode: .fit) + .frame(width: 48) + .foregroundStyle(Color.black) + } + } +} From b63a685ae629f5a11dd425e125d4f7a4b7a879cd Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Fri, 17 Jan 2025 07:55:53 -0600 Subject: [PATCH 2/2] fix tapping on a user in the list detail view --- Nos/Views/Lists/AuthorListDetailView.swift | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Nos/Views/Lists/AuthorListDetailView.swift b/Nos/Views/Lists/AuthorListDetailView.swift index 95b18136b..06a4e8d17 100644 --- a/Nos/Views/Lists/AuthorListDetailView.swift +++ b/Nos/Views/Lists/AuthorListDetailView.swift @@ -39,19 +39,22 @@ struct AuthorListDetailView: View { LazyVStack { ForEach(list.allAuthors.sorted(by: { ($0.displayName ?? "") < ($1.displayName ?? "") })) { author in - AuthorObservationView(authorID: author.hexadecimalPublicKey) { author in - AuthorCard(author: author) { - router.push(author) - } - .padding(.horizontal, 13) - .padding(.top, 5) - .readabilityPadding() - .task { - subscriptions[author.id] = - await relayService.requestMetadata( - for: author.hexadecimalPublicKey, - since: author.lastUpdatedMetadata - ) + NavigationLink { + ProfileView(author: author) + } label: { + AuthorObservationView(authorID: author.hexadecimalPublicKey) { author in + AuthorCard(author: author) + .padding(.horizontal, 13) + .padding(.top, 5) + .readabilityPadding() + .task { + subscriptions[author.id] = + await relayService.requestMetadata( + for: author.hexadecimalPublicKey, + since: author.lastUpdatedMetadata + ) + } + .disabled(true) // skips the onTap action in AuthorCard } } }