Skip to content

feat: full offline search for Blog, Catalogue, and Quotes - #227

Closed
danieltmbr wants to merge 5 commits into
mainfrom
feat/search-native
Closed

feat: full offline search for Blog, Catalogue, and Quotes#227
danieltmbr wants to merge 5 commits into
mainfrom
feat/search-native

Conversation

@danieltmbr

Copy link
Copy Markdown
Owner

Summary

  • Per-tab .searchable bars on Blog, Catalogue, and Quotes tabs — each searches only its own content, respecting active language/category/source filters
  • Dedicated Search tab shows results sectioned by type (Posts / Catalogue / Quotes) with a scope bar (All / Blog / Catalogue / Quotes)
  • Every search runs a local SwiftData predicate query for instant cached results, then fires a network search that upserts results for offline parity
  • CatalogueSearchEngine (@MainActor @Observable) drives catalogue results since @Query can't cross the PreviewRecordNoteRecord UUID link; delegates all DB work to CatalogueStore.search

Persistence layer changes:

  • PreviewRecord.secondaryInfo: String?String = "" — SwiftData cannot generate CONTAINS[cd] SQL with TERNARY/optional-chain on the LHS; non-optional unblocks pure DB text search. DTO (PreviewResponse.secondaryInfo: String?) unchanged; coalesced at sync boundary
  • PreviewRecord.search(term:categories:languages:): composed predicate — textMatch (2 expressions) + filterMatch via evaluate to avoid compiler type-check timeout
  • NoteRecord.search(term:languages:), PostRecord.search(term:languages:), QuoteRecord.search(term:sources:languages:): standard single-predicate text + filter
  • CatalogueStore.search(...): two predicate-driven fetches (previews + notes) + targeted ID fetch for note→preview resolution; no full-table scans
  • CatalogueStore.upsertSearch(_:): safe partial upsert — upserts items and note-body matches without quote reconciliation (preserves existing quotes)

Test plan

  • Xcode build passes for app-persistence, app-core, and Reader target
  • Blog tab: search term filters posts; result updates after network search upserts previously-unsynced posts
  • Catalogue tab: title match and subtitle match both return results with no crash; note-body match surfaces with snippet; category + language filters narrow while searching
  • Quotes tab: body search works; source + language filters apply
  • Search tab: sectioned results, scope bar narrows correctly; catalogue section shows note snippets
  • Airplane mode: repeat a previously-run search — cached results (incl. note snippets) render without network

Note: If the app crashes on launch with a SwiftData migration error due to secondaryInfo optionality change, delete the app to reset the cache — Reader DB is a disposable network cache.

🤖 Generated with Claude Code

danieltmbr and others added 5 commits July 6, 2026 13:06
Adds per-tab .searchable bars (Blog, Catalogue, Quotes) and wires up the
dedicated Search tab. Every search runs a local SwiftData predicate query
(instant cached results) and concurrently fires a network search that
upserts results, keeping offline and online parity.

Catalogue search required two-step DB queries: PreviewRecord.search
(title/subtitle + category/language via #Predicate composition) and
NoteRecord.search (body text + language). Makes PreviewRecord.secondaryInfo
non-optional (String = "") to enable SQL CONTAINS[cd] predicates —
SwiftData cannot generate SQL for TERNARY/optional-chain on the CONTAINS LHS.
CatalogueSearchResult and CatalogueSearchEngine moved to app-persistence
so predicates run against internal record fields directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typed detail sections (Song/Album/Book/Movie/Podcast/Playlist) now pass
`item: PreviewRecord` instead of `previewID: UUID`, always render a
`CatalogueItemHeader` coalescing from the typed record when available or
the preview as fallback, publish `catalogueItemRefresh` unconditionally
from `item.sourceID`, and fire a single-item fetch on appear only when
the typed record is absent — so search-upserted previews materialise
immediately on navigation rather than silently showing nothing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…point URL

PostSearchRequest now targets api/posts (the list endpoint) with
PageResult<PostResponse> as the response type, matching the backend
change that folded search into the list via the ?term= query param.
PostSearchQuery moves to tmbr-core alongside CatalogueSearchQuery and
QuoteSearchQuery. ReaderApp uses results.items to unwrap the page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e/quotes?term=

Backend folded quote search into the list endpoint; mirror that on the native
side by re-adding QuoteSearchQuery (same shape as PostSearchQuery) and updating
the path from the now-removed /api/catalogue/quotes/search.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuoteSearchQuery, QuoteSearchRequest, and QuoteSearchLoader were defined but
never injected into QuotesModel in any app target. Removing to keep the
search request surface honest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@danieltmbr
danieltmbr force-pushed the feat/search-native branch from 65b033c to ac359cd Compare July 6, 2026 12:07
@danieltmbr

Copy link
Copy Markdown
Owner Author

Split into #228 (persistence & networking) and #229 (SwiftUI feature layer).

@danieltmbr danieltmbr closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant