Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 82f42e734c50b34de31e8aff1e7ced248ab6e96f 243 lines 7.6 kB view raw
1import {useMemo, useState} from 'react' 2import {View} from 'react-native' 3import { 4 type AppBskyActorDefs, 5 AppBskyActorStatus, 6 type AppBskyEmbedExternal, 7} from '@atproto/api' 8import {msg, Trans} from '@lingui/macro' 9import {useLingui} from '@lingui/react' 10import {differenceInMinutes} from 'date-fns' 11 12import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' 13import {cleanError} from '#/lib/strings/errors' 14import {definitelyUrl} from '#/lib/strings/url-helpers' 15import {useTickEveryMinute} from '#/state/shell' 16import {atoms as a, platform, useTheme, web} from '#/alf' 17import {Admonition} from '#/components/Admonition' 18import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19import * as Dialog from '#/components/Dialog' 20import * as TextField from '#/components/forms/TextField' 21import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock' 22import {Loader} from '#/components/Loader' 23import {Text} from '#/components/Typography' 24import { 25 displayDuration, 26 useLiveLinkMetaQuery, 27 useRemoveLiveStatusMutation, 28 useUpsertLiveStatusMutation, 29} from '#/features/liveNow' 30import {LinkPreview} from '#/features/liveNow/components/LinkPreview' 31 32export function EditLiveDialog({ 33 control, 34 status, 35 embed, 36}: { 37 control: Dialog.DialogControlProps 38 status: AppBskyActorDefs.StatusView 39 embed: AppBskyEmbedExternal.View 40}) { 41 return ( 42 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 43 <Dialog.Handle /> 44 <DialogInner status={status} embed={embed} /> 45 </Dialog.Outer> 46 ) 47} 48 49function DialogInner({ 50 status, 51 embed, 52}: { 53 status: AppBskyActorDefs.StatusView 54 embed: AppBskyEmbedExternal.View 55}) { 56 const control = Dialog.useDialogContext() 57 const {_, i18n} = useLingui() 58 const t = useTheme() 59 60 const [liveLink, setLiveLink] = useState(embed.external.uri) 61 const [liveLinkError, setLiveLinkError] = useState('') 62 const tick = useTickEveryMinute() 63 64 const liveLinkUrl = definitelyUrl(liveLink) 65 const debouncedUrl = useDebouncedValue(liveLinkUrl, 500) 66 67 const isDirty = liveLinkUrl !== embed.external.uri 68 69 const { 70 data: linkMeta, 71 isSuccess: hasValidLinkMeta, 72 isLoading: linkMetaLoading, 73 error: linkMetaError, 74 } = useLiveLinkMetaQuery(debouncedUrl) 75 76 const record = useMemo(() => { 77 if (!AppBskyActorStatus.isRecord(status.record)) return null 78 const validation = AppBskyActorStatus.validateRecord(status.record) 79 if (validation.success) { 80 return validation.value 81 } 82 return null 83 }, [status]) 84 85 const { 86 mutate: goLive, 87 isPending: isGoingLive, 88 error: goLiveError, 89 } = useUpsertLiveStatusMutation( 90 record?.durationMinutes ?? 0, 91 linkMeta, 92 record?.createdAt, 93 ) 94 95 const { 96 mutate: removeLiveStatus, 97 isPending: isRemovingLiveStatus, 98 error: removeLiveStatusError, 99 } = useRemoveLiveStatusMutation() 100 101 const {minutesUntilExpiry, expiryDateTime} = useMemo(() => { 102 void tick 103 104 const expiry = new Date(status.expiresAt ?? new Date()) 105 return { 106 expiryDateTime: expiry, 107 minutesUntilExpiry: differenceInMinutes(expiry, new Date()), 108 } 109 }, [tick, status.expiresAt]) 110 111 const submitDisabled = 112 isGoingLive || 113 !hasValidLinkMeta || 114 debouncedUrl !== liveLinkUrl || 115 isRemovingLiveStatus 116 117 return ( 118 <Dialog.ScrollableInner 119 label={_(msg`You are Live`)} 120 style={web({maxWidth: 420})}> 121 <View style={[a.gap_lg]}> 122 <View style={[a.gap_sm]}> 123 <Text style={[a.font_semi_bold, a.text_2xl]}> 124 <Trans>You are Live</Trans> 125 </Text> 126 <View style={[a.flex_row, a.align_center, a.gap_xs]}> 127 <ClockIcon style={[t.atoms.text_contrast_high]} size="sm" /> 128 <Text 129 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> 130 {typeof record?.durationMinutes === 'number' ? ( 131 <Trans> 132 Expires in {displayDuration(i18n, minutesUntilExpiry)} at{' '} 133 {i18n.date(expiryDateTime, { 134 hour: 'numeric', 135 minute: '2-digit', 136 hour12: true, 137 })} 138 </Trans> 139 ) : ( 140 <Trans>No expiry set</Trans> 141 )} 142 </Text> 143 </View> 144 </View> 145 <View style={[a.gap_sm]}> 146 <View> 147 <TextField.LabelText> 148 <Trans>Live link</Trans> 149 </TextField.LabelText> 150 <TextField.Root isInvalid={!!liveLinkError || !!linkMetaError}> 151 <TextField.Input 152 label={_(msg`Live link`)} 153 placeholder={_(msg`www.mylivestream.tv`)} 154 value={liveLink} 155 onChangeText={setLiveLink} 156 onFocus={() => setLiveLinkError('')} 157 onBlur={() => { 158 if (!definitelyUrl(liveLink)) { 159 setLiveLinkError('Invalid URL') 160 } 161 }} 162 returnKeyType="done" 163 autoCapitalize="none" 164 autoComplete="url" 165 autoCorrect={false} 166 onSubmitEditing={() => { 167 if (isDirty && !submitDisabled) { 168 goLive() 169 } 170 }} 171 /> 172 </TextField.Root> 173 </View> 174 {(liveLinkError || linkMetaError) && ( 175 <Admonition type="error"> 176 {liveLinkError ? ( 177 <Trans>This is not a valid link</Trans> 178 ) : ( 179 cleanError(linkMetaError) 180 )} 181 </Admonition> 182 )} 183 184 <LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} /> 185 </View> 186 187 {goLiveError && ( 188 <Admonition type="error">{cleanError(goLiveError)}</Admonition> 189 )} 190 {removeLiveStatusError && ( 191 <Admonition type="error"> 192 {cleanError(removeLiveStatusError)} 193 </Admonition> 194 )} 195 196 <View 197 style={platform({ 198 native: [a.gap_md, a.pt_lg], 199 web: [a.flex_row_reverse, a.gap_md, a.align_center], 200 })}> 201 {isDirty ? ( 202 <Button 203 label={_(msg`Save`)} 204 size={platform({native: 'large', web: 'small'})} 205 color="primary" 206 variant="solid" 207 onPress={() => goLive()} 208 disabled={submitDisabled}> 209 <ButtonText> 210 <Trans>Save</Trans> 211 </ButtonText> 212 {isGoingLive && <ButtonIcon icon={Loader} />} 213 </Button> 214 ) : ( 215 <Button 216 label={_(msg`Close`)} 217 size={platform({native: 'large', web: 'small'})} 218 color="primary" 219 variant="solid" 220 onPress={() => control.close()}> 221 <ButtonText> 222 <Trans>Close</Trans> 223 </ButtonText> 224 </Button> 225 )} 226 <Button 227 label={_(msg`Remove live status`)} 228 onPress={() => removeLiveStatus()} 229 size={platform({native: 'large', web: 'small'})} 230 color="negative_subtle" 231 variant="solid" 232 disabled={isRemovingLiveStatus || isGoingLive}> 233 <ButtonText> 234 <Trans>Remove live status</Trans> 235 </ButtonText> 236 {isRemovingLiveStatus && <ButtonIcon icon={Loader} />} 237 </Button> 238 </View> 239 </View> 240 <Dialog.Close /> 241 </Dialog.ScrollableInner> 242 ) 243}