From eae00213b50cf81fa3fe5bc1ecebcf0c77139cf1 Mon Sep 17 00:00:00 2001 From: scanash00 Date: Sun, 28 Jun 2026 23:16:49 -0800 Subject: [PATCH] fixes --- Dockerfile | 3 + backend/internal/api/collections.go | 2 + backend/internal/api/handler.go | 131 ++++++++++++++++++++-- web/public/locales/cs/translation.json | 3 + web/public/locales/de/translation.json | 3 + web/public/locales/en/translation.json | 3 + web/public/locales/es/translation.json | 3 + web/public/locales/pt-BR/translation.json | 3 + web/src/components/feed/Composer.tsx | 49 +++++++- 9 files changed, 187 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 777dc8a..6fb6235 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,4 +73,7 @@ exit 1 EOF RUN chmod +x /app/start.sh +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- "http://localhost:$API_PORT/health" >/dev/null 2>&1 && wget -qO- "http://localhost:$PORT/" >/dev/null 2>&1 || exit 1 + CMD ["/app/start.sh"] diff --git a/backend/internal/api/collections.go b/backend/internal/api/collections.go index 65078c6..c449b60 100644 --- a/backend/internal/api/collections.go +++ b/backend/internal/api/collections.go @@ -310,6 +310,8 @@ func (s *CollectionService) GetCollectionItems(w http.ResponseWriter, r *http.Re viewerDID = session.DID } + items = filterHiddenCollectionItems(r.Context(), s.db, viewerDID, items) + enrichedItems, err := hydrateCollectionItems(s.db, items, viewerDID) if err != nil { logger.Error("Hydration error: %v", err) diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go index cfb84d1..0ed973a 100644 --- a/backend/internal/api/handler.go +++ b/backend/internal/api/handler.go @@ -328,7 +328,10 @@ func (h *Handler) GetAnnotations(w http.ResponseWriter, r *http.Request) { return } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) @@ -444,6 +447,7 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { if len(collItems) > 0 { viewerDID := h.getViewerDID(r) + collItems = h.filterHiddenCollectionItems(r.Context(), viewerDID, collItems) hydrated, hydrateErr := hydrateCollectionItems(h.db, collItems, viewerDID) if hydrateErr == nil { noteURIsInFeed := make(map[string]bool, len(resp.Items)) @@ -586,14 +590,19 @@ func (h *Handler) GetByTarget(w http.ResponseWriter, r *http.Request) { notes = mergeNotes(notes, rawNotes) } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) } annotations, highlights, bookmarks := notesByMotivation(items) - if len(items) == 0 { + if viewerDID != "" { + w.Header().Set("Cache-Control", "private, max-age=0, no-store") + } else if len(items) == 0 { w.Header().Set("Cache-Control", "public, max-age=60, s-maxage=300") } else { w.Header().Set("Cache-Control", "private, max-age=0, no-store") @@ -767,6 +776,33 @@ func (h *Handler) DiscoverForURL(w http.ResponseWriter, r *http.Request) { } viewerDID := h.getViewerDID(r) + hidden := h.hiddenDIDSet(r.Context(), viewerDID) + if len(hidden) > 0 { + filteredAnnos := mergedAnnotations[:0] + for _, a := range mergedAnnotations { + if !hidden[a.AuthorDID] { + filteredAnnos = append(filteredAnnos, a) + } + } + mergedAnnotations = filteredAnnos + + filteredHighs := mergedHighlights[:0] + for _, hi := range mergedHighlights { + if !hidden[hi.AuthorDID] { + filteredHighs = append(filteredHighs, hi) + } + } + mergedHighlights = filteredHighs + + filteredBooks := mergedBookmarks[:0] + for _, b := range mergedBookmarks { + if !hidden[b.AuthorDID] { + filteredBooks = append(filteredBooks, b) + } + } + mergedBookmarks = filteredBooks + } + enrichedAnnotations, _ := hydrateAnnotations(h.db, mergedAnnotations, viewerDID) enrichedHighlights, _ := hydrateHighlights(h.db, mergedHighlights, viewerDID) enrichedBookmarks, _ := hydrateBookmarks(h.db, mergedBookmarks, viewerDID) @@ -802,7 +838,10 @@ func (h *Handler) GetHighlights(w http.ResponseWriter, r *http.Request) { return } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) @@ -837,7 +876,10 @@ func (h *Handler) GetBookmarks(w http.ResponseWriter, r *http.Request) { return } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) @@ -879,6 +921,8 @@ func (h *Handler) GetUserAnnotations(w http.ResponseWriter, r *http.Request) { return } + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { @@ -922,6 +966,8 @@ func (h *Handler) GetUserHighlights(w http.ResponseWriter, r *http.Request) { return } + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { @@ -965,6 +1011,8 @@ func (h *Handler) GetUserBookmarks(w http.ResponseWriter, r *http.Request) { return } + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { @@ -1012,7 +1060,10 @@ func (h *Handler) GetUserTargetItems(w http.ResponseWriter, r *http.Request) { return } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) @@ -1407,6 +1458,69 @@ func (h *Handler) getViewerDID(r *http.Request) string { return sess.DID } +func hiddenDIDSet(ctx context.Context, database *db.DB, viewerDID string) map[string]bool { + hidden := make(map[string]bool) + if database == nil { + return hidden + } + if banned, err := database.GetBannedDIDs(ctx); err == nil { + for _, did := range banned { + hidden[did] = true + } + } + if viewerDID == "" { + return hidden + } + + all, err := database.GetAllHiddenDIDs(ctx, viewerDID) + if err == nil { + for did := range all { + hidden[did] = true + } + } + return hidden +} + +func filterHiddenNotes(ctx context.Context, database *db.DB, viewerDID string, notes []db.Note) []db.Note { + hidden := hiddenDIDSet(ctx, database, viewerDID) + if len(hidden) == 0 { + return notes + } + filtered := notes[:0] + for _, n := range notes { + if !hidden[n.AuthorDID] { + filtered = append(filtered, n) + } + } + return filtered +} + +func filterHiddenCollectionItems(ctx context.Context, database *db.DB, viewerDID string, items []db.CollectionItem) []db.CollectionItem { + hidden := hiddenDIDSet(ctx, database, viewerDID) + if len(hidden) == 0 { + return items + } + filtered := items[:0] + for _, ci := range items { + if !hidden[ci.AuthorDID] { + filtered = append(filtered, ci) + } + } + return filtered +} + +func (h *Handler) hiddenDIDSet(ctx context.Context, viewerDID string) map[string]bool { + return hiddenDIDSet(ctx, h.db, viewerDID) +} + +func (h *Handler) filterHiddenNotes(ctx context.Context, viewerDID string, notes []db.Note) []db.Note { + return filterHiddenNotes(ctx, h.db, viewerDID, notes) +} + +func (h *Handler) filterHiddenCollectionItems(ctx context.Context, viewerDID string, items []db.CollectionItem) []db.CollectionItem { + return filterHiddenCollectionItems(ctx, h.db, viewerDID, items) +} + type fullProfileRepository struct { db *db.DB } @@ -1455,7 +1569,10 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { return } - lc, _ := h.hydration.Load(r.Context(), notes, h.getViewerDID(r)) + viewerDID := h.getViewerDID(r) + notes = h.filterHiddenNotes(r.Context(), viewerDID, notes) + + lc, _ := h.hydration.Load(r.Context(), notes, viewerDID) items := make([]service.APINote, len(notes)) for i, n := range notes { items[i] = h.hydration.ToAPINote(n, lc) diff --git a/web/public/locales/cs/translation.json b/web/public/locales/cs/translation.json index 5b6413d..a8dcd9c 100644 --- a/web/public/locales/cs/translation.json +++ b/web/public/locales/cs/translation.json @@ -267,10 +267,13 @@ "newHighlight": "Nový úryvek", "newAnnotation": "Nový vpisek", "newNote": "Nová poznámka", + "newBookmark": "Nová záložka", "saveHighlight": "Uložit úryvek", "postAnnotation": "Vložit vpisek", "postNote": "Vložit poznámku", + "saveBookmark": "Uložit záložku", "highlightHint": "Ukládáte pasáž bez komentáře. Přidejte text níže a změňte ji na vpisek.", + "bookmarkHint": "Ukládáte tuto URL jako záložku. Přidejte text níže a změňte ji na poznámku.", "addQuote": "+ Přidat citaci ze stránky", "quotePlaceholder": "Vložte nebo napište text, který anotujete…", "removeQuote": "Odebrat citaci", diff --git a/web/public/locales/de/translation.json b/web/public/locales/de/translation.json index 3c312b9..291e019 100644 --- a/web/public/locales/de/translation.json +++ b/web/public/locales/de/translation.json @@ -261,10 +261,13 @@ "newHighlight": "Neue Markierung", "newAnnotation": "Neue Anmerkung", "newNote": "Neue Notiz", + "newBookmark": "Neues Lesezeichen", "saveHighlight": "Markierung speichern", "postAnnotation": "Anmerkung veröffentlichen", "postNote": "Notiz veröffentlichen", + "saveBookmark": "Lesezeichen speichern", "highlightHint": "Du speicherst eine Passage ohne Kommentar. Füg unten Text hinzu, um daraus eine Anmerkung zu machen.", + "bookmarkHint": "Du speicherst diese URL als Lesezeichen. Füg unten Text hinzu, um daraus eine Notiz zu machen.", "addQuote": "+ Zitat von der Seite hinzufügen", "quotePlaceholder": "Füg den Text ein, den du kommentierst, oder tipp ihn …", "removeQuote": "Zitat entfernen", diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json index 8eb4b2c..63b1660 100644 --- a/web/public/locales/en/translation.json +++ b/web/public/locales/en/translation.json @@ -261,10 +261,13 @@ "newHighlight": "New highlight", "newAnnotation": "New annotation", "newNote": "New note", + "newBookmark": "New bookmark", "saveHighlight": "Save highlight", "postAnnotation": "Post annotation", "postNote": "Post note", + "saveBookmark": "Save bookmark", "highlightHint": "Saving a passage without a comment. Add text below to turn it into an annotation.", + "bookmarkHint": "Saving this URL as a bookmark. Add text below to turn it into a note instead.", "addQuote": "+ Add a quote from the page", "quotePlaceholder": "Paste or type the text you're annotating…", "removeQuote": "Remove Quote", diff --git a/web/public/locales/es/translation.json b/web/public/locales/es/translation.json index 695ee65..e32521e 100644 --- a/web/public/locales/es/translation.json +++ b/web/public/locales/es/translation.json @@ -261,10 +261,13 @@ "newHighlight": "Nuevo resaltado", "newAnnotation": "Nueva anotación", "newNote": "Nueva nota", + "newBookmark": "Nuevo marcador", "saveHighlight": "Guardar resaltado", "postAnnotation": "Publicar anotación", "postNote": "Publicar nota", + "saveBookmark": "Guardar marcador", "highlightHint": "Guardando un pasaje sin comentario. Añade texto debajo para convertirlo en una anotación.", + "bookmarkHint": "Guardando esta URL como marcador. Añade texto debajo para convertirla en una nota.", "addQuote": "+ Añadir una cita de la página", "quotePlaceholder": "Pega o escribe el texto que estás anotando…", "removeQuote": "Eliminar cita", diff --git a/web/public/locales/pt-BR/translation.json b/web/public/locales/pt-BR/translation.json index ec63c56..5d0d29d 100644 --- a/web/public/locales/pt-BR/translation.json +++ b/web/public/locales/pt-BR/translation.json @@ -264,10 +264,13 @@ "newHighlight": "Novo destaque", "newAnnotation": "Nova anotação", "newNote": "Nova nota", + "newBookmark": "Novo favorito", "saveHighlight": "Salvar destaque", "postAnnotation": "Publicar anotação", "postNote": "Publicar nota", + "saveBookmark": "Salvar favorito", "highlightHint": "Salvando um trecho sem comentário. Adicione um texto abaixo para transformá-lo em uma anotação.", + "bookmarkHint": "Salvando esta URL como favorito. Adicione um texto abaixo para transformá-la em uma nota.", "addQuote": "+ Adicionar uma citação da página", "quotePlaceholder": "Cole ou digite o texto que você está anotando…", "removeQuote": "Remover Citação", diff --git a/web/src/components/feed/Composer.tsx b/web/src/components/feed/Composer.tsx index 4dc3e81..9af01fa 100644 --- a/web/src/components/feed/Composer.tsx +++ b/web/src/components/feed/Composer.tsx @@ -3,13 +3,14 @@ import { useTranslation } from "react-i18next"; import { createAnnotation, createHighlight, + createBookmark, sessionAtom, getUserTags, getTrendingTags, } from "../../api/client"; import type { Selector, ContentLabelValue } from "../../types"; import { asTextQuote } from "../../types"; -import { X, ShieldAlert, Highlighter, PenLine } from "lucide-react"; +import { X, ShieldAlert, Highlighter, PenLine, Bookmark } from "lucide-react"; import TagInput from "../ui/TagInput"; import { analytics } from "../../lib/analytics"; @@ -74,8 +75,14 @@ export default function Composer({ const hasQuote = !!(highlightedText || quoteText.trim()); const hasText = !!text.trim(); - const mode: "highlight" | "annotation" | "note" = - hasQuote && !hasText ? "highlight" : hasQuote ? "annotation" : "note"; + const mode: "highlight" | "annotation" | "note" | "bookmark" = + hasQuote && !hasText + ? "highlight" + : hasQuote + ? "annotation" + : hasText + ? "note" + : "bookmark"; const modeCopy = { highlight: { @@ -96,12 +103,24 @@ export default function Composer({ submit: t("composer.postNote"), hint: null, }, + bookmark: { + title: t("composer.newBookmark"), + icon: Bookmark, + submit: t("composer.saveBookmark"), + hint: t("composer.bookmarkHint"), + }, }[mode]; const ModeIcon = modeCopy.icon; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!text.trim() && !highlightedText && !quoteText.trim()) return; + if ( + mode !== "bookmark" && + !text.trim() && + !highlightedText && + !quoteText.trim() + ) + return; try { setLoading(true); @@ -118,7 +137,20 @@ export default function Composer({ const quoteSelector = asTextQuote(finalSelector); const tagList = tags.filter(Boolean); - if (!text.trim()) { + if (mode === "bookmark") { + const result = await createBookmark({ + url, + tags: tagList.length > 0 ? tagList : undefined, + }); + if (result && "error" in result && result.error) { + throw new Error(result.error); + } + analytics.capture("bookmark_created", { + url, + tag_count: tagList.length, + source: "web", + }); + } else if (!text.trim()) { if (!quoteSelector) throw new Error("No text selected"); await createHighlight({ url, @@ -338,7 +370,12 @@ export default function Composer({ type="submit" className="bg-primary-600 hover:bg-primary-700 text-white font-medium px-4 py-1.5 rounded-lg transition-colors disabled:opacity-50 text-sm" disabled={ - loading || (!text.trim() && !highlightedText && !quoteText.trim()) + loading || + (mode !== "bookmark" && + !text.trim() && + !highlightedText && + !quoteText.trim()) || + (mode === "bookmark" && !url) } > {loading ? "…" : modeCopy.submit} -- knotmirror 0.0.1