An HTML-only Bluesky frontend

BREAKING: wipe for restart

+41 -418
-20
.build.yml
··· 1 - image: alpine/edge 2 - packages: 3 - - deno 4 - - unzip 5 - - curl 6 - secrets: 7 - - 1395de5b-a4f1-452a-9825-5e82a18d656c 8 - sources: 9 - - https://git.sr.ht/~jordanreger/htmlsky 10 - tasks: 11 - - install-deployctl: | 12 - deno install -Arf jsr:@deno/deployctl 13 - - deploy: | 14 - set +x 15 - export DENO_DEPLOY_TOKEN=$(cat ~/.deno_deploy_token) 16 - set -x 17 - export DENO_INSTALL="/home/build/.deno" 18 - export PATH="$DENO_INSTALL/bin:$PATH" 19 - cd htmlsky 20 - deployctl deploy --prod
+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 --watch -A main.ts", 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": {
+29
main.ts
··· 1 + Deno.serve((req) => { 2 + const url = new URL(req.url); 3 + const path = url.pathname; 4 + 5 + /* Handle trailing slashes */ 6 + if (path.at(-1) !== "/") { 7 + return Response.redirect( 8 + `${url.protocol}${url.host}${url.pathname}/`, 9 + ); 10 + } 11 + 12 + /* Profile */ 13 + const profilePattern = new URLPattern({ pathname: "/profile/:actor/" }); 14 + if (profilePattern.test(url)) { 15 + const actorName = profilePattern.exec(url)?.pathname.groups.actor; 16 + return new Response(actorName, headers); 17 + } 18 + 19 + /* Redirect to homepage */ 20 + return Response.redirect( 21 + `${url.protocol}${url.host}/profile/did:plc:sxouh4kxso3dufvnafa2zggn/`, 22 + ); 23 + }); 24 + 25 + const headers: ResponseInit = { 26 + "headers": { 27 + "Content-Type": "text/html;charset=utf-8", 28 + }, 29 + };
-87
main.tsx
··· 1 - import { renderToStringAsync } from "npm:preact-render-to-string"; 2 - import { AtpAgent } from "npm:@atproto/api"; 3 - 4 - // page imports 5 - import * as pages from "./pages/mod.ts"; 6 - 7 - export const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 8 - 9 - Deno.serve({ 10 - port: 8080, 11 - }, async (req) => { 12 - const url = new URL(req.url); 13 - const path = url.pathname; 14 - 15 - // handle trailing slashes 16 - if (path.at(-1) !== "/") { 17 - return Response.redirect( 18 - `${url.protocol}${url.host}${url.pathname}/`, 19 - ); 20 - } 21 - 22 - const profilePattern = new URLPattern({ pathname: "/profile/:actor/" }); 23 - if (profilePattern.test(url)) { 24 - const actorName = profilePattern.exec(url)?.pathname.groups.actor; 25 - 26 - try { 27 - const actor = await agent.api.app.bsky.actor.getProfile({ 28 - actor: actorName!, 29 - }).then((res) => res.data); 30 - 31 - return new Response( 32 - await renderToStringAsync(await pages.Actor({ actor: actor })), 33 - headers, 34 - ); 35 - } catch (e) { 36 - // TODO: add error page 37 - return new Response(e.message, headers); 38 - } 39 - } 40 - const profilePagePattern = new URLPattern({ 41 - pathname: "/profile/:actor/:page/", 42 - }); 43 - if (profilePagePattern.test(url)) { 44 - const actorName = profilePagePattern.exec(url)?.pathname.groups.actor; 45 - const actor = await agent.api.app.bsky.actor.getProfile({ 46 - actor: actorName!, 47 - }).then((res) => res.data); 48 - const page = profilePagePattern.exec(url)?.pathname.groups.page; 49 - 50 - if (page === "followers") { 51 - return new Response( 52 - await renderToStringAsync( 53 - await pages.ActorFollowers({ url: url, actor: actor }), 54 - ), 55 - headers, 56 - ); 57 - } 58 - if (page === "follows") { 59 - return new Response( 60 - await renderToStringAsync( 61 - await pages.ActorFollows({ url: url, actor: actor }), 62 - ), 63 - headers, 64 - ); 65 - } 66 - 67 - /*if (page === "posts") { 68 - return new Response( 69 - await renderToStringAsync( 70 - await pages.ActorFeed({ url: url, actor: actor }), 71 - ), 72 - headers, 73 - ); 74 - }*/ 75 - } 76 - 77 - return Response.redirect( 78 - `${url.protocol}${url.host}/profile/htmlsky.app/`, 79 - 303, 80 - ); 81 - }); 82 - 83 - const headers: ResponseInit = { 84 - "headers": { 85 - "Content-Type": "text/html;charset=utf-8", 86 - }, 87 - };
-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";
-8
pages/head.tsx
··· 1 - export const Head = ({ title }: { title: string }) => { 2 - return ( 3 - <head> 4 - <meta name="color-scheme" content="light dark" /> 5 - <title>{title} &#8212; HTMLsky</title> 6 - </head> 7 - ); 8 - };
-23
pages/header.tsx
··· 1 - import { AppBskyActorDefs } from "npm:@atproto/api"; 2 - 3 - export function ActorHeader(actor: AppBskyActorDefs.ProfileViewDetailed) { 4 - return ( 5 - <header> 6 - <nav> 7 - <span> 8 - <b> 9 - <i>HTMLsky</i> 10 - </b>&nbsp; 11 - </span> 12 - [ <a href="/">Home</a> ] [{" "} 13 - <a href="https://sr.ht/~jordanreger/htmlsky">Source</a> ] [{" "} 14 - <a href={`https://bsky.app/profile/${actor.did}`}> 15 - View on Bluesky 16 - </a>{" "} 17 - ] 18 - </nav> 19 - 20 - <hr /> 21 - </header> 22 - ); 23 - }
-3
pages/mod.ts
··· 1 - export * from "./actor/mod.ts"; 2 - export * from "./head.tsx"; 3 - export * from "./header.tsx";