Various AT Protocol integrations with obsidian
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

fetch and render margin/comunity images (#28)

* fetch image for margin/community

* renames and don't refetch failed images

authored by

treethought and committed by
GitHub
9166ac9f 3cdc3447

+63 -17
+3 -3
src/components/cardDetailModal.ts
··· 21 21 contentEl.empty(); 22 22 contentEl.addClass("atmosphere-detail-modal"); 23 23 24 - this.renderBody(contentEl); 24 + void this.renderBody(contentEl); 25 25 26 26 const collections = this.item.getCollections(); 27 27 if (collections.length > 0) { ··· 60 60 } 61 61 } 62 62 63 - private renderBody(contentEl: HTMLElement) { 63 + private async renderBody(contentEl: HTMLElement) { 64 64 const body = contentEl.createEl("div", { cls: "atmosphere-detail-body" }); 65 65 66 66 const title = this.item.getTitle(); ··· 68 68 body.createEl("h2", { text: title, cls: "atmosphere-detail-title" }); 69 69 } 70 70 71 - const imageUrl = this.item.getImageUrl(); 71 + const imageUrl = await this.item.getImageUrl(); 72 72 if (imageUrl) { 73 73 const img = body.createEl("img", { cls: "atmosphere-detail-image" }); 74 74 img.src = imageUrl;
+2 -2
src/main.ts
··· 1 1 import { Notice, Plugin, WorkspaceLeaf } from "obsidian"; 2 2 import { DEFAULT_SETTINGS, AtProtoSettings, SettingTab } from "./settings"; 3 - import { AtmosphereView, VIEW_TYPE_ATMOSPHERE_BOOKMARKS } from "./views/bookmarks"; 3 + import { BookmarksView, VIEW_TYPE_ATMOSPHERE_BOOKMARKS } from "./views/bookmarks"; 4 4 import { publishFileAsDocument } from "./commands/publishDocument"; 5 5 import { StandardFeedView, VIEW_ATMOSPHERE_STANDARD_FEED } from "views/standardfeed"; 6 6 import { ATClient } from "lib/client"; ··· 35 35 }); 36 36 37 37 this.registerView(VIEW_TYPE_ATMOSPHERE_BOOKMARKS, (leaf) => { 38 - return new AtmosphereView(leaf, this); 38 + return new BookmarksView(leaf, this); 39 39 }); 40 40 41 41 this.registerView(VIEW_ATMOSPHERE_STANDARD_FEED, (leaf) => {
+9 -2
src/sources/bookmark.ts src/sources/community.ts
··· 5 5 import type { ATBookmarkItem, DataSource, SourceFilter } from "./types"; 6 6 import { EditBookmarkModal } from "../components/editBookmarkModal"; 7 7 import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 8 + import { fetchOgImage } from "../util" 8 9 9 10 type BookmarkRecord = Record & { value: Bookmark }; 10 11 ··· 68 69 return enriched?.description || this.record.value.description || undefined; 69 70 } 70 71 71 - getImageUrl(): string | undefined { 72 + async getImageUrl(): Promise<string | undefined> { 72 73 const enriched = this.record.value.enriched; 73 - return enriched?.image || enriched?.thumb || undefined; 74 + if (enriched?.image) { 75 + return enriched.image; 76 + } else if (enriched?.thumb) { 77 + return enriched.thumb; 78 + } else { 79 + return await fetchOgImage(this.record.value.subject); 80 + } 74 81 } 75 82 76 83 getUrl(): string | undefined {
+3 -2
src/sources/margin.ts
··· 7 7 import type { Main as MarginCollection } from "../lexicons/types/at/margin/collection"; 8 8 import type { Main as MarginCollectionItem } from "../lexicons/types/at/margin/collectionItem"; 9 9 import { EditMarginBookmarkModal } from "../components/editMarginBookmarkModal"; 10 + import { fetchOgImage } from "../util" 10 11 11 12 type MarginBookmarkRecord = Record & { value: MarginBookmark }; 12 13 type MarginCollectionRecord = Record & { value: MarginCollection }; ··· 63 64 return this.record.value.description || undefined; 64 65 } 65 66 66 - getImageUrl(): string | undefined { 67 - return undefined; 67 + async getImageUrl(): Promise<string | undefined> { 68 + return fetchOgImage(this.record.value.source); 68 69 } 69 70 70 71 getUrl(): string | undefined {
+6 -2
src/sources/semble.ts
··· 7 7 import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink"; 8 8 import type { ATBookmarkItem, CollectionAssociation, DataSource, SourceFilter } from "./types"; 9 9 import { EditCardModal } from "../components/editCardModal"; 10 + import { fetchOgImage } from "../util" 10 11 11 12 type CardRecord = Record & { value: Card }; 12 13 type CollectionRecord = Record & { value: Collection }; ··· 75 76 return undefined; 76 77 } 77 78 78 - getImageUrl(): string | undefined { 79 + async getImageUrl(): Promise<string | undefined> { 79 80 const card = this.record.value; 80 81 if (card.type === "URL") { 81 - return (card.content as UrlContent).metadata?.imageUrl || undefined; 82 + if ((card.content as UrlContent).metadata?.imageUrl) { 83 + return (card.content as UrlContent).metadata?.imageUrl; 84 + } 85 + return fetchOgImage((card.content as UrlContent).url); 82 86 } 83 87 return undefined; 84 88 }
+1 -1
src/sources/types.ts
··· 11 11 getSource(): "semble" | "bookmark" | "margin"; 12 12 getTitle(): string | undefined; 13 13 getDescription(): string | undefined; 14 - getImageUrl(): string | undefined; 14 + getImageUrl(): Promise<string | undefined>; 15 15 getUrl(): string | undefined; 16 16 getSiteName(): string | undefined; 17 17 getTags(): string[];
+34
src/util.ts
··· 1 + import { requestUrl } from "obsidian"; 2 + 3 + const imageCache = new Map<string, string>(); 4 + 5 + function isValidUrl(url: string): boolean { 6 + try { 7 + const u = new URL(url); 8 + return u.protocol === "http:" || u.protocol === "https:"; 9 + } catch { 10 + return false; 11 + } 12 + } 13 + 14 + export async function fetchOgImage(url: string): Promise<string | undefined> { 15 + if (imageCache.has(url)) { 16 + return imageCache.get(url) || undefined; 17 + } 18 + if (!isValidUrl(url)) { 19 + return undefined; 20 + } 21 + 22 + try { 23 + const res = await requestUrl({ url, method: "GET" }); 24 + const match = res.text.match( 25 + /<meta[^>]+(?:property="og:image"|name="twitter:image")[^>]+content="([^"]+)"/i 26 + ) || res.text.match( 27 + /<meta[^>]+content="([^"]+)"[^>]+(?:property="og:image"|name="twitter:image")/i 28 + ); 29 + imageCache.set(url, match?.[1] ?? ""); 30 + return match?.[1]; 31 + } catch (e) { 32 + return undefined; 33 + } 34 + }
+5 -5
src/views/bookmarks.ts
··· 5 5 import { CreateTagModal } from "../components/createTagModal"; 6 6 import type { ATBookmarkItem, DataSource, SourceFilter } from "../sources/types"; 7 7 import { SembleSource } from "../sources/semble"; 8 - import { BookmarkSource } from "../sources/bookmark"; 8 + import { BookmarkSource } from "../sources/community"; 9 9 import { MarginSource } from "../sources/margin"; 10 10 import { renderLoginMessage } from "components/loginMessage"; 11 11 ··· 13 13 14 14 type SourceName = "semble" | "bookmark" | "margin"; 15 15 16 - export class AtmosphereView extends ItemView { 16 + export class BookmarksView extends ItemView { 17 17 plugin: AtmospherePlugin; 18 18 activeSources: Set<SourceName> = new Set(["semble"]); 19 19 selectedCollections: Set<string> = new Set(); ··· 145 145 const grid = container.createEl("div", { cls: "atmosphere-grid" }); 146 146 for (const item of items) { 147 147 try { 148 - this.renderItem(grid, item); 148 + void this.renderItem(grid, item); 149 149 } catch (err) { 150 150 const message = err instanceof Error ? err.message : String(err); 151 151 console.error(`Failed to render item ${item.getUri()}: ${message}`); ··· 370 370 menu.showAtMouseEvent(e); 371 371 } 372 372 373 - private renderItem(container: HTMLElement, item: ATBookmarkItem) { 373 + private async renderItem(container: HTMLElement, item: ATBookmarkItem) { 374 374 const el = container.createEl("div", { cls: "atmosphere-item" }); 375 375 376 376 el.addEventListener("click", (e) => { ··· 412 412 } 413 413 } 414 414 415 - const imageUrl = item.getImageUrl(); 415 + const imageUrl = await item.getImageUrl(); 416 416 if (imageUrl) { 417 417 const img = content.createEl("img", { cls: "atmosphere-item-image" }); 418 418 img.src = imageUrl;