diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index de4189afe..0b1161831 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -396,6 +396,7 @@ 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; + 5C02F2C22BA3C767002BF29D /* SearchContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C02F2C12BA3C767002BF29D /* SearchContentView.swift */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; @@ -405,6 +406,7 @@ 5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; }; 5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; }; 5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; }; + 5C8E588B2BACB252003D0A45 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8E588A2BACB252003D0A45 /* PostingTimelineView.swift */; }; 5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; }; 5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; }; 5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; }; @@ -1319,6 +1321,7 @@ 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = ""; }; 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = ""; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; + 5C02F2C12BA3C767002BF29D /* SearchContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContentView.swift; sourceTree = ""; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = ""; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = ""; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = ""; }; @@ -1328,6 +1331,7 @@ 5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = ""; }; 5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = ""; }; 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = ""; }; + 5C8E588A2BACB252003D0A45 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = ""; }; 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = ""; }; 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = ""; }; 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = ""; }; @@ -2400,6 +2404,7 @@ 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */, 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */, 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */, + 5C02F2C12BA3C767002BF29D /* SearchContentView.swift */, ); path = Search; sourceTree = ""; @@ -2408,6 +2413,7 @@ isa = PBXGroup; children = ( 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */, + 5C8E588A2BACB252003D0A45 /* PostingTimelineView.swift */, ); path = Timeline; sourceTree = ""; @@ -3120,6 +3126,7 @@ 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */, 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */, BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */, + 5C8E588B2BACB252003D0A45 /* PostingTimelineView.swift in Sources */, 4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */, 4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */, 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */, @@ -3365,6 +3372,7 @@ 6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */, + 5C02F2C22BA3C767002BF29D /* SearchContentView.swift in Sources */, 4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */, 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, 4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 930f163b0..55578d925 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -90,59 +90,6 @@ struct ContentView: View { // connect retry timer let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - var mystery: some View { - Text("Are you lost?", comment: "Text asking the user if they are lost in the app.") - .id("what") - } - - func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) { - var filters = ContentFilters.defaults(damus_state: damus_state!) - filters.append(fstate.filter) - return ContentFilters(filters: filters).filter - } - - var PostingTimelineView: some View { - VStack { - ZStack { - TabView(selection: $filter_state) { - // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. - mystery - - contentTimelineView(filter: content_filter(.posts)) - .tag(FilterState.posts) - .id(FilterState.posts) - contentTimelineView(filter: content_filter(.posts_and_replies)) - .tag(FilterState.posts_and_replies) - .id(FilterState.posts_and_replies) - } - .tabViewStyle(.page(indexDisplayMode: .never)) - - if privkey != nil { - PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) { - self.active_sheet = .post(.posting(.none)) - } - } - } - } - .safeAreaInset(edge: .top, spacing: 0) { - VStack(spacing: 0) { - CustomPicker(selection: $filter_state, content: { - Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts) - Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies) - }) - Divider() - .frame(height: 1) - } - .background(colorScheme == .dark ? Color.black : Color.white) - } - } - - func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { - TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) { - PullDownSearchView(state: damus_state, on_cancel: {}) - } - } - func navIsAtRoot() -> Bool { return navigationCoordinator.isAtRoot() } @@ -170,7 +117,7 @@ struct ContentView: View { } case .home: - PostingTimelineView + PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet) case .notifications: NotificationsView(state: damus, notifications: home.notifications) diff --git a/damus/Views/Search/PullDownSearch.swift b/damus/Views/Search/PullDownSearch.swift index 21e98559f..42f05529e 100644 --- a/damus/Views/Search/PullDownSearch.swift +++ b/damus/Views/Search/PullDownSearch.swift @@ -10,12 +10,12 @@ import Foundation import SwiftUI struct PullDownSearchView: View { - @State private var search_text = "" - @State private var results: [NostrEvent] = [] - @State private var is_active: Bool = false let debouncer: Debouncer = Debouncer(interval: 0.25) let state: DamusState - let on_cancel: () -> Void + + @Binding var search_text: String + @Binding var results: [NostrEvent] + @FocusState private var isFocused: Bool func do_search(query: String) { let limit = 16 @@ -51,12 +51,16 @@ struct PullDownSearchView: View { results = res_ } } - - var body: some View { - VStack(alignment: .leading) { - HStack { - TextField(NSLocalizedString("Search", comment: "Title of the text field for searching."), text: $search_text) - .textFieldStyle(RoundedBorderTextFieldStyle()) + + var SearchInput: some View { + HStack { + HStack{ + Image("search") + .foregroundColor(.gray) + TextField(NSLocalizedString("Search", comment: "Placeholder text to prompt entry of search query."), text: $search_text) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .focused($isFocused) .onChange(of: search_text) { query in debouncer.debounce { Task.detached { @@ -64,63 +68,33 @@ struct PullDownSearchView: View { } } } - .onTapGesture { - is_active = true - } - - if is_active { - Button(action: { - search_text = "" - results = [] - end_editing() - on_cancel() - }, label: { - Text("Cancel", comment: "Button to cancel out of search text entry mode.") - }) - } } - .padding() - - if results.count > 0 { - HStack { - Image("search") - Text(NSLocalizedString("Top hits", comment: "A label indicating that the notes being displayed below it are all top note search results")) - Spacer() - } - .padding(.horizontal) - .foregroundColor(.secondary) - - ForEach(results, id: \.self) { note in - EventView(damus: state, event: note) - .onTapGesture { - let event = note.get_inner_event(cache: state.events) ?? note - let thread = ThreadModel(event: event, damus_state: state) - state.nav.push(route: Route.Thread(thread: thread)) - } - } - - HStack { - Image("notes.fill") - Text(NSLocalizedString("Notes", comment: "A label indicating that the notes being displayed below it are from a timeline, not search results")) - Spacer() - } - .foregroundColor(.secondary) - .padding(.horizontal) - } else if results.count == 0 && !search_text.isEmpty { - HStack { - Image("search") - Text(NSLocalizedString("No results", comment: "A label indicating that note search resulted in no results")) - Spacer() - } - .padding(.horizontal) - .foregroundColor(.secondary) + .padding(7) + .background(.secondary.opacity(0.2)) + .cornerRadius(15) + + if(!search_text.isEmpty || isFocused) { + Button(action: { + search_text = "" + isFocused = false + results = [] + }, label: { + Text("Cancel", comment: "Button to cancel out of search text entry mode.") + }) } } + .padding([.horizontal, .top], 10) + } + + var body: some View { + VStack(alignment: .leading) { + SearchInput + } } } struct PullDownSearchView_Previews: PreviewProvider { static var previews: some View { - PullDownSearchView(state: test_damus_state, on_cancel: {}) + PullDownSearchView(state: test_damus_state, search_text: .constant(""), results: .constant([])) } } diff --git a/damus/Views/Search/SearchContentView.swift b/damus/Views/Search/SearchContentView.swift new file mode 100644 index 000000000..38e6fd92f --- /dev/null +++ b/damus/Views/Search/SearchContentView.swift @@ -0,0 +1,74 @@ +// +// SearchContentView.swift +// damus +// +// Created by eric on 3/14/24. +// + +import SwiftUI + +struct SearchContentView: View { + let state: DamusState + @Binding var search_text: String + @Binding var results: [NostrEvent] + + var event_options: EventViewOptions { + if self.state.settings.truncate_timeline_text { + return [.wide, .truncate_content] + } + return [.wide] + } + + var body: some View { + ScrollView { + if results.count > 0 { + HStack { + Image("search") + Text(NSLocalizedString("Top hits", comment: "A label indicating that the notes being displayed below it are all top note search results")) + Spacer() + } + .foregroundColor(.secondary) + .padding() + + ForEach(results, id: \.self) { note in + EventView(damus: state, event: note, options: event_options) + .onTapGesture { + let event = note.get_inner_event(cache: state.events) ?? note + let thread = ThreadModel(event: event, damus_state: state) + state.nav.push(route: Route.Thread(thread: thread)) + } + .padding(.top, 7) + + ThiccDivider() + .padding([.top], 7) + } + + } else if results.count == 0 && !search_text.isEmpty { + VStack(alignment: .center) { + HStack { + Image("search") + Text(NSLocalizedString("No results", comment: "A label indicating that note search resulted in no results")) + } + } + .padding(.vertical) + .foregroundColor(.secondary) + } else if search_text.isEmpty { + VStack(alignment: .center) { + Text(NSLocalizedString("Try searching for keywords", comment: "A label suggesting the user search for keywords")) + } + .padding(.vertical) + .foregroundColor(.secondary) + } + } + .onChange(of: search_text) { _ in + if search_text.isEmpty { + results = [NostrEvent]() + } + } + .onChange(of: results) { _ in + if search_text.isEmpty { + results = [NostrEvent]() + } + } + } +} diff --git a/damus/Views/Timeline/PostingTimelineView.swift b/damus/Views/Timeline/PostingTimelineView.swift new file mode 100644 index 000000000..397d19a6f --- /dev/null +++ b/damus/Views/Timeline/PostingTimelineView.swift @@ -0,0 +1,125 @@ +// +// PostingTimelineView.swift +// damus +// +// Created by eric on 3/21/24. +// + +import SwiftUI + +struct PostingTimelineView: View { + let damus_state: DamusState + var home: HomeModel + @State var search: String = "" + @State var results: [NostrEvent] = [] + @State var initialOffset: CGFloat? + @State var offset: CGFloat? + @State var showSearch: Bool = true + @Binding var active_sheet: Sheets? + @FocusState private var isSearchFocused: Bool + @SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies + + var mystery: some View { + Text("Are you lost?", comment: "Text asking the user if they are lost in the app.") + .id("what") + } + + func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) { + var filters = ContentFilters.defaults(damus_state: damus_state) + filters.append(fstate.filter) + return ContentFilters(filters: filters).filter + } + + func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { + VStack(spacing: 0) { + if self.showSearch { + PullDownSearchView(state: damus_state, search_text: $search, results: $results) + .focused($isSearchFocused) + } + if !isSearchFocused && search.isEmpty { + TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) { + GeometryReader { geometry in + Color.clear.preference(key: OffsetKey.self, value: geometry.frame(in: .global).minY) + .frame(height: 0) + } + } + } else { + SearchContentView(state: damus_state, search_text: $search, results: $results) + .padding(.top) + .scrollDismissesKeyboard(.immediately) + } + } + .onPreferenceChange(OffsetKey.self) { + if self.initialOffset == nil || self.initialOffset == 0 { + self.initialOffset = $0 + } + + self.offset = $0 + + guard let initialOffset = self.initialOffset, + let offset = self.offset else { + return + } + + if(initialOffset > offset){ + self.showSearch = false + } else { + self.showSearch = true + } + } + } + + var body: some View { + VStack { + ZStack { + TabView(selection: $filter_state) { + // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. + mystery + + contentTimelineView(filter: content_filter(.posts)) + .tag(FilterState.posts) + .id(FilterState.posts) + contentTimelineView(filter: content_filter(.posts_and_replies)) + .tag(FilterState.posts_and_replies) + .id(FilterState.posts_and_replies) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + + if damus_state.keypair.privkey != nil && (!isSearchFocused && search.isEmpty) { + PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { + active_sheet = .post(.posting(.none)) + } + } + } + } + .safeAreaInset(edge: .top, spacing: 0) { + if !isSearchFocused && search.isEmpty { + VStack(spacing: 0) { + CustomPicker(selection: $filter_state, content: { + Text("Notes", comment: "Filter label for seeing only notes (instead of notes and replies).").tag(FilterState.posts) + Text("Notes & Replies", comment: "Filter label for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies) + }) + Divider() + .frame(height: 1) + } + .background(DamusColors.adaptableWhite) + .transition(.opacity) + } + } + } +} + +struct OffsetKey: PreferenceKey { + static let defaultValue: CGFloat? = nil + static func reduce(value: inout CGFloat?, + nextValue: () -> CGFloat?) { + value = value ?? nextValue() + } +} + +struct PostingTimelineView_Previews: PreviewProvider { + static var previews: some View { + let home: HomeModel = HomeModel() + PostingTimelineView(damus_state: test_damus_state, home: home, active_sheet: .constant(.none)) + } +}