Files
notes/Notes/Views/Notes/NotesListView.swift
T
rzen 35e4d6750b Refactor capture UX: single New Note toolbar button, text-first editor
- NotesListView: remove two-button header; trailing New Note toolbar item (Cmd-N)
- NoteEditorView: text entry focused by default; extensible add-media bar
  (CaptureMedium enum, audio only for now) revealing the inline recorder;
  startRecording param removed
- Changelog entry updated
2026-07-15 18:39:11 -04:00

110 lines
3.5 KiB
Swift

import IndieSync
import SwiftData
import SwiftUI
/// The main list: a "Right here, right now" shelf of context-relevant notes
/// on top, then everything else newest-first. Search lives in its own tab
/// (`SearchView`, the tab bar's trailing magnifier).
struct NotesListView: View {
@Environment(SyncEngine.self) private var syncEngine
@Environment(ContextService.self) private var contextService
@Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity]
@State private var composing = false
@State private var editingNote: NoteEntity?
var body: some View {
NavigationStack {
list
.navigationTitle("Notes")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
composing = true
} label: {
Label("New Note", systemImage: "square.and.pencil")
}
.keyboardShortcut("n", modifiers: .command)
}
}
.sheet(isPresented: $composing) {
NoteEditorView(mode: .create)
}
.sheet(item: $editingNote) { note in
NoteEditorView(mode: .edit(note))
}
}
}
private var list: some View {
List {
if let error = syncEngine.lastSyncError {
Section {
Label(error, systemImage: "exclamationmark.icloud")
.foregroundStyle(.red)
.font(.footnote)
}
}
if !relevantNotes.isEmpty {
Section("Right here, right now") {
ForEach(relevantNotes) { note in
row(note)
}
}
}
Section("All notes") {
ForEach(notes) { note in
row(note)
}
}
}
.overlay {
if notes.isEmpty {
ContentUnavailableView(
"No notes yet",
systemImage: "square.and.pencil",
description: Text("Jot something down — Notes remembers where and when, so it can resurface at the right moment.")
)
}
}
}
private func row(_ note: NoteEntity) -> some View {
Button {
editingNote = note
} label: {
NoteRowView(note: note)
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(note)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
/// Top context matches — notes whose capture context resembles "here and
/// now" strongly enough to earn the shelf.
private var relevantNotes: [NoteEntity] {
let query = RelevanceQuery(context: contextService)
return notes
.filter { $0.isLive }
.map { (note: $0, score: Relevance.score(RelevanceSignals(entity: $0), query: query)) }
.filter { $0.score >= Relevance.surfacingThreshold }
.sorted { $0.score > $1.score }
.prefix(5)
.map(\.note)
}
private func delete(_ note: NoteEntity) {
guard let id = ULID(string: note.id) else { return }
Task {
try? await syncEngine.deleteNote(id: id)
}
}
}