An HTML-only Bluesky frontend

add jsx demo

+280 -314
+7 -3
README.md
··· 2 2 3 3 An HTML-only [Bluesky](https://bsky.social) frontend. 4 4 5 - Just replace [bsky.app](https://bsky.app) with [htmlsky.app](https://htmlsky.app)! 5 + Just replace [bsky.app](https://bsky.app) with 6 + [htmlsky.app](https://htmlsky.app)! 6 7 7 8 ## Announcements 8 9 9 - HTMLsky has an account for all announcements, notifications, and other communication. You can follow it at [@htmlsky.app](https://bsky.app/profile/htmlsky.app) 10 + HTMLsky has an account for all announcements, notifications, and other 11 + communication. You can follow it at 12 + [@htmlsky.app](https://bsky.app/profile/htmlsky.app) 10 13 11 14 ## Contributing 12 15 13 - Send patches/bug reports to <~jordanreger/public-inbox@lists.sr.ht> with the subject `[PATCH] htmlsky` 16 + Send patches/bug reports to <~jordanreger/public-inbox@lists.sr.ht> with the 17 + subject `[PATCH] htmlsky`
+4 -11
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "deno run -A --watch main.tsx" 3 + "dev": "deno task clean && deno run -A --watch main.tsx", 4 + "clean": "deno fmt && deno lint" 4 5 }, 5 6 "compilerOptions": { 6 7 "jsx": "react-jsx", 7 - "jsxImportSource": "https://esm.sh/preact@10.22.0" 8 - }, 9 - "deploy": { 10 - "project": "htmlsky", 11 - "exclude": [ 12 - "**/node_modules" 13 - ], 14 - "include": [], 15 - "entrypoint": "main.tsx" 8 + "jsxImportSource": "npm:preact@10.22.0" 16 9 } 17 - } 10 + }
+1
deno.lock
··· 5 5 "npm:@atproto/api": "npm:@atproto/api@0.12.8", 6 6 "npm:preact": "npm:preact@10.22.0", 7 7 "npm:preact-render-to-string": "npm:preact-render-to-string@6.5.5_preact@10.22.0", 8 + "npm:preact@10.22.0": "npm:preact@10.22.0", 8 9 "npm:sanitize-html": "npm:sanitize-html@2.13.0" 9 10 }, 10 11 "npm": {
-26
facets.ts
··· 1 - import { AtpAgent, RichText } from "npm:@atproto/api"; 2 - 3 - const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 4 - 5 - export async function GetDescriptionFacets( 6 - description: string, 7 - ): Promise<string> { 8 - const rt = new RichText({ text: description }); 9 - await rt.detectFacets(agent); 10 - 11 - let descriptionWithFacets = ""; 12 - 13 - for (const segment of rt.segments()) { 14 - if (segment.isLink()) { 15 - descriptionWithFacets += 16 - `<a href="${segment.link?.uri}">${segment.text}</a>`; 17 - } else if (segment.isMention()) { 18 - descriptionWithFacets += 19 - `<a href="/profile/${segment.mention?.did}">${segment.text}</a>`; 20 - } else { 21 - descriptionWithFacets += segment.text; 22 - } 23 - } 24 - 25 - return descriptionWithFacets; 26 - }
+28
facets.tsx
··· 1 + import { RichText } from "npm:@atproto/api"; 2 + 3 + import { agent } from "./main.tsx"; 4 + 5 + export async function GetDescriptionFacets( 6 + description: string, 7 + ): Promise<preact.VNode[]> { 8 + const rt = new RichText({ text: description }); 9 + await rt.detectFacets(agent); 10 + 11 + const descriptionWithFacets: preact.VNode[] = []; 12 + 13 + for (const segment of rt.segments()) { 14 + if (segment.isLink()) { 15 + descriptionWithFacets.push( 16 + <a href={`${segment.link?.uri}`}>{segment.text}</a>, 17 + ); 18 + } else if (segment.isMention()) { 19 + descriptionWithFacets.push( 20 + <a href={`/profile/${segment.mention?.did}`}>{segment.text}</a>, 21 + ); 22 + } else { 23 + descriptionWithFacets.push(<>{segment.text</>); 24 + } 25 + } 26 + 27 + return descriptionWithFacets; 28 + }
+12 -8
main.tsx
··· 1 1 import { renderToStringAsync } from "npm:preact-render-to-string"; 2 2 import { AtpAgent } from "npm:@atproto/api"; 3 3 4 - // page imports 5 - import * as pages from "./pages/mod.ts"; 4 + import { 5 + Actor, 6 + ActorFeed, 7 + ActorFollowers, 8 + ActorFollows, 9 + } from "./pages/actor.tsx"; 6 10 7 11 export const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 8 12 ··· 29 33 }).then((res) => res.data); 30 34 31 35 return new Response( 32 - await renderToStringAsync(await pages.Actor({ actor: actor })), 36 + await renderToStringAsync(await Actor({ actor: actor })), 33 37 headers, 34 38 ); 35 39 } catch (e) { ··· 50 54 if (page === "followers") { 51 55 return new Response( 52 56 await renderToStringAsync( 53 - await pages.ActorFollowers({ url: url, actor: actor }), 57 + await ActorFollowers({ url: url, actor: actor }), 54 58 ), 55 59 headers, 56 60 ); ··· 58 62 if (page === "follows") { 59 63 return new Response( 60 64 await renderToStringAsync( 61 - await pages.ActorFollows({ url: url, actor: actor }), 65 + await ActorFollows({ url: url, actor: actor }), 62 66 ), 63 67 headers, 64 68 ); 65 69 } 66 70 67 - /*if (page === "posts") { 71 + if (page === "posts") { 68 72 return new Response( 69 73 await renderToStringAsync( 70 - await pages.ActorFeed({ url: url, actor: actor }), 74 + await ActorFeed({ url: url, actor: actor }), 71 75 ), 72 76 headers, 73 77 ); 74 - }*/ 78 + } 75 79 } 76 80 77 81 return Response.redirect(
+228
pages/actor.tsx
··· 1 + import { AppBskyActorDefs /*AppBskyFeedDefs*/ } from "npm:@atproto/api"; 2 + import sanitizeHtml from "npm:sanitize-html"; 3 + 4 + import { agent } from "../main.tsx"; 5 + import { GetDescriptionFacets } from "../facets.tsx"; 6 + 7 + // Components 8 + import { Head } from "../components/head.tsx"; 9 + 10 + /* MAIN PAGE */ 11 + export async function Actor( 12 + { actor }: { actor: AppBskyActorDefs.ProfileViewDetailed }, 13 + ) { 14 + return ( 15 + <> 16 + <Head 17 + title={actor.displayName 18 + ? actor.displayName + ` (@${actor.handle})` 19 + : `@${actor.handle}`} 20 + /> 21 + <table draggable={false}> 22 + <tr> 23 + <td> 24 + <img 25 + src={actor.avatar 26 + ? actor.avatar 27 + : "https://htmlsky.app/avatar.jpeg"} 28 + alt={`${sanitizeHtml(actor.displayName)}'s avatar`} 29 + width="90" 30 + /> 31 + </td> 32 + <td>&nbsp;</td> 33 + <td> 34 + <small> 35 + <small> 36 + <small> 37 + <span>&nbsp;</span> 38 + </small> 39 + </small> 40 + </small> 41 + <h1> 42 + <span 43 + dangerouslySetInnerHTML={{ 44 + __html: actor.displayName ? actor.displayName : actor.handle, 45 + }} 46 + > 47 + </span> 48 + <br /> 49 + <small> 50 + <small> 51 + <small> 52 + <i>@{actor.handle}</i> 53 + </small> 54 + </small> 55 + </small> 56 + </h1> 57 + </td> 58 + </tr> 59 + </table> 60 + 61 + <p> 62 + <span> 63 + <a href="./followers"> 64 + <b>{actor.followersCount}</b> followers 65 + </a> 66 + </span>&nbsp; 67 + <span> 68 + <a href="./follows"> 69 + <b>{actor.followsCount}</b> following 70 + </a> 71 + </span>&nbsp; 72 + <span> 73 + <b>{actor.postsCount}</b> posts 74 + </span>&nbsp; 75 + </p> 76 + 77 + <p> 78 + {await GetDescriptionFacets(sanitizeHtml(actor.description))} 79 + </p> 80 + 81 + <hr /> 82 + 83 + <p>A list of posts will be here eventually.</p> 84 + </> 85 + ); 86 + } 87 + 88 + /* FOLLOWS */ 89 + export async function ActorFollows( 90 + { url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 91 + ) { 92 + const cursor = url.searchParams.get("cursor")!; 93 + 94 + const follows: AppBskyActorDefs.ProfileViewDetailed[] = await agent.api.app 95 + .bsky.graph.getFollows({ actor: actor.did, cursor: cursor }).then((res) => 96 + res.data 97 + ).then( 98 + (res) => { 99 + //cursor = res.cursor!; 100 + return res.follows; 101 + }, 102 + ); 103 + 104 + const followList: preact.VNode[] = []; 105 + 106 + follows.forEach((follow) => { 107 + followList.push( 108 + <p> 109 + <b>{follow.displayName ? follow.displayName : follow.handle}</b> 110 + <br />{" "} 111 + <a 112 + href={`/profile/${ 113 + follow.handle !== "handle.invalid" ? follow.handle : follow.did 114 + }/`} 115 + > 116 + @{follow.handle} 117 + </a> 118 + </p>, 119 + ); 120 + }); 121 + 122 + if (cursor) { 123 + followList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 124 + } 125 + 126 + return ( 127 + <> 128 + <Head title={`People followed by @${actor.handle}`} /> 129 + <header> 130 + <a href="..">Back</a>&nbsp; 131 + <span> 132 + <b> 133 + <i> 134 + {actor.displayName ? actor.displayName : actor.handle}'s Follows 135 + </i> 136 + </b> 137 + </span> 138 + 139 + <hr /> 140 + </header> 141 + {followList} 142 + </> 143 + ); 144 + } 145 + 146 + /* FOLLOWERS */ 147 + export async function ActorFollowers( 148 + { url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 149 + ) { 150 + const cursor = url.searchParams.get("cursor")!; 151 + 152 + const followers: AppBskyActorDefs.ProfileViewDetailed[] = await agent.api.app 153 + .bsky.graph.getFollowers({ actor: actor.did, cursor: cursor }).then((res) => 154 + res.data 155 + ).then( 156 + (res) => { 157 + //cursor = res.cursor!; 158 + return res.followers; 159 + }, 160 + ); 161 + 162 + const followersList: preact.VNode[] = []; 163 + 164 + followers.forEach((follower) => { 165 + followersList.push( 166 + <p> 167 + <b>{follower.displayName ? follower.displayName : follower.handle}</b> 168 + <br /> <a href={`/profile/${follower.handle}/`}>@{follower.handle}</a> 169 + </p>, 170 + ); 171 + }); 172 + 173 + if (cursor) { 174 + followersList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 175 + } 176 + 177 + return ( 178 + <> 179 + <Head title={`People following @${actor.handle}`} /> 180 + <header> 181 + <a href="..">Back</a>&nbsp; 182 + <span> 183 + <b> 184 + <i> 185 + {actor.displayName ? actor.displayName : actor.handle}'s Followers 186 + </i> 187 + </b> 188 + </span> 189 + 190 + <hr /> 191 + </header> 192 + {followersList} 193 + </> 194 + ); 195 + } 196 + 197 + /* FEED */ 198 + export async function ActorFeed( 199 + { url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 200 + ) { 201 + // TODO: implement cursor 202 + const _prevCursor = url.searchParams.get("cursor")!; 203 + 204 + const { data } = await agent.api.app 205 + .bsky.feed.getAuthorFeed({ actor: actor.did }); 206 + 207 + const { feed, cursor } = data; 208 + 209 + const feedList: preact.VNode[] = []; 210 + 211 + feed.forEach((post) => { 212 + feedList.push( 213 + <p> 214 + {post.post} 215 + </p>, 216 + ); 217 + }); 218 + 219 + if (cursor) { 220 + feedList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 221 + } 222 + 223 + return ( 224 + <> 225 + {feedList} 226 + </> 227 + ); 228 + }
-48
pages/actor/feed.tsx
··· 1 - import { 2 - AppBskyActorDefs, 3 - AppBskyFeedDefs, 4 - AppBskyFeedPost, 5 - } from "npm:@atproto/api"; 6 - 7 - // import sanitizeHtml from "npm:sanitize-html"; 8 - 9 - // import { GetDescriptionFacets } from "../../facets.ts"; 10 - import { agent } from "../../main.tsx"; 11 - 12 - export async function ActorFeed( 13 - { _url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 14 - ) { 15 - // let cursor = url.searchParams.get("cursor")!; 16 - 17 - const { data } = await agent.api.app 18 - .bsky.feed.getAuthorFeed({ actor: actor.did }); 19 - 20 - const { feed, cursor } = data; 21 - /*.then( 22 - (res) => { 23 - cursor = res.cursor!; 24 - return res.feed; 25 - }, 26 - );*/ 27 - 28 - const feedList: preact.VNode[] = []; 29 - 30 - feed.forEach((post) => { 31 - console.log(post.post); 32 - feedList.push( 33 - <p> 34 - {post.post} 35 - </p>, 36 - ); 37 - }); 38 - 39 - if (cursor) { 40 - feedList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 41 - } 42 - 43 - return ( 44 - <> 45 - {feedList} 46 - </> 47 - ); 48 - }
-58
pages/actor/followers.tsx
··· 1 - import { AppBskyActorDefs /*AppBskyFeedDefs*/ } from "npm:@atproto/api"; 2 - // import sanitizeHtml from "npm:sanitize-html"; 3 - 4 - // import { GetDescriptionFacets } from "../../facets.ts"; 5 - import { agent } from "../../main.tsx"; 6 - import { Head } from "../mod.ts"; 7 - 8 - export async function ActorFollowers( 9 - { url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 10 - ) { 11 - // TODO: implement cursor 12 - 13 - let cursor = url.searchParams.get("cursor")!; 14 - 15 - const followers: AppBskyActorDefs.ProfileViewDetailed[] = await agent.api.app 16 - .bsky.graph.getFollowers({ actor: actor.did, cursor: cursor }).then((res) => 17 - res.data 18 - ).then( 19 - (res) => { 20 - cursor = res.cursor!; 21 - return res.followers; 22 - }, 23 - ); 24 - 25 - const followersList: preact.VNode[] = []; 26 - 27 - followers.forEach((follower) => { 28 - followersList.push( 29 - <p> 30 - <b>{follower.displayName ? follower.displayName : follower.handle}</b> 31 - <br /> <a href={`/profile/${follower.handle}/`}>@{follower.handle}</a> 32 - </p>, 33 - ); 34 - }); 35 - 36 - if (cursor) { 37 - followersList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 38 - } 39 - 40 - return ( 41 - <> 42 - <Head title={`People following @${actor.handle}`} /> 43 - <header> 44 - <a href="..">Back</a>&nbsp; 45 - <span> 46 - <b> 47 - <i> 48 - {actor.displayName ? actor.displayName : actor.handle}'s Followers 49 - </i> 50 - </b> 51 - </span> 52 - 53 - <hr /> 54 - </header> 55 - {followersList} 56 - </> 57 - ); 58 - }
-63
pages/actor/follows.tsx
··· 1 - import { AppBskyActorDefs /*AppBskyFeedDefs*/ } from "npm:@atproto/api"; 2 - // import sanitizeHtml from "npm:sanitize-html"; 3 - 4 - // import { GetDescriptionFacets } from "../../facets.ts"; 5 - import { agent } from "../../main.tsx"; 6 - import { Head } from "../mod.ts"; 7 - 8 - export async function ActorFollows( 9 - { url, actor }: { url: URL; actor: AppBskyActorDefs.ProfileViewDetailed }, 10 - ) { 11 - let cursor = url.searchParams.get("cursor")!; 12 - 13 - const follows: AppBskyActorDefs.ProfileViewDetailed[] = await agent.api.app 14 - .bsky.graph.getFollows({ actor: actor.did, cursor: cursor }).then((res) => 15 - res.data 16 - ).then( 17 - (res) => { 18 - cursor = res.cursor!; 19 - return res.follows; 20 - }, 21 - ); 22 - 23 - const followList: preact.VNode[] = []; 24 - 25 - follows.forEach((follow) => { 26 - followList.push( 27 - <p> 28 - <b>{follow.displayName ? follow.displayName : follow.handle}</b> 29 - <br />{" "} 30 - <a 31 - href={`/profile/${ 32 - follow.handle !== "handle.invalid" ? follow.handle : follow.did 33 - }/`} 34 - > 35 - @{follow.handle} 36 - </a> 37 - </p>, 38 - ); 39 - }); 40 - 41 - if (cursor) { 42 - followList.push(<a href={`?cursor=${cursor}`}>Next page</a>); 43 - } 44 - 45 - return ( 46 - <> 47 - <Head title={`People followed by @${actor.handle}`} /> 48 - <header> 49 - <a href="..">Back</a>&nbsp; 50 - <span> 51 - <b> 52 - <i> 53 - {actor.displayName ? actor.displayName : actor.handle}'s Follows 54 - </i> 55 - </b> 56 - </span> 57 - 58 - <hr /> 59 - </header> 60 - {followList} 61 - </> 62 - ); 63 - }
-90
pages/actor/index.tsx
··· 1 - import { AppBskyActorDefs /*AppBskyFeedDefs*/ } from "npm:@atproto/api"; 2 - import sanitizeHtml from "npm:sanitize-html"; 3 - 4 - import { GetDescriptionFacets } from "../../facets.ts"; 5 - 6 - // Components 7 - import { ActorHeader, Head } from "../mod.ts"; 8 - 9 - export async function Actor( 10 - { actor }: { actor: AppBskyActorDefs.ProfileViewDetailed }, 11 - ) { 12 - return ( 13 - <> 14 - <Head 15 - title={actor.displayName 16 - ? actor.displayName + ` (@${actor.handle})` 17 - : `@${actor.handle}`} 18 - /> 19 - <ActorHeader {...actor} /> 20 - <table draggable={false}> 21 - <tr> 22 - <td> 23 - <img 24 - src={actor.avatar 25 - ? actor.avatar 26 - : "https://htmlsky.app/avatar.jpeg"} 27 - alt={`${sanitizeHtml(actor.displayName)}'s avatar`} 28 - width="90" 29 - /> 30 - </td> 31 - <td>&nbsp;</td> 32 - <td> 33 - <small> 34 - <small> 35 - <small> 36 - <span>&nbsp;</span> 37 - </small> 38 - </small> 39 - </small> 40 - <h1> 41 - <span 42 - dangerouslySetInnerHTML={{ 43 - __html: actor.displayName ? actor.displayName : actor.handle, 44 - }} 45 - > 46 - </span> 47 - <br /> 48 - <small> 49 - <small> 50 - <small> 51 - <i>@{actor.handle}</i> 52 - </small> 53 - </small> 54 - </small> 55 - </h1> 56 - </td> 57 - </tr> 58 - </table> 59 - 60 - <p> 61 - <span> 62 - <a href="./followers"> 63 - <b>{actor.followersCount}</b> followers 64 - </a> 65 - </span>&nbsp; 66 - <span> 67 - <a href="./follows"> 68 - <b>{actor.followsCount}</b> following 69 - </a> 70 - </span>&nbsp; 71 - <span> 72 - <b>{actor.postsCount}</b> posts 73 - </span>&nbsp; 74 - </p> 75 - 76 - <p 77 - dangerouslySetInnerHTML={{ 78 - __html: (await GetDescriptionFacets( 79 - sanitizeHtml(actor.description), 80 - )).replaceAll("\n", "<br>"), 81 - }} 82 - > 83 - </p> 84 - 85 - <hr /> 86 - 87 - <p>A list of posts will be here eventually.</p> 88 - </> 89 - ); 90 - }
-4
pages/actor/mod.ts
··· 1 - export * from "./index.tsx"; 2 - export * from "./followers.tsx"; 3 - export * from "./follows.tsx"; 4 - export * from "./feed.tsx";
pages/head.tsx components/head.tsx
pages/header.tsx components/header.tsx
-3
pages/mod.ts
··· 1 - export * from "./actor/mod.ts"; 2 - export * from "./head.tsx"; 3 - export * from "./header.tsx";