diff --git a/bskyembed/package.json b/bskyembed/package.json
index e269b1c953..c47c4218da 100644
--- a/bskyembed/package.json
+++ b/bskyembed/package.json
@@ -6,24 +6,25 @@
"dev": "vite",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
- "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src"
+ "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
- "@atproto/api": "0.13.6",
- "@preact/preset-vite": "^2.8.2",
- "@vitejs/plugin-legacy": "^5.3.2",
- "preact": "^10.4.8",
- "terser": "^5.30.3"
+ "@atproto/api": "^0.15.25",
+ "preact": "^10.4.8"
},
"devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@vitejs/plugin-legacy": "^7.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.19.0",
"eslint-config-preact": "^1.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
- "typescript": "^5.5.4",
- "vite": "^5.2.8",
- "vite-tsconfig-paths": "^4.3.2"
+ "terser": "^5.43.1",
+ "typescript": "^5.8.3",
+ "vite": "^7.0.4",
+ "vite-tsconfig-paths": "^5.1.4"
}
}
diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx
index 8e142a25be..bafc497aea 100644
--- a/bskyembed/src/components/container.tsx
+++ b/bskyembed/src/components/container.tsx
@@ -49,7 +49,7 @@ export function Container({
}
}}>
{href && }
-
{children}
+ {children}
)
}
diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx
index 428782b642..52618a89da 100644
--- a/bskyembed/src/components/embed.tsx
+++ b/bskyembed/src/components/embed.tsx
@@ -17,8 +17,11 @@ import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
import starterPackIcon from '../../assets/starterPack.svg'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
-import {getRkey} from '../utils'
+import * as bsky from '../types/bsky'
+import {getRkey} from '../util/rkey'
+import {getVerificationState} from '../util/verification-state'
import {Link} from './link'
+import {VerificationCheck} from './verification-check'
export function Embed({
content,
@@ -75,23 +78,35 @@ export function Embed({
CONTENT_LABELS.includes(label.val),
)
+ const verification = getVerificationState({profile: record.author})
+
return (
-
+
-
- {record.author.displayName}
-
+
+
+ {record.author.displayName?.trim() || record.author.handle}
+
+ {verification.isVerified && (
+
+ )}
+
@{record.author.handle}
-
-
+
+
{text &&
{text}
}
{record.embeds?.map(embed => (
@@ -404,7 +419,12 @@ function StarterPackEmbed({
}: {
content: AppBskyGraphDefs.StarterPackViewBasic
}) {
- if (!AppBskyGraphStarterpack.isRecord(content.record)) {
+ if (
+ !bsky.dangerousIsType
(
+ content.record,
+ AppBskyGraphStarterpack.isRecord,
+ )
+ ) {
return null
}
@@ -443,7 +463,9 @@ function StarterPackEmbed({
}
// from #/lib/strings/starter-pack.ts
-function getStarterPackImage(starterPack: AppBskyGraphDefs.StarterPackView) {
+function getStarterPackImage(
+ starterPack: AppBskyGraphDefs.StarterPackViewBasic,
+) {
const rkey = getRkey({uri: starterPack.uri})
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
}
diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx
index 6ecac57965..d216ce0e54 100644
--- a/bskyembed/src/components/post.tsx
+++ b/bskyembed/src/components/post.tsx
@@ -11,10 +11,15 @@ import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import {CONTENT_LABELS} from '../labels'
-import {getRkey, niceDate, prettyNumber} from '../utils'
+import * as bsky from '../types/bsky'
+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 {
thread: AppBskyFeedDefs.ThreadViewPost
@@ -28,16 +33,25 @@ export function Post({thread}: Props) {
)
let record: AppBskyFeedPost.Record | null = null
- if (AppBskyFeedPost.isRecord(post.record)) {
+ if (
+ bsky.dangerousIsType(
+ post.record,
+ AppBskyFeedPost.isRecord,
+ )
+ ) {
record = post.record
}
+ const verification = getVerificationState({profile: post.author})
+
const href = `/profile/${post.author.did}/post/${getRkey(post)}`
return (
-
-
+
+
-
+
+
+
+ {post.author.displayName?.trim() || post.author.handle}
+
+ {verification.isVerified && (
+
+ )}
+
-
{post.author.displayName}
-
-
-
@{post.author.handle}
+ className="block text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
+ @{post.author.handle}
-
@@ -133,7 +155,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
:
+}
+
+export function VerifiedCheck({size, className}: IconProps) {
+ return (
+
+ )
+}
+
+export function VerifierCheck({size, className}: IconProps) {
+ return (
+
+ )
+}
diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx
index 880b713372..bb650241f4 100644
--- a/bskyembed/src/screens/landing.tsx
+++ b/bskyembed/src/screens/landing.tsx
@@ -14,9 +14,11 @@ import {
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
-import {niceDate} from '../utils'
+import * as bsky from '../types/bsky'
+import {niceDate} from '../util/nice-date'
-const DEFAULT_POST = 'https://bsky.app/profile/emilyliu.me/post/3jzn6g7ixgq2y'
+const DEFAULT_POST =
+ 'https://bsky.app/profile/did:plc:vjug55kidv6sye7ykr5faxxn/post/3jzn6g7ixgq2y'
const DEFAULT_URI =
'at://did:plc:vjug55kidv6sye7ykr5faxxn/app.bsky.feed.post/3jzn6g7ixgq2y'
@@ -222,7 +224,12 @@ function Snippet({
const snippet = useMemo(() => {
const record = thread.post.record
- if (!AppBskyFeedPost.isRecord(record)) {
+ if (
+ !bsky.dangerousIsType
(
+ record,
+ AppBskyFeedPost.isRecord,
+ )
+ ) {
return ''
}
diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx
index 4cd72b69bc..83914f66f3 100644
--- a/bskyembed/src/screens/post.tsx
+++ b/bskyembed/src/screens/post.tsx
@@ -8,7 +8,7 @@ import {applyTheme, initSystemColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
-import {getRkey} from '../utils'
+import {getRkey} from '../util/rkey'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
diff --git a/bskyembed/src/types/bsky/index.ts b/bskyembed/src/types/bsky/index.ts
new file mode 100644
index 0000000000..c462cdf654
--- /dev/null
+++ b/bskyembed/src/types/bsky/index.ts
@@ -0,0 +1,49 @@
+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(item, AppBskyFeedPost.isRecord)) {
+ * // `item` has type `$Typed` here
+ * }
+ * ```
+ */
+export function dangerousIsType(
+ record: unknown,
+ identity: (v: V) => v is V & {$type: NonNullable},
+): 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` here
+ * }
+ * ```
+ */
+export function validate(
+ record: unknown,
+ validator: (v: unknown) => ValidationResult,
+): record is R {
+ return validator(record).success
+}
diff --git a/bskyembed/src/types/bsky/profile.ts b/bskyembed/src/types/bsky/profile.ts
new file mode 100644
index 0000000000..12c8146ae1
--- /dev/null
+++ b/bskyembed/src/types/bsky/profile.ts
@@ -0,0 +1,10 @@
+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
diff --git a/bskyembed/src/util/nice-date.ts b/bskyembed/src/util/nice-date.ts
new file mode 100644
index 0000000000..016c97a694
--- /dev/null
+++ b/bskyembed/src/util/nice-date.ts
@@ -0,0 +1,11 @@
+export function niceDate(date: number | string | Date) {
+ const d = new Date(date)
+ return `${d.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })} at ${d.toLocaleTimeString(undefined, {
+ hour: 'numeric',
+ minute: '2-digit',
+ })}`
+}
diff --git a/bskyembed/src/util/parse-embed.ts b/bskyembed/src/util/parse-embed.ts
new file mode 100644
index 0000000000..97c3dc33e7
--- /dev/null
+++ b/bskyembed/src/util/parse-embed.ts
@@ -0,0 +1,152 @@
+/**
+ * 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 = Extract