Something went wrong. Try again.
tangled on ur phone
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import Foundationimport Observation
/// Resolves and caches handles and avatars for DIDs, so any view can show/// a person given only their DID.@MainActor@Observablefinal class ActorCache { struct Actor: Hashable { var handle: String? var avatar: URL? var pds: URL? }
private(set) var actors: [String: Actor] = [:] /// "owner/name" for repo DIDs we've looked up. private(set) var repoNames: [String: String] = [:] @ObservationIgnored private var inFlight: Set<String> = [] @ObservationIgnored private var bobbin = Bobbin()
func handle(for did: String) -> String? { actors[did]?.handle } func avatar(for did: String) -> URL? { actors[did]?.avatar }
/// Seeds the cache from a view that already carries handle and avatar. func remember(_ profile: ProfileViewBasic) { var actor = actors[profile.did] ?? Actor() actor.handle = profile.handle actors[profile.did] = actor }
func remember(_ profile: ProfileViewDetailed) { remember(ProfileViewBasic(did: profile.did, handle: profile.handle, avatar: profile.avatar)) }
func load(_ did: String) async { guard did.hasPrefix("did:"), !inFlight.contains(did) else { return } if let existing = actors[did], existing.handle != nil, existing.pds != nil, existing.avatar != nil { return } inFlight.insert(did) defer { inFlight.remove(did) }
guard let document = try? await IdentityResolver.shared.document(for: did) else { return } var actor = actors[did] ?? Actor() actor.handle = actor.handle ?? document.handle actor.pds = document.pds actors[did] = actor
if actor.avatar == nil { actors[did]?.avatar = await avatarURL(for: did) } }
/// Avatars are served from Bluesky's CDN: the Tangled profile's avatar /// blob if it has one, otherwise the Bluesky profile picture. private func avatarURL(for did: String) async -> URL? { if let profile = try? await bobbin.profile(did: did), let cid = profile.value.avatar?.cidString { return URL(string: "https://cdn.bsky.app/img/avatar_thumbnail/plain/\(did)/\(cid)@jpeg") } struct BskyProfile: Decodable { var avatar: String? } var params = XRPCParams() params.add("actor", did) let client = XRPCClient(service: URL(string: "https://public.api.bsky.app")!) let profile: BskyProfile? = try? await client.query("app.bsky.actor.getProfile", params) // Ask for the small JPEG thumbnail rather than the full-size image. return profile?.avatar .map { $0.replacingOccurrences(of: "/img/avatar/plain/", with: "/img/avatar_thumbnail/plain/") } .map { $0.contains("@") ? $0 : $0 + "@jpeg" } .flatMap(URL.init(string:)) }
func loadRepoName(_ repoDID: String) async { guard repoNames[repoDID] == nil, !inFlight.contains(repoDID) else { return } inFlight.insert(repoDID) defer { inFlight.remove(repoDID) } guard let record = try? await bobbin.repo(repoDID: repoDID) else { return } let summary = RepoSummary(record: record) await load(summary.ownerDID) let owner = handle(for: summary.ownerDID) ?? summary.ownerDID repoNames[repoDID] = "\(owner)/\(summary.name)" }
/// Drops everything cached from the previous API service. func reset(service: URL) { bobbin = Bobbin(service: service) actors = [:] repoNames = [:] }}