use bsky.app/sdk

This commit is contained in:
Samuel Newman
2026-07-17 11:19:59 +03:00
parent 6e17bc4ec7
commit 59811328c4
15 changed files with 429 additions and 544 deletions
+4
View File
@@ -4,6 +4,7 @@ import {defineConfig} from 'eslint/config'
import tseslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import {importX} from 'eslint-plugin-import-x'
import {createTypeScriptImportResolver} from 'eslint-import-resolver-typescript'
import globals from 'globals'
export default defineConfig(
@@ -55,5 +56,8 @@ export default defineConfig(
],
'import-x/consistent-type-specifier-style': ['warn', 'prefer-inline'],
},
settings: {
'import-x/resolver-next': [createTypeScriptImportResolver()],
},
},
)
-107
View File
@@ -1,107 +0,0 @@
{
"version": 1,
"lexicons": [
"app.bsky.feed.getPosts",
"app.bsky.feed.post",
"app.bsky.graph.starterpack",
"com.atproto.identity.resolveHandle"
],
"resolutions": {
"app.bsky.actor.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.defs",
"cid": "bafyreielcp7ccfxuvtdthry5qqdqmvcqo5wx3shkuz32xd5rlypmhw6ufa"
},
"app.bsky.actor.status": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.status",
"cid": "bafyreifdg4b64wohpwkh5lydc6tckvol2rspnpni6dec6recy2rhvlnz4a"
},
"app.bsky.embed.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.defs",
"cid": "bafyreia42uud4qil67wknywzbxfyxc3b7woewsii54cakq2ould3ldetei"
},
"app.bsky.embed.external": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.external",
"cid": "bafyreigwach36azu4xg3jvcmb6fe53umfln3wkuj5srqsvcjaypa6rw534"
},
"app.bsky.embed.gallery": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.gallery",
"cid": "bafyreia4tixag4sadkirfo63qfma6fz3ifbqanify4e6ce6swkl5lwjnfq"
},
"app.bsky.embed.images": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.images",
"cid": "bafyreifw77mkqkzlvm5csjwutkrnaivsjvlhg2qxdlwwabqpahlyyjrpwi"
},
"app.bsky.embed.record": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.record",
"cid": "bafyreia7yq4c6rqkz32fjf6gj5rcvxsirguducn545s3dvls7qutnn2ali"
},
"app.bsky.embed.recordWithMedia": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.recordWithMedia",
"cid": "bafyreidszvpeqf7copdmowm4ln4eyhahlqxktlvootjjjdmtkwy6s62jya"
},
"app.bsky.embed.video": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video",
"cid": "bafyreie3nug4ezpwodl6yrpgv5edkazzn22t7ea4yaeuun4rctyekkngai"
},
"app.bsky.feed.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs",
"cid": "bafyreibg2zvz4n25vtd5yfqlodqra72s4h4lz7vwdexoutv3523wbw6jca"
},
"app.bsky.feed.getPosts": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getPosts",
"cid": "bafyreihy4tanaizkynghab46n3qztsspvoaqtbrkzinvxi4igeylg5ghji"
},
"app.bsky.feed.post": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.post",
"cid": "bafyreia7rgxu7467phwpcgzaw62l5bzmxufkpikrc5zm4q3xhjmuhq4pti"
},
"app.bsky.feed.postgate": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.postgate",
"cid": "bafyreiai5efexyluyptv5tbl6kqbqlnneczqzexcqnxmitmulyjfaftgva"
},
"app.bsky.feed.threadgate": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.threadgate",
"cid": "bafyreiht77wd6duduz4yqp62m6dwma5dy7gdihps4g2nd73acfzqlglvdi"
},
"app.bsky.graph.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.defs",
"cid": "bafyreifcipomli7yggtl46xufgxlnrw7se6xmsdxmzgfcz2tiu76ljatxm"
},
"app.bsky.graph.starterpack": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.starterpack",
"cid": "bafyreiejgf46gibbkpmjtgt5yyiir3wyxgrtvnfxtcdeyxrdrdnsvwgjc4"
},
"app.bsky.labeler.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.labeler.defs",
"cid": "bafyreicxx5i36v5dbqk5vvfzhnta5gajrvc544mnepux4wksrkid7mw3q4"
},
"app.bsky.notification.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.defs",
"cid": "bafyreifzktxnol5rafjoznauk5j7ub2gsxxm7qjskg3vqmcgn6krrsz55u"
},
"app.bsky.richtext.facet": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.richtext.facet",
"cid": "bafyreidg56eo7zynf6ihz4xb627vwoqf5idnevkmwp7sxc4tijg6xngbu4"
},
"com.atproto.identity.resolveHandle": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.resolveHandle",
"cid": "bafyreigckmqtt3jrtzd7tvigjatnz6ajqyafs26h5pwcudwag2anedxnmu"
},
"com.atproto.label.defs": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.defs",
"cid": "bafyreidp2wpcbzl2qiob2uwoc7lhntojx3tjcr553mmahw2f5cumtm3wpm"
},
"com.atproto.moderation.defs": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.moderation.defs",
"cid": "bafyreideawy4rlpgces2oebk5q4kpurbonhb5qtl4pes7dvxsc5osaiksy"
},
"com.atproto.repo.strongRef": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.strongRef",
"cid": "bafyreifrkdbnkvfjujntdaeigolnrjj3srrs53tfixjhmacclps72qlov4"
},
"tools.ozone.report.defs": {
"uri": "at://did:plc:33dt5kftu3jq2h5h4jjlqezt/com.atproto.lexicon.schema/tools.ozone.report.defs",
"cid": "bafyreic3l2rmh2ugirt3jz372wcvy333m7t2ynlyzj2k54oshijs6lxdfu"
}
}
}
+5 -9
View File
@@ -10,18 +10,13 @@
"analyze": "ANALYZE=1 vite build",
"lint": "eslint --cache src",
"typecheck": "tsc --noEmit",
"format": "prettier -w src",
"lex": "lex",
"lex:install": "lex install",
"lex:update": "lex install --update",
"lex:ci": "lex install --ci",
"lex:build": "lex build --lexicons ./lexicons --out ./src/lexicons --clear",
"postinstall": "pnpm lex:ci && pnpm lex:build"
"format": "prettier -w src"
},
"dependencies": {
"@atproto/lex": "^0.3.0",
"@atproto/syntax": "^0.7.2",
"@typescript/native": "npm:typescript@^7.0.2",
"@bsky.app/sdk": "npm:bsky-sdk-preview@0.0.1-preview.1",
"@tailwindcss/vite": "^4.3.2",
"preact": "^10.29.7"
},
"devDependencies": {
@@ -31,6 +26,7 @@
"@vitejs/plugin-legacy": "^8.2.1",
"autoprefixer": "^10.5.3",
"eslint": "^10.7.0",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-simple-import-sort": "^13.0.0",
"globals": "^17.7.0",
@@ -39,7 +35,7 @@
"serve": "^14.2.6",
"tailwindcss": "^4.3.2",
"terser": "^5.49.0",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.64.0",
"vite": "^8.1.5",
"vite-bundle-analyzer": "^1.3.9"
+366 -299
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -5,3 +5,4 @@ minimumReleaseAgeExclude:
- '@atproto/*'
- '@atproto-labs/*'
- '@bsky.app/*'
- 'bsky-sdk-preview@0.0.1-preview.1'
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+12 -11
View File
@@ -1,16 +1,17 @@
import {app} from '@bsky.app/sdk/lexicons'
import {ComponentChildren, h} from 'preact'
import {useMemo} from 'preact/hooks'
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import {Link} from '#/components/link'
import {VerificationCheck} from '#/components/verification-check'
import {Globe} from '#/icons/Globe'
import {CONTENT_LABELS, labelsToInfo} from '#/labels'
import {getRkey} from '#/util/rkey'
import {getVerificationState} from '#/util/verification-state'
import infoIcon from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import playIcon from '../../assets/play_filled_corner0_rounded.svg'
import starterPackIcon from '../../assets/starterPack.svg'
import {Globe} from '../icons/Globe'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
import * as app from '../lexicons/app'
import {getRkey} from '../util/rkey'
import {getVerificationState} from '../util/verification-state'
import {Link} from './link'
import {VerificationCheck} from './verification-check'
export function Embed({
content,
@@ -32,7 +33,7 @@ export function Embed({
}
// Case 1b: Gallery (Photos v2)
if (AppBskyEmbedGallery.isView(content)) {
if (app.bsky.embed.gallery.view.isTypeOf(content)) {
return <GalleryEmbed content={content} labelInfo={labelInfo} />
}
@@ -253,14 +254,14 @@ function GalleryEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedGallery.View
content: app.bsky.embed.gallery.View
labelInfo?: string
}) {
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
const images = content.items
.filter(AppBskyEmbedGallery.isViewImage)
.filter(i => app.bsky.embed.gallery.viewImage.isTypeOf(i))
.map(i => ({thumb: i.thumbnail, alt: i.alt}))
return <ImageGrid images={images} />
}
+17 -16
View File
@@ -1,21 +1,22 @@
import {app} from '@bsky.app/sdk/lexicons'
import {RichText} from '@bsky.app/sdk/richtext'
import {ComponentChild, h} from 'preact'
import {Container} from '#/components/container'
import {Embed} from '#/components/embed'
import {Link} from '#/components/link'
import {VerificationCheck} from '#/components/verification-check'
import {Like as LikeIcon} from '#/icons/Like'
import {Reply as ReplyIcon} from '#/icons/Reply'
import {Repost as RepostIcon} from '#/icons/Repost'
import {Robot as RobotIcon} from '#/icons/Robot'
import {CONTENT_LABELS} from '#/labels'
import {niceDate} from '#/util/nice-date'
import {prettyNumber} from '#/util/pretty-number'
import {getRkey} from '#/util/rkey'
import {getVerificationState} from '#/util/verification-state'
import logo from '../../assets/logo_full_name.svg'
import {Like as LikeIcon} from '../icons/Like'
import {Reply as ReplyIcon} from '../icons/Reply'
import {Repost as RepostIcon} from '../icons/Repost'
import {Robot as RobotIcon} from '../icons/Robot'
import {CONTENT_LABELS} from '../labels'
import * as app from '../lexicons/app'
import {FacetRenderer} from '../util/facet-renderer'
import {niceDate} from '../util/nice-date'
import {prettyNumber} from '../util/pretty-number'
import {getRkey} from '../util/rkey'
import {getVerificationState} from '../util/verification-state'
import {Container} from './container'
import {Embed} from './embed'
import {Link} from './link'
import {VerificationCheck} from './verification-check'
interface Props {
post: app.bsky.feed.defs.PostView
@@ -147,7 +148,7 @@ function PostContent({record}: {record: app.bsky.feed.post.Main | null}) {
// render an empty <p> that adds an extra flex gap above the embed
if (!record?.text) return null
const rt = new FacetRenderer({text: record.text, facets: record.facets})
const rt = new RichText({text: record.text, facets: record.facets})
const richText: ComponentChild[] = []
let counter = 0
+1 -1
View File
@@ -1,4 +1,4 @@
import * as app from './lexicons/app'
import {app} from '@bsky.app/sdk/lexicons'
export const CONTENT_LABELS = ['porn', 'sexual', 'nudity', 'graphic-media']
+10 -10
View File
@@ -2,22 +2,22 @@ import '../index.css'
import {AtUriString, Client, type HandleString} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {app, com} from '@bsky.app/sdk/lexicons'
import {h, render} from 'preact'
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {
assertColorModeValues,
ColorModeValues,
initSystemColorMode,
} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
import * as app from '../lexicons/app'
import * as com from '../lexicons/com'
import {niceDate} from '../util/nice-date'
} from '#/color-mode'
import {Container} from '#/components/container'
import {Link} from '#/components/link'
import {Post} from '#/components/post'
import {niceDate} from '#/util/nice-date'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
const DEFAULT_POST =
'https://bsky.app/profile/did:plc:vjug55kidv6sye7ykr5faxxn/post/3jzn6g7ixgq2y'
@@ -85,7 +85,7 @@ function LandingPage() {
atUri = `at://${did}/app.bsky.feed.post/${rkey}`
} catch (err) {
console.log(err)
throw new Error('Invalid Bluesky URL')
throw new Error('Invalid Bluesky URL', {cause: err})
}
}
}
+7 -6
View File
@@ -1,15 +1,16 @@
import '../index.css'
import {Client, isAtUriString} from '@atproto/lex'
import {app} from '@bsky.app/sdk/lexicons'
import {h, render} from 'preact'
import {applyTheme, initSystemColorMode} from '#/color-mode'
import {Container} from '#/components/container'
import {Link} from '#/components/link'
import {Post} from '#/components/post'
import {getRkey} from '#/util/rkey'
import logo from '../../assets/logo.svg'
import {applyTheme, initSystemColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
import * as app from '../lexicons/app'
import {getRkey} from '../util/rkey'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
-78
View File
@@ -1,78 +0,0 @@
import * as app from '../lexicons/app'
type Facet = app.bsky.richtext.facet.Main
const encoder = new TextEncoder()
const decoder = new TextDecoder()
export class FacetSegment {
constructor(
public text: string,
public facet?: Facet,
) {}
get link() {
return this.facet?.features.find(app.bsky.richtext.facet.link.$isTypeOf)
}
get mention() {
return this.facet?.features.find(app.bsky.richtext.facet.mention.$isTypeOf)
}
get tag() {
return this.facet?.features.find(app.bsky.richtext.facet.tag.$isTypeOf)
}
}
export class FacetRenderer {
private utf8: Uint8Array
private facets: Facet[]
constructor({text, facets}: {text: string; facets?: Facet[]}) {
this.utf8 = encoder.encode(text)
this.facets = (facets ?? [])
.filter(f => f.index.byteStart <= f.index.byteEnd)
.sort((a, b) => a.index.byteStart - b.index.byteStart)
}
private slice(start: number, end: number): string {
return decoder.decode(this.utf8.slice(start, end))
}
*segments(): Generator<FacetSegment, void, void> {
if (!this.facets.length) {
yield new FacetSegment(decoder.decode(this.utf8))
return
}
let textCursor = 0
let facetCursor = 0
do {
const currFacet = this.facets[facetCursor]
if (textCursor < currFacet.index.byteStart) {
yield new FacetSegment(
this.slice(textCursor, currFacet.index.byteStart),
)
} else if (textCursor > currFacet.index.byteStart) {
facetCursor++
continue
}
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
const subtext = this.slice(
currFacet.index.byteStart,
currFacet.index.byteEnd,
)
if (!subtext.trim()) {
yield new FacetSegment(subtext)
} else {
yield new FacetSegment(subtext, currFacet)
}
}
textCursor = currFacet.index.byteEnd
facetCursor++
} while (facetCursor < this.facets.length)
if (textCursor < this.utf8.byteLength) {
yield new FacetSegment(this.slice(textCursor, this.utf8.byteLength))
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as app from '../lexicons/app'
import {app} from '@bsky.app/sdk/lexicons'
export type VerificationState = {
role: 'default' | 'verifier'
+3
View File
@@ -16,6 +16,9 @@
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"paths": {
"#/*": ["./src/*"],
}
},
"include": ["src", "snippet", "vite.config.ts", "*.config.cjs", "eslint.config.mjs"]
}
+2
View File
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import {resolve} from 'node:path'
import {preact} from '@preact/preset-vite'
import tailwindcss from '@tailwindcss/vite'
import legacy from '@vitejs/plugin-legacy'
import type {Plugin, UserConfig} from 'vite'
import {analyzer} from 'vite-bundle-analyzer'
@@ -38,6 +39,7 @@ function devOnlyRouter(): Plugin {
const config: UserConfig = {
plugins: [
preact(),
tailwindcss(),
legacy({
targets: ['defaults', 'not IE 11'],
}),