about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-05-10 00:06:06 +0300
committerGitHub <noreply@github.com>2025-05-10 00:06:06 +0300
commita0bd8042621e108f47e09dd096cf0d73fe1cee53 (patch)
tree0cc120c864ae8fea7f513ff242a1097ece0f1b8b /src/lib
parent2e80fa3dac4d869640f5bce8ad43eb401c8e3141 (diff)
downloadvoidsky-a0bd8042621e108f47e09dd096cf0d73fe1cee53.tar.zst
Live (#8354)
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/actor-status.ts51
-rw-r--r--src/lib/api/resolve.ts14
-rw-r--r--src/lib/constants.ts1
-rw-r--r--src/lib/strings/url-helpers.ts30
4 files changed, 89 insertions, 7 deletions
diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts
new file mode 100644
index 000000000..30921a88a
--- /dev/null
+++ b/src/lib/actor-status.ts
@@ -0,0 +1,51 @@
+import {useMemo} from 'react'
+import {
+  type $Typed,
+  type AppBskyActorDefs,
+  type AppBskyEmbedExternal,
+} from '@atproto/api'
+import {isAfter, parseISO} from 'date-fns'
+
+import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
+import {useTickEveryMinute} from '#/state/shell'
+import {temp__canBeLive, temp__isStatusValid} from '#/components/live/temp'
+import type * as bsky from '#/types/bsky'
+
+export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
+  const shadowed = useMaybeProfileShadow(actor)
+  const tick = useTickEveryMinute()
+  return useMemo(() => {
+    tick! // revalidate every minute
+
+    if (
+      shadowed &&
+      temp__canBeLive(shadowed) &&
+      'status' in shadowed &&
+      shadowed.status &&
+      temp__isStatusValid(shadowed.status) &&
+      isStatusStillActive(shadowed.status.expiresAt)
+    ) {
+      return {
+        isActive: true,
+        status: 'app.bsky.actor.status#live',
+        embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
+        expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
+        record: shadowed.status.record,
+      } satisfies AppBskyActorDefs.StatusView
+    } else {
+      return {
+        status: '',
+        isActive: false,
+        record: {},
+      } satisfies AppBskyActorDefs.StatusView
+    }
+  }, [shadowed, tick])
+}
+
+export function isStatusStillActive(timeStr: string | undefined) {
+  if (!timeStr) return false
+  const now = new Date()
+  const expiry = parseISO(timeStr)
+
+  return isAfter(expiry, now)
+}
diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts
index 371062350..93d16ff0c 100644
--- a/src/lib/api/resolve.ts
+++ b/src/lib/api/resolve.ts
@@ -1,10 +1,10 @@
 import {
-  AppBskyFeedDefs,
-  AppBskyGraphDefs,
-  ComAtprotoRepoStrongRef,
+  type AppBskyFeedDefs,
+  type AppBskyGraphDefs,
+  type ComAtprotoRepoStrongRef,
 } from '@atproto/api'
 import {AtUri} from '@atproto/api'
-import {BskyAgent} from '@atproto/api'
+import {type BskyAgent} from '@atproto/api'
 
 import {POST_IMG_MAX} from '#/lib/constants'
 import {getLinkMeta} from '#/lib/link-meta/link-meta'
@@ -22,9 +22,9 @@ import {
   isBskyStartUrl,
   isShortLink,
 } from '#/lib/strings/url-helpers'
-import {ComposerImage} from '#/state/gallery'
+import {type ComposerImage} from '#/state/gallery'
 import {createComposerImage} from '#/state/gallery'
-import {Gif} from '#/state/queries/tenor'
+import {type Gif} from '#/state/queries/tenor'
 import {createGIFDescription} from '../gif-alt-text'
 import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
 
@@ -213,7 +213,7 @@ async function resolveExternal(
   }
 }
 
-async function imageToThumb(
+export async function imageToThumb(
   imageUri: string,
 ): Promise<ComposerImage | undefined> {
   try {
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index bb98f9fc8..dca03647a 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -31,6 +31,7 @@ export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
   'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
   'did:plc:vjug55kidv6sye7ykr5faxxn': true, // emilyliu.me
   'did:plc:tgqseeot47ymot4zro244fj3': true, // iwsmith.bsky.social
+  'did:plc:2dzyut5lxna5ljiaasgeuffz': true, // mrnuma.bsky.social
 }
 
 const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index 20c3fabbc..ad194714a 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -372,3 +372,33 @@ export function getServiceAuthAudFromUrl(url: string | URL): string | null {
   }
   return `did:web:${hostname}`
 }
+
+// passes URL.parse, and has a TLD etc
+export function definitelyUrl(maybeUrl: string) {
+  try {
+    if (maybeUrl.endsWith('.')) return null
+
+    // Prepend 'https://' if the input doesn't start with a protocol
+    if (!maybeUrl.startsWith('https://') && !maybeUrl.startsWith('http://')) {
+      maybeUrl = 'https://' + maybeUrl
+    }
+
+    const url = new URL(maybeUrl)
+
+    // Extract the hostname and split it into labels
+    const hostname = url.hostname
+    const labels = hostname.split('.')
+
+    // Ensure there are at least two labels (e.g., 'example' and 'com')
+    if (labels.length < 2) return null
+
+    const tld = labels[labels.length - 1]
+
+    // Check that the TLD is at least two characters long and contains only letters
+    if (!/^[a-z]{2,}$/i.test(tld)) return null
+
+    return url.toString()
+  } catch {
+    return null
+  }
+}