ui: add followed hashtags to FollowingView
When users view who a certain person follows, now they will see an extra tab to see the hashtags that they follow. This new tab contains a list of followed hashtags, each of which includes an option to follow/unfollow the hashtag, as well as the ability to visit the hashtag timeline Testing **iOS:** 17.0 (iPhone 14 Pro Simulator) **Damus:** (This commit) **Test steps:** 1. Go to search view, search for a couple of hashtags: #apple, #orange, #kiwi 2. Go to the test accounts own profile via the drawer menu 3. Click on "Following". Make sure there are two tabs now. 4. Scroll down, switch tabs between "People" and "Hashtags". Make sure that scrolling and switching tabs work 5. Unfollow and follow a user. Make sure that this still works 6. Make sure that #apple, #orange, #kiwi hashtags are visible under the "Hashtags" tab 7. Unfollow "#kiwi". Check that the button label now switches from "Unfollow" to "Follow" 8. Click on "#kiwi". Make sure that it takes you to the page where posts with that hashtag appears 9. Go to @jb55's profile 10. Click on "Following" 11. Ensure that there is a "Hashtags" tab 12. Check that @jb55's followed hashtags are shown (not your own) 13. Follow one of the same hashtags as @jb55's 14. Go back to your own profile and go to your own following view again. 15. Make sure that this newly added tag is present on the list, and that #kiwi is not. Closes: https://github.com/damus-io/damus/issues/606 Changelog-Added: Add followed hashtags to your following list Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
440e37c1d3
commit
8586eed635
@@ -25,22 +25,11 @@ struct SearchHeaderView: View {
|
|||||||
|
|
||||||
var Icon: some View {
|
var Icon: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
|
||||||
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
|
||||||
.frame(width: 54, height: 54)
|
|
||||||
|
|
||||||
switch described {
|
switch described {
|
||||||
case .hashtag:
|
case .hashtag:
|
||||||
Text(verbatim: "#")
|
SingleCharacterAvatar(character: "#")
|
||||||
.font(.largeTitle.bold())
|
case .unknown:
|
||||||
.foregroundStyle(PinkGradient)
|
SystemIconAvatar(system_name: "magnifyingglass")
|
||||||
.mask(Text(verbatim: "#")
|
|
||||||
.font(.largeTitle.bold()))
|
|
||||||
|
|
||||||
case .unknown:
|
|
||||||
Image(systemName: "magnifyingglass")
|
|
||||||
.font(.title.bold())
|
|
||||||
.foregroundStyle(PinkGradient)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,32 +38,6 @@ struct SearchHeaderView: View {
|
|||||||
Text(described.description)
|
Text(described.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow(_ hashtag: String) {
|
|
||||||
is_following = false
|
|
||||||
handle_unfollow(state: state, unfollow: FollowRef.hashtag(hashtag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func follow(_ hashtag: String) {
|
|
||||||
is_following = true
|
|
||||||
handle_follow(state: state, follow: .hashtag(hashtag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FollowButton(_ ht: String) -> some View {
|
|
||||||
return Button(action: { follow(ht) }) {
|
|
||||||
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
|
||||||
.font(.footnote.bold())
|
|
||||||
}
|
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnfollowButton(_ ht: String) -> some View {
|
|
||||||
return Button(action: { unfollow(ht) }) {
|
|
||||||
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
|
||||||
.font(.footnote.bold())
|
|
||||||
}
|
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 30) {
|
HStack(alignment: .center, spacing: 30) {
|
||||||
Icon
|
Icon
|
||||||
@@ -86,9 +49,9 @@ struct SearchHeaderView: View {
|
|||||||
|
|
||||||
if state.is_privkey_user, case .hashtag(let ht) = described {
|
if state.is_privkey_user, case .hashtag(let ht) = described {
|
||||||
if is_following {
|
if is_following {
|
||||||
UnfollowButton(ht)
|
HashtagUnfollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||||
} else {
|
} else {
|
||||||
FollowButton(ht)
|
HashtagFollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +67,87 @@ struct SearchHeaderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SystemIconAvatar: View {
|
||||||
|
let system_name: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NonImageAvatar {
|
||||||
|
Image(systemName: system_name)
|
||||||
|
.font(.title.bold())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SingleCharacterAvatar: View {
|
||||||
|
let character: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NonImageAvatar {
|
||||||
|
Text(verbatim: character)
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
.mask(Text(verbatim: character)
|
||||||
|
.font(.largeTitle.bold()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NonImageAvatar<Content: View>: View {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(@ViewBuilder content: () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||||
|
.frame(width: 54, height: 54)
|
||||||
|
|
||||||
|
content
|
||||||
|
.foregroundStyle(PinkGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HashtagUnfollowButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let hashtag: String
|
||||||
|
@Binding var is_following: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
return Button(action: { unfollow(hashtag) }) {
|
||||||
|
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow(_ hashtag: String) {
|
||||||
|
is_following = false
|
||||||
|
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HashtagFollowButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let hashtag: String
|
||||||
|
@Binding var is_following: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
return Button(action: { follow(hashtag) }) {
|
||||||
|
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func follow(_ hashtag: String) {
|
||||||
|
is_following = true
|
||||||
|
handle_follow(state: damus_state, follow: .hashtag(hashtag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
|
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
|
||||||
guard case .hashtag(let follow_ht) = ref,
|
guard case .hashtag(let follow_ht) = ref,
|
||||||
case .hashtag(let search_ht) = desc,
|
case .hashtag(let search_ht) = desc,
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ class Contacts {
|
|||||||
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func follows(hashtag: Hashtag) -> Bool {
|
||||||
|
guard let ev = self.event else { return false }
|
||||||
|
return ev.referenced_hashtags.first(where: { $0 == hashtag }) != nil
|
||||||
|
}
|
||||||
|
|
||||||
func add_friend_pubkey(_ pubkey: Pubkey) {
|
func add_friend_pubkey(_ pubkey: Pubkey) {
|
||||||
friends.insert(pubkey)
|
friends.insert(pubkey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ class FollowingModel {
|
|||||||
var needs_sub: Bool = true
|
var needs_sub: Bool = true
|
||||||
|
|
||||||
let contacts: [Pubkey]
|
let contacts: [Pubkey]
|
||||||
|
let hashtags: [Hashtag]
|
||||||
|
|
||||||
let sub_id: String = UUID().description
|
let sub_id: String = UUID().description
|
||||||
|
|
||||||
init(damus_state: DamusState, contacts: [Pubkey]) {
|
init(damus_state: DamusState, contacts: [Pubkey], hashtags: [Hashtag]) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.contacts = contacts
|
self.contacts = contacts
|
||||||
|
self.hashtags = hashtags
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_filter() -> NostrFilter {
|
func get_filter() -> NostrFilter {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ struct Privkey: IdType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct Hashtag: TagConvertible {
|
struct Hashtag: TagConvertible, Hashable {
|
||||||
let hashtag: String
|
let hashtag: String
|
||||||
|
|
||||||
static func from_tag(tag: TagSequence) -> Hashtag? {
|
static func from_tag(tag: TagSequence) -> Hashtag? {
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ let test_private_zap = Zap(event: test_note, invoice: test_zap_invoice, zapper:
|
|||||||
|
|
||||||
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||||
|
|
||||||
|
let test_following_model = FollowingModel(damus_state: test_damus_state(), contacts: [test_pubkey, test_pubkey_2], hashtags: [Hashtag(hashtag: "grownostr"), Hashtag(hashtag: "zapathon")])
|
||||||
|
|
||||||
|
|
||||||
func test_damus_state() -> DamusState {
|
func test_damus_state() -> DamusState {
|
||||||
let damus = DamusState.empty
|
let damus = DamusState.empty
|
||||||
|
|||||||
@@ -24,6 +24,53 @@ struct FollowUserView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FollowHashtagView: View {
|
||||||
|
let hashtag: Hashtag
|
||||||
|
let damus_state: DamusState
|
||||||
|
@State var is_following: Bool
|
||||||
|
|
||||||
|
init(hashtag: Hashtag, damus_state: DamusState) {
|
||||||
|
self.hashtag = hashtag
|
||||||
|
self.damus_state = damus_state
|
||||||
|
self.is_following = damus_state.contacts.follows(hashtag: hashtag)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
HStack {
|
||||||
|
SingleCharacterAvatar(character: "#")
|
||||||
|
|
||||||
|
Text("#\(hashtag.hashtag)")
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
let search = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag.hashtag]))
|
||||||
|
damus_state.nav.push(route: Route.Search(search: search))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
if is_following {
|
||||||
|
HashtagUnfollowButton(damus_state: damus_state, hashtag: hashtag.hashtag, is_following: $is_following)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
HashtagFollowButton(damus_state: damus_state, hashtag: hashtag.hashtag, is_following: $is_following)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.followed)) { follow in
|
||||||
|
guard case .hashtag(let ht) = follow, ht == hashtag.hashtag else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.is_following = true
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.unfollowed)) { follow in
|
||||||
|
guard case .hashtag(let ht) = follow, ht == hashtag.hashtag else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.is_following = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct FollowersYouKnowView: View {
|
struct FollowersYouKnowView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let friended_followers: [Pubkey]
|
let friended_followers: [Pubkey]
|
||||||
@@ -65,21 +112,44 @@ struct FollowersView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FollowingViewTabSelection: Int {
|
||||||
|
case people = 0
|
||||||
|
case hashtags = 1
|
||||||
|
}
|
||||||
|
|
||||||
struct FollowingView: View {
|
struct FollowingView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
let following: FollowingModel
|
let following: FollowingModel
|
||||||
|
@State var tab_selection: FollowingViewTabSelection = .people
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
TabView(selection: $tab_selection) {
|
||||||
LazyVStack(alignment: .leading) {
|
ScrollView {
|
||||||
ForEach(following.contacts.reversed(), id: \.self) { pk in
|
LazyVStack(alignment: .leading) {
|
||||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
ForEach(following.contacts.reversed(), id: \.self) { pk in
|
||||||
|
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.tag(FollowingViewTabSelection.people)
|
||||||
|
.id(FollowingViewTabSelection.people)
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading) {
|
||||||
|
ForEach(following.hashtags, id: \.self) { ht in
|
||||||
|
FollowHashtagView(hashtag: ht, damus_state: damus_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.tag(FollowingViewTabSelection.hashtags)
|
||||||
|
.id(FollowingViewTabSelection.hashtags)
|
||||||
}
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
following.subscribe()
|
following.subscribe()
|
||||||
}
|
}
|
||||||
@@ -87,13 +157,24 @@ struct FollowingView: View {
|
|||||||
following.unsubscribe()
|
following.unsubscribe()
|
||||||
}
|
}
|
||||||
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
|
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
|
||||||
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
CustomPicker(selection: $tab_selection, content: {
|
||||||
|
Text("People", comment: "Label for filter for seeing only people follows.").tag(FollowingViewTabSelection.people)
|
||||||
|
Text("Hashtags", comment: "Label for filter for seeing only hashtag follows.").tag(FollowingViewTabSelection.hashtags)
|
||||||
|
})
|
||||||
|
Divider()
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
struct FollowingView_Previews: PreviewProvider {
|
struct FollowingView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
FollowingView(damus_state: test_damus_state, following: test_following_model)
|
FollowingView(damus_state: test_damus_state, following: test_following_model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
@@ -370,7 +370,8 @@ struct ProfileView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
if let contact = profile.contacts {
|
if let contact = profile.contacts {
|
||||||
let contacts = Array(contact.referenced_pubkeys)
|
let contacts = Array(contact.referenced_pubkeys)
|
||||||
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
|
let hashtags = Array(contact.referenced_hashtags)
|
||||||
|
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts, hashtags: hashtags)
|
||||||
NavigationLink(value: Route.Following(following: following_model)) {
|
NavigationLink(value: Route.Following(following: following_model)) {
|
||||||
HStack {
|
HStack {
|
||||||
let noun_text = Text(verbatim: "\(pluralizedString(key: "following_count", count: profile.following))").font(.subheadline).foregroundColor(.gray)
|
let noun_text = Text(verbatim: "\(pluralizedString(key: "following_count", count: profile.following))").font(.subheadline).foregroundColor(.gray)
|
||||||
|
|||||||
Reference in New Issue
Block a user