forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useMemo} from 'react'
2import {
3 type $Typed,
4 type AppBskyActorDefs,
5 AppBskyEmbedExternal,
6 AtUri,
7} from '@atproto/api'
8import {isAfter, parseISO} from 'date-fns'
9
10import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
11import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config'
12import {useTickEveryMinute} from '#/state/shell'
13import type * as bsky from '#/types/bsky'
14
15export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
16 const shadowed = useMaybeProfileShadow(actor)
17 const tick = useTickEveryMinute()
18 const config = useLiveNowConfig()
19
20 return useMemo(() => {
21 void tick // revalidate every minute
22
23 if (shadowed && 'status' in shadowed && shadowed.status) {
24 const isValid = isStatusValidForViewers(shadowed.status, config)
25 const isDisabled = shadowed.status.isDisabled || false
26 const isActive = isStatusStillActive(shadowed.status.expiresAt)
27 if (isValid && !isDisabled && isActive) {
28 return {
29 uri: shadowed.status.uri,
30 cid: shadowed.status.cid,
31 isDisabled: false,
32 isActive: true,
33 status: 'app.bsky.actor.status#live',
34 embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
35 expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
36 record: shadowed.status.record,
37 } satisfies AppBskyActorDefs.StatusView
38 }
39 return {
40 uri: shadowed.status.uri,
41 cid: shadowed.status.cid,
42 isDisabled,
43 isActive: false,
44 status: 'app.bsky.actor.status#live',
45 embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
46 expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
47 record: shadowed.status.record,
48 } satisfies AppBskyActorDefs.StatusView
49 } else {
50 return {
51 status: '',
52 isDisabled: false,
53 isActive: false,
54 record: {},
55 } satisfies AppBskyActorDefs.StatusView
56 }
57 }, [shadowed, config, tick])
58}
59
60export function isStatusStillActive(timeStr: string | undefined) {
61 if (!timeStr) return false
62 const now = new Date()
63 const expiry = parseISO(timeStr)
64
65 return isAfter(expiry, now)
66}
67
68/**
69 * Validates whether the live status is valid for display in the app. Does NOT
70 * validate if the status is valid for the acting user e.g. as they go live.
71 */
72export function isStatusValidForViewers(
73 status: AppBskyActorDefs.StatusView,
74 config: LiveNowConfig,
75) {
76 if (status.status !== 'app.bsky.actor.status#live') return false
77 if (!status.uri) return false // should not happen, just backwards compat
78 try {
79 const {host: liveDid} = new AtUri(status.uri)
80 if (AppBskyEmbedExternal.isView(status.embed)) {
81 const url = new URL(status.embed.external.uri)
82 const exception = config.allowedHostsExceptionsByDid.get(liveDid)
83 const isValidException = exception ? exception.has(url.hostname) : false
84 const isValidForAnyone = config.defaultAllowedHosts.has(url.hostname)
85 return isValidException || isValidForAnyone
86 } else {
87 return false
88 }
89 } catch {
90 return false
91 }
92}