migrate bskyembed to @atproto/lex
This commit is contained in:
@@ -1,16 +1,3 @@
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedGallery,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphStarterpack,
|
||||
AppBskyLabelerDefs,
|
||||
} from '@atproto/api'
|
||||
import {ComponentChildren, h} from 'preact'
|
||||
import {useMemo} from 'preact/hooks'
|
||||
|
||||
@@ -19,7 +6,7 @@ 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 bsky from '../types/bsky'
|
||||
import * as app from '../lexicons/app'
|
||||
import {getRkey} from '../util/rkey'
|
||||
import {getVerificationState} from '../util/verification-state'
|
||||
import {Link} from './link'
|
||||
@@ -30,8 +17,8 @@ export function Embed({
|
||||
labels,
|
||||
hideRecord,
|
||||
}: {
|
||||
content: AppBskyFeedDefs.PostView['embed']
|
||||
labels: AppBskyFeedDefs.PostView['labels']
|
||||
content: app.bsky.feed.defs.PostView['embed']
|
||||
labels: app.bsky.feed.defs.PostView['labels']
|
||||
hideRecord?: boolean
|
||||
}) {
|
||||
const labelInfo = useMemo(() => labelsToInfo(labels), [labels])
|
||||
@@ -40,7 +27,7 @@ export function Embed({
|
||||
|
||||
try {
|
||||
// Case 1: Image
|
||||
if (AppBskyEmbedImages.isView(content)) {
|
||||
if (app.bsky.embed.images.view.isTypeOf(content)) {
|
||||
return <ImageEmbed content={content} labelInfo={labelInfo} />
|
||||
}
|
||||
|
||||
@@ -50,12 +37,12 @@ export function Embed({
|
||||
}
|
||||
|
||||
// Case 2: External link
|
||||
if (AppBskyEmbedExternal.isView(content)) {
|
||||
if (app.bsky.embed.external.view.isTypeOf(content)) {
|
||||
return <ExternalEmbed content={content} labelInfo={labelInfo} />
|
||||
}
|
||||
|
||||
// Case 3: Record (quote or linked post)
|
||||
if (AppBskyEmbedRecord.isView(content)) {
|
||||
if (app.bsky.embed.record.view.isTypeOf(content)) {
|
||||
if (hideRecord) {
|
||||
return null
|
||||
}
|
||||
@@ -63,7 +50,7 @@ export function Embed({
|
||||
const record = content.record
|
||||
|
||||
// Case 3.1: Post
|
||||
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||
if (app.bsky.embed.record.viewRecord.isTypeOf(record)) {
|
||||
const pwiOptOut = !!record.author.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
@@ -77,8 +64,9 @@ export function Embed({
|
||||
}
|
||||
|
||||
let text
|
||||
if (AppBskyFeedPost.isRecord(record.value)) {
|
||||
text = record.value.text
|
||||
const validation = app.bsky.feed.post.$safeValidate(record.value)
|
||||
if (validation.success) {
|
||||
text = validation.value.text
|
||||
}
|
||||
|
||||
const isAuthorLabeled = record.author.labels?.some(label =>
|
||||
@@ -129,14 +117,17 @@ export function Embed({
|
||||
}
|
||||
|
||||
// Case 3.2: List
|
||||
if (AppBskyGraphDefs.isListView(record)) {
|
||||
if (app.bsky.graph.defs.listView.isTypeOf(record)) {
|
||||
if (labelInfo) {
|
||||
return <Info>{labelInfo}</Info>
|
||||
}
|
||||
return (
|
||||
<GenericWithImageEmbed
|
||||
image={record.avatar}
|
||||
title={record.name}
|
||||
href={`/profile/${record.creator.did}/lists/${getRkey(record)}`}
|
||||
subtitle={
|
||||
record.purpose === AppBskyGraphDefs.MODLIST
|
||||
record.purpose === app.bsky.graph.defs.modlist.value
|
||||
? `Moderation list by @${record.creator.handle}`
|
||||
: `User list by @${record.creator.handle}`
|
||||
}
|
||||
@@ -146,7 +137,10 @@ export function Embed({
|
||||
}
|
||||
|
||||
// Case 3.3: Feed
|
||||
if (AppBskyFeedDefs.isGeneratorView(record)) {
|
||||
if (app.bsky.feed.defs.generatorView.isTypeOf(record)) {
|
||||
if (labelInfo) {
|
||||
return <Info>{labelInfo}</Info>
|
||||
}
|
||||
return (
|
||||
<GenericWithImageEmbed
|
||||
image={record.avatar}
|
||||
@@ -159,28 +153,28 @@ export function Embed({
|
||||
}
|
||||
|
||||
// Case 3.4: Labeler
|
||||
if (AppBskyLabelerDefs.isLabelerView(record)) {
|
||||
if (app.bsky.labeler.defs.labelerView.isTypeOf(record)) {
|
||||
// Embed type does not exist in the app, so show nothing
|
||||
return null
|
||||
}
|
||||
|
||||
// Case 3.5: Starter pack
|
||||
if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
|
||||
if (app.bsky.graph.defs.starterPackViewBasic.isTypeOf(record)) {
|
||||
return <StarterPackEmbed content={record} />
|
||||
}
|
||||
|
||||
// Case 3.6: Post not found
|
||||
if (AppBskyEmbedRecord.isViewNotFound(record)) {
|
||||
if (app.bsky.embed.record.viewNotFound.isTypeOf(record)) {
|
||||
return <Info>Quoted post not found, it may have been deleted.</Info>
|
||||
}
|
||||
|
||||
// Case 3.7: Post blocked
|
||||
if (AppBskyEmbedRecord.isViewBlocked(record)) {
|
||||
if (app.bsky.embed.record.viewBlocked.isTypeOf(record)) {
|
||||
return <Info>The quoted post is blocked.</Info>
|
||||
}
|
||||
|
||||
// Case 3.8: Detached quote post
|
||||
if (AppBskyEmbedRecord.isViewDetached(record)) {
|
||||
if (app.bsky.embed.record.viewDetached.isTypeOf(record)) {
|
||||
// Just don't show anything
|
||||
return null
|
||||
}
|
||||
@@ -190,14 +184,14 @@ export function Embed({
|
||||
}
|
||||
|
||||
// Case 4: Video
|
||||
if (AppBskyEmbedVideo.isView(content)) {
|
||||
if (app.bsky.embed.video.view.isTypeOf(content)) {
|
||||
return <VideoEmbed content={content} />
|
||||
}
|
||||
|
||||
// Case 5: Record with media
|
||||
if (
|
||||
AppBskyEmbedRecordWithMedia.isView(content) &&
|
||||
AppBskyEmbedRecord.isViewRecord(content.record.record)
|
||||
app.bsky.embed.recordWithMedia.view.isTypeOf(content) &&
|
||||
app.bsky.embed.record.viewRecord.isTypeOf(content.record.record)
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -242,7 +236,7 @@ function ImageEmbed({
|
||||
content,
|
||||
labelInfo,
|
||||
}: {
|
||||
content: AppBskyEmbedImages.View
|
||||
content: app.bsky.embed.images.View
|
||||
labelInfo?: string
|
||||
}) {
|
||||
if (labelInfo) {
|
||||
@@ -355,7 +349,7 @@ function ExternalEmbed({
|
||||
content,
|
||||
labelInfo,
|
||||
}: {
|
||||
content: AppBskyEmbedExternal.View
|
||||
content: app.bsky.embed.external.View
|
||||
labelInfo?: string
|
||||
}) {
|
||||
function toNiceDomain(url: string): string {
|
||||
@@ -389,7 +383,7 @@ function ExternalEmbed({
|
||||
<p className="text-sm leading-snug text-textLight dark:text-textDimmed line-clamp-2 mt-0.5">
|
||||
{content.external.description}
|
||||
</p>
|
||||
<div className="flex flex-row items-center gap-1 border-t dark:border-slate-600 mt-1 pt-1.5">
|
||||
<div className="flex flex-row items-end gap-1 border-t dark:border-slate-600 mt-1 pt-1.5">
|
||||
<Globe size={12} className="text-textLight dark:text-textDimmed" />
|
||||
<p className="text-sm leading-none text-textLight dark:text-textDimmed line-clamp-1">
|
||||
{toNiceDomain(content.external.uri)}
|
||||
@@ -443,7 +437,7 @@ function GenericWithImageEmbed({
|
||||
)
|
||||
}
|
||||
|
||||
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||
function VideoEmbed({content}: {content: app.bsky.embed.video.View}) {
|
||||
let aspectRatio = 1
|
||||
|
||||
if (content.aspectRatio) {
|
||||
@@ -493,16 +487,13 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||
function StarterPackEmbed({
|
||||
content,
|
||||
}: {
|
||||
content: AppBskyGraphDefs.StarterPackViewBasic
|
||||
content: app.bsky.graph.defs.StarterPackViewBasic
|
||||
}) {
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
content.record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
const validation = app.bsky.graph.starterpack.$safeValidate(content.record)
|
||||
if (!validation.success) {
|
||||
return null
|
||||
}
|
||||
const record = validation.value
|
||||
|
||||
const starterPackHref = getStarterPackHref(content)
|
||||
const imageUri = getStarterPackImage(content)
|
||||
@@ -516,17 +507,15 @@ function StarterPackEmbed({
|
||||
<div className="flex space-x-2 items-center">
|
||||
<img src={starterPackIcon} className="w-10 h-10" />
|
||||
<div>
|
||||
<p className="font-semibold leading-[21px]">
|
||||
{content.record.name}
|
||||
</p>
|
||||
<p className="font-semibold leading-[21px]">{record.name}</p>
|
||||
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
|
||||
Starter pack by{' '}
|
||||
{content.creator.displayName || `@${content.creator.handle}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{content.record.description && (
|
||||
<p className="text-sm mt-1">{content.record.description}</p>
|
||||
{record.description && (
|
||||
<p className="text-sm mt-1">{record.description}</p>
|
||||
)}
|
||||
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
|
||||
<p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1">
|
||||
@@ -540,14 +529,14 @@ function StarterPackEmbed({
|
||||
|
||||
// from #/lib/strings/starter-pack.ts
|
||||
function getStarterPackImage(
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
starterPack: app.bsky.graph.defs.StarterPackViewBasic,
|
||||
) {
|
||||
const rkey = getRkey({uri: starterPack.uri})
|
||||
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
|
||||
}
|
||||
|
||||
function getStarterPackHref(
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
starterPack: app.bsky.graph.defs.StarterPackViewBasic,
|
||||
) {
|
||||
const rkey = getRkey({uri: starterPack.uri})
|
||||
const handleOrDid = starterPack.creator.handle || starterPack.creator.did
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {h} from 'preact'
|
||||
import {h, HTMLAttributes} from 'preact'
|
||||
|
||||
export function Link({
|
||||
href,
|
||||
@@ -9,7 +9,7 @@ export function Link({
|
||||
href: string
|
||||
className?: string
|
||||
disableTracking?: boolean
|
||||
} & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
|
||||
} & HTMLAttributes<HTMLAnchorElement>) {
|
||||
const searchParam = new URLSearchParams(window.location.search)
|
||||
const ref_url = searchParam.get('ref_url')
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyRichtextFacet,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {h} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo_full_name.svg'
|
||||
@@ -12,7 +6,7 @@ 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 bsky from '../types/bsky'
|
||||
import * as app from '../lexicons/app'
|
||||
import {niceDate} from '../util/nice-date'
|
||||
import {prettyNumber} from '../util/pretty-number'
|
||||
import {getRkey} from '../util/rkey'
|
||||
@@ -23,24 +17,18 @@ import {Link} from './link'
|
||||
import {VerificationCheck} from './verification-check'
|
||||
|
||||
interface Props {
|
||||
thread: AppBskyFeedDefs.ThreadViewPost
|
||||
post: app.bsky.feed.defs.PostView
|
||||
}
|
||||
|
||||
export function Post({thread}: Props) {
|
||||
const post = thread.post
|
||||
|
||||
export function Post({post}: Props) {
|
||||
const isAuthorLabeled = post.author.labels?.some(label =>
|
||||
CONTENT_LABELS.includes(label.val),
|
||||
)
|
||||
|
||||
let record: AppBskyFeedPost.Record | null = null
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
record = post.record
|
||||
let record = null
|
||||
const validation = app.bsky.feed.post.$safeValidate(post.record)
|
||||
if (validation.success) {
|
||||
record = validation.value
|
||||
}
|
||||
|
||||
const verification = getVerificationState({profile: post.author})
|
||||
@@ -153,70 +141,70 @@ export function Post({thread}: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
function PostContent({record}: {record: app.bsky.feed.post.Main | null}) {
|
||||
// text-only check - posts with no text (e.g. gallery posts) would otherwise
|
||||
// render an empty <p> that adds an extra flex gap above the embed
|
||||
if (!record?.text) return null
|
||||
|
||||
const rt = new RichText({
|
||||
text: record.text,
|
||||
facets: record.facets,
|
||||
})
|
||||
// const rt = new RichText({
|
||||
// text: record.text,
|
||||
// facets: record.facets,
|
||||
// })
|
||||
|
||||
const richText = []
|
||||
// const richText = []
|
||||
|
||||
let counter = 0
|
||||
for (const segment of rt.segments()) {
|
||||
if (
|
||||
segment.link &&
|
||||
AppBskyRichtextFacet.validateLink(segment.link).success
|
||||
) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={segment.link.uri}
|
||||
className="text-brand hover:underline"
|
||||
disableTracking={
|
||||
!segment.link.uri.startsWith('https://bsky.app') &&
|
||||
!segment.link.uri.startsWith('https://go.bsky.app')
|
||||
}>
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
} else if (
|
||||
segment.mention &&
|
||||
AppBskyRichtextFacet.validateMention(segment.mention).success
|
||||
) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={`/profile/${segment.mention.did}`}
|
||||
className="text-brand hover:underline">
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
} else if (
|
||||
segment.tag &&
|
||||
AppBskyRichtextFacet.validateTag(segment.tag).success
|
||||
) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={`/hashtag/${segment.tag.tag}`}
|
||||
className="text-brand hover:underline">
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
} else {
|
||||
richText.push(segment.text)
|
||||
}
|
||||
// let counter = 0
|
||||
// for (const segment of rt.segments()) {
|
||||
// if (
|
||||
// segment.link &&
|
||||
// app.bsky.richtext.facet.link.$safeValidate(segment.link).success
|
||||
// ) {
|
||||
// richText.push(
|
||||
// <Link
|
||||
// key={counter}
|
||||
// href={segment.link.uri}
|
||||
// className="text-brand hover:underline"
|
||||
// disableTracking={
|
||||
// !segment.link.uri.startsWith('https://bsky.app') &&
|
||||
// !segment.link.uri.startsWith('https://go.bsky.app')
|
||||
// }>
|
||||
// {segment.text}
|
||||
// </Link>,
|
||||
// )
|
||||
// } else if (
|
||||
// segment.mention &&
|
||||
// app.bsky.richtext.facet.mention.safeValidate(segment.mention).success
|
||||
// ) {
|
||||
// richText.push(
|
||||
// <Link
|
||||
// key={counter}
|
||||
// href={`/profile/${segment.mention.did}`}
|
||||
// className="text-brand hover:underline">
|
||||
// {segment.text}
|
||||
// </Link>,
|
||||
// )
|
||||
// } else if (
|
||||
// segment.tag &&
|
||||
// app.bsky.richtext.facet.tag.safeValidate(segment.tag).success
|
||||
// ) {
|
||||
// richText.push(
|
||||
// <Link
|
||||
// key={counter}
|
||||
// href={`/hashtag/${segment.tag.tag}`}
|
||||
// className="text-brand hover:underline">
|
||||
// {segment.text}
|
||||
// </Link>,
|
||||
// )
|
||||
// } else {
|
||||
// richText.push(segment.text)
|
||||
// }
|
||||
|
||||
counter++
|
||||
}
|
||||
// counter++
|
||||
// }
|
||||
|
||||
return (
|
||||
<p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
|
||||
{richText}
|
||||
</p>
|
||||
)
|
||||
// return (
|
||||
// <p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
|
||||
// {richText}
|
||||
// </p>
|
||||
// )
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import * as app from './lexicons/app'
|
||||
|
||||
export const CONTENT_LABELS = ['porn', 'sexual', 'nudity', 'graphic-media']
|
||||
|
||||
export function labelsToInfo(
|
||||
labels?: AppBskyFeedDefs.PostView['labels'],
|
||||
labels?: app.bsky.feed.defs.PostView['labels'],
|
||||
): string | undefined {
|
||||
const label = labels?.find(label => CONTENT_LABELS.includes(label.val))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../index.css'
|
||||
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
|
||||
import {AtUriString, Client, type HandleString} from '@atproto/lex'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {h, render} from 'preact'
|
||||
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||
|
||||
@@ -14,7 +15,8 @@ import {
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
import * as bsky from '../types/bsky'
|
||||
import * as app from '../lexicons/app'
|
||||
import * as com from '../lexicons/com'
|
||||
import {niceDate} from '../util/nice-date'
|
||||
|
||||
const DEFAULT_POST =
|
||||
@@ -30,9 +32,7 @@ if (!root) throw new Error('No root element')
|
||||
|
||||
initSystemColorMode({additionalBodyClasses: 'dark:bg-dimmedBgDarken'})
|
||||
|
||||
const agent = new AtpAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
const client = new Client('https://public.api.bsky.app')
|
||||
|
||||
render(<LandingPage />, root)
|
||||
|
||||
@@ -42,14 +42,12 @@ function LandingPage() {
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
||||
null,
|
||||
)
|
||||
const [post, setPost] = useState<app.bsky.feed.defs.PostView | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
setError(null)
|
||||
setThread(null)
|
||||
setPost(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
let atUri = DEFAULT_URI
|
||||
@@ -74,13 +72,14 @@ function LandingPage() {
|
||||
|
||||
let did = didOrHandle
|
||||
if (!didOrHandle.startsWith('did:')) {
|
||||
const resolution = await agent.resolveHandle({
|
||||
handle: didOrHandle,
|
||||
})
|
||||
if (!resolution.data.did) {
|
||||
const resolution = await client.call(
|
||||
com.atproto.identity.resolveHandle,
|
||||
{handle: didOrHandle as HandleString},
|
||||
)
|
||||
if (!resolution.did) {
|
||||
throw new Error('No DID found')
|
||||
}
|
||||
did = resolution.data.did
|
||||
did = resolution.did
|
||||
}
|
||||
|
||||
atUri = `at://${did}/app.bsky.feed.post/${rkey}`
|
||||
@@ -91,16 +90,16 @@ function LandingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const {data} = await agent.getPostThread({
|
||||
uri: atUri,
|
||||
depth: 0,
|
||||
parentHeight: 0,
|
||||
const {posts} = await client.call(app.bsky.feed.getPosts, {
|
||||
uris: [atUri as AtUriString],
|
||||
})
|
||||
|
||||
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
|
||||
const post = posts[0]
|
||||
|
||||
if (!post) {
|
||||
throw new Error('Post not found')
|
||||
}
|
||||
const pwiOptOut = !!data.thread.post.author.labels?.find(
|
||||
const pwiOptOut = !!post.author.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
if (pwiOptOut) {
|
||||
@@ -108,7 +107,7 @@ function LandingPage() {
|
||||
'The author of this post has requested their posts not be displayed on external sites.',
|
||||
)
|
||||
}
|
||||
setThread(data.thread)
|
||||
setPost(post)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError(err instanceof Error ? err.message : 'Invalid Bluesky URL')
|
||||
@@ -166,11 +165,11 @@ function LandingPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||
{!error && thread && uri && (
|
||||
<Snippet thread={thread} colorMode={colorMode} />
|
||||
{!error && post && uri && (
|
||||
<Snippet post={post} colorMode={colorMode} />
|
||||
)}
|
||||
<div className={colorMode}>
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
{!error && post && <Post post={post} key={post.uri} />}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
|
||||
@@ -203,10 +202,10 @@ function Skeleton() {
|
||||
}
|
||||
|
||||
function Snippet({
|
||||
thread,
|
||||
post,
|
||||
colorMode,
|
||||
}: {
|
||||
thread: AppBskyFeedDefs.ThreadViewPost
|
||||
post: app.bsky.feed.defs.PostView
|
||||
colorMode: ColorModeValues
|
||||
}) {
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
@@ -223,24 +222,17 @@ function Snippet({
|
||||
}, [copied])
|
||||
|
||||
const snippet = useMemo(() => {
|
||||
const record = thread.post.record
|
||||
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
const validation = app.bsky.feed.post.$safeValidate(post.record)
|
||||
if (!validation.success) {
|
||||
return ''
|
||||
}
|
||||
const record = validation.value
|
||||
|
||||
const lang = record.langs && record.langs.length > 0 ? record.langs[0] : ''
|
||||
const profileHref = toShareUrl(
|
||||
['/profile', thread.post.author.did].join('/'),
|
||||
)
|
||||
const urip = new AtUri(thread.post.uri)
|
||||
const profileHref = toShareUrl(['/profile', post.author.did].join('/'))
|
||||
const urip = new AtUri(post.uri)
|
||||
const href = toShareUrl(
|
||||
['/profile', thread.post.author.did, 'post', urip.rkey].join('/'),
|
||||
['/profile', post.author.did, 'post', urip.rkey].join('/'),
|
||||
)
|
||||
|
||||
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
||||
@@ -248,9 +240,9 @@ function Snippet({
|
||||
// Also, keep this code synced with the app code in Embed.tsx.
|
||||
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||
thread.post.uri,
|
||||
post.uri,
|
||||
)}" data-bluesky-cid="${escapeHtml(
|
||||
thread.post.cid,
|
||||
post.cid,
|
||||
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||
colorMode,
|
||||
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||
@@ -258,13 +250,13 @@ function Snippet({
|
||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||
: ''
|
||||
}</p>— ${escapeHtml(
|
||||
thread.post.author.displayName || thread.post.author.handle,
|
||||
post.author.displayName || post.author.handle,
|
||||
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
|
||||
thread.post.author.handle,
|
||||
post.author.handle,
|
||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||
niceDate(thread.post.indexedAt),
|
||||
niceDate(post.indexedAt),
|
||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||
}, [thread, colorMode])
|
||||
}, [post, colorMode])
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '../index.css'
|
||||
|
||||
import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
||||
import {type AtUriString, Client} from '@atproto/lex'
|
||||
import {h, render} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo.svg'
|
||||
@@ -8,14 +8,13 @@ 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')
|
||||
|
||||
const agent = new AtpAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
const client = new Client('https://public.api.bsky.app')
|
||||
|
||||
const uri = `at://${window.location.pathname.slice('/embed/'.length)}`
|
||||
if (!uri) {
|
||||
@@ -40,23 +39,25 @@ switch (colorMode) {
|
||||
break
|
||||
}
|
||||
|
||||
agent
|
||||
.getPostThread({
|
||||
uri,
|
||||
depth: 0,
|
||||
parentHeight: 0,
|
||||
client
|
||||
.call(app.bsky.feed.getPosts, {
|
||||
uris: [uri as AtUriString],
|
||||
})
|
||||
.then(({data}) => {
|
||||
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
|
||||
throw new Error('Expected a ThreadViewPost')
|
||||
.then(({posts}) => {
|
||||
const post = posts[0]
|
||||
|
||||
if (!post) {
|
||||
throw new Error('Post not found')
|
||||
}
|
||||
const pwiOptOut = !!data.thread.post.author.labels?.find(
|
||||
|
||||
const pwiOptOut = !!post.author.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
|
||||
if (pwiOptOut) {
|
||||
render(<PwiOptOut thread={data.thread} />, root)
|
||||
render(<PwiOptOut post={post} />, root)
|
||||
} else {
|
||||
render(<Post thread={data.thread} />, root)
|
||||
render(<Post post={post} />, root)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -64,8 +65,8 @@ agent
|
||||
render(<ErrorMessage />, root)
|
||||
})
|
||||
|
||||
function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
const href = `/profile/${thread.post.author.did}/post/${getRkey(thread.post)}`
|
||||
function PwiOptOut({post}: {post: app.bsky.feed.defs.PostView}) {
|
||||
const href = `/profile/${post.author.did}/post/${getRkey({uri: post.uri})}`
|
||||
return (
|
||||
<Container href={href}>
|
||||
<Link
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import {ValidationResult} from '@atproto/lexicon'
|
||||
|
||||
export * as profile from './profile'
|
||||
|
||||
/**
|
||||
* Fast type checking without full schema validation, for use with data we
|
||||
* trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
|
||||
* utils do not assert the type of the entire object, only the `$type` string.
|
||||
*
|
||||
* For full validation of the object schema, use the `validate` export from
|
||||
* this file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
|
||||
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function dangerousIsType<R extends {$type?: string}>(
|
||||
record: unknown,
|
||||
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
|
||||
): record is R {
|
||||
return identity(record)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully validates the object schema, which has a performance cost.
|
||||
*
|
||||
* For faster checks with data we trust, like that from our app view, use the
|
||||
* `dangerousIsType` export from this same file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
|
||||
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function validate<R extends {$type?: string}>(
|
||||
record: unknown,
|
||||
validator: (v: unknown) => ValidationResult<R>,
|
||||
): record is R {
|
||||
return validator(record).success
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
|
||||
|
||||
/**
|
||||
* Matches any profile view exported by our SDK
|
||||
*/
|
||||
export type AnyProfileView =
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileView
|
||||
| AppBskyActorDefs.ProfileViewDetailed
|
||||
| ChatBskyActorDefs.ProfileViewBasic
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* This file is a copy of what exists in the social-app
|
||||
*/
|
||||
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyLabelerDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
export type Embed =
|
||||
| {
|
||||
type: 'post'
|
||||
view: AppBskyEmbedRecord.ViewRecord
|
||||
}
|
||||
| {
|
||||
type: 'post_not_found'
|
||||
view: AppBskyEmbedRecord.ViewNotFound
|
||||
}
|
||||
| {
|
||||
type: 'post_blocked'
|
||||
view: AppBskyEmbedRecord.ViewBlocked
|
||||
}
|
||||
| {
|
||||
type: 'post_detached'
|
||||
view: AppBskyEmbedRecord.ViewDetached
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
view: AppBskyFeedDefs.GeneratorView
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
view: AppBskyGraphDefs.ListView
|
||||
}
|
||||
| {
|
||||
type: 'labeler'
|
||||
view: AppBskyLabelerDefs.LabelerView
|
||||
}
|
||||
| {
|
||||
type: 'starter_pack'
|
||||
view: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}
|
||||
| {
|
||||
type: 'images'
|
||||
view: AppBskyEmbedImages.View
|
||||
}
|
||||
| {
|
||||
type: 'link'
|
||||
view: AppBskyEmbedExternal.View
|
||||
}
|
||||
| {
|
||||
type: 'video'
|
||||
view: AppBskyEmbedVideo.View
|
||||
}
|
||||
| {
|
||||
type: 'post_with_media'
|
||||
view: Embed
|
||||
media: Embed
|
||||
}
|
||||
| {
|
||||
type: 'unknown'
|
||||
view: null
|
||||
}
|
||||
|
||||
export type EmbedType<T extends Embed['type']> = Extract<Embed, {type: T}>
|
||||
|
||||
export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
|
||||
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||
return {
|
||||
type: 'post',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(record)) {
|
||||
return {
|
||||
type: 'post_not_found',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(record)) {
|
||||
return {
|
||||
type: 'post_blocked',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewDetached(record)) {
|
||||
return {
|
||||
type: 'post_detached',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isGeneratorView(record)) {
|
||||
return {
|
||||
type: 'feed',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isListView(record)) {
|
||||
return {
|
||||
type: 'list',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyLabelerDefs.isLabelerView(record)) {
|
||||
return {
|
||||
type: 'labeler',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
|
||||
return {
|
||||
type: 'starter_pack',
|
||||
view: record,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'unknown',
|
||||
view: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
return {
|
||||
type: 'images',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedExternal.isView(embed)) {
|
||||
return {
|
||||
type: 'link',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedVideo.isView(embed)) {
|
||||
return {
|
||||
type: 'video',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isView(embed)) {
|
||||
return parseEmbedRecordView(embed)
|
||||
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return {
|
||||
type: 'post_with_media',
|
||||
view: parseEmbedRecordView(embed.record),
|
||||
media: parseEmbed(embed.media),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'unknown',
|
||||
view: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
|
||||
export function getRkey({uri}: {uri: string}): string {
|
||||
const at = new AtUri(uri)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as bsky from '../types/bsky'
|
||||
import * as app from '../lexicons/app'
|
||||
|
||||
export type VerificationState = {
|
||||
role: 'default' | 'verifier'
|
||||
@@ -8,7 +8,7 @@ export type VerificationState = {
|
||||
export function getVerificationState({
|
||||
profile,
|
||||
}: {
|
||||
profile?: bsky.profile.AnyProfileView
|
||||
profile?: app.bsky.actor.defs.ProfileViewBasic
|
||||
}): VerificationState {
|
||||
if (!profile || !profile.verification) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user