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 options: EventViewOptions
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let pubkey: Pubkey
|
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.event = event
|
||||||
self.options = options
|
self.options = options
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = pubkey ?? event.pubkey
|
self.pubkey = pubkey ?? event.pubkey
|
||||||
|
self.highlightTerms = highlightTerms
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -48,7 +50,7 @@ struct EventView: View {
|
|||||||
} else if event.known_kind == .highlight {
|
} else if event.known_kind == .highlight {
|
||||||
HighlightView(state: damus, event: event, options: options)
|
HighlightView(state: damus, event: event, options: options)
|
||||||
} else {
|
} else {
|
||||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options, highlightTerms: highlightTerms)
|
||||||
//.padding([.top], 6)
|
//.padding([.top], 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,4 +160,3 @@ struct EventView_Previews: PreviewProvider {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import LinkPresentation
|
|||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
import MarkdownUI
|
import MarkdownUI
|
||||||
import Translation
|
import Translation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
struct Blur: UIViewRepresentable {
|
struct Blur: UIViewRepresentable {
|
||||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||||
@@ -49,6 +50,7 @@ struct NoteContentView: View {
|
|||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
let preview_height: CGFloat?
|
let preview_height: CGFloat?
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
|
let highlightTerms: [String]
|
||||||
|
|
||||||
@State var isAppleTranslationPopoverPresented: Bool = false
|
@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)))
|
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.damus_state = damus_state
|
||||||
self.event = event
|
self.event = event
|
||||||
self.blur_images = blur_images
|
self.blur_images = blur_images
|
||||||
self.size = size
|
self.size = size
|
||||||
self.options = options
|
self.options = options
|
||||||
|
self.highlightTerms = highlightTerms
|
||||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
||||||
let cached = damus_state.events.get_cache_data(event.id)
|
let cached = damus_state.events.get_cache_data(event.id)
|
||||||
self._preview_model = ObservedObject(wrappedValue: cached.preview_model)
|
self._preview_model = ObservedObject(wrappedValue: cached.preview_model)
|
||||||
@@ -168,20 +171,22 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MainContent(artifacts: NoteArtifactsSeparated) -> some View {
|
func MainContent(artifacts: NoteArtifactsSeparated) -> some View {
|
||||||
VStack(alignment: .leading) {
|
let contentToRender = highlightedContent(artifacts.content)
|
||||||
|
|
||||||
|
return VStack(alignment: .leading) {
|
||||||
if size == .selected {
|
if size == .selected {
|
||||||
if with_padding {
|
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)
|
.padding(.horizontal)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if with_padding {
|
if with_padding {
|
||||||
truncatedText(content: artifacts.content)
|
truncatedText(content: contentToRender)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
} else {
|
} else {
|
||||||
truncatedText(content: artifacts.content)
|
truncatedText(content: contentToRender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +395,51 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.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 {
|
var body: some View {
|
||||||
ArtifactContent
|
ArtifactContent
|
||||||
.onReceive(handle_notify(.profile_updated)) { profile in
|
.onReceive(handle_notify(.profile_updated)) { profile in
|
||||||
|
|||||||
@@ -38,13 +38,15 @@ struct TextEvent: View {
|
|||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
let evdata: EventData
|
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.damus = damus
|
||||||
self.event = event
|
self.event = event
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.options = options
|
self.options = options
|
||||||
self.evdata = damus.events.get_cache_data(event.id)
|
self.evdata = damus.events.get_cache_data(event.id)
|
||||||
|
self.highlightTerms = highlightTerms
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -62,7 +64,8 @@ struct TextEvent: View {
|
|||||||
event: event,
|
event: event,
|
||||||
blur_images: blur_imgs,
|
blur_images: blur_imgs,
|
||||||
size: .small,
|
size: .small,
|
||||||
options: options)
|
options: options,
|
||||||
|
highlightTerms: highlightTerms)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoteContentView(
|
return NoteContentView(
|
||||||
@@ -70,7 +73,8 @@ struct TextEvent: View {
|
|||||||
event: event,
|
event: event,
|
||||||
blur_images: blur_imgs,
|
blur_images: blur_imgs,
|
||||||
size: .normal,
|
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
|
let damus_state: DamusState
|
||||||
@Binding var results: [NostrEvent]
|
@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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -24,9 +56,16 @@ struct NDBSearchView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if !highlightTerms.isEmpty {
|
||||||
|
Text("Search: \(searchQuery)")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(results, id: \.self) { note in
|
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 {
|
.onTapGesture {
|
||||||
let event = note.get_inner_event(cache: damus_state.events) ?? note
|
let event = note.get_inner_event(cache: damus_state.events) ?? note
|
||||||
let thread = ThreadModel(event: event, damus_state: damus_state)
|
let thread = ThreadModel(event: event, damus_state: damus_state)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ struct InnerSearchResults: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TextSearch(_ txt: String) -> some View {
|
func TextSearch(_ txt: String) -> some View {
|
||||||
return NavigationLink(value: Route.NDBSearch(results: $results)) {
|
return NavigationLink(value: Route.NDBSearch(results: $results, query: txt)) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Search word: \(txt)", comment: "Navigation link to search for a word.")
|
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 green = Color("DamusGreen")
|
||||||
static let purple = Color("DamusPurple")
|
static let purple = Color("DamusPurple")
|
||||||
static let deepPurple = Color("DamusDeepPurple")
|
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 blue = Color("DamusBlue")
|
||||||
static let bitcoin = Color("Bitcoin")
|
static let bitcoin = Color("Bitcoin")
|
||||||
static let success = Color("DamusSuccessPrimary")
|
static let success = Color("DamusSuccessPrimary")
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ enum Route: Hashable {
|
|||||||
case Reactions(reactions: EventsModel)
|
case Reactions(reactions: EventsModel)
|
||||||
case Zaps(target: ZapTarget)
|
case Zaps(target: ZapTarget)
|
||||||
case Search(search: SearchModel)
|
case Search(search: SearchModel)
|
||||||
case NDBSearch(results: Binding<[NostrEvent]>)
|
case NDBSearch(results: Binding<[NostrEvent]>, query: String)
|
||||||
case EULA
|
case EULA
|
||||||
case Login
|
case Login
|
||||||
case CreateAccount
|
case CreateAccount
|
||||||
@@ -115,8 +115,8 @@ enum Route: Hashable {
|
|||||||
ZapsView(state: damusState, target: target)
|
ZapsView(state: damusState, target: target)
|
||||||
case .Search(let search):
|
case .Search(let search):
|
||||||
SearchView(appstate: damusState, search: search)
|
SearchView(appstate: damusState, search: search)
|
||||||
case .NDBSearch(let results):
|
case .NDBSearch(let results, let query):
|
||||||
NDBSearchView(damus_state: damusState, results: results)
|
NDBSearchView(damus_state: damusState, results: results, searchQuery: query)
|
||||||
case .EULA:
|
case .EULA:
|
||||||
EULAView(nav: navigationCoordinator)
|
EULAView(nav: navigationCoordinator)
|
||||||
case .Login:
|
case .Login:
|
||||||
@@ -226,8 +226,9 @@ enum Route: Hashable {
|
|||||||
case .Search(let search):
|
case .Search(let search):
|
||||||
hasher.combine("search")
|
hasher.combine("search")
|
||||||
hasher.combine(search.search)
|
hasher.combine(search.search)
|
||||||
case .NDBSearch:
|
case .NDBSearch(_, let query):
|
||||||
hasher.combine("results")
|
hasher.combine("results")
|
||||||
|
hasher.combine(query)
|
||||||
case .EULA:
|
case .EULA:
|
||||||
hasher.combine("eula")
|
hasher.combine("eula")
|
||||||
case .Login:
|
case .Login:
|
||||||
|
|||||||
Reference in New Issue
Block a user