diff --git a/backend/internal/api/hydration.go b/backend/internal/api/hydration.go index 6c505cf..b9d8a2c 100644 --- a/backend/internal/api/hydration.go +++ b/backend/internal/api/hydration.go @@ -822,7 +822,8 @@ func hydrateCollectionItemsWithData(database *db.DB, items []db.CollectionItem, annotationURIs = append(annotationURIs, uri) case strings.Contains(uri, "at.margin.highlight"): highlightURIs = append(highlightURIs, uri) - case strings.Contains(uri, "at.margin.bookmark"): + case strings.Contains(uri, "at.margin.bookmark"), + strings.Contains(uri, "wiki.lichen.bookmark"): bookmarkURIs = append(bookmarkURIs, uri) case strings.Contains(uri, "network.cosmik.card"): annotationURIs = append(annotationURIs, uri) diff --git a/backend/internal/firehose/ingester.go b/backend/internal/firehose/ingester.go index 4faad7f..2552d75 100644 --- a/backend/internal/firehose/ingester.go +++ b/backend/internal/firehose/ingester.go @@ -95,6 +95,7 @@ func NewIngester(database *db.DB, syncService *internal_sync.Service) *Ingester i.RegisterHandler(CollectionDocument, i.handleDocument) i.RegisterHandler(xrpc.CollectionNote, i.handleNote) i.RegisterHandler(xrpc.CollectionCommunityBookmark, i.handleCommunityBookmark) + i.RegisterHandler(xrpc.CollectionLichenBookmark, i.handleLichenBookmark) return i } @@ -331,6 +332,8 @@ func (i *Ingester) handleDelete(collection, uri string) { i.db.DeleteNote(uri) case xrpc.CollectionCommunityBookmark: i.db.DeleteNote(uri) + case xrpc.CollectionLichenBookmark: + i.db.DeleteBookmark(uri) } } @@ -1302,3 +1305,35 @@ func (i *Ingester) handleDocument(event *FirehoseEvent) { } }) } + +func (i *Ingester) handleLichenBookmark(event *FirehoseEvent) { + var record xrpc.LichenBookmark + if err := json.Unmarshal(event.Record, &record); err != nil { + return + } + + source := xrpc.LichenWikiURLFromRef(record.WikiRef) + if source == "" { + return + } + + uri := fmt.Sprintf("at://%s/%s/%s", event.Repo, event.Collection, event.Rkey) + createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) + if err != nil { + createdAt = time.Now() + } + + bookmark := &db.Bookmark{ + URI: uri, + AuthorDID: event.Repo, + Source: source, + SourceHash: db.HashURL(source), + CreatedAt: createdAt, + IndexedAt: time.Now(), + } + if err := i.db.CreateBookmark(bookmark); err != nil { + logger.Error("Failed to index Lichen bookmark: %v", err) + } else { + logger.Info("Indexed Lichen bookmark from %s: %s", event.Repo, source) + } +} diff --git a/backend/internal/sync/service.go b/backend/internal/sync/service.go index 68d7678..81cc090 100644 --- a/backend/internal/sync/service.go +++ b/backend/internal/sync/service.go @@ -41,6 +41,7 @@ func (s *Service) PerformSync(ctx context.Context, did string, getClient func(co xrpc.CollectionSembleCard, xrpc.CollectionSembleCollection, xrpc.CollectionSembleCollectionLink, + xrpc.CollectionLichenBookmark, xrpc.CollectionDocument, } @@ -208,6 +209,9 @@ func (s *Service) PerformSync(ctx context.Context, did string, getClient func(co } else { err = e } + case xrpc.CollectionLichenBookmark: + localURIs, err = s.db.GetBookmarkURIs(did) + localURIs = filterURIsByCollection(localURIs, xrpc.CollectionLichenBookmark) } if err == nil { @@ -239,6 +243,8 @@ func (s *Service) PerformSync(ctx context.Context, did string, getClient func(co _ = s.db.DeleteAPIKeyByURI(uri) case xrpc.CollectionPreferences: _ = s.db.DeletePreferences(uri) + case xrpc.CollectionLichenBookmark: + _ = s.db.DeleteBookmark(uri) } deletedCount++ } @@ -629,6 +635,27 @@ func (s *Service) upsertRecord(did, collection, uri, cid string, value json.RawM IndexedAt: time.Now(), }) + case xrpc.CollectionLichenBookmark: + var record xrpc.LichenBookmark + if err := json.Unmarshal(value, &record); err != nil { + return err + } + source := xrpc.LichenWikiURLFromRef(record.WikiRef) + if source == "" { + return nil + } + createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) + + return s.db.CreateBookmark(&db.Bookmark{ + URI: uri, + AuthorDID: did, + Source: source, + SourceHash: db.HashURL(source), + CreatedAt: createdAt, + IndexedAt: time.Now(), + CID: cidPtr, + }) + case xrpc.CollectionAPIKey: var record xrpc.APIKeyRecord if err := json.Unmarshal(value, &record); err != nil { diff --git a/backend/internal/xrpc/lichen.go b/backend/internal/xrpc/lichen.go new file mode 100644 index 0000000..5d00407 --- /dev/null +++ b/backend/internal/xrpc/lichen.go @@ -0,0 +1,26 @@ +package xrpc + +import "strings" + +const ( + CollectionLichenBookmark = "wiki.lichen.bookmark" + CollectionLichenWiki = "wiki.lichen.wiki" +) + +type LichenBookmark struct { + WikiRef string `json:"wikiRef"` + CreatedAt string `json:"createdAt"` +} + +func LichenWikiURLFromRef(wikiRef string) string { + rest := strings.TrimPrefix(wikiRef, "at://") + parts := strings.Split(rest, "/") + if len(parts) < 3 || parts[1] != CollectionLichenWiki { + return "" + } + rkey := parts[len(parts)-1] + if rkey == "" { + return "" + } + return "https://lichen.wiki/wiki/" + rkey +} diff --git a/web/public/lichen-logo.svg b/web/public/lichen-logo.svg new file mode 100644 index 0000000..0d5a63e --- /dev/null +++ b/web/public/lichen-logo.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/web/public/locales/cs/translation.json b/web/public/locales/cs/translation.json index 041fab8..a9d2766 100644 --- a/web/public/locales/cs/translation.json +++ b/web/public/locales/cs/translation.json @@ -1,229 +1,230 @@ { - "nav": { - "discover": "Objevuj", - "activity": "Notifikace", - "bookmarks": "Záložky", - "annotations": "Vpisky", - "highlights": "Úryvky", - "collections": "Složky", - "signIn": "Přihlásit se", - "logOut": "Odhlásit se", - "feed": "Feed", - "settings": "Nastavení", - "new": "Anotovat", - "themeLight": "Světlo", - "themeDark": "Tma", - "themeSystem": "Režim podle OS" + "nav": { + "discover": "Objevuj", + "activity": "Notifikace", + "bookmarks": "Záložky", + "annotations": "Vpisky", + "highlights": "Úryvky", + "collections": "Složky", + "signIn": "Přihlásit se", + "logOut": "Odhlásit se", + "feed": "Feed", + "settings": "Nastavení", + "new": "Anotovat", + "themeLight": "Světlo", + "themeDark": "Tma", + "themeSystem": "Režim podle OS" + }, + "appTitle": "Margin", + "feed": { + "mine": "Pouze já", + "everyone": "Všichni", + "filters": { + "bookmarks": "Záložky", + "highlights": "Úryvky" }, - "appTitle": "Margin", - "feed": { - "mine": "Pouze já", - "everyone": "Všichni", - "filters": { - "bookmarks": "Záložky", - "highlights": "Úryvky" - }, - "itemsWithTag": "Položky se štítkem:", - "tabs": { - "recent": "Nedávné", - "popular": "Populární", - "shelved": "Málo diskutované" - }, - "welcome": "Vítejte na Marginu", - "getStarted": "Pustit se do toho", - "clearFilter": "Odepnout filtr", - "loading": "Načítání…", - "nothingHereYet": "Prozatím tady nic není" + "itemsWithTag": "Položky se štítkem:", + "tabs": { + "recent": "Nedávné", + "popular": "Populární", + "shelved": "Málo diskutované" }, - "collections": { - "title": "Složky", - "namePlaceholderEdit": "Název složky", - "nameLabel": "Název", - "iconLabel": "Ikona", - "iconsTab": "Ikony", - "emojisTab": "Emotikony", - "selectedIcon": "Vybráno:", - "descriptionLabel": "Popisek (volitelně)", - "descriptionPlaceholder": "K čemu by měla tato složka sloužit?", - "descriptionPlaceholderEdit": "O čem by měla být tato složka?", - "cancel": "Zrušit", - "creating": "Vytvářím…", - "save": "Uložit změny", - "saving": "Ukládám…", - "subtitle": "Uspořádejte si vpisky a úryvky", - "namePlaceholder": "Moje složka", - "createButton": "Vytvořit složku", - "editTitle": "Upravit složku", - "newTitle": "Nová složka", - "create": "Vytvořit složku", - "noneMessage": "Vytvořte si novou složku k organizaci Vašich vpisků a úryvků." + "welcome": "Vítejte na Marginu", + "getStarted": "Pustit se do toho", + "clearFilter": "Odepnout filtr", + "loading": "Načítání…", + "nothingHereYet": "Prozatím tady nic není" + }, + "collections": { + "title": "Složky", + "namePlaceholderEdit": "Název složky", + "nameLabel": "Název", + "iconLabel": "Ikona", + "iconsTab": "Ikony", + "emojisTab": "Emotikony", + "selectedIcon": "Vybráno:", + "descriptionLabel": "Popisek (volitelně)", + "descriptionPlaceholder": "K čemu by měla tato složka sloužit?", + "descriptionPlaceholderEdit": "O čem by měla být tato složka?", + "cancel": "Zrušit", + "creating": "Vytvářím…", + "save": "Uložit změny", + "saving": "Ukládám…", + "subtitle": "Uspořádejte si vpisky a úryvky", + "namePlaceholder": "Moje složka", + "createButton": "Vytvořit složku", + "editTitle": "Upravit složku", + "newTitle": "Nová složka", + "create": "Vytvořit složku", + "noneMessage": "Vytvořte si novou složku k organizaci Vašich vpisků a úryvků." + }, + "about": { + "footer": { + "contact": "Kontakt", + "terms": "Smluvní podmínky", + "privacy": "Ochrana soukromí" }, - "about": { - "footer": { - "contact": "Kontakt", - "terms": "Smluvní podmínky", - "privacy": "Ochrana soukromí" - }, - "cta": { - "viewGitHub": "GitHub", - "viewTangled": "Tangled", - "signIn": "Přihlásit se", - "title": "Začněte psát pod čarou" - }, - "protocol": { - "descriptionPre": "Margin je vystavěn na" - }, - "extension": { - "features": { - "keyboard": { - "title": "Klávesové zkratky" - } - } - } - }, - "profile": { - "unblock_action": "Zablokovat", - "unmute_action": "Ztišit", - "emptyCollectionsOwn": "Prozatím jste si nevytvořili žádné složky.", - "emptyCollectionsOther": "Žádné složky", - "show": "Zobrazit", - "hide": "Skrýt" - }, - "pageTitles": { - "discover": "Objevuj — Margin", - "home": "Úvod — Margin", - "bookmarks": "Záložky — Margin", - "settings": "Nastavení — Margin", - "collections": "Složky — Margin", - "admin": "Administrace — Margin", - "search": "Vyhledávání — Margin", - "notifications": "Notifikace — Margin", - "new": "Nový vpisek — Margin", - "highlights": "Úryvky — Margin", - "annotations": "Vpisky — Margin" - }, - "sidebar": { - "downloadForFirefox": "Stáhnout pro Firefox", - "downloadForEdge": "Stáhnout pro Edge", - "downloadForChrome": "Stáhnout pro Chrome", - "trending": "Populární", - "searchPlaceholder": "Vyhledávejte napříč uživateli, tagy, URL…", - "getExtension": "Stáhnout rozšíření", - "nothingTrending": "Aktuálně není nic moc v kurzu." - }, - "mobileNav": { - "iosShortcut": "Zkratka iOS" + "cta": { + "viewGitHub": "GitHub", + "viewTangled": "Tangled", + "signIn": "Přihlásit se", + "title": "Začněte psát pod čarou" }, - "discover": { - "tabs": { - "new": "Nové", - "popular": "Populární", - "forYou": "Výběr" - }, - "comingSoon": "Již brzy", - "forYouNotAvailable": "Personalizovaná doporučení nejsou na tomto serveru prozatím k dispozici.", - "loadMore": "Načíst další", - "noDocumentsYet": "Prozatím tady nejsou žádné dokumenty. Zkuste to znova později." + "protocol": { + "descriptionPre": "Margin je vystavěn na" }, - "search": { - "noResults": "Nic nenalezeno", - "emptyTitle": "Vyhledávejte ve Vaší knihovně", - "filters": { - "all": "Všechno" - }, - "loadMore": "Načíst další" - }, - "notifications": { - "inReplyTo": "v odpovědi na", - "aReply": "odpověď", - "anAnnotation": "vpisek", - "likedAnnotation": "se líbí Váš vpisek", - "likedHighlight": "se líbí Váš úryvek", - "likedBookmark": "se líbí Vaše záložka", - "likedReply": "se líbí Vaše odpověď", - "likedPost": "se líbí Váš příspěvek", - "repliedToReply": "reagoval*a na Vaši odpověď", - "repliedToAnnotation": "odpověděl*a na Váš vpisek", - "mentionedInAnnotation": "Vás zmínil*a ve vpisku", - "followedYou": "Vás začal*a sledovat", - "title": "Události", - "noActivity": "Prozatím tady nic není", - "highlightedPage": "si vytáhnul*a úryvek z Vaší stránky", - "noActivityMessage": "Reakce na Váš obsah se objeví tady." - }, - "login": { - "termsLink": "Smluvní podmínky", - "termsAnd": "a", - "privacyLink": "Zásady ochrany osobních údajů" - }, - "signUp": { - "atProtocol": "Protokol AT", - "pdsAddressLabel": "Adresa PDS", - "pdsAddressPlaceholder": "pds.vasedomena.cz", - "connecting": "Připojuji…", - "back": "Zpátky", - "continue": "Pokračovat", - "invite": "Pozvat", - "providerError": "K tomuto poskytovateli se teď nelze připojit. Zkuste to znova.", - "customPdsError": "K tomuto PDS se teď nelze připojit. Zkontrolujte, zda zadáváte správnou adresu.", - "providers": { - "margin": { - "name": "Margin" - }, - "customPds": { - "description": "PDS už máte? Pokud ano, zadejte jeho adresu.", - "name": "Použít vlastní PDS" - }, - "bluesky": { - "name": "Bluesky", - "description": "Největší a nejrozšířenější komunita" - }, - "blacksky": { - "name": "Blacksky" - }, - "northsky": { - "description": "Kanadské družstvo vlastněné a provozované zaměstnanci", - "name": "Northsky" - }, - "eurosky": { - "description": "Eurosky je Vaším kouskem Evropy v Atmosféře" - }, - "selfhostedSocial": { - "name": "selfhosted.social", - "description": "Prostor pro všechny umělce, kutily a objevitele" - }, - "tophhie": { - "name": "Tophhie", - "description": "Komunita plná přátelské atmosféry" - } + "extension": { + "features": { + "keyboard": { + "title": "Klávesové zkratky" } + } + } + }, + "profile": { + "unblock_action": "Zablokovat", + "unmute_action": "Ztišit", + "emptyCollectionsOwn": "Prozatím jste si nevytvořili žádné složky.", + "emptyCollectionsOther": "Žádné složky", + "show": "Zobrazit", + "hide": "Skrýt" + }, + "pageTitles": { + "discover": "Objevuj — Margin", + "home": "Úvod — Margin", + "bookmarks": "Záložky — Margin", + "settings": "Nastavení — Margin", + "collections": "Složky — Margin", + "admin": "Administrace — Margin", + "search": "Vyhledávání — Margin", + "notifications": "Notifikace — Margin", + "new": "Nový vpisek — Margin", + "highlights": "Úryvky — Margin", + "annotations": "Vpisky — Margin" + }, + "sidebar": { + "downloadForFirefox": "Stáhnout pro Firefox", + "downloadForEdge": "Stáhnout pro Edge", + "downloadForChrome": "Stáhnout pro Chrome", + "trending": "Populární", + "searchPlaceholder": "Vyhledávejte napříč uživateli, tagy, URL…", + "getExtension": "Stáhnout rozšíření", + "nothingTrending": "Aktuálně není nic moc v kurzu." + }, + "mobileNav": { + "iosShortcut": "Zkratka iOS" + }, + "discover": { + "tabs": { + "new": "Nové", + "popular": "Populární", + "forYou": "Výběr" }, - "composer": { - "newHighlight": "Nový úryvek", - "newAnnotation": "Nový vpisek", - "newNote": "Nová poznámka", - "saveHighlight": "Uložit úryvek", - "postAnnotation": "Vložit vpisek", - "postNote": "Vložit poznámku", - "removeQuote": "Odebrat citaci", - "labels": { - "sexual": "Sexualita", - "nudity": "Nahota", - "violence": "Násilí", - "gore": "Gore", - "spam": "Spam", - "misleading": "Manipulace" - }, - "contentWarning": "Upozornění na obsah", - "cancel": "Zrušit" + "comingSoon": "Již brzy", + "forYouNotAvailable": "Personalizovaná doporučení nejsou na tomto serveru prozatím k dispozici.", + "loadMore": "Načíst další", + "noDocumentsYet": "Prozatím tady nejsou žádné dokumenty. Zkuste to znova později." + }, + "search": { + "noResults": "Nic nenalezeno", + "emptyTitle": "Vyhledávejte ve Vaší knihovně", + "filters": { + "all": "Všechno" }, - "card": { - "communityBookmark": "Komunitní záložka", - "untitledBookmark": "Nepojmenovaná záložka", - "editTitle": "Upravit", - "deleteTitle": "Smazat", - "report": "Nahlásit", - "muteUser": "Ztišit @{{handle}}", - "blockUser": "Zablokovat @{{handle}}" + "loadMore": "Načíst další" + }, + "notifications": { + "inReplyTo": "v odpovědi na", + "aReply": "odpověď", + "anAnnotation": "vpisek", + "likedAnnotation": "se líbí Váš vpisek", + "likedHighlight": "se líbí Váš úryvek", + "likedBookmark": "se líbí Vaše záložka", + "likedReply": "se líbí Vaše odpověď", + "likedPost": "se líbí Váš příspěvek", + "repliedToReply": "reagoval*a na Vaši odpověď", + "repliedToAnnotation": "odpověděl*a na Váš vpisek", + "mentionedInAnnotation": "Vás zmínil*a ve vpisku", + "followedYou": "Vás začal*a sledovat", + "title": "Události", + "noActivity": "Prozatím tady nic není", + "highlightedPage": "si vytáhnul*a úryvek z Vaší stránky", + "noActivityMessage": "Reakce na Váš obsah se objeví tady." + }, + "login": { + "termsLink": "Smluvní podmínky", + "termsAnd": "a", + "privacyLink": "Zásady ochrany osobních údajů" + }, + "signUp": { + "atProtocol": "Protokol AT", + "pdsAddressLabel": "Adresa PDS", + "pdsAddressPlaceholder": "pds.vasedomena.cz", + "connecting": "Připojuji…", + "back": "Zpátky", + "continue": "Pokračovat", + "invite": "Pozvat", + "providerError": "K tomuto poskytovateli se teď nelze připojit. Zkuste to znova.", + "customPdsError": "K tomuto PDS se teď nelze připojit. Zkontrolujte, zda zadáváte správnou adresu.", + "providers": { + "margin": { + "name": "Margin" + }, + "customPds": { + "description": "PDS už máte? Pokud ano, zadejte jeho adresu.", + "name": "Použít vlastní PDS" + }, + "bluesky": { + "name": "Bluesky", + "description": "Největší a nejrozšířenější komunita" + }, + "blacksky": { + "name": "Blacksky" + }, + "northsky": { + "description": "Kanadské družstvo vlastněné a provozované zaměstnanci", + "name": "Northsky" + }, + "eurosky": { + "description": "Eurosky je Vaším kouskem Evropy v Atmosféře" + }, + "selfhostedSocial": { + "name": "selfhosted.social", + "description": "Prostor pro všechny umělce, kutily a objevitele" + }, + "tophhie": { + "name": "Tophhie", + "description": "Komunita plná přátelské atmosféry" + } } + }, + "composer": { + "newHighlight": "Nový úryvek", + "newAnnotation": "Nový vpisek", + "newNote": "Nová poznámka", + "saveHighlight": "Uložit úryvek", + "postAnnotation": "Vložit vpisek", + "postNote": "Vložit poznámku", + "removeQuote": "Odebrat citaci", + "labels": { + "sexual": "Sexualita", + "nudity": "Nahota", + "violence": "Násilí", + "gore": "Gore", + "spam": "Spam", + "misleading": "Manipulace" + }, + "contentWarning": "Upozornění na obsah", + "cancel": "Zrušit" + }, + "card": { + "communityBookmark": "Komunitní záložka", + "lichenBookmark": "Záložka Lichen", + "untitledBookmark": "Nepojmenovaná záložka", + "editTitle": "Upravit", + "deleteTitle": "Smazat", + "report": "Nahlásit", + "muteUser": "Ztišit @{{handle}}", + "blockUser": "Zablokovat @{{handle}}" + } } diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json index af3a555..19032e0 100644 --- a/web/public/locales/en/translation.json +++ b/web/public/locales/en/translation.json @@ -285,6 +285,7 @@ "addedToLower": "added to", "and": "and", "communityBookmark": "Community bookmark", + "lichenBookmark": "Lichen bookmark", "openInSemble": "Open in Semble", "deleteConfirm": "Delete this item?", "hideContent": "Hide Content", diff --git a/web/public/locales/es/translation.json b/web/public/locales/es/translation.json index 40af1eb..6047474 100644 --- a/web/public/locales/es/translation.json +++ b/web/public/locales/es/translation.json @@ -278,6 +278,7 @@ "addedToLower": "añadido a", "and": "y", "communityBookmark": "Marcador de la comunidad", + "lichenBookmark": "Marcador de Lichen", "openInSemble": "Abrir en Semble", "deleteConfirm": "¿Eliminar este elemento?", "hideContent": "Ocultar contenido", diff --git a/web/src/components/common/Card.tsx b/web/src/components/common/Card.tsx index ffee1fc..d5b2cfc 100644 --- a/web/src/components/common/Card.tsx +++ b/web/src/components/common/Card.tsx @@ -178,6 +178,8 @@ export default function Card({ const isSemble = item.uri?.includes("network.cosmik") || item.uri?.includes("semble"); + const isLichen = item.uri?.includes("wiki.lichen.bookmark"); + const isCommunityBookmark = item.uri?.includes( "community.lexicon.bookmarks.bookmark", ); @@ -505,6 +507,21 @@ export default function Card({ ); })()} + {isLichen && ( + + + · + + + Lichen + + {t("card.lichenBookmark")} + + + + + )} + {isCommunityBookmark && (