mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at thread-bug 261 lines 8.7 kB view raw
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, theyll 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}