mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useCallback, useState} from 'react'
2import {View} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {cleanError} from '#/lib/strings/errors'
7import {definitelyUrl} from '#/lib/strings/url-helpers'
8import {useModerationOpts} from '#/state/preferences/moderation-opts'
9import {useTickEveryMinute} from '#/state/shell'
10import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
11import {Admonition} from '#/components/Admonition'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
13import * as Dialog from '#/components/Dialog'
14import * as TextField from '#/components/forms/TextField'
15import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
16import {Loader} from '#/components/Loader'
17import * as ProfileCard from '#/components/ProfileCard'
18import * as Select from '#/components/Select'
19import {Text} from '#/components/Typography'
20import type * as bsky from '#/types/bsky'
21import {LinkPreview} from './LinkPreview'
22import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
23import {displayDuration, useDebouncedValue} from './utils'
24
25export function GoLiveDialog({
26 control,
27 profile,
28}: {
29 control: Dialog.DialogControlProps
30 profile: bsky.profile.AnyProfileView
31}) {
32 return (
33 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
34 <Dialog.Handle />
35 <DialogInner profile={profile} />
36 </Dialog.Outer>
37 )
38}
39
40// Possible durations: max 4 hours, 5 minute intervals
41const DURATIONS = Array.from({length: (4 * 60) / 5}).map((_, i) => (i + 1) * 5)
42
43function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
44 const control = Dialog.useDialogContext()
45 const {_, i18n} = useLingui()
46 const t = useTheme()
47 const [liveLink, setLiveLink] = useState('')
48 const [liveLinkError, setLiveLinkError] = useState('')
49 const [duration, setDuration] = useState(60)
50 const moderationOpts = useModerationOpts()
51 const tick = useTickEveryMinute()
52
53 const time = useCallback(
54 (offset: number) => {
55 tick!
56
57 const date = new Date()
58 date.setMinutes(date.getMinutes() + offset)
59 return i18n
60 .date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
61 .toLocaleUpperCase()
62 .replace(' ', '')
63 },
64 [tick, i18n],
65 )
66
67 const onChangeDuration = useCallback((newDuration: string) => {
68 setDuration(Number(newDuration))
69 }, [])
70
71 const liveLinkUrl = definitelyUrl(liveLink)
72 const debouncedUrl = useDebouncedValue(liveLinkUrl, 500)
73
74 const {
75 data: linkMeta,
76 isSuccess: hasValidLinkMeta,
77 isLoading: linkMetaLoading,
78 error: linkMetaError,
79 } = useLiveLinkMetaQuery(debouncedUrl)
80
81 const {
82 mutate: goLive,
83 isPending: isGoingLive,
84 error: goLiveError,
85 } = useUpsertLiveStatusMutation(duration, linkMeta)
86
87 const isSourceInvalid = !!liveLinkError || !!linkMetaError
88
89 const hasLink = !!debouncedUrl && !isSourceInvalid
90
91 return (
92 <Dialog.ScrollableInner
93 label={_(msg`Go Live`)}
94 style={web({maxWidth: 420})}>
95 <View style={[a.gap_xl]}>
96 <View style={[a.gap_sm]}>
97 <Text style={[a.font_bold, a.text_2xl]}>
98 <Trans>Go Live</Trans>
99 </Text>
100 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
101 <Trans>
102 Add a temporary live status to your profile. When someone clicks
103 on your avatar, they’ll see information about your live event.
104 </Trans>
105 </Text>
106 </View>
107 {moderationOpts && (
108 <ProfileCard.Header>
109 <ProfileCard.Avatar
110 profile={profile}
111 moderationOpts={moderationOpts}
112 liveOverride
113 disabledPreview
114 />
115 <ProfileCard.NameAndHandle
116 profile={profile}
117 moderationOpts={moderationOpts}
118 />
119 </ProfileCard.Header>
120 )}
121 <View style={[a.gap_sm]}>
122 <View>
123 <TextField.LabelText>
124 <Trans>Live link</Trans>
125 </TextField.LabelText>
126 <TextField.Root isInvalid={isSourceInvalid}>
127 <TextField.Input
128 label={_(msg`Live link`)}
129 placeholder={_(msg`www.mylivestream.tv`)}
130 value={liveLink}
131 onChangeText={setLiveLink}
132 onFocus={() => setLiveLinkError('')}
133 onBlur={() => {
134 if (!definitelyUrl(liveLink)) {
135 setLiveLinkError('Invalid URL')
136 }
137 }}
138 returnKeyType="done"
139 autoCapitalize="none"
140 autoComplete="url"
141 autoCorrect={false}
142 />
143 </TextField.Root>
144 </View>
145 {(liveLinkError || linkMetaError) && (
146 <View style={[a.flex_row, a.gap_xs, a.align_center]}>
147 <WarningIcon
148 style={[{color: t.palette.negative_500}]}
149 size="sm"
150 />
151 <Text
152 style={[
153 a.text_sm,
154 a.leading_snug,
155 a.flex_1,
156 a.font_bold,
157 {color: t.palette.negative_500},
158 ]}>
159 {liveLinkError ? (
160 <Trans>This is not a valid link</Trans>
161 ) : (
162 cleanError(linkMetaError)
163 )}
164 </Text>
165 </View>
166 )}
167
168 <LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
169 </View>
170
171 {hasLink && (
172 <View>
173 <TextField.LabelText>
174 <Trans>Go live for</Trans>
175 </TextField.LabelText>
176 <Select.Root
177 value={String(duration)}
178 onValueChange={onChangeDuration}>
179 <Select.Trigger label={_(msg`Select duration`)}>
180 <Text style={[ios(a.py_xs)]}>
181 {displayDuration(i18n, duration)}
182 {' '}
183 <Text style={[t.atoms.text_contrast_low]}>
184 {time(duration)}
185 </Text>
186 </Text>
187
188 <Select.Icon />
189 </Select.Trigger>
190 <Select.Content
191 renderItem={(item, _i, selectedValue) => {
192 const label = displayDuration(i18n, item)
193 return (
194 <Select.Item value={String(item)} label={label}>
195 <Select.ItemIndicator />
196 <Select.ItemText>
197 {label}
198 {' '}
199 <Text
200 style={[
201 native(a.text_md),
202 web(a.ml_xs),
203 selectedValue === String(item)
204 ? t.atoms.text_contrast_medium
205 : t.atoms.text_contrast_low,
206 a.font_normal,
207 ]}>
208 {time(item)}
209 </Text>
210 </Select.ItemText>
211 </Select.Item>
212 )
213 }}
214 items={DURATIONS}
215 valueExtractor={d => String(d)}
216 />
217 </Select.Root>
218 </View>
219 )}
220
221 {goLiveError && (
222 <Admonition type="error">{cleanError(goLiveError)}</Admonition>
223 )}
224
225 <View
226 style={platform({
227 native: [a.gap_md, a.pt_lg],
228 web: [a.flex_row_reverse, a.gap_md, a.align_center],
229 })}>
230 {hasLink && (
231 <Button
232 label={_(msg`Go Live`)}
233 size={platform({native: 'large', web: 'small'})}
234 color="primary"
235 variant="solid"
236 onPress={() => goLive()}
237 disabled={
238 isGoingLive || !hasValidLinkMeta || debouncedUrl !== liveLinkUrl
239 }>
240 <ButtonText>
241 <Trans>Go Live</Trans>
242 </ButtonText>
243 {isGoingLive && <ButtonIcon icon={Loader} />}
244 </Button>
245 )}
246 <Button
247 label={_(msg`Cancel`)}
248 onPress={() => control.close()}
249 size={platform({native: 'large', web: 'small'})}
250 color="secondary"
251 variant={platform({native: 'solid', web: 'ghost'})}>
252 <ButtonText>
253 <Trans>Cancel</Trans>
254 </ButtonText>
255 </Button>
256 </View>
257 </View>
258 <Dialog.Close />
259 </Dialog.ScrollableInner>
260 )
261}