alternative tangled frontend (extremely wip)

feat: more lexicons and abstractions

serenity e0de3231 6ae320bc

+73 -27
+43 -3
src/lib/queries/get-avatar.ts
··· 1 1 import { err, ok, Result } from "@/lib/result"; 2 + import { 3 + AppBskyActorProfile, 4 + appBskyActorProfileSchema, 5 + } from "@/lib/types/lexicons/app/bsky/actor/profile"; 2 6 import { comAtprotoRepoGetRecordOutputSchema } from "@/lib/types/lexicons/com/atproto/repo/getRecord"; 3 - import { shTangledActorProfileSchema } from "@/lib/types/lexicons/sh/tangled/actor/profile"; 7 + import { 8 + ShTangledActorProfile, 9 + shTangledActorProfileSchema, 10 + } from "@/lib/types/lexicons/sh/tangled/actor/profile"; 4 11 import z from "zod"; 5 12 6 13 export const getAvatar = async ({ ··· 39 46 }: { 40 47 did: string; 41 48 repoUrl: string; 42 - }): Promise<Result<z.infer<typeof shTangledActorProfileSchema>, unknown>> => { 49 + }): Promise<Result<ShTangledActorProfile, unknown>> => { 43 50 const req = new Request( 44 51 `${repoUrl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=sh.tangled.actor.profile&rkey=self`, 45 52 ); ··· 60 67 61 68 if (!tangledProfile.avatar) 62 69 return err( 63 - "Avatar field on Tangled profile record. You should fall back to Bluesky's records.", 70 + "No `avatar` field on Tangled profile record. You should fall back to Bluesky's records.", 64 71 ); 65 72 66 73 return ok(tangledProfile); 67 74 }; 75 + 76 + const getBskyAvatar = async ({ 77 + did, 78 + repoUrl, 79 + }: { 80 + did: string; 81 + repoUrl: string; 82 + }): Promise<Result<AppBskyActorProfile, unknown>> => { 83 + const req = new Request( 84 + `${repoUrl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self`, 85 + ); 86 + const res = await fetch(req); 87 + const data: unknown = await res.json(); 88 + 89 + const { 90 + success, 91 + error, 92 + data: parseData, 93 + } = comAtprotoRepoGetRecordOutputSchema( 94 + appBskyActorProfileSchema, 95 + ).safeParse(data); 96 + 97 + if (!success) return err(error); 98 + 99 + const { value: bskyProfile } = parseData; 100 + 101 + if (!bskyProfile.avatar) 102 + return err( 103 + "No `avatar` field on Bluesky profile record. Profile likely has no avatar on Bluesky.", 104 + ); 105 + 106 + return ok(bskyProfile); 107 + };
+17
src/lib/types/lexicons/app/bsky/actor/profile.ts
··· 1 + import { blobSchema } from "@/lib/types/lexicons/shared/blob"; 2 + import z from "zod"; 3 + 4 + export const appBskyActorProfileSchema = z.object({ 5 + displayName: z.string().max(640).optional(), 6 + description: z.string().max(2560).optional(), 7 + pronouns: z.string().max(200).optional(), 8 + website: z.url().optional(), 9 + avatar: blobSchema.optional(), 10 + banner: blobSchema.optional(), 11 + labels: z.unknown().optional(), 12 + joinedViaStarterPack: z.unknown().optional(), 13 + pinnedPost: z.unknown().optional(), 14 + createdAt: z.iso.datetime().optional(), 15 + }); 16 + 17 + export type AppBskyActorProfile = z.infer<typeof appBskyActorProfileSchema>;
+3 -24
src/lib/types/lexicons/sh/tangled/actor/profile.ts
··· 1 + import { blobSchema } from "@/lib/types/lexicons/shared/blob"; 1 2 import { z } from "zod/v4"; 2 3 3 - /** 4 - * Zod v4 schema for the ATProto lexicon: sh.tangled.actor.profile 5 - * 6 - * A declaration of a Tangled account profile. 7 - * Record key: "literal:self" 8 - */ 9 - 10 - const blobSchema = z.object({ 11 - $type: z.literal("blob"), 12 - ref: z.object({ 13 - $link: z.string(), 14 - }), 15 - mimeType: z.enum(["image/png", "image/jpeg"]), 16 - size: z.number().int().max(1000000), 17 - }); 18 - 19 4 const _statsEnum = z.enum([ 20 5 "merged-pull-request-count", 21 6 "closed-pull-request-count", ··· 28 13 29 14 export const shTangledActorProfileSchema = z.object({ 30 15 $type: z.literal("sh.tangled.actor.profile"), 31 - 32 16 avatar: blobSchema 33 17 .describe( 34 18 "Small image to be displayed next to posts from account. AKA, 'profile picture'", 35 19 ) 36 20 .optional(), 37 - 38 21 description: z 39 22 .string() 40 23 .max(2560) 41 24 .describe("Free-form profile description text.") 42 25 .optional(), 43 - 44 26 links: z 45 27 .array( 46 28 z ··· 52 34 .min(0) 53 35 .max(5) 54 36 .optional(), 55 - 56 37 stats: z 57 38 .array(_statsEnum.describe("Vanity stats.")) 58 39 .min(0) 59 40 .max(2) 60 41 .optional(), 61 - 62 42 bluesky: z.boolean().describe("Include link to this account on Bluesky."), 63 - 64 43 location: z 65 44 .string() 66 45 .max(400) 67 46 .describe("Free-form location text.") 68 47 .optional(), 69 - 70 48 pinnedRepositories: z 71 49 .array(z.string().describe("AT-URI reference to a repository.")) 72 50 .min(0) 73 51 .max(6) 74 52 .describe("Any ATURI, it is up to appviews to validate these fields.") 75 53 .optional(), 76 - 77 54 pronouns: z 78 55 .string() 79 56 .max(40) 80 57 .describe("Preferred gender pronouns.") 81 58 .optional(), 82 59 }); 60 + 61 + export type ShTangledActorProfile = z.infer<typeof shTangledActorProfileSchema>;
+10
src/lib/types/lexicons/shared/blob.ts
··· 1 + import { z } from "zod/v4"; 2 + 3 + export const blobSchema = z.object({ 4 + $type: z.literal("blob"), 5 + ref: z.object({ 6 + $link: z.string(), 7 + }), 8 + mimeType: z.enum(["image/png", "image/jpeg"]), 9 + size: z.number().int().max(1000000), 10 + });