import {useCallback, useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQuery} from '@tanstack/react-query' import {getLinkMeta} from '#/lib/link-meta/link-meta' import {cleanError} from '#/lib/strings/errors' import {toNiceDomain} from '#/lib/strings/url-helpers' import {definitelyUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLiveNowConfig} from '#/state/service-config' import {useAgent, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {atoms as a, ios, native, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Select from '#/components/Select' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {useUpsertLiveStatusMutation} from './queries' import {displayDuration, useDebouncedValue} from './utils' export function GoLiveDialog({ control, profile, }: { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView }) { return ( ) } // Possible durations: max 4 hours, 5 minute intervals const DURATIONS = Array.from({length: (4 * 60) / 5}).map((_, i) => (i + 1) * 5) function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { const control = Dialog.useDialogContext() const {_, i18n} = useLingui() const t = useTheme() const agent = useAgent() const [liveLink, setLiveLink] = useState('') const [liveLinkError, setLiveLinkError] = useState('') const [imageLoadError, setImageLoadError] = useState(false) const [duration, setDuration] = useState(60) const moderationOpts = useModerationOpts() const tick = useTickEveryMinute() const liveNowConfig = useLiveNowConfig() const {currentAccount} = useSession() const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did) const time = useCallback( (offset: number) => { tick! const date = new Date() date.setMinutes(date.getMinutes() + offset) return i18n .date(date, {hour: 'numeric', minute: '2-digit', hour12: true}) .toLocaleUpperCase() .replace(' ', '') }, [tick, i18n], ) const onChangeDuration = useCallback((newDuration: string) => { setDuration(Number(newDuration)) }, []) const liveLinkUrl = definitelyUrl(liveLink) const debouncedUrl = useDebouncedValue(liveLinkUrl, 500) const { data: linkMeta, isSuccess: hasValidLinkMeta, isLoading: linkMetaLoading, error: linkMetaError, } = useQuery({ enabled: !!debouncedUrl, queryKey: ['link-meta', debouncedUrl], queryFn: async () => { if (!debouncedUrl) return null if (!config) throw new Error(_(msg`You are not allowed to go live`)) const urlp = new URL(debouncedUrl) if (!config.domains.includes(urlp.hostname)) { throw new Error(_(msg`${urlp.hostname} is not a valid URL`)) } return getLinkMeta(agent, debouncedUrl) }, }) const { mutate: goLive, isPending: isGoingLive, error: goLiveError, } = useUpsertLiveStatusMutation(duration, linkMeta) const isSourceInvalid = !!liveLinkError || !!linkMetaError const hasLink = !!debouncedUrl && !isSourceInvalid return ( Go Live Add a temporary live status to your profile. When someone clicks on your avatar, they’ll see information about your live event. {moderationOpts && ( )} Live link setLiveLinkError('')} onBlur={() => { if (!definitelyUrl(liveLink)) { setLiveLinkError('Invalid URL') } }} returnKeyType="done" autoCapitalize="none" autoComplete="url" autoCorrect={false} /> {(liveLinkError || linkMetaError) && ( {liveLinkError ? ( This is not a valid link ) : ( cleanError(linkMetaError) )} )} {(linkMeta || linkMetaLoading) && ( {(!linkMeta || linkMeta.image) && ( {linkMeta?.image && ( setImageLoadError(false)} onError={() => setImageLoadError(true)} /> )} {linkMeta && imageLoadError && ( )} )} {linkMeta ? ( <> {linkMeta.title || linkMeta.url} {toNiceDomain(linkMeta.url)} ) : ( <> )} )} {hasLink && ( Go live for {displayDuration(i18n, duration)} {' '} {time(duration)} { const label = displayDuration(i18n, item) return ( {label} {' '} {time(item)} ) }} items={DURATIONS} valueExtractor={d => String(d)} /> )} {goLiveError && ( {cleanError(goLiveError)} )} {hasLink && ( )} ) }