Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { awaitIndexedOrSaved } from "$lib/api/awaitRecord";import type { BobbinContext } from "$lib/api/client";import { httpStatusFor } from "$lib/api/load";import { PROFILE_COLLECTION, putProfile, uploadProfileAvatar } from "$lib/api/profile";import { getProfile, type ProfileRecord } from "$lib/api/records";
type ProfileLink = NonNullable<ProfileRecord["links"]>[number];
export const normalizeProfileLink = (raw: string): ProfileLink | undefined => { const trimmed = raw.trim(); if (!trimmed) return undefined;
let url: URL; try { url = new URL(/^[a-z][a-z\d+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`); } catch { throw new Error("Links must be valid URLs."); } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error("Links must use http or https."); } return url.toString() as ProfileLink;};
export interface ProfileForm { description: string; pronouns: string; location: string; links: string[]; bluesky: boolean; isOrganization: boolean;}
export const LINK_SLOTS = 3;
export const loadProfile = (ctx: BobbinContext, did: string): Promise<ProfileRecord | null> => getProfile(ctx, did) .then((view) => view.value) .catch((cause) => { if (httpStatusFor(cause) === 404) return null; throw cause; });
export const formFromProfile = (profile: ProfileRecord | null): ProfileForm => { const links: string[] = [...(profile?.links ?? [])]; while (links.length < LINK_SLOTS) links.push(""); return { description: profile?.description ?? "", pronouns: profile?.pronouns ?? "", location: profile?.location ?? "", links, bluesky: profile?.bluesky ?? false, isOrganization: profile?.isOrganization ?? false };};
export const profileFromForm = ( form: ProfileForm, current: ProfileRecord | null): ProfileRecord => { const description = form.description.trim(); const pronouns = form.pronouns.trim(); const location = form.location.trim(); const links = form.links.map(normalizeProfileLink).filter((link) => link !== undefined); const { description: _description, pronouns: _pronouns, location: _location, links: _links, isOrganization: _isOrganization, ...rest } = current ?? {}; return { ...rest, $type: PROFILE_COLLECTION, bluesky: form.bluesky, ...(form.isOrganization ? { isOrganization: true } : {}), ...(description ? { description } : {}), ...(pronouns ? { pronouns } : {}), ...(location ? { location } : {}), ...(links.length > 0 ? { links } : {}) };};
export const writeProfile = async ( ctx: BobbinContext, agent: OAuthUserAgent, record: ProfileRecord): Promise<ProfileRecord> => { const written = await putProfile(agent, record); await awaitIndexedOrSaved(ctx, written); return record;};
export const saveAvatar = async ( ctx: BobbinContext, agent: OAuthUserAgent, image: Blob | null, current: ProfileRecord | null): Promise<ProfileRecord> => { const avatar = image ? await uploadProfileAvatar(agent, image) : undefined; const { avatar: _avatar, ...rest } = current ?? {}; const record: ProfileRecord = { ...rest, $type: PROFILE_COLLECTION, bluesky: current?.bluesky ?? false, ...(avatar ? { avatar } : {}) }; return writeProfile(ctx, agent, record);};