BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 177 lines 5.4 kB view raw
1import { parseFeedResponse } from "$/lib/feeds"; 2import { isReplyItem } from "$/lib/feeds/type-guards"; 3import { asModerationLabels } from "$/lib/moderation"; 4import type { 5 ActorListResponse, 6 FeedResponse, 7 FeedViewPost, 8 ProfileLookupResult, 9 ProfileUnavailableReason, 10 ProfileViewBasic, 11 ProfileViewDetailed, 12 RichTextFacet, 13} from "$/lib/types"; 14import { asArray, asRecord, optionalNumber, optionalString } from "./type-guards"; 15 16export type ProfileTab = "posts" | "replies" | "media" | "likes" | "context"; 17 18export function buildProfileRoute(actor?: string | null) { 19 const trimmed = actor?.trim(); 20 if (!trimmed) { 21 return "/profile"; 22 } 23 24 return `/profile/${encodeURIComponent(trimmed)}`; 25} 26 27export function decodeProfileRouteActor(value?: string | null) { 28 if (!value) { 29 return null; 30 } 31 32 try { 33 return decodeURIComponent(value); 34 } catch { 35 return value; 36 } 37} 38 39export function getProfileRouteActor(actor: { did: string; handle?: string | null }) { 40 return actor.handle?.trim() || actor.did; 41} 42 43function parseProfile(value: unknown): ProfileViewDetailed { 44 const record = asRecord(value); 45 if (!record || typeof record.did !== "string" || typeof record.handle !== "string") { 46 throw new Error("profile payload is invalid"); 47 } 48 49 const pinnedPost = asRecord(record.pinnedPost); 50 51 return { 52 avatar: optionalString(record.avatar), 53 banner: optionalString(record.banner), 54 createdAt: optionalString(record.createdAt), 55 description: optionalString(record.description), 56 descriptionFacets: parseRichTextFacets(record.descriptionFacets), 57 did: record.did, 58 displayName: optionalString(record.displayName), 59 followersCount: optionalNumber(record.followersCount), 60 followsCount: optionalNumber(record.followsCount), 61 handle: record.handle, 62 indexedAt: optionalString(record.indexedAt), 63 labels: asModerationLabels(record), 64 pinnedPost: pinnedPost && typeof pinnedPost.uri === "string" 65 ? { cid: optionalString(pinnedPost.cid), uri: pinnedPost.uri } 66 : null, 67 postsCount: optionalNumber(record.postsCount), 68 pronouns: optionalString(record.pronouns), 69 viewer: parseProfileViewer(record.viewer), 70 website: optionalString(record.website), 71 }; 72} 73 74function parseRichTextFacets(value: unknown): RichTextFacet[] | null { 75 const facets = asArray(value); 76 return (facets as RichTextFacet[] | null) || null; 77} 78 79export function parseProfileResult(value: unknown): ProfileLookupResult { 80 const record = asRecord(value); 81 if (!record || record.status === "available" && !asRecord(record.profile)) { 82 throw new Error("profile result payload is invalid"); 83 } 84 85 if (record.status === "available") { 86 return { status: "available", profile: parseProfile(record.profile) }; 87 } 88 89 if ( 90 record.status !== "unavailable" 91 || typeof record.requestedActor !== "string" 92 || typeof record.message !== "string" 93 || !isProfileUnavailableReason(record.reason) 94 ) { 95 throw new Error("profile result payload is invalid"); 96 } 97 98 return { 99 status: "unavailable", 100 requestedActor: record.requestedActor, 101 did: optionalString(record.did), 102 handle: optionalString(record.handle), 103 reason: record.reason, 104 message: record.message, 105 }; 106} 107 108export function parseProfileFeed(value: unknown): FeedResponse { 109 return parseFeedResponse(value); 110} 111 112export function parseActorList(value: unknown, listKey: "followers" | "follows"): ActorListResponse { 113 const record = asRecord(value); 114 if (!record) { 115 throw new Error("actor list payload is invalid"); 116 } 117 118 const rawActors = asArray(record[listKey]) ?? []; 119 const actors = rawActors.map((item) => parseProfileBasic(item)).filter(Boolean) as ProfileViewBasic[]; 120 121 return { cursor: optionalString(record.cursor), actors }; 122} 123 124function parseProfileBasic(value: unknown): ProfileViewBasic | null { 125 const record = asRecord(value); 126 if (!record || typeof record.did !== "string" || typeof record.handle !== "string") { 127 return null; 128 } 129 130 return { 131 did: record.did, 132 handle: record.handle, 133 displayName: optionalString(record.displayName), 134 avatar: optionalString(record.avatar), 135 description: optionalString(record.description), 136 labels: asModerationLabels(record), 137 viewer: asRecord(record.viewer) ? { following: optionalString(asRecord(record.viewer)?.following) } : null, 138 }; 139} 140 141export function filterProfileFeed(items: FeedViewPost[], tab: ProfileTab) { 142 switch (tab) { 143 case "posts": { 144 return items.filter((item) => !isReplyItem(item)); 145 } 146 case "replies": { 147 return items.filter((item) => isReplyItem(item)); 148 } 149 case "media": { 150 return items.filter((item) => !!item.post.embed); 151 } 152 case "context": { 153 return []; 154 } 155 default: { 156 return items; 157 } 158 } 159} 160 161function parseProfileViewer(value: unknown) { 162 const record = asRecord(value); 163 if (!record) { 164 return null; 165 } 166 167 return { 168 blockedBy: typeof record.blockedBy === "boolean" ? record.blockedBy : null, 169 followedBy: optionalString(record.followedBy), 170 following: optionalString(record.following), 171 muted: typeof record.muted === "boolean" ? record.muted : null, 172 }; 173} 174 175function isProfileUnavailableReason(value: unknown): value is ProfileUnavailableReason { 176 return value === "notFound" || value === "suspended" || value === "deactivated" || value === "unavailable"; 177}