diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 26d825a1..11caf01e 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; 4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; }; + 4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; }; 4C687C272A6039500092C550 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C262A6039500092C550 /* TestData.swift */; }; 4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; @@ -619,6 +620,7 @@ 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = ""; }; 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = ""; }; 4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = ""; }; + 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = ""; }; 4C687C262A6039500092C550 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = ""; }; 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; @@ -1099,6 +1101,14 @@ path = Notifications; sourceTree = ""; }; + 4C687C2A2A6058450092C550 /* Search */ = { + isa = PBXGroup; + children = ( + 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */, + ); + path = Search; + sourceTree = ""; + }; 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( @@ -1404,6 +1414,7 @@ 4CE4F9DF285287A000C00DD9 /* Components */ = { isa = PBXGroup; children = ( + 4C687C2A2A6058450092C550 /* Search */, 4C7D09702A0AEF4C00943473 /* Gradients */, 31D2E846295218AF006D67F8 /* Shimmer.swift */, 4CD7641A28A1641400B6928F /* EndBlock.swift */, @@ -1946,6 +1957,7 @@ 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */, 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */, 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */, + 4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */, 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */, 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */, diff --git a/damus/Components/Search/SearchHeaderView.swift b/damus/Components/Search/SearchHeaderView.swift new file mode 100644 index 00000000..fb88b56c --- /dev/null +++ b/damus/Components/Search/SearchHeaderView.swift @@ -0,0 +1,134 @@ +// +// SearchIconView.swift +// damus +// +// Created by William Casarin on 2023-07-12. +// + +import SwiftUI + +struct SearchHeaderView: View { + let state: DamusState + let described: DescribedSearch + @State var is_following: Bool + + init(state: DamusState, described: DescribedSearch) { + self.state = state + self.described = described + + let is_following = (described.is_hashtag.map { + ht in is_following_hashtag(contacts: state.contacts.event, hashtag: ht) + }) ?? false + + self._is_following = State(wrappedValue: is_following) + } + + 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("#") + .font(.largeTitle.bold()) + .foregroundStyle(PinkGradient) + .mask(Text("#") + .font(.largeTitle.bold())) + + case .unknown: + Image(systemName: "magnifyingglass") + .font(.title.bold()) + .foregroundStyle(PinkGradient) + } + } + } + + var SearchText: Text { + switch described { + case .hashtag(let ht): + Text(verbatim: "#" + ht) + case .unknown: + Text("Search") + } + } + + func unfollow(_ hashtag: String) { + is_following = false + handle_unfollow(state: state, unfollow: .t(hashtag)) + } + + func follow(_ hashtag: String) { + is_following = true + handle_follow(state: state, follow: .t(hashtag)) + } + + func FollowButton(_ ht: String) -> some View { + return Button(action: { follow(ht) }) { + Text("Follow hashtag") + .font(.footnote.bold()) + } + .buttonStyle(GradientButtonStyle(padding: 10)) + } + + func UnfollowButton(_ ht: String) -> some View { + return Button(action: { unfollow(ht) }) { + Text("Unfollow hashtag") + .font(.footnote.bold()) + } + .buttonStyle(GradientButtonStyle(padding: 10)) + } + + var body: some View { + HStack(alignment: .center, spacing: 30) { + Icon + + VStack(alignment: .leading, spacing: 10.0) { + SearchText + .foregroundStyle(DamusLogoGradient.gradient) + .font(.title.bold()) + + if state.is_privkey_user, case .hashtag(let ht) = described { + if is_following { + UnfollowButton(ht) + } else { + FollowButton(ht) + } + } + } + } + .onReceive(handle_notify(.followed)) { notif in + let ref = notif.object as! ReferencedId + guard hashtag_matches_search(desc: self.described, ref: ref) else { return } + self.is_following = true + } + .onReceive(handle_notify(.unfollowed)) { notif in + let ref = notif.object as! ReferencedId + guard hashtag_matches_search(desc: self.described, ref: ref) else { return } + self.is_following = false + } + } +} + +func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool { + guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht + else { return false } + return true +} + +func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool { + guard let contacts else { return false } + return is_already_following(contacts: contacts, follow: .t(hashtag)) +} + + +struct SearchHeaderView_Previews: PreviewProvider { + static var previews: some View { + VStack(alignment: .leading) { + SearchHeaderView(state: test_damus_state(), described: .hashtag("damus")) + + SearchHeaderView(state: test_damus_state(), described: .unknown) + } + } +} diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index dd328372..818861b8 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -11,32 +11,70 @@ struct SearchView: View { let appstate: DamusState @ObservedObject var search: SearchModel @Environment(\.dismiss) var dismiss - + + let height: CGFloat = 250.0 + var body: some View { - TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) - .navigationBarTitle(describe_search(search.search)) - .onReceive(handle_notify(.switched_timeline)) { obj in - dismiss() - } - .onAppear() { - search.subscribe() - } - .onDisappear() { - search.unsubscribe() - } - .onReceive(handle_notify(.new_mutes)) { notif in - search.filter_muted() + TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) { + ZStack(alignment: .leading) { + DamusBackground(maxHeight: height) + .mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom)) + SearchHeaderView(state: appstate, described: described_search) + .padding(.leading, 30) + .padding(.top, 100) } + } + .ignoresSafeArea() + .onReceive(handle_notify(.switched_timeline)) { obj in + dismiss() + } + .onAppear() { + search.subscribe() + } + .onDisappear() { + search.unsubscribe() + } + .onReceive(handle_notify(.new_mutes)) { notif in + search.filter_muted() + } + } + + var described_search: DescribedSearch { + return describe_search(search.search) } } -func describe_search(_ filter: NostrFilter) -> String { - if let hashtags = filter.hashtag { - if hashtags.count >= 1 { - return "#" + hashtags[0] +enum DescribedSearch { + case hashtag(String) + case unknown + + var is_hashtag: String? { + switch self { + case .hashtag(let ht): + return ht + case .unknown: + return nil } } - return "Search" + + var description: String { + switch self { + case .hashtag(let s): + return "#" + s + case .unknown: + return "Search" + } + } +} + +func describe_search(_ filter: NostrFilter) -> DescribedSearch { + if let hashtags = filter.hashtag { + if hashtags.count >= 1 { + return .hashtag(hashtags[0]) + } + } + + return .unknown } struct SearchView_Previews: PreviewProvider {