an app to share curated trails sidetrail.app
atproto nextjs react rsc
50
fork

Configure Feed

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

code split embeds

+149 -13
+18 -10
app/at/(trail)/[handle]/trail/[rkey]/StopEmbed.tsx
··· 1 1 "use client"; 2 2 3 - import { use, createContext } from "react"; 3 + import { use, createContext, lazy } from "react"; 4 4 import type { ExternalEmbed } from "@/data/queries"; 5 5 import { LinkPreview } from "./LinkPreview"; 6 - import { BlueskyPostEmbed } from "./embeds/BlueskyPostEmbed"; 7 6 import { loadTrailCard } from "@/app/loadTrailCard"; 8 - import { 9 - getBlueskyPost, 10 - getLinkMetadata, 11 - type BlueskyPost, 12 - type LinkMetadata, 13 - } from "./utils/embed-resolver"; 7 + import type { BlueskyPost, LinkMetadata } from "./utils/embed-resolver"; 14 8 import "./embeds/TrailEmbed.css"; 15 9 import "@/app/TrailCard.css"; 10 + 11 + const BlueskyPostEmbed = lazy(() => 12 + import("./embeds/BlueskyPostEmbed").then((m) => ({ default: m.BlueskyPostEmbed })), 13 + ); 14 + 15 + async function fetchBlueskyPost(uri: string): Promise<BlueskyPost | null> { 16 + const { getBlueskyPost } = await import("./utils/embed-resolver"); 17 + return getBlueskyPost(uri); 18 + } 19 + 20 + async function fetchLinkMetadata(uri: string): Promise<LinkMetadata | null> { 21 + const { getLinkMetadata } = await import("./utils/embed-resolver"); 22 + return getLinkMetadata(uri); 23 + } 16 24 17 25 type EmbedPromise = Promise<BlueskyPost | LinkMetadata | React.ReactElement | null>; 18 26 export type EmbedCache = Map<string, EmbedPromise>; ··· 44 52 if (isTrailUri(uri)) { 45 53 promise = Promise.resolve().then(() => loadTrailCard(uri)); 46 54 } else if (isBlueskyPostUri(uri)) { 47 - promise = getBlueskyPost(uri); 55 + promise = fetchBlueskyPost(uri); 48 56 } else { 49 - promise = getLinkMetadata(uri); 57 + promise = fetchLinkMetadata(uri); 50 58 } 51 59 52 60 cache.set(uri, promise);
+5 -2
app/at/(trail)/[handle]/trail/[rkey]/embeds/BlueskyPostEmbed.tsx
··· 1 1 "use client"; 2 2 3 - import { useRef, useState } from "react"; 3 + import { useRef, useState, lazy } from "react"; 4 4 import { 5 5 parseEmbedPlayerFromUrl, 6 6 getPlayerAspect, ··· 15 15 import * as embedVideo from "@/lib/lexicons/app/bsky/embed/video"; 16 16 import * as embedRecord from "@/lib/lexicons/app/bsky/embed/record"; 17 17 import * as embedRecordWithMedia from "@/lib/lexicons/app/bsky/embed/recordWithMedia"; 18 - import { BlueskyVideoPlayer } from "./BlueskyVideoPlayer"; 19 18 import "./BlueskyPostEmbed.css"; 19 + 20 + const BlueskyVideoPlayer = lazy(() => 21 + import("./BlueskyVideoPlayer").then((m) => ({ default: m.BlueskyVideoPlayer })), 22 + ); 20 23 21 24 interface BlueskyPostEmbedProps { 22 25 post: BlueskyPost;
+1 -1
app/at/(trail)/[handle]/trail/[rkey]/embeds/BlueskyRichText.tsx
··· 1 - import { RichText } from "@atproto/api"; 1 + import { RichText } from "../utils/rich-text"; 2 2 import type * as feedPost from "@/lib/lexicons/app/bsky/feed/post"; 3 3 import * as facet from "@/lib/lexicons/app/bsky/richtext/facet"; 4 4
+125
app/at/(trail)/[handle]/trail/[rkey]/utils/rich-text.ts
··· 1 + /** 2 + * Minimal RichText implementation for rendering Bluesky post text. 3 + * Vendored from @atproto/api to avoid pulling in the full SDK (~150KB). 4 + * 5 + * Only implements segment iteration for read-only rendering. 6 + * Does not include text manipulation or facet detection. 7 + */ 8 + 9 + import type * as facet from "@/lib/lexicons/app/bsky/richtext/facet"; 10 + 11 + export type Facet = facet.Main; 12 + export type FacetLink = facet.Link; 13 + export type FacetMention = facet.Mention; 14 + export type FacetTag = facet.Tag; 15 + 16 + type Feature = Facet["features"][number]; 17 + 18 + const encoder = new TextEncoder(); 19 + const decoder = new TextDecoder(); 20 + 21 + class UnicodeString { 22 + utf16: string; 23 + utf8: Uint8Array; 24 + 25 + constructor(utf16: string) { 26 + this.utf16 = utf16; 27 + this.utf8 = encoder.encode(utf16); 28 + } 29 + 30 + get length() { 31 + return this.utf8.byteLength; 32 + } 33 + 34 + slice(start?: number, end?: number): string { 35 + return decoder.decode(this.utf8.slice(start, end)); 36 + } 37 + } 38 + 39 + function isLink(f: Feature): f is FacetLink & { $type: "app.bsky.richtext.facet#link" } { 40 + return f.$type === "app.bsky.richtext.facet#link"; 41 + } 42 + 43 + function isMention(f: Feature): f is FacetMention & { $type: "app.bsky.richtext.facet#mention" } { 44 + return f.$type === "app.bsky.richtext.facet#mention"; 45 + } 46 + 47 + function isTag(f: Feature): f is FacetTag & { $type: "app.bsky.richtext.facet#tag" } { 48 + return f.$type === "app.bsky.richtext.facet#tag"; 49 + } 50 + 51 + export class RichTextSegment { 52 + constructor( 53 + public text: string, 54 + public facet?: Facet, 55 + ) {} 56 + 57 + get link(): FacetLink | undefined { 58 + const found = this.facet?.features.find(isLink); 59 + return found ? { uri: found.uri } : undefined; 60 + } 61 + 62 + get mention(): FacetMention | undefined { 63 + const found = this.facet?.features.find(isMention); 64 + return found ? { did: found.did } : undefined; 65 + } 66 + 67 + get tag(): FacetTag | undefined { 68 + const found = this.facet?.features.find(isTag); 69 + return found ? { tag: found.tag } : undefined; 70 + } 71 + } 72 + 73 + export interface RichTextProps { 74 + text: string; 75 + facets?: Facet[]; 76 + } 77 + 78 + export class RichText { 79 + private unicodeText: UnicodeString; 80 + private facets?: Facet[]; 81 + 82 + constructor(props: RichTextProps) { 83 + this.unicodeText = new UnicodeString(props.text); 84 + if (props.facets?.length) { 85 + this.facets = props.facets 86 + .filter((f) => f.index.byteStart <= f.index.byteEnd) 87 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 88 + } 89 + } 90 + 91 + *segments(): Generator<RichTextSegment, void, void> { 92 + const facets = this.facets || []; 93 + if (!facets.length) { 94 + yield new RichTextSegment(this.unicodeText.utf16); 95 + return; 96 + } 97 + 98 + let textCursor = 0; 99 + let facetCursor = 0; 100 + 101 + do { 102 + const currFacet = facets[facetCursor]; 103 + if (textCursor < currFacet.index.byteStart) { 104 + yield new RichTextSegment(this.unicodeText.slice(textCursor, currFacet.index.byteStart)); 105 + } else if (textCursor > currFacet.index.byteStart) { 106 + facetCursor++; 107 + continue; 108 + } 109 + if (currFacet.index.byteStart < currFacet.index.byteEnd) { 110 + const subtext = this.unicodeText.slice(currFacet.index.byteStart, currFacet.index.byteEnd); 111 + if (!subtext.trim()) { 112 + yield new RichTextSegment(subtext); 113 + } else { 114 + yield new RichTextSegment(subtext, currFacet); 115 + } 116 + } 117 + textCursor = currFacet.index.byteEnd; 118 + facetCursor++; 119 + } while (facetCursor < facets.length); 120 + 121 + if (textCursor < this.unicodeText.length) { 122 + yield new RichTextSegment(this.unicodeText.slice(textCursor, this.unicodeText.length)); 123 + } 124 + } 125 + }