diff --git a/damus/Components/Search/SearchHeaderView.swift b/damus/Components/Search/SearchHeaderView.swift index 743f096e..78ce111a 100644 --- a/damus/Components/Search/SearchHeaderView.swift +++ b/damus/Components/Search/SearchHeaderView.swift @@ -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") } } } @@ -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 @@ -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) } } } @@ -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: 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, diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index 9c07c44c..afb2cb94 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -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) diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift index 5a2306a2..4b70ec3b 100644 --- a/damus/Models/FollowingModel.swift +++ b/damus/Models/FollowingModel.swift @@ -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 { diff --git a/damus/Nostr/Id.swift b/damus/Nostr/Id.swift index 912736b5..f97bf7a6 100644 --- a/damus/Nostr/Id.swift +++ b/damus/Nostr/Id.swift @@ -65,7 +65,7 @@ struct Privkey: IdType { } -struct Hashtag: TagConvertible { +struct Hashtag: TagConvertible, Hashable { let hashtag: String static func from_tag(tag: TagSequence) -> Hashtag? { diff --git a/damus/TestData.swift b/damus/TestData.swift index 2f225ba2..16278ff4 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -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 diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift index 2f2693f6..6cb911c7 100644 --- a/damus/Views/FollowingView.swift +++ b/damus/Views/FollowingView.swift @@ -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] @@ -65,21 +112,44 @@ 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() } @@ -87,13 +157,24 @@ struct FollowingView: View { 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) } } - */ + diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 6cab5c5e..0f7ce174 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -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)