From a0bd8042621e108f47e09dd096cf0d73fe1cee53 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 10 May 2025 00:06:06 +0300 Subject: Live (#8354) --- src/components/live/GoLiveDialog.tsx | 352 +++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 src/components/live/GoLiveDialog.tsx (limited to 'src/components/live/GoLiveDialog.tsx') diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx new file mode 100644 index 000000000..2fad009fd --- /dev/null +++ b/src/components/live/GoLiveDialog.tsx @@ -0,0 +1,352 @@ +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 {useAgent} 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 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 hasLink = !!debouncedUrl + + const { + data: linkMeta, + isSuccess: hasValidLinkMeta, + isLoading: linkMetaLoading, + error: linkMetaError, + } = useQuery({ + enabled: !!debouncedUrl, + queryKey: ['link-meta', debouncedUrl], + queryFn: async () => { + if (!debouncedUrl) return null + return getLinkMeta(agent, debouncedUrl) + }, + }) + + const { + mutate: goLive, + isPending: isGoingLive, + error: goLiveError, + } = useUpsertLiveStatusMutation(duration, linkMeta) + + 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 && ( + + )} + + + + + + ) +} -- cgit 1.4.1