Skip to content

Commit

Permalink
ui: add followed hashtags to FollowingView
Browse files Browse the repository at this point in the history
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: #606
Changelog-Added: Add followed hashtags to your following list
Signed-off-by: Daniel D’Aquino <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Sep 21, 2023
1 parent 440e37c commit 8586eed
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 53 deletions.
130 changes: 87 additions & 43 deletions damus/Components/Search/SearchHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,11 @@ struct SearchHeaderView: View {

var Icon: some View {
ZStack {
Circle()
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
.frame(width: 54, height: 54)

switch described {
case .hashtag:
Text(verbatim: "#")
.font(.largeTitle.bold())
.foregroundStyle(PinkGradient)
.mask(Text(verbatim: "#")
.font(.largeTitle.bold()))

case .unknown:
Image(systemName: "magnifyingglass")
.font(.title.bold())
.foregroundStyle(PinkGradient)
case .hashtag:
SingleCharacterAvatar(character: "#")
case .unknown:
SystemIconAvatar(system_name: "magnifyingglass")
}
}
}
Expand All @@ -49,32 +38,6 @@ struct SearchHeaderView: View {
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 {
HStack(alignment: .center, spacing: 30) {
Icon
Expand All @@ -86,9 +49,9 @@ struct SearchHeaderView: View {

if state.is_privkey_user, case .hashtag(let ht) = described {
if is_following {
UnfollowButton(ht)
HashtagUnfollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
} else {
FollowButton(ht)
HashtagFollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
}
}
}
Expand All @@ -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 {
guard case .hashtag(let follow_ht) = ref,
case .hashtag(let search_ht) = desc,
Expand Down
5 changes: 5 additions & 0 deletions damus/Models/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ class Contacts {
guard let ev = self.event else { return Set() }
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) {
friends.insert(pubkey)
Expand Down
4 changes: 3 additions & 1 deletion damus/Models/FollowingModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ class FollowingModel {
var needs_sub: Bool = true

let contacts: [Pubkey]
let hashtags: [Hashtag]

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.contacts = contacts
self.hashtags = hashtags
}

func get_filter() -> NostrFilter {
Expand Down
2 changes: 1 addition & 1 deletion damus/Nostr/Id.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct Privkey: IdType {
}


struct Hashtag: TagConvertible {
struct Hashtag: TagConvertible, Hashable {
let hashtag: String

static func from_tag(tag: TagSequence) -> Hashtag? {
Expand Down
2 changes: 2 additions & 0 deletions damus/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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_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 {
let damus = DamusState.empty
Expand Down
95 changes: 88 additions & 7 deletions damus/Views/FollowingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
let damus_state: DamusState
let friended_followers: [Pubkey]
Expand Down Expand Up @@ -65,35 +112,69 @@ struct FollowersView: View {
}
}

enum FollowingViewTabSelection: Int {
case people = 0
case hashtags = 1
}

struct FollowingView: View {
let damus_state: DamusState

let following: FollowingModel
@State var tab_selection: FollowingViewTabSelection = .people
@Environment(\.colorScheme) var colorScheme


var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(following.contacts.reversed(), id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
TabView(selection: $tab_selection) {
ScrollView {
LazyVStack(alignment: .leading) {
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 {
following.subscribe()
}
.onDisappear {
following.unsubscribe()
}
.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 {
static var previews: some View {
FollowingView(damus_state: test_damus_state, following: test_following_model)
}
}
*/

3 changes: 2 additions & 1 deletion damus/Views/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ struct ProfileView: View {
HStack {
if let contact = profile.contacts {
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)) {
HStack {
let noun_text = Text(verbatim: "\(pluralizedString(key: "following_count", count: profile.following))").font(.subheadline).foregroundColor(.gray)
Expand Down

0 comments on commit 8586eed

Please sign in to comment.