search: highlight terms in note search results
Changelog-Changed: Highlight note search results Signed-off-by: alltheseas
This commit is contained in:
committed by
William Casarin
parent
9eda7e5886
commit
d3a54458f5
@@ -21,12 +21,14 @@ struct EventView: View {
|
||||
let options: EventViewOptions
|
||||
let damus: DamusState
|
||||
let pubkey: Pubkey
|
||||
let highlightTerms: [String]
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, options: EventViewOptions = []) {
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, options: EventViewOptions = [], highlightTerms: [String] = []) {
|
||||
self.event = event
|
||||
self.options = options
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey ?? event.pubkey
|
||||
self.highlightTerms = highlightTerms
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -48,7 +50,7 @@ struct EventView: View {
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightView(state: damus, event: event, options: options)
|
||||
} else {
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options, highlightTerms: highlightTerms)
|
||||
//.padding([.top], 6)
|
||||
}
|
||||
}
|
||||
@@ -158,4 +160,3 @@ struct EventView_Previews: PreviewProvider {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import LinkPresentation
|
||||
import NaturalLanguage
|
||||
import MarkdownUI
|
||||
import Translation
|
||||
import UIKit
|
||||
|
||||
struct Blur: UIViewRepresentable {
|
||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||
@@ -49,6 +50,7 @@ struct NoteContentView: View {
|
||||
let size: EventViewKind
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
let highlightTerms: [String]
|
||||
|
||||
@State var isAppleTranslationPopoverPresented: Bool = false
|
||||
|
||||
@@ -63,12 +65,13 @@ struct NoteContentView: View {
|
||||
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, blur_images: Bool, size: EventViewKind, options: EventViewOptions) {
|
||||
init(damus_state: DamusState, event: NostrEvent, blur_images: Bool, size: EventViewKind, options: EventViewOptions, highlightTerms: [String] = []) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.blur_images = blur_images
|
||||
self.size = size
|
||||
self.options = options
|
||||
self.highlightTerms = highlightTerms
|
||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
||||
let cached = damus_state.events.get_cache_data(event.id)
|
||||
self._preview_model = ObservedObject(wrappedValue: cached.preview_model)
|
||||
@@ -168,20 +171,22 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
func MainContent(artifacts: NoteArtifactsSeparated) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
let contentToRender = highlightedContent(artifacts.content)
|
||||
|
||||
return VStack(alignment: .leading) {
|
||||
if size == .selected {
|
||||
if with_padding {
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size)
|
||||
}
|
||||
} else {
|
||||
if with_padding {
|
||||
truncatedText(content: artifacts.content)
|
||||
truncatedText(content: contentToRender)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
truncatedText(content: artifacts.content)
|
||||
truncatedText(content: contentToRender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +395,51 @@ struct NoteContentView: View {
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
|
||||
var normalizedHighlightTerms: [String] {
|
||||
var output: [String] = []
|
||||
var seen = Set<String>()
|
||||
|
||||
let preparedTerms = highlightTerms
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.flatMap { term -> [String] in
|
||||
if term.hasPrefix("#") {
|
||||
let stripped = String(term.dropFirst())
|
||||
return [term, stripped]
|
||||
}
|
||||
return [term]
|
||||
}
|
||||
|
||||
for term in preparedTerms {
|
||||
let lower = term.lowercased()
|
||||
if !lower.isEmpty && seen.insert(lower).inserted {
|
||||
output.append(lower)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func highlightedContent(_ content: CompatibleText) -> CompatibleText {
|
||||
guard !normalizedHighlightTerms.isEmpty else { return content }
|
||||
|
||||
var attributed = content.attributed
|
||||
highlightAttributedString(&attributed)
|
||||
return CompatibleText(attributed: attributed)
|
||||
}
|
||||
|
||||
func highlightAttributedString(_ attributed: inout AttributedString) {
|
||||
for term in normalizedHighlightTerms {
|
||||
var searchStart = attributed.startIndex
|
||||
|
||||
while let range = attributed[searchStart...].range(of: term, options: .caseInsensitive) {
|
||||
attributed[range].backgroundColor = DamusColors.highlight
|
||||
searchStart = range.upperBound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ArtifactContent
|
||||
.onReceive(handle_notify(.profile_updated)) { profile in
|
||||
|
||||
@@ -38,13 +38,15 @@ struct TextEvent: View {
|
||||
let pubkey: Pubkey
|
||||
let options: EventViewOptions
|
||||
let evdata: EventData
|
||||
let highlightTerms: [String]
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey, options: EventViewOptions) {
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey, options: EventViewOptions, highlightTerms: [String] = []) {
|
||||
self.damus = damus
|
||||
self.event = event
|
||||
self.pubkey = pubkey
|
||||
self.options = options
|
||||
self.evdata = damus.events.get_cache_data(event.id)
|
||||
self.highlightTerms = highlightTerms
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -62,7 +64,8 @@ struct TextEvent: View {
|
||||
event: event,
|
||||
blur_images: blur_imgs,
|
||||
size: .small,
|
||||
options: options)
|
||||
options: options,
|
||||
highlightTerms: highlightTerms)
|
||||
}
|
||||
|
||||
return NoteContentView(
|
||||
@@ -70,7 +73,8 @@ struct TextEvent: View {
|
||||
event: event,
|
||||
blur_images: blur_imgs,
|
||||
size: .normal,
|
||||
options: options
|
||||
options: options,
|
||||
highlightTerms: highlightTerms
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,4 +91,3 @@ struct TextEvent_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,38 @@ struct NDBSearchView: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
@Binding var results: [NostrEvent]
|
||||
let searchQuery: String
|
||||
|
||||
var highlightTerms: [String] {
|
||||
let trimmed = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return [] }
|
||||
|
||||
let parts = trimmed.split(whereSeparator: { $0.isWhitespace })
|
||||
var terms: [String] = []
|
||||
|
||||
for part in parts {
|
||||
let term = String(part)
|
||||
let strippedHashtag = term.hasPrefix("#") ? String(term.dropFirst()) : nil
|
||||
|
||||
if let stripped = strippedHashtag, !stripped.isEmpty {
|
||||
terms.append(stripped)
|
||||
}
|
||||
|
||||
if !term.isEmpty {
|
||||
terms.append(term)
|
||||
}
|
||||
}
|
||||
|
||||
var deduped: [String] = []
|
||||
var seen = Set<String>()
|
||||
for term in terms.map({ $0.lowercased() }) {
|
||||
if seen.insert(term).inserted {
|
||||
deduped.append(term)
|
||||
}
|
||||
}
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -24,9 +56,16 @@ struct NDBSearchView: View {
|
||||
.padding()
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if !highlightTerms.isEmpty {
|
||||
Text("Search: \(searchQuery)")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
|
||||
LazyVStack {
|
||||
ForEach(results, id: \.self) { note in
|
||||
EventView(damus: damus_state, event: note, options: [.truncate_content])
|
||||
EventView(damus: damus_state, event: note, options: [.truncate_content], highlightTerms: highlightTerms)
|
||||
.onTapGesture {
|
||||
let event = note.get_inner_event(cache: damus_state.events) ?? note
|
||||
let thread = ThreadModel(event: event, damus_state: damus_state)
|
||||
|
||||
@@ -68,7 +68,7 @@ struct InnerSearchResults: View {
|
||||
}
|
||||
|
||||
func TextSearch(_ txt: String) -> some View {
|
||||
return NavigationLink(value: Route.NDBSearch(results: $results)) {
|
||||
return NavigationLink(value: Route.NDBSearch(results: $results, query: txt)) {
|
||||
HStack {
|
||||
Text("Search word: \(txt)", comment: "Navigation link to search for a word.")
|
||||
}
|
||||
@@ -296,4 +296,3 @@ func search_profiles(profiles: Profiles, contacts: Contacts, search: String) ->
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,15 @@ class DamusColors {
|
||||
static let green = Color("DamusGreen")
|
||||
static let purple = Color("DamusPurple")
|
||||
static let deepPurple = Color("DamusDeepPurple")
|
||||
static let highlight = Color("DamusHighlight")
|
||||
static let highlight = Color(UIColor { traits in
|
||||
if traits.userInterfaceStyle == .dark {
|
||||
// Vivid Damus magenta tuned for dark backgrounds (strong contrast without glow)
|
||||
return UIColor(red: 0.95, green: 0.43, blue: 0.82, alpha: 0.78)
|
||||
} else {
|
||||
// Slightly deeper pink on light so text stays legible
|
||||
return UIColor(red: 0.88, green: 0.32, blue: 0.74, alpha: 0.62)
|
||||
}
|
||||
})
|
||||
static let blue = Color("DamusBlue")
|
||||
static let bitcoin = Color("Bitcoin")
|
||||
static let success = Color("DamusSuccessPrimary")
|
||||
|
||||
@@ -39,7 +39,7 @@ enum Route: Hashable {
|
||||
case Reactions(reactions: EventsModel)
|
||||
case Zaps(target: ZapTarget)
|
||||
case Search(search: SearchModel)
|
||||
case NDBSearch(results: Binding<[NostrEvent]>)
|
||||
case NDBSearch(results: Binding<[NostrEvent]>, query: String)
|
||||
case EULA
|
||||
case Login
|
||||
case CreateAccount
|
||||
@@ -115,8 +115,8 @@ enum Route: Hashable {
|
||||
ZapsView(state: damusState, target: target)
|
||||
case .Search(let search):
|
||||
SearchView(appstate: damusState, search: search)
|
||||
case .NDBSearch(let results):
|
||||
NDBSearchView(damus_state: damusState, results: results)
|
||||
case .NDBSearch(let results, let query):
|
||||
NDBSearchView(damus_state: damusState, results: results, searchQuery: query)
|
||||
case .EULA:
|
||||
EULAView(nav: navigationCoordinator)
|
||||
case .Login:
|
||||
@@ -226,8 +226,9 @@ enum Route: Hashable {
|
||||
case .Search(let search):
|
||||
hasher.combine("search")
|
||||
hasher.combine(search.search)
|
||||
case .NDBSearch:
|
||||
case .NDBSearch(_, let query):
|
||||
hasher.combine("results")
|
||||
hasher.combine(query)
|
||||
case .EULA:
|
||||
hasher.combine("eula")
|
||||
case .Login:
|
||||
|
||||
Reference in New Issue
Block a user