Bluesky app fork with some witchin' additions 💫

Merge branch 'main' of https://github.com/bluesky-social/social-app

+1141 -1604
+55 -22
bskylink/src/bin.ts
··· 1 1 import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' 2 + 2 3 async function main() { 3 - const env = readEnv() 4 - const cfg = envToCfg(env) 5 - if (cfg.db.migrationUrl) { 6 - const migrateDb = Database.postgres({ 7 - url: cfg.db.migrationUrl, 8 - schema: cfg.db.schema, 9 - }) 10 - await migrateDb.migrateToLatestOrThrow() 11 - await migrateDb.close() 12 - } 4 + try { 5 + httpLogger.info('Starting blink service') 6 + 7 + const env = readEnv() 8 + const cfg = envToCfg(env) 9 + 10 + httpLogger.info( 11 + { 12 + port: cfg.service.port, 13 + safelinkEnabled: cfg.service.safelinkEnabled, 14 + hasDbUrl: !!cfg.db.url, 15 + hasDbMigrationUrl: !!cfg.db.migrationUrl, 16 + }, 17 + 'Configuration loaded', 18 + ) 19 + 20 + if (cfg.db.migrationUrl) { 21 + httpLogger.info('Running database migrations') 22 + const migrateDb = Database.postgres({ 23 + url: cfg.db.migrationUrl, 24 + schema: cfg.db.schema, 25 + }) 26 + await migrateDb.migrateToLatestOrThrow() 27 + await migrateDb.close() 28 + httpLogger.info('Database migrations completed') 29 + } 30 + 31 + httpLogger.info('Creating LinkService') 32 + const link = await LinkService.create(cfg) 33 + 34 + if (link.ctx.cfg.service.safelinkEnabled) { 35 + httpLogger.info('Starting Safelink client') 36 + link.ctx.safelinkClient.runFetchEvents() 37 + } 13 38 14 - const link = await LinkService.create(cfg) 39 + await link.start() 40 + httpLogger.info('Link service is running') 15 41 16 - if (link.ctx.cfg.service.safelinkEnabled) { 17 - link.ctx.safelinkClient.runFetchEvents() 42 + process.on('SIGTERM', async () => { 43 + httpLogger.info('Link service is stopping') 44 + await link.destroy() 45 + httpLogger.info('Link service is stopped') 46 + }) 47 + } catch (error) { 48 + httpLogger.error( 49 + { 50 + error: String(error), 51 + stack: error instanceof Error ? error.stack : undefined, 52 + }, 53 + 'Failed to start blink service', 54 + ) 55 + process.exit(1) 18 56 } 19 - 20 - await link.start() 21 - httpLogger.info('link service is running') 22 - process.on('SIGTERM', async () => { 23 - httpLogger.info('link service is stopping') 24 - await link.destroy() 25 - httpLogger.info('link service is stopped') 26 - }) 27 57 } 28 58 29 - main() 59 + main().catch(error => { 60 + console.error('Unhandled startup error:', error) 61 + process.exit(1) 62 + })
+10
bskylink/src/db/index.ts
··· 34 34 35 35 static postgres(opts: PgOptions): Database { 36 36 const {schema, url, txLockNonce} = opts 37 + log.info( 38 + { 39 + schema, 40 + poolSize: opts.poolSize, 41 + poolMaxUses: opts.poolMaxUses, 42 + poolIdleTimeoutMs: opts.poolIdleTimeoutMs, 43 + }, 44 + 'Creating database connection', 45 + ) 46 + 37 47 const pool = 38 48 opts.pool ?? 39 49 new Pg.Pool({
+26 -4
bskyogcard/src/index.ts
··· 62 62 // Start main application server 63 63 this.server = this.app.listen(this.ctx.cfg.service.port) 64 64 this.server.keepAliveTimeout = 90000 65 - this.terminator = createHttpTerminator({server: this.server}) 65 + this.terminator = createHttpTerminator({ 66 + server: this.server, 67 + gracefulTerminationTimeout: 15000, // 15s timeout for in-flight requests 68 + }) 66 69 await events.once(this.server, 'listening') 67 70 68 71 // Start separate metrics server ··· 73 76 }) 74 77 75 78 this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort) 76 - this.metricsTerminator = createHttpTerminator({server: this.metricsServer}) 79 + this.metricsTerminator = createHttpTerminator({ 80 + server: this.metricsServer, 81 + gracefulTerminationTimeout: 2000, // 2s timeout for metrics server 82 + }) 77 83 await events.once(this.metricsServer, 'listening') 78 84 } 79 85 80 86 async destroy() { 87 + const startTime = Date.now() 88 + 81 89 this.ctx.abortController.abort() 82 - await this.terminator?.terminate() 83 - await this.metricsTerminator?.terminate() 90 + 91 + const shutdownPromises = [] 92 + 93 + if (this.terminator) { 94 + shutdownPromises.push(this.terminator.terminate()) 95 + } 96 + 97 + if (this.metricsTerminator) { 98 + shutdownPromises.push(this.metricsTerminator.terminate()) 99 + } 100 + 101 + await Promise.all(shutdownPromises) 102 + 103 + const elapsed = Date.now() - startTime 104 + const {httpLogger} = await import('./logger.js') 105 + httpLogger.info(`Graceful shutdown completed in ${elapsed}ms`) 84 106 } 85 107 }
+16 -19
src/App.native.tsx
··· 38 38 } from '#/state/geolocation' 39 39 import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' 40 40 import {Provider as HomeBadgeProvider} from '#/state/home-badge' 41 - import {Provider as InvitesStateProvider} from '#/state/invites' 42 41 import {Provider as LightboxStateProvider} from '#/state/lightbox' 43 42 import {MessagesProvider} from '#/state/messages' 44 43 import {Provider as ModalStateProvider} from '#/state/modals' ··· 225 224 <PrefsStateProvider> 226 225 <I18nProvider> 227 226 <ShellStateProvider> 228 - <InvitesStateProvider> 229 - <ModalStateProvider> 230 - <DialogStateProvider> 231 - <LightboxStateProvider> 232 - <PortalProvider> 233 - <BottomSheetProvider> 234 - <StarterPackProvider> 235 - <SafeAreaProvider 236 - initialMetrics={initialWindowMetrics}> 237 - <InnerApp /> 238 - </SafeAreaProvider> 239 - </StarterPackProvider> 240 - </BottomSheetProvider> 241 - </PortalProvider> 242 - </LightboxStateProvider> 243 - </DialogStateProvider> 244 - </ModalStateProvider> 245 - </InvitesStateProvider> 227 + <ModalStateProvider> 228 + <DialogStateProvider> 229 + <LightboxStateProvider> 230 + <PortalProvider> 231 + <BottomSheetProvider> 232 + <StarterPackProvider> 233 + <SafeAreaProvider 234 + initialMetrics={initialWindowMetrics}> 235 + <InnerApp /> 236 + </SafeAreaProvider> 237 + </StarterPackProvider> 238 + </BottomSheetProvider> 239 + </PortalProvider> 240 + </LightboxStateProvider> 241 + </DialogStateProvider> 242 + </ModalStateProvider> 246 243 </ShellStateProvider> 247 244 </I18nProvider> 248 245 </PrefsStateProvider>
+11 -14
src/App.web.tsx
··· 26 26 Provider as GeolocationProvider, 27 27 } from '#/state/geolocation' 28 28 import {Provider as HomeBadgeProvider} from '#/state/home-badge' 29 - import {Provider as InvitesStateProvider} from '#/state/invites' 30 29 import {Provider as LightboxStateProvider} from '#/state/lightbox' 31 30 import {MessagesProvider} from '#/state/messages' 32 31 import {Provider as ModalStateProvider} from '#/state/modals' ··· 199 198 <PrefsStateProvider> 200 199 <I18nProvider> 201 200 <ShellStateProvider> 202 - <InvitesStateProvider> 203 - <ModalStateProvider> 204 - <DialogStateProvider> 205 - <LightboxStateProvider> 206 - <PortalProvider> 207 - <StarterPackProvider> 208 - <InnerApp /> 209 - </StarterPackProvider> 210 - </PortalProvider> 211 - </LightboxStateProvider> 212 - </DialogStateProvider> 213 - </ModalStateProvider> 214 - </InvitesStateProvider> 201 + <ModalStateProvider> 202 + <DialogStateProvider> 203 + <LightboxStateProvider> 204 + <PortalProvider> 205 + <StarterPackProvider> 206 + <InnerApp /> 207 + </StarterPackProvider> 208 + </PortalProvider> 209 + </LightboxStateProvider> 210 + </DialogStateProvider> 211 + </ModalStateProvider> 215 212 </ShellStateProvider> 216 213 </I18nProvider> 217 214 </PrefsStateProvider>
+4 -1
src/components/Dialog/index.tsx
··· 267 267 scrollEventThrottle={50} 268 268 onScroll={isAndroid ? onScroll : undefined} 269 269 keyboardShouldPersistTaps="handled" 270 - stickyHeaderIndices={header ? [0] : undefined}> 270 + // TODO: figure out why this positions the header absolutely (rather than stickily) 271 + // on Android. fine to disable for now, because we don't have any 272 + // dialogs that use this that actually scroll -sfn 273 + stickyHeaderIndices={ios(header ? [0] : undefined)}> 271 274 {header} 272 275 {children} 273 276 </KeyboardAwareScrollView>
+40
src/components/ScreenTransition.tsx
··· 1 + import {type StyleProp, type ViewStyle} from 'react-native' 2 + import Animated, { 3 + Easing, 4 + FadeIn, 5 + FadeOut, 6 + SlideInLeft, 7 + SlideInRight, 8 + } from 'react-native-reanimated' 9 + import type React from 'react' 10 + 11 + import {isWeb} from '#/platform/detection' 12 + 13 + export function ScreenTransition({ 14 + direction, 15 + style, 16 + children, 17 + enabledWeb, 18 + }: { 19 + direction: 'Backward' | 'Forward' 20 + style?: StyleProp<ViewStyle> 21 + children: React.ReactNode 22 + enabledWeb?: boolean 23 + }) { 24 + const entering = 25 + direction === 'Forward' 26 + ? SlideInRight.easing(Easing.out(Easing.exp)) 27 + : SlideInLeft.easing(Easing.out(Easing.exp)) 28 + const webEntering = enabledWeb ? FadeIn.duration(90) : undefined 29 + const exiting = FadeOut.duration(90) // Totally vibes based 30 + const webExiting = enabledWeb ? FadeOut.duration(90) : undefined 31 + 32 + return ( 33 + <Animated.View 34 + entering={isWeb ? webEntering : entering} 35 + exiting={isWeb ? webExiting : exiting} 36 + style={style}> 37 + {children} 38 + </Animated.View> 39 + ) 40 + }
-31
src/components/StarterPack/Wizard/ScreenTransition.tsx
··· 1 - import {type StyleProp, type ViewStyle} from 'react-native' 2 - import Animated, { 3 - FadeIn, 4 - FadeOut, 5 - SlideInLeft, 6 - SlideInRight, 7 - } from 'react-native-reanimated' 8 - import type React from 'react' 9 - 10 - import {isWeb} from '#/platform/detection' 11 - 12 - export function ScreenTransition({ 13 - direction, 14 - style, 15 - children, 16 - }: { 17 - direction: 'Backward' | 'Forward' 18 - style?: StyleProp<ViewStyle> 19 - children: React.ReactNode 20 - }) { 21 - const entering = direction === 'Forward' ? SlideInRight : SlideInLeft 22 - 23 - return ( 24 - <Animated.View 25 - entering={isWeb ? FadeIn.duration(90) : entering} 26 - exiting={FadeOut.duration(90)} // Totally vibes based 27 - style={style}> 28 - {children} 29 - </Animated.View> 30 - ) 31 - }
+454
src/components/dialogs/lists/CreateOrEditListDialog.tsx
··· 1 + import {useCallback, useEffect, useMemo, useState} from 'react' 2 + import {useWindowDimensions, View} from 'react-native' 3 + import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' 4 + import {msg, Plural, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {cleanError} from '#/lib/strings/errors' 8 + import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' 9 + import {richTextToString} from '#/lib/strings/rich-text-helpers' 10 + import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 11 + import {logger} from '#/logger' 12 + import {isWeb} from '#/platform/detection' 13 + import {type ImageMeta} from '#/state/gallery' 14 + import { 15 + useListCreateMutation, 16 + useListMetadataMutation, 17 + } from '#/state/queries/list' 18 + import {useAgent} from '#/state/session' 19 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 20 + import * as Toast from '#/view/com/util/Toast' 21 + import {EditableUserAvatar} from '#/view/com/util/UserAvatar' 22 + import {atoms as a, useTheme, web} from '#/alf' 23 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 24 + import * as Dialog from '#/components/Dialog' 25 + import * as TextField from '#/components/forms/TextField' 26 + import {Loader} from '#/components/Loader' 27 + import * as Prompt from '#/components/Prompt' 28 + import {Text} from '#/components/Typography' 29 + 30 + const DISPLAY_NAME_MAX_GRAPHEMES = 64 31 + const DESCRIPTION_MAX_GRAPHEMES = 300 32 + 33 + export function CreateOrEditListDialog({ 34 + control, 35 + list, 36 + purpose, 37 + onSave, 38 + }: { 39 + control: Dialog.DialogControlProps 40 + list?: AppBskyGraphDefs.ListView 41 + purpose?: AppBskyGraphDefs.ListPurpose 42 + onSave?: (uri: string) => void 43 + }) { 44 + const {_} = useLingui() 45 + const cancelControl = Dialog.useDialogControl() 46 + const [dirty, setDirty] = useState(false) 47 + const {height} = useWindowDimensions() 48 + 49 + // 'You might lose unsaved changes' warning 50 + useEffect(() => { 51 + if (isWeb && dirty) { 52 + const abortController = new AbortController() 53 + const {signal} = abortController 54 + window.addEventListener('beforeunload', evt => evt.preventDefault(), { 55 + signal, 56 + }) 57 + return () => { 58 + abortController.abort() 59 + } 60 + } 61 + }, [dirty]) 62 + 63 + const onPressCancel = useCallback(() => { 64 + if (dirty) { 65 + cancelControl.open() 66 + } else { 67 + control.close() 68 + } 69 + }, [dirty, control, cancelControl]) 70 + 71 + return ( 72 + <Dialog.Outer 73 + control={control} 74 + nativeOptions={{ 75 + preventDismiss: dirty, 76 + minHeight: height, 77 + }} 78 + testID="createOrEditListDialog"> 79 + <DialogInner 80 + list={list} 81 + purpose={purpose} 82 + onSave={onSave} 83 + setDirty={setDirty} 84 + onPressCancel={onPressCancel} 85 + /> 86 + 87 + <Prompt.Basic 88 + control={cancelControl} 89 + title={_(msg`Discard changes?`)} 90 + description={_(msg`Are you sure you want to discard your changes?`)} 91 + onConfirm={() => control.close()} 92 + confirmButtonCta={_(msg`Discard`)} 93 + confirmButtonColor="negative" 94 + /> 95 + </Dialog.Outer> 96 + ) 97 + } 98 + 99 + function DialogInner({ 100 + list, 101 + purpose, 102 + onSave, 103 + setDirty, 104 + onPressCancel, 105 + }: { 106 + list?: AppBskyGraphDefs.ListView 107 + purpose?: AppBskyGraphDefs.ListPurpose 108 + onSave?: (uri: string) => void 109 + setDirty: (dirty: boolean) => void 110 + onPressCancel: () => void 111 + }) { 112 + const activePurpose = useMemo(() => { 113 + if (list?.purpose) { 114 + return list.purpose 115 + } 116 + if (purpose) { 117 + return purpose 118 + } 119 + return 'app.bsky.graph.defs#curatelist' 120 + }, [list, purpose]) 121 + const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' 122 + 123 + const {_} = useLingui() 124 + const t = useTheme() 125 + const agent = useAgent() 126 + const control = Dialog.useDialogContext() 127 + const { 128 + mutateAsync: createListMutation, 129 + error: createListError, 130 + isError: isCreateListError, 131 + isPending: isCreatingList, 132 + } = useListCreateMutation() 133 + const { 134 + mutateAsync: updateListMutation, 135 + error: updateListError, 136 + isError: isUpdateListError, 137 + isPending: isUpdatingList, 138 + } = useListMetadataMutation() 139 + const [imageError, setImageError] = useState('') 140 + const [displayNameTooShort, setDisplayNameTooShort] = useState(false) 141 + const initialDisplayName = list?.name || '' 142 + const [displayName, setDisplayName] = useState(initialDisplayName) 143 + const initialDescription = list?.description || '' 144 + const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => { 145 + const text = list?.description 146 + const facets = list?.descriptionFacets 147 + 148 + if (!text || !facets) { 149 + return new RichTextAPI({text: text || ''}) 150 + } 151 + 152 + // We want to be working with a blank state here, so let's get the 153 + // serialized version and turn it back into a RichText 154 + const serialized = richTextToString(new RichTextAPI({text, facets}), false) 155 + 156 + const richText = new RichTextAPI({text: serialized}) 157 + richText.detectFacetsWithoutResolution() 158 + 159 + return richText 160 + }) 161 + 162 + const [listAvatar, setListAvatar] = useState<string | undefined | null>( 163 + list?.avatar, 164 + ) 165 + const [newListAvatar, setNewListAvatar] = useState< 166 + ImageMeta | undefined | null 167 + >() 168 + 169 + const dirty = 170 + displayName !== initialDisplayName || 171 + descriptionRt.text !== initialDescription || 172 + listAvatar !== list?.avatar 173 + 174 + useEffect(() => { 175 + setDirty(dirty) 176 + }, [dirty, setDirty]) 177 + 178 + const onSelectNewAvatar = useCallback( 179 + (img: ImageMeta | null) => { 180 + setImageError('') 181 + if (img === null) { 182 + setNewListAvatar(null) 183 + setListAvatar(null) 184 + return 185 + } 186 + try { 187 + setNewListAvatar(img) 188 + setListAvatar(img.path) 189 + } catch (e: any) { 190 + setImageError(cleanError(e)) 191 + } 192 + }, 193 + [setNewListAvatar, setListAvatar, setImageError], 194 + ) 195 + 196 + const onPressSave = useCallback(async () => { 197 + setImageError('') 198 + setDisplayNameTooShort(false) 199 + try { 200 + if (displayName.length === 0) { 201 + setDisplayNameTooShort(true) 202 + return 203 + } 204 + 205 + let richText = new RichTextAPI( 206 + {text: descriptionRt.text.trimEnd()}, 207 + {cleanNewlines: true}, 208 + ) 209 + 210 + await richText.detectFacets(agent) 211 + richText = shortenLinks(richText) 212 + richText = stripInvalidMentions(richText) 213 + 214 + if (list) { 215 + await updateListMutation({ 216 + uri: list.uri, 217 + name: displayName, 218 + description: richText.text, 219 + descriptionFacets: richText.facets, 220 + avatar: newListAvatar, 221 + }) 222 + Toast.show( 223 + isCurateList 224 + ? _(msg({message: 'User list updated', context: 'toast'})) 225 + : _(msg({message: 'Moderation list updated', context: 'toast'})), 226 + ) 227 + control.close(() => onSave?.(list.uri)) 228 + } else { 229 + const {uri} = await createListMutation({ 230 + purpose: activePurpose, 231 + name: displayName, 232 + description: richText.text, 233 + descriptionFacets: richText.facets, 234 + avatar: newListAvatar, 235 + }) 236 + Toast.show( 237 + isCurateList 238 + ? _(msg({message: 'User list created', context: 'toast'})) 239 + : _(msg({message: 'Moderation list created', context: 'toast'})), 240 + ) 241 + control.close(() => onSave?.(uri)) 242 + } 243 + } catch (e: any) { 244 + logger.error('Failed to create/edit list', {message: String(e)}) 245 + } 246 + }, [ 247 + list, 248 + createListMutation, 249 + updateListMutation, 250 + onSave, 251 + control, 252 + displayName, 253 + descriptionRt, 254 + newListAvatar, 255 + setImageError, 256 + activePurpose, 257 + isCurateList, 258 + agent, 259 + _, 260 + ]) 261 + 262 + const displayNameTooLong = useWarnMaxGraphemeCount({ 263 + text: displayName, 264 + maxCount: DISPLAY_NAME_MAX_GRAPHEMES, 265 + }) 266 + const descriptionTooLong = useWarnMaxGraphemeCount({ 267 + text: descriptionRt, 268 + maxCount: DESCRIPTION_MAX_GRAPHEMES, 269 + }) 270 + 271 + const cancelButton = useCallback( 272 + () => ( 273 + <Button 274 + label={_(msg`Cancel`)} 275 + onPress={onPressCancel} 276 + size="small" 277 + color="primary" 278 + variant="ghost" 279 + style={[a.rounded_full]} 280 + testID="editProfileCancelBtn"> 281 + <ButtonText style={[a.text_md]}> 282 + <Trans>Cancel</Trans> 283 + </ButtonText> 284 + </Button> 285 + ), 286 + [onPressCancel, _], 287 + ) 288 + 289 + const saveButton = useCallback( 290 + () => ( 291 + <Button 292 + label={_(msg`Save`)} 293 + onPress={onPressSave} 294 + disabled={ 295 + !dirty || 296 + isCreatingList || 297 + isUpdatingList || 298 + displayNameTooLong || 299 + descriptionTooLong 300 + } 301 + size="small" 302 + color="primary" 303 + variant="ghost" 304 + style={[a.rounded_full]} 305 + testID="editProfileSaveBtn"> 306 + <ButtonText style={[a.text_md, !dirty && t.atoms.text_contrast_low]}> 307 + <Trans>Save</Trans> 308 + </ButtonText> 309 + {(isCreatingList || isUpdatingList) && <ButtonIcon icon={Loader} />} 310 + </Button> 311 + ), 312 + [ 313 + _, 314 + t, 315 + dirty, 316 + onPressSave, 317 + isCreatingList, 318 + isUpdatingList, 319 + displayNameTooLong, 320 + descriptionTooLong, 321 + ], 322 + ) 323 + 324 + const onChangeDisplayName = useCallback( 325 + (text: string) => { 326 + setDisplayName(text) 327 + if (text.length > 0 && displayNameTooShort) { 328 + setDisplayNameTooShort(false) 329 + } 330 + }, 331 + [displayNameTooShort], 332 + ) 333 + 334 + const onChangeDescription = useCallback( 335 + (newText: string) => { 336 + const richText = new RichTextAPI({text: newText}) 337 + richText.detectFacetsWithoutResolution() 338 + 339 + setDescriptionRt(richText) 340 + }, 341 + [setDescriptionRt], 342 + ) 343 + 344 + const title = list 345 + ? isCurateList 346 + ? _(msg`Edit user list`) 347 + : _(msg`Edit moderation list`) 348 + : isCurateList 349 + ? _(msg`Create user list`) 350 + : _(msg`Create moderation list`) 351 + 352 + return ( 353 + <Dialog.ScrollableInner 354 + label={title} 355 + style={[a.overflow_hidden, web({maxWidth: 500})]} 356 + contentContainerStyle={[a.px_0, a.pt_0]} 357 + header={ 358 + <Dialog.Header renderLeft={cancelButton} renderRight={saveButton}> 359 + <Dialog.HeaderText>{title}</Dialog.HeaderText> 360 + </Dialog.Header> 361 + }> 362 + {isUpdateListError && ( 363 + <ErrorMessage message={cleanError(updateListError)} /> 364 + )} 365 + {isCreateListError && ( 366 + <ErrorMessage message={cleanError(createListError)} /> 367 + )} 368 + {imageError !== '' && <ErrorMessage message={imageError} />} 369 + <View style={[a.pt_xl, a.px_xl, a.gap_xl]}> 370 + <View> 371 + <TextField.LabelText> 372 + <Trans>List avatar</Trans> 373 + </TextField.LabelText> 374 + <View style={[a.align_start]}> 375 + <EditableUserAvatar 376 + size={80} 377 + avatar={listAvatar} 378 + onSelectNewAvatar={onSelectNewAvatar} 379 + type="list" 380 + /> 381 + </View> 382 + </View> 383 + <View> 384 + <TextField.LabelText> 385 + <Trans>List name</Trans> 386 + </TextField.LabelText> 387 + <TextField.Root isInvalid={displayNameTooLong || displayNameTooShort}> 388 + <Dialog.Input 389 + defaultValue={displayName} 390 + onChangeText={onChangeDisplayName} 391 + label={_(msg`Name`)} 392 + placeholder={_(msg`e.g. Great Posters`)} 393 + testID="editListNameInput" 394 + /> 395 + </TextField.Root> 396 + {(displayNameTooLong || displayNameTooShort) && ( 397 + <Text 398 + style={[ 399 + a.text_sm, 400 + a.mt_xs, 401 + a.font_bold, 402 + {color: t.palette.negative_400}, 403 + ]}> 404 + {displayNameTooLong ? ( 405 + <Trans> 406 + List name is too long.{' '} 407 + <Plural 408 + value={DISPLAY_NAME_MAX_GRAPHEMES} 409 + other="The maximum number of characters is #." 410 + /> 411 + </Trans> 412 + ) : displayNameTooShort ? ( 413 + <Trans>List must have a name.</Trans> 414 + ) : null} 415 + </Text> 416 + )} 417 + </View> 418 + 419 + <View> 420 + <TextField.LabelText> 421 + <Trans>List description</Trans> 422 + </TextField.LabelText> 423 + <TextField.Root isInvalid={descriptionTooLong}> 424 + <Dialog.Input 425 + defaultValue={descriptionRt.text} 426 + onChangeText={onChangeDescription} 427 + multiline 428 + label={_(msg`Description`)} 429 + placeholder={_(msg`e.g. The posters that never miss.`)} 430 + testID="editProfileDescriptionInput" 431 + /> 432 + </TextField.Root> 433 + {descriptionTooLong && ( 434 + <Text 435 + style={[ 436 + a.text_sm, 437 + a.mt_xs, 438 + a.font_bold, 439 + {color: t.palette.negative_400}, 440 + ]}> 441 + <Trans> 442 + List description is too long.{' '} 443 + <Plural 444 + value={DESCRIPTION_MAX_GRAPHEMES} 445 + other="The maximum number of characters is #." 446 + /> 447 + </Trans> 448 + </Text> 449 + )} 450 + </View> 451 + </View> 452 + </Dialog.ScrollableInner> 453 + ) 454 + }
+9 -2
src/lib/strings/helpers.ts
··· 1 1 import {useCallback, useMemo} from 'react' 2 + import {type RichText} from '@atproto/api' 2 3 import Graphemer from 'graphemer' 4 + 5 + import {shortenLinks} from './rich-text-manip' 3 6 4 7 export function enforceLen( 5 8 str: string, ··· 45 48 text, 46 49 maxCount, 47 50 }: { 48 - text: string 51 + text: string | RichText 49 52 maxCount: number 50 53 }) { 51 54 const splitter = useMemo(() => new Graphemer(), []) 52 55 53 56 return useMemo(() => { 54 - return splitter.countGraphemes(text) > maxCount 57 + if (typeof text === 'string') { 58 + return splitter.countGraphemes(text) > maxCount 59 + } else { 60 + return shortenLinks(text).graphemeLength > maxCount 61 + } 55 62 }, [splitter, maxCount, text]) 56 63 } 57 64
+177 -228
src/locale/locales/en/messages.po
··· 150 150 msgid "{0} is not a valid URL" 151 151 msgstr "" 152 152 153 - #: src/screens/Signup/StepHandle/index.tsx:189 153 + #: src/screens/Signup/StepHandle/index.tsx:188 154 154 msgid "{0} is not available" 155 155 msgstr "" 156 156 ··· 220 220 msgid "{count, plural, one {# unread item} other {# unread items}}" 221 221 msgstr "" 222 222 223 - #: src/screens/Profile/Header/EditProfileDialog.tsx:385 223 + #: src/screens/Profile/Header/EditProfileDialog.tsx:383 224 224 msgid "{DESCRIPTION_MAX_GRAPHEMES, plural, other {Description is too long. The maximum number of characters is #.}}" 225 225 msgstr "" 226 226 227 - #: src/screens/Profile/Header/EditProfileDialog.tsx:334 227 + #: src/screens/Profile/Header/EditProfileDialog.tsx:332 228 228 msgid "{DISPLAY_NAME_MAX_GRAPHEMES, plural, other {Display name is too long. The maximum number of characters is #.}}" 229 229 msgstr "" 230 230 ··· 991 991 msgid "An error occurred while generating your starter pack. Want to try again?" 992 992 msgstr "" 993 993 994 - #: src/components/Post/Embed/VideoEmbed/index.tsx:157 994 + #: src/components/Post/Embed/VideoEmbed/index.tsx:160 995 995 msgid "An error occurred while loading the video. Please try again later." 996 996 msgstr "" 997 997 998 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:232 998 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:244 999 999 msgid "An error occurred while loading the video. Please try again." 1000 1000 msgstr "" 1001 1001 ··· 1204 1204 msgid "Are you sure you want to delete this starter pack?" 1205 1205 msgstr "" 1206 1206 1207 - #: src/screens/Profile/Header/EditProfileDialog.tsx:81 1207 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:90 1208 + #: src/screens/Profile/Header/EditProfileDialog.tsx:80 1208 1209 msgid "Are you sure you want to discard your changes?" 1209 1210 msgstr "" 1210 1211 ··· 1297 1298 msgid "Back to Chats" 1298 1299 msgstr "" 1299 1300 1300 - #: src/view/screens/Lists.tsx:53 1301 - #: src/view/screens/ModerationModlists.tsx:53 1301 + #: src/view/screens/Lists.tsx:42 1302 + #: src/view/screens/ModerationModlists.tsx:42 1302 1303 msgid "Before creating a list, you must first verify your email." 1303 1304 msgstr "" 1304 1305 ··· 1604 1605 #: src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx:131 1605 1606 #: src/components/dialogs/InAppBrowserConsent.tsx:98 1606 1607 #: src/components/dialogs/InAppBrowserConsent.tsx:104 1608 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:274 1609 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:282 1607 1610 #: src/components/live/GoLiveDialog.tsx:247 1608 1611 #: src/components/live/GoLiveDialog.tsx:253 1609 1612 #: src/components/Menu/index.tsx:350 ··· 1611 1614 #: src/components/Prompt.tsx:144 1612 1615 #: src/components/Prompt.tsx:146 1613 1616 #: src/screens/Deactivated.tsx:158 1614 - #: src/screens/Profile/Header/EditProfileDialog.tsx:220 1615 - #: src/screens/Profile/Header/EditProfileDialog.tsx:228 1617 + #: src/screens/Profile/Header/EditProfileDialog.tsx:218 1618 + #: src/screens/Profile/Header/EditProfileDialog.tsx:226 1616 1619 #: src/screens/Search/Shell.tsx:349 1617 1620 #: src/screens/Settings/AppIconSettings/index.tsx:44 1618 1621 #: src/screens/Settings/AppIconSettings/index.tsx:225 ··· 1627 1630 #: src/view/com/composer/Composer.tsx:1016 1628 1631 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 1629 1632 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 1630 - #: src/view/com/modals/CreateOrEditList.tsx:333 1631 - #: src/view/com/modals/CropImage.web.tsx:97 1632 1633 #: src/view/shell/desktop/LeftNav.tsx:213 1633 1634 msgid "Cancel" 1634 1635 msgstr "" 1635 1636 1636 - #: src/view/com/modals/CreateOrEditList.tsx:338 1637 1637 #: src/view/com/modals/DeleteAccount.tsx:170 1638 1638 #: src/view/com/modals/DeleteAccount.tsx:278 1639 1639 msgctxt "action" ··· 1645 1645 msgid "Cancel account deletion" 1646 1646 msgstr "" 1647 1647 1648 - #: src/view/com/modals/CropImage.web.tsx:94 1649 - msgid "Cancel image crop" 1650 - msgstr "" 1651 - 1652 1648 #: src/components/PostControls/RepostButton.tsx:204 1653 1649 msgid "Cancel quote post" 1654 1650 msgstr "" ··· 1847 1843 msgid "Choose your own timeline! Feeds built by the community help you find content you love." 1848 1844 msgstr "" 1849 1845 1850 - #: src/screens/Signup/StepInfo/index.tsx:245 1846 + #: src/screens/Signup/StepInfo/index.tsx:244 1851 1847 msgid "Choose your password" 1852 1848 msgstr "" 1853 1849 1854 - #: src/screens/Signup/index.tsx:175 1850 + #: src/screens/Signup/index.tsx:180 1855 1851 msgid "Choose your username" 1856 1852 msgstr "" 1857 1853 ··· 2062 2058 msgid "Complete onboarding and start using your account" 2063 2059 msgstr "" 2064 2060 2065 - #: src/screens/Signup/index.tsx:177 2061 + #: src/screens/Signup/index.tsx:182 2066 2062 msgid "Complete the challenge" 2067 2063 msgstr "" 2068 2064 ··· 2150 2146 msgstr "" 2151 2147 2152 2148 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:155 2153 - #: src/screens/Signup/index.tsx:212 2154 - #: src/screens/Signup/index.tsx:215 2149 + #: src/screens/Signup/index.tsx:221 2150 + #: src/screens/Signup/index.tsx:224 2155 2151 msgid "Contact support" 2156 2152 msgstr "" 2157 2153 ··· 2255 2251 msgid "Cooking" 2256 2252 msgstr "" 2257 2253 2258 - #: src/view/com/modals/InviteCodes.tsx:183 2259 - msgid "Copied" 2260 - msgstr "" 2261 - 2262 2254 #: src/screens/Settings/AboutSettings.tsx:151 2263 2255 msgid "Copied build version to clipboard" 2264 2256 msgstr "" ··· 2269 2261 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:72 2270 2262 #: src/lib/sharing.ts:25 2271 2263 #: src/lib/sharing.ts:41 2272 - #: src/view/com/modals/InviteCodes.tsx:153 2273 2264 msgid "Copied to clipboard" 2274 2265 msgstr "" 2275 2266 ··· 2324 2315 msgid "Copy Link" 2325 2316 msgstr "" 2326 2317 2327 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:172 2328 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:176 2318 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:165 2319 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:169 2329 2320 msgid "Copy link to list" 2330 2321 msgstr "" 2331 2322 ··· 2444 2435 #: src/components/LoggedOutCTA.tsx:76 2445 2436 #: src/components/WelcomeModal.tsx:155 2446 2437 #: src/components/WelcomeModal.tsx:163 2447 - #: src/view/com/auth/SplashScreen.tsx:55 2438 + #: src/view/com/auth/SplashScreen.tsx:72 2448 2439 #: src/view/com/auth/SplashScreen.web.tsx:117 2449 2440 #: src/view/shell/bottom-bar/BottomBar.tsx:345 2450 2441 #: src/view/shell/bottom-bar/BottomBar.tsx:350 ··· 2455 2446 msgid "Create account" 2456 2447 msgstr "" 2457 2448 2458 - #: src/screens/Signup/index.tsx:122 2449 + #: src/screens/Signup/index.tsx:124 2459 2450 msgid "Create Account" 2460 2451 msgstr "" 2461 2452 ··· 2478 2469 msgid "Create another" 2479 2470 msgstr "" 2480 2471 2481 - #: src/view/com/auth/SplashScreen.tsx:47 2472 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:350 2473 + msgid "Create moderation list" 2474 + msgstr "" 2475 + 2476 + #: src/view/com/auth/SplashScreen.tsx:64 2482 2477 #: src/view/com/auth/SplashScreen.web.tsx:109 2483 2478 msgid "Create new account" 2484 2479 msgstr "" ··· 2491 2486 #: src/components/dialogs/StarterPackDialog.tsx:107 2492 2487 #: src/components/dialogs/StarterPackDialog.tsx:196 2493 2488 msgid "Create starter pack" 2489 + msgstr "" 2490 + 2491 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:349 2492 + msgid "Create user list" 2494 2493 msgstr "" 2495 2494 2496 2495 #: src/screens/Settings/AppPasswords.tsx:174 ··· 2543 2542 msgid "Dark theme" 2544 2543 msgstr "" 2545 2544 2546 - #: src/screens/Signup/StepInfo/index.tsx:273 2545 + #: src/screens/Signup/StepInfo/index.tsx:272 2547 2546 msgid "Date of birth" 2548 2547 msgstr "" 2549 2548 ··· 2572 2571 #: src/components/dms/MessageContextMenu.tsx:185 2573 2572 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:704 2574 2573 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 2575 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:285 2574 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:280 2576 2575 #: src/screens/Settings/AppPasswords.tsx:212 2577 2576 #: src/screens/StarterPack/StarterPackScreen.tsx:599 2578 2577 #: src/screens/StarterPack/StarterPackScreen.tsx:688 ··· 2621 2620 msgid "Delete for me" 2622 2621 msgstr "" 2623 2622 2624 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:211 2625 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:214 2623 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:204 2624 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:207 2626 2625 msgid "Delete list" 2627 2626 msgstr "" 2628 2627 ··· 2653 2652 msgid "Delete starter pack?" 2654 2653 msgstr "" 2655 2654 2656 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:280 2655 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:275 2657 2656 msgid "Delete this list?" 2658 2657 msgstr "" 2659 2658 ··· 2677 2676 msgid "Deleted list" 2678 2677 msgstr "" 2679 2678 2680 - #: src/screens/Profile/Header/EditProfileDialog.tsx:365 2681 - #: src/view/com/modals/CreateOrEditList.tsx:278 2682 - #: src/view/com/modals/CreateOrEditList.tsx:299 2679 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:428 2680 + #: src/screens/Profile/Header/EditProfileDialog.tsx:363 2681 + #: src/screens/Profile/Header/EditProfileDialog.tsx:370 2683 2682 msgid "Description" 2684 2683 msgstr "" 2685 2684 ··· 2751 2750 msgid "Disabled" 2752 2751 msgstr "" 2753 2752 2754 - #: src/screens/Profile/Header/EditProfileDialog.tsx:83 2753 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:92 2754 + #: src/screens/Profile/Header/EditProfileDialog.tsx:82 2755 2755 #: src/view/com/composer/Composer.tsx:762 2756 2756 #: src/view/com/composer/Composer.tsx:957 2757 2757 msgid "Discard" 2758 2758 msgstr "" 2759 2759 2760 - #: src/screens/Profile/Header/EditProfileDialog.tsx:80 2760 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:89 2761 + #: src/screens/Profile/Header/EditProfileDialog.tsx:79 2761 2762 msgid "Discard changes?" 2762 2763 msgstr "" 2763 2764 ··· 2784 2785 msgid "Discover New Feeds" 2785 2786 msgstr "" 2786 2787 2787 - #: src/components/Dialog/index.tsx:370 2788 + #: src/components/Dialog/index.tsx:373 2788 2789 msgid "Dismiss" 2789 2790 msgstr "" 2790 2791 ··· 2809 2810 msgid "Display larger alt text badges" 2810 2811 msgstr "" 2811 2812 2812 - #: src/screens/Profile/Header/EditProfileDialog.tsx:315 2813 - #: src/screens/Profile/Header/EditProfileDialog.tsx:321 2814 - #: src/screens/Profile/Header/EditProfileDialog.tsx:372 2813 + #: src/screens/Profile/Header/EditProfileDialog.tsx:313 2814 + #: src/screens/Profile/Header/EditProfileDialog.tsx:319 2815 2815 msgid "Display name" 2816 2816 msgstr "" 2817 2817 ··· 2871 2871 #: src/view/com/composer/select-language/PostLanguageSelectDialog.tsx:281 2872 2872 #: src/view/com/composer/videos/SubtitleDialog.tsx:168 2873 2873 #: src/view/com/composer/videos/SubtitleDialog.tsx:178 2874 - #: src/view/com/modals/CropImage.web.tsx:112 2875 - #: src/view/com/modals/InviteCodes.tsx:81 2876 - #: src/view/com/modals/InviteCodes.tsx:124 2877 2874 msgid "Done" 2878 2875 msgstr "" 2879 2876 ··· 2891 2888 msgid "Double tap or long press the message to add a reaction" 2892 2889 msgstr "" 2893 2890 2894 - #: src/components/Dialog/index.tsx:371 2891 + #: src/components/Dialog/index.tsx:374 2895 2892 msgid "Double tap to close the dialog" 2896 2893 msgstr "" 2897 2894 ··· 2920 2917 msgid "e.g. alice" 2921 2918 msgstr "" 2922 2919 2923 - #: src/screens/Profile/Header/EditProfileDialog.tsx:322 2920 + #: src/screens/Profile/Header/EditProfileDialog.tsx:320 2924 2921 msgid "e.g. Alice Lastname" 2925 2922 msgstr "" 2926 2923 ··· 2932 2929 msgid "E.g. artistic nudes." 2933 2930 msgstr "" 2934 2931 2935 - #: src/view/com/modals/CreateOrEditList.tsx:261 2932 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:392 2936 2933 msgid "e.g. Great Posters" 2937 2934 msgstr "" 2938 2935 2939 - #: src/view/com/modals/CreateOrEditList.tsx:262 2940 - msgid "e.g. Spammers" 2941 - msgstr "" 2942 - 2943 - #: src/view/com/modals/CreateOrEditList.tsx:290 2944 - msgid "e.g. The posters who never miss." 2945 - msgstr "" 2946 - 2947 - #: src/view/com/modals/CreateOrEditList.tsx:291 2948 - msgid "e.g. Users that repeatedly reply with ads." 2949 - msgstr "" 2950 - 2951 - #: src/view/com/modals/InviteCodes.tsx:97 2952 - msgid "Each code works once. You'll receive more invite codes periodically." 2936 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:429 2937 + msgid "e.g. The posters that never miss." 2953 2938 msgstr "" 2954 2939 2955 2940 #: src/screens/Settings/AccountSettings.tsx:145 ··· 2991 2976 msgid "Edit interests" 2992 2977 msgstr "" 2993 2978 2994 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:203 2995 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:206 2979 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:196 2980 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:199 2996 2981 msgid "Edit list details" 2997 2982 msgstr "" 2998 2983 ··· 3001 2986 msgid "Edit live status" 3002 2987 msgstr "" 3003 2988 3004 - #: src/view/com/modals/CreateOrEditList.tsx:228 3005 - msgid "Edit Moderation List" 2989 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:347 2990 + msgid "Edit moderation list" 3006 2991 msgstr "" 3007 2992 3008 2993 #: src/Navigation.tsx:356 ··· 3023 3008 msgid "Edit post interaction settings" 3024 3009 msgstr "" 3025 3010 3026 - #: src/screens/Profile/Header/EditProfileDialog.tsx:270 3027 - #: src/screens/Profile/Header/EditProfileDialog.tsx:276 3011 + #: src/screens/Profile/Header/EditProfileDialog.tsx:268 3012 + #: src/screens/Profile/Header/EditProfileDialog.tsx:274 3028 3013 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:183 3029 3014 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 3030 3015 msgid "Edit profile" ··· 3039 3024 msgid "Edit starter pack" 3040 3025 msgstr "" 3041 3026 3042 - #: src/view/com/modals/CreateOrEditList.tsx:223 3043 - msgid "Edit User List" 3027 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:346 3028 + msgid "Edit user list" 3044 3029 msgstr "" 3045 3030 3046 3031 #: src/components/WhoCanReply.tsx:97 ··· 3061 3046 msgstr "" 3062 3047 3063 3048 #: src/screens/Settings/AccountSettings.tsx:66 3064 - #: src/screens/Signup/StepInfo/index.tsx:197 3049 + #: src/screens/Signup/StepInfo/index.tsx:196 3065 3050 msgid "Email" 3066 3051 msgstr "" 3067 3052 ··· 3217 3202 msgstr "" 3218 3203 3219 3204 #: src/screens/Login/ForgotPasswordForm.tsx:99 3220 - #: src/screens/Signup/StepInfo/index.tsx:217 3205 + #: src/screens/Signup/StepInfo/index.tsx:216 3221 3206 msgid "Enter your email address" 3222 3207 msgstr "" 3223 3208 ··· 3225 3210 msgid "Enter your password" 3226 3211 msgstr "" 3227 3212 3228 - #: src/screens/Login/index.tsx:123 3213 + #: src/screens/Login/index.tsx:137 3229 3214 msgid "Enter your username and password" 3230 3215 msgstr "" 3231 3216 ··· 3258 3243 msgid "Error occurred while saving file" 3259 3244 msgstr "" 3260 3245 3261 - #: src/screens/Signup/StepCaptcha/index.tsx:124 3246 + #: src/screens/Signup/StepCaptcha/index.tsx:123 3262 3247 msgid "Error receiving captcha response." 3263 3248 msgstr "" 3264 3249 ··· 3320 3305 msgid "Exits account deletion process" 3321 3306 msgstr "" 3322 3307 3323 - #: src/view/com/modals/CropImage.web.tsx:95 3324 - msgid "Exits image cropping process" 3325 - msgstr "" 3326 - 3327 3308 #: src/view/com/lightbox/Lightbox.web.tsx:111 3328 3309 msgid "Exits image view" 3329 3310 msgstr "" ··· 3454 3435 msgid "Failed to create starter pack" 3455 3436 msgstr "" 3456 3437 3457 - #: src/view/com/modals/CreateOrEditList.tsx:184 3458 - msgid "Failed to create the list. Check your internet connection and try again." 3459 - msgstr "" 3460 - 3461 3438 #: src/screens/Messages/components/RequestButtons.tsx:64 3462 3439 #: src/screens/Messages/components/RequestButtons.tsx:291 3463 3440 msgctxt "toast" ··· 3586 3563 msgid "Failed to toggle thread mute, please try again" 3587 3564 msgstr "" 3588 3565 3589 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:111 3566 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:104 3590 3567 msgid "Failed to unpin list" 3591 3568 msgstr "" 3592 3569 ··· 3950 3927 msgid "Forget the noise" 3951 3928 msgstr "" 3952 3929 3953 - #: src/screens/Login/index.tsx:153 3954 - #: src/screens/Login/index.tsx:168 3930 + #: src/screens/Login/index.tsx:167 3931 + #: src/screens/Login/index.tsx:182 3955 3932 msgid "Forgot Password" 3956 3933 msgstr "" 3957 3934 ··· 4218 4195 msgid "Have a code? <0>Click here.</0>" 4219 4196 msgstr "" 4220 4197 4221 - #: src/screens/Signup/index.tsx:210 4198 + #: src/screens/Signup/index.tsx:219 4222 4199 msgid "Having trouble?" 4223 4200 msgstr "" 4224 4201 ··· 4416 4393 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." 4417 4394 msgstr "" 4418 4395 4419 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:282 4396 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:277 4420 4397 msgid "If you delete this list, you won't be able to recover it." 4421 4398 msgstr "" 4422 4399 ··· 4583 4560 msgid "Invalid Verification Code" 4584 4561 msgstr "" 4585 4562 4586 - #: src/view/com/modals/InviteCodes.tsx:94 4587 - msgid "Invite a Friend" 4588 - msgstr "" 4589 - 4590 - #: src/screens/Signup/StepInfo/index.tsx:167 4563 + #: src/screens/Signup/StepInfo/index.tsx:166 4591 4564 msgid "Invite code" 4592 4565 msgstr "" 4593 4566 4594 - #: src/screens/Signup/state.ts:340 4567 + #: src/screens/Signup/state.ts:342 4595 4568 msgid "Invite code not accepted. Check that you input it correctly and try again." 4596 - msgstr "" 4597 - 4598 - #: src/view/com/modals/InviteCodes.tsx:171 4599 - msgid "Invite codes: {0} available" 4600 - msgstr "" 4601 - 4602 - #: src/view/com/modals/InviteCodes.tsx:170 4603 - msgid "Invite codes: 1 available" 4604 4569 msgstr "" 4605 4570 4606 4571 #: src/components/StarterPack/ShareDialog.tsx:81 ··· 4619 4584 msgid "Is your location not accurate? <0>Tap here to confirm your location.</0>" 4620 4585 msgstr "" 4621 4586 4622 - #: src/screens/Signup/StepInfo/index.tsx:293 4587 + #: src/screens/Signup/StepInfo/index.tsx:292 4623 4588 msgid "It's correct" 4624 4589 msgstr "" 4625 4590 ··· 4734 4699 #: src/components/verification/VerificationsDialog.tsx:170 4735 4700 #: src/components/verification/VerifierDialog.tsx:137 4736 4701 #: src/screens/Moderation/VerificationSettings.tsx:48 4737 - #: src/screens/Profile/Header/EditProfileDialog.tsx:351 4702 + #: src/screens/Profile/Header/EditProfileDialog.tsx:349 4738 4703 #: src/screens/Settings/components/ChangeHandleDialog.tsx:213 4739 4704 #: src/screens/Settings/components/ChangeHandleDialog.tsx:277 4740 4705 msgctxt "english-only-resource" ··· 4829 4794 msgid "Let me choose" 4830 4795 msgstr "" 4831 4796 4832 - #: src/screens/Login/index.tsx:154 4833 - #: src/screens/Login/index.tsx:169 4797 + #: src/screens/Login/index.tsx:168 4798 + #: src/screens/Login/index.tsx:183 4834 4799 msgid "Let's get your password reset!" 4835 4800 msgstr "" 4836 4801 ··· 4887 4852 4888 4853 #: src/screens/Post/PostLikedBy.tsx:41 4889 4854 #: src/screens/Profile/ProfileLabelerLikedBy.tsx:32 4890 - #: src/view/screens/ProfileFeedLikedBy.tsx:33 4855 + #: src/view/screens/ProfileFeedLikedBy.tsx:34 4891 4856 msgid "Liked By" 4892 4857 msgstr "" 4893 4858 ··· 4933 4898 msgid "List" 4934 4899 msgstr "" 4935 4900 4936 - #: src/view/com/modals/CreateOrEditList.tsx:239 4937 - msgid "List Avatar" 4901 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:372 4902 + msgid "List avatar" 4938 4903 msgstr "" 4939 4904 4940 4905 #: src/screens/ProfileList/components/SubscribeMenu.tsx:50 ··· 4959 4924 msgid "List creator" 4960 4925 msgstr "" 4961 4926 4962 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:97 4927 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:90 4963 4928 msgctxt "toast" 4964 4929 msgid "List deleted" 4965 4930 msgstr "" 4966 4931 4932 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:421 4933 + msgid "List description" 4934 + msgstr "" 4935 + 4936 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:441 4937 + msgid "List description is too long. {DESCRIPTION_MAX_GRAPHEMES, plural, other {The maximum number of characters is #.}}" 4938 + msgstr "" 4939 + 4967 4940 #: src/screens/List/ListHiddenScreen.tsx:129 4968 4941 msgid "List has been hidden" 4969 4942 msgstr "" 4970 4943 4971 4944 #: src/screens/ProfileList/index.tsx:172 4972 4945 msgid "List Hidden" 4946 + msgstr "" 4947 + 4948 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:413 4949 + msgid "List must have a name." 4973 4950 msgstr "" 4974 4951 4975 4952 #: src/screens/ProfileList/components/SubscribeMenu.tsx:31 ··· 4977 4954 msgid "List muted" 4978 4955 msgstr "" 4979 4956 4980 - #: src/view/com/modals/CreateOrEditList.tsx:253 4981 - msgid "List Name" 4957 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:385 4958 + msgid "List name" 4959 + msgstr "" 4960 + 4961 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:405 4962 + msgid "List name is too long. {DISPLAY_NAME_MAX_GRAPHEMES, plural, other {The maximum number of characters is #.}}" 4982 4963 msgstr "" 4983 4964 4984 4965 #: src/screens/ProfileList/components/Header.tsx:116 4985 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:138 4966 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:131 4986 4967 msgctxt "toast" 4987 4968 msgid "List unblocked" 4988 4969 msgstr "" 4989 4970 4990 4971 #: src/screens/ProfileList/components/Header.tsx:98 4991 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:120 4972 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:113 4992 4973 msgctxt "toast" 4993 4974 msgid "List unmuted" 4994 4975 msgstr "" 4995 4976 4996 4977 #: src/Navigation.tsx:172 4997 - #: src/view/screens/Lists.tsx:65 4978 + #: src/view/screens/Lists.tsx:67 4998 4979 #: src/view/screens/Profile.tsx:224 4999 4980 #: src/view/screens/Profile.tsx:232 5000 4981 #: src/view/shell/desktop/LeftNav.tsx:746 ··· 5242 5223 msgid "Moderation list by you" 5243 5224 msgstr "" 5244 5225 5245 - #: src/view/com/modals/CreateOrEditList.tsx:175 5226 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:239 5246 5227 msgctxt "toast" 5247 5228 msgid "Moderation list created" 5248 5229 msgstr "" 5249 5230 5250 - #: src/view/com/modals/CreateOrEditList.tsx:161 5231 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:225 5251 5232 msgctxt "toast" 5252 5233 msgid "Moderation list updated" 5253 5234 msgstr "" ··· 5257 5238 msgstr "" 5258 5239 5259 5240 #: src/Navigation.tsx:182 5260 - #: src/view/screens/ModerationModlists.tsx:65 5241 + #: src/view/screens/ModerationModlists.tsx:67 5261 5242 msgid "Moderation Lists" 5262 5243 msgstr "" 5263 5244 ··· 5288 5269 msgid "More languages..." 5289 5270 msgstr "" 5290 5271 5291 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:156 5272 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:149 5292 5273 #: src/view/com/profile/ProfileMenu.tsx:223 5293 5274 #: src/view/com/profile/ProfileMenu.tsx:229 5294 5275 msgid "More options" ··· 5418 5399 msgid "My Feeds" 5419 5400 msgstr "" 5420 5401 5421 - #: src/view/com/modals/CreateOrEditList.tsx:268 5402 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:391 5422 5403 msgid "Name" 5423 5404 msgstr "" 5424 5405 5425 - #: src/view/com/modals/CreateOrEditList.tsx:133 5426 - msgid "Name is required" 5427 - msgstr "" 5428 - 5429 5406 #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:59 5430 5407 #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:98 5431 5408 #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:106 ··· 5481 5458 msgid "New" 5482 5459 msgstr "" 5483 5460 5484 - #: src/view/screens/Lists.tsx:77 5485 - #: src/view/screens/ModerationModlists.tsx:77 5461 + #: src/view/screens/Lists.tsx:79 5462 + #: src/view/screens/ModerationModlists.tsx:79 5486 5463 msgctxt "action" 5487 5464 msgid "New" 5488 5465 msgstr "" ··· 5527 5504 msgid "New handle" 5528 5505 msgstr "" 5529 5506 5530 - #: src/view/screens/Lists.tsx:69 5531 - #: src/view/screens/ModerationModlists.tsx:69 5507 + #: src/view/screens/Lists.tsx:71 5508 + #: src/view/screens/ModerationModlists.tsx:71 5532 5509 msgid "New list" 5533 5510 msgstr "" 5534 5511 ··· 5536 5513 msgid "New messages" 5537 5514 msgstr "" 5538 5515 5539 - #: src/view/com/modals/CreateOrEditList.tsx:230 5540 - msgid "New Moderation List" 5541 - msgstr "" 5542 - 5543 5516 #: src/screens/Login/SetNewPasswordForm.tsx:141 5544 5517 #: src/screens/Settings/components/ChangePasswordDialog.tsx:203 5545 5518 #: src/screens/Settings/components/ChangePasswordDialog.tsx:207 ··· 5579 5552 5580 5553 #: src/components/NewskieDialog.tsx:120 5581 5554 msgid "New user info dialog" 5582 - msgstr "" 5583 - 5584 - #: src/view/com/modals/CreateOrEditList.tsx:225 5585 - msgid "New User List" 5586 5555 msgstr "" 5587 5556 5588 5557 #: src/screens/PostThread/components/HeaderDropdown.tsx:93 ··· 5646 5615 msgid "No featured GIFs found. There may be an issue with Tenor." 5647 5616 msgstr "" 5648 5617 5649 - #: src/screens/StarterPack/Wizard/StepFeeds.tsx:119 5618 + #: src/screens/StarterPack/Wizard/StepFeeds.tsx:122 5650 5619 msgid "No feeds found. Try searching for something else." 5651 5620 msgstr "" 5652 5621 ··· 5762 5731 msgid "Nobody has reposted this yet. Maybe you should be the first!" 5763 5732 msgstr "" 5764 5733 5765 - #: src/screens/StarterPack/Wizard/StepProfiles.tsx:104 5734 + #: src/screens/StarterPack/Wizard/StepProfiles.tsx:107 5766 5735 msgid "Nobody was found. Try searching for someone else." 5767 5736 msgstr "" 5768 5737 ··· 6086 6055 msgid "Opens emoji picker" 6087 6056 msgstr "" 6088 6057 6089 - #: src/view/com/auth/SplashScreen.tsx:49 6058 + #: src/view/com/auth/SplashScreen.tsx:66 6090 6059 #: src/view/com/auth/SplashScreen.web.tsx:111 6091 6060 msgid "Opens flow to create a new Bluesky account" 6092 6061 msgstr "" 6093 6062 6094 - #: src/view/com/auth/SplashScreen.tsx:63 6063 + #: src/view/com/auth/SplashScreen.tsx:83 6095 6064 #: src/view/com/auth/SplashScreen.web.tsx:125 6096 6065 msgid "Opens flow to sign in to your existing Bluesky account" 6097 6066 msgstr "" ··· 6106 6075 6107 6076 #: src/components/dialogs/LinkWarning.tsx:97 6108 6077 msgid "Opens link {0}" 6109 - msgstr "" 6110 - 6111 - #: src/view/com/modals/InviteCodes.tsx:173 6112 - msgid "Opens list of invite codes" 6113 6078 msgstr "" 6114 6079 6115 6080 #: src/view/com/util/UserAvatar.tsx:581 ··· 6200 6165 #: src/screens/Login/LoginForm.tsx:228 6201 6166 #: src/screens/Settings/AccountSettings.tsx:121 6202 6167 #: src/screens/Settings/AccountSettings.tsx:125 6203 - #: src/screens/Signup/StepInfo/index.tsx:232 6168 + #: src/screens/Signup/StepInfo/index.tsx:231 6204 6169 #: src/view/com/modals/DeleteAccount.tsx:239 6205 6170 #: src/view/com/modals/DeleteAccount.tsx:246 6206 6171 msgid "Password" ··· 6214 6179 msgid "Password must be at least 8 characters long." 6215 6180 msgstr "" 6216 6181 6217 - #: src/screens/Login/index.tsx:181 6182 + #: src/screens/Login/index.tsx:195 6218 6183 msgid "Password updated" 6219 6184 msgstr "" 6220 6185 ··· 6323 6288 msgid "Play {0}" 6324 6289 msgstr "" 6325 6290 6326 - #: src/components/Post/Embed/VideoEmbed/index.tsx:132 6291 + #: src/components/Post/Embed/VideoEmbed/index.tsx:135 6327 6292 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321 6328 6293 msgid "Play video" 6329 6294 msgstr "" ··· 6356 6321 msgid "Please check your email inbox for further instructions. It may take a minute or two to arrive." 6357 6322 msgstr "" 6358 6323 6359 - #: src/screens/Signup/state.ts:287 6324 + #: src/screens/Signup/state.ts:289 6360 6325 msgid "Please choose your handle." 6361 6326 msgstr "" 6362 6327 6363 - #: src/screens/Signup/state.ts:279 6364 - #: src/screens/Signup/StepInfo/index.tsx:123 6328 + #: src/screens/Signup/state.ts:281 6329 + #: src/screens/Signup/StepInfo/index.tsx:122 6365 6330 msgid "Please choose your password." 6366 6331 msgstr "" 6367 6332 ··· 6369 6334 msgid "Please click on the link in the email we just sent you to verify your new email address. This is an important step to allow you to continue enjoying all the features of Bluesky." 6370 6335 msgstr "" 6371 6336 6372 - #: src/screens/Signup/state.ts:302 6337 + #: src/screens/Signup/state.ts:304 6373 6338 msgid "Please complete the verification captcha." 6374 6339 msgstr "" 6375 6340 6376 6341 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:101 6377 - #: src/screens/Signup/StepInfo/index.tsx:112 6342 + #: src/screens/Signup/StepInfo/index.tsx:111 6378 6343 msgid "Please double-check that you have entered your email address correctly." 6379 6344 msgstr "" 6380 6345 ··· 6421 6386 msgid "Please enter the security code we sent to your previous email address." 6422 6387 msgstr "" 6423 6388 6424 - #: src/screens/Signup/state.ts:263 6425 - #: src/screens/Signup/StepInfo/index.tsx:94 6389 + #: src/screens/Signup/state.ts:265 6390 + #: src/screens/Signup/StepInfo/index.tsx:93 6426 6391 msgid "Please enter your email." 6427 6392 msgstr "" 6428 6393 6429 - #: src/screens/Signup/StepInfo/index.tsx:87 6394 + #: src/screens/Signup/StepInfo/index.tsx:86 6430 6395 msgid "Please enter your invite code." 6431 6396 msgstr "" 6432 6397 ··· 6693 6658 msgid "Profile" 6694 6659 msgstr "" 6695 6660 6696 - #: src/screens/Profile/Header/EditProfileDialog.tsx:191 6661 + #: src/screens/Profile/Header/EditProfileDialog.tsx:189 6697 6662 msgctxt "toast" 6698 6663 msgid "Profile updated" 6699 6664 msgstr "" ··· 6968 6933 6969 6934 #: src/screens/Profile/components/ProfileFeedHeader.tsx:319 6970 6935 #: src/screens/Profile/components/ProfileFeedHeader.tsx:325 6971 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:188 6972 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:191 6936 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:181 6937 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:184 6973 6938 #: src/screens/SavedFeeds.tsx:340 6974 6939 msgid "Remove from my feeds" 6975 6940 msgstr "" ··· 7191 7156 msgid "Report feed" 7192 7157 msgstr "" 7193 7158 7194 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:222 7195 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:225 7159 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:215 7160 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:218 7196 7161 msgid "Report list" 7197 7162 msgstr "" 7198 7163 ··· 7325 7290 msgid "Require an email code to sign in to your account." 7326 7291 msgstr "" 7327 7292 7328 - #: src/screens/Signup/StepInfo/index.tsx:181 7293 + #: src/screens/Signup/StepInfo/index.tsx:180 7329 7294 msgid "Required for this provider" 7330 7295 msgstr "" 7331 7296 ··· 7428 7393 msgstr "" 7429 7394 7430 7395 #: src/components/dialogs/BirthDateSettings.tsx:156 7396 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 7397 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 7431 7398 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:483 7432 7399 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:489 7433 7400 #: src/components/live/EditLiveDialog.tsx:216 7434 7401 #: src/components/live/EditLiveDialog.tsx:223 7435 7402 #: src/components/StarterPack/QrCodeDialog.tsx:204 7436 - #: src/screens/Profile/Header/EditProfileDialog.tsx:238 7437 - #: src/screens/Profile/Header/EditProfileDialog.tsx:252 7403 + #: src/screens/Profile/Header/EditProfileDialog.tsx:236 7404 + #: src/screens/Profile/Header/EditProfileDialog.tsx:250 7438 7405 #: src/screens/SavedFeeds.tsx:120 7439 7406 #: src/screens/Settings/components/ChangeHandleDialog.tsx:267 7440 7407 #: src/view/com/composer/GifAltText.tsx:193 ··· 7443 7410 #: src/view/com/composer/photos/EditImageDialog.web.tsx:75 7444 7411 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:152 7445 7412 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:162 7446 - #: src/view/com/modals/CreateOrEditList.tsx:315 7447 7413 msgid "Save" 7448 7414 msgstr "" 7449 7415 7450 7416 #: src/view/com/lightbox/ImageViewing/index.tsx:610 7451 - #: src/view/com/modals/CreateOrEditList.tsx:323 7452 7417 msgctxt "action" 7453 7418 msgid "Save" 7454 7419 msgstr "" ··· 7469 7434 msgid "Save image" 7470 7435 msgstr "" 7471 7436 7472 - #: src/view/com/modals/CropImage.web.tsx:104 7473 - msgid "Save image crop" 7474 - msgstr "" 7475 - 7476 7437 #: src/screens/Settings/components/ChangeHandleDialog.tsx:253 7477 7438 msgid "Save new handle" 7478 7439 msgstr "" ··· 7505 7466 #: src/screens/Profile/components/ProfileFeedHeader.tsx:132 7506 7467 #: src/screens/ProfileList/components/Header.tsx:85 7507 7468 msgid "Saved to your feeds" 7508 - msgstr "" 7509 - 7510 - #: src/view/com/modals/CropImage.web.tsx:105 7511 - msgid "Saves image crop settings" 7512 7469 msgstr "" 7513 7470 7514 7471 #: src/components/dms/ChatEmptyPill.tsx:33 ··· 7722 7679 msgid "Select duration" 7723 7680 msgstr "" 7724 7681 7725 - #: src/screens/Login/index.tsx:144 7682 + #: src/screens/Login/index.tsx:158 7726 7683 msgid "Select from an existing account" 7727 7684 msgstr "" 7728 7685 ··· 7797 7754 msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." 7798 7755 msgstr "" 7799 7756 7800 - #: src/screens/Signup/StepInfo/index.tsx:274 7757 + #: src/screens/Signup/StepInfo/index.tsx:273 7801 7758 msgid "Select your date of birth" 7802 7759 msgstr "" 7803 7760 ··· 8035 7992 8036 7993 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:117 8037 7994 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:120 8038 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:172 8039 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:178 7995 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:165 7996 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:171 8040 7997 #: src/screens/StarterPack/StarterPackScreen.tsx:611 8041 7998 #: src/screens/StarterPack/StarterPackScreen.tsx:619 8042 7999 #: src/view/com/profile/ProfileMenu.tsx:246 ··· 8167 8124 #: src/components/dialogs/Signin.tsx:99 8168 8125 #: src/components/WelcomeModal.tsx:194 8169 8126 #: src/components/WelcomeModal.tsx:206 8170 - #: src/screens/Login/index.tsx:122 8171 - #: src/screens/Login/index.tsx:143 8127 + #: src/screens/Login/index.tsx:136 8128 + #: src/screens/Login/index.tsx:157 8172 8129 #: src/screens/Login/LoginForm.tsx:181 8173 8130 #: src/screens/Search/SearchResults.tsx:258 8174 - #: src/view/com/auth/SplashScreen.tsx:61 8175 - #: src/view/com/auth/SplashScreen.tsx:69 8131 + #: src/view/com/auth/SplashScreen.tsx:81 8132 + #: src/view/com/auth/SplashScreen.tsx:89 8176 8133 #: src/view/com/auth/SplashScreen.web.tsx:123 8177 8134 #: src/view/com/auth/SplashScreen.web.tsx:131 8178 8135 #: src/view/shell/bottom-bar/BottomBar.tsx:355 ··· 8341 8298 msgid "Something wrong? Let us know." 8342 8299 msgstr "" 8343 8300 8344 - #: src/App.native.tsx:126 8345 - #: src/App.web.tsx:102 8301 + #: src/App.native.tsx:125 8302 + #: src/App.web.tsx:101 8346 8303 msgid "Sorry! Your session expired. Please sign in again." 8347 8304 msgstr "" 8348 8305 ··· 8440 8397 msgid "Status Page" 8441 8398 msgstr "" 8442 8399 8443 - #: src/screens/Signup/index.tsx:163 8400 + #: src/screens/Signup/index.tsx:168 8444 8401 msgid "Step {0} of {1}" 8445 8402 msgstr "" 8446 8403 ··· 8629 8586 msgid "Tell a joke!" 8630 8587 msgstr "" 8631 8588 8632 - #: src/screens/Profile/Header/EditProfileDialog.tsx:373 8589 + #: src/screens/Profile/Header/EditProfileDialog.tsx:371 8633 8590 msgid "Tell us a bit about yourself" 8634 8591 msgstr "" 8635 8592 ··· 8708 8665 msgid "That starter pack could not be found." 8709 8666 msgstr "" 8710 8667 8711 - #: src/screens/Signup/StepHandle/index.tsx:78 8668 + #: src/screens/Signup/StepHandle/index.tsx:77 8712 8669 msgid "That username is already taken" 8713 8670 msgstr "" 8714 8671 ··· 8913 8870 #: src/screens/List/ListHiddenScreen.tsx:99 8914 8871 #: src/screens/ProfileList/components/Header.tsx:107 8915 8872 #: src/screens/ProfileList/components/Header.tsx:125 8916 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:129 8917 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:147 8873 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:122 8874 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:140 8918 8875 #: src/screens/ProfileList/components/SubscribeMenu.tsx:40 8919 8876 #: src/screens/ProfileList/components/SubscribeMenu.tsx:59 8920 8877 msgid "There was an issue. Please check your internet connection and try again." ··· 9310 9267 msgstr "" 9311 9268 9312 9269 #: src/screens/Login/ForgotPasswordForm.tsx:68 9313 - #: src/screens/Login/index.tsx:79 9270 + #: src/screens/Login/index.tsx:93 9314 9271 #: src/screens/Login/LoginForm.tsx:169 9315 9272 #: src/screens/Login/SetNewPasswordForm.tsx:81 9316 - #: src/screens/Signup/index.tsx:76 9273 + #: src/screens/Signup/index.tsx:77 9317 9274 msgid "Unable to contact your service. Please check your Internet connection." 9318 9275 msgstr "" 9319 9276 ··· 9366 9323 msgid "Unblock Account?" 9367 9324 msgstr "" 9368 9325 9369 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:254 9370 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:257 9326 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:247 9327 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:250 9371 9328 msgid "Unblock list" 9372 9329 msgstr "" 9373 9330 ··· 9452 9409 msgid "Unmute conversation" 9453 9410 msgstr "" 9454 9411 9455 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:264 9456 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:267 9412 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:257 9413 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:260 9457 9414 msgid "Unmute list" 9458 9415 msgstr "" 9459 9416 ··· 9491 9448 msgid "Unpin from profile" 9492 9449 msgstr "" 9493 9450 9494 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:237 9495 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:240 9451 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:230 9452 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:233 9496 9453 msgid "Unpin moderation list" 9497 9454 msgstr "" 9498 9455 ··· 9504 9461 msgid "Unpinned from your feeds" 9505 9462 msgstr "" 9506 9463 9507 - #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:109 9464 + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:102 9508 9465 msgid "Unpinned list" 9509 9466 msgstr "" 9510 9467 ··· 9655 9612 msgid "Use your account email address, or another real email address you control, in case KWS or Bluesky needs to contact you." 9656 9613 msgstr "" 9657 9614 9658 - #: src/view/com/modals/InviteCodes.tsx:201 9659 - msgid "Used by:" 9660 - msgstr "" 9661 - 9662 9615 #: src/components/dms/ReportDialog.tsx:329 9663 9616 msgctxt "toast" 9664 9617 msgid "User blocked" ··· 9697 9650 msgid "User list by you" 9698 9651 msgstr "" 9699 9652 9700 - #: src/view/com/modals/CreateOrEditList.tsx:174 9653 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:238 9701 9654 msgctxt "toast" 9702 9655 msgid "User list created" 9703 9656 msgstr "" 9704 9657 9705 - #: src/view/com/modals/CreateOrEditList.tsx:160 9658 + #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:224 9706 9659 msgctxt "toast" 9707 9660 msgid "User list updated" 9708 9661 msgstr "" 9709 9662 9710 - #: src/screens/Signup/StepHandle/index.tsx:234 9663 + #: src/screens/Signup/StepHandle/index.tsx:233 9711 9664 msgid "Username cannot be longer than {MAX_SERVICE_HANDLE_LENGTH, plural, other {# characters}}" 9712 9665 msgstr "" 9713 9666 9714 - #: src/screens/Signup/StepHandle/index.tsx:218 9667 + #: src/screens/Signup/StepHandle/index.tsx:217 9715 9668 msgid "Username cannot begin or end with a hyphen" 9716 9669 msgstr "" 9717 9670 9718 - #: src/screens/Signup/StepHandle/index.tsx:222 9671 + #: src/screens/Signup/StepHandle/index.tsx:221 9719 9672 msgid "Username must only contain letters (a-z), numbers, and hyphens" 9720 9673 msgstr "" 9721 9674 ··· 9870 9823 msgid "Video is playing" 9871 9824 msgstr "" 9872 9825 9873 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:225 9826 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:237 9874 9827 msgid "Video not found." 9875 9828 msgstr "" 9876 9829 ··· 10014 9967 msgid "View your verifications" 10015 9968 msgstr "" 10016 9969 10017 - #: src/view/com/util/images/AutoSizedImage.tsx:205 10018 - #: src/view/com/util/images/AutoSizedImage.tsx:227 9970 + #: src/view/com/util/images/AutoSizedImage.tsx:207 9971 + #: src/view/com/util/images/AutoSizedImage.tsx:229 10019 9972 msgid "Views full image" 10020 9973 msgstr "" 10021 9974 ··· 10160 10113 msgid "We’re introducing a new layer of verification on Bluesky — an easy-to-see checkmark." 10161 10114 msgstr "" 10162 10115 10163 - #: src/screens/Signup/index.tsx:123 10116 + #: src/screens/Signup/index.tsx:125 10164 10117 msgid "We're so excited to have you join us!" 10165 10118 msgstr "" 10166 10119 ··· 10233 10186 msgid "What do you want to call your starter pack?" 10234 10187 msgstr "" 10235 10188 10236 - #: src/view/com/auth/SplashScreen.tsx:38 10189 + #: src/view/com/auth/SplashScreen.tsx:51 10237 10190 #: src/view/com/auth/SplashScreen.web.tsx:99 10238 10191 #: src/view/com/composer/Composer.tsx:812 10239 10192 msgid "What's up?" ··· 10414 10367 msgid "You are verified" 10415 10368 msgstr "" 10416 10369 10417 - #: src/screens/Profile/Header/EditProfileDialog.tsx:346 10370 + #: src/screens/Profile/Header/EditProfileDialog.tsx:344 10418 10371 msgid "You are verified. You will lose your verification status if you change your display name. <0>Learn more.</0>" 10419 10372 msgstr "" 10420 10373 ··· 10447 10400 msgid "You can now choose to be notified when specific people post. If there’s someone you want timely updates from, go to their profile and find the new bell icon near the follow button." 10448 10401 msgstr "" 10449 10402 10450 - #: src/screens/Login/index.tsx:182 10403 + #: src/screens/Login/index.tsx:196 10451 10404 #: src/screens/Login/PasswordUpdatedForm.tsx:26 10452 10405 msgid "You can now sign in with your new password." 10453 10406 msgstr "" ··· 10491 10444 msgid "You don't have any chat requests at the moment." 10492 10445 msgstr "" 10493 10446 10494 - #: src/view/com/modals/InviteCodes.tsx:67 10495 - msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." 10496 - msgstr "" 10497 - 10498 10447 #: src/screens/SavedFeeds.tsx:149 10499 10448 msgid "You don't have any pinned feeds." 10500 10449 msgstr "" ··· 10708 10657 msgid "You: {short}" 10709 10658 msgstr "" 10710 10659 10711 - #: src/screens/Signup/index.tsx:139 10660 + #: src/screens/Signup/index.tsx:141 10712 10661 msgid "You'll follow the suggested users and feeds once you finish creating your account!" 10713 10662 msgstr "" 10714 10663 10715 - #: src/screens/Signup/index.tsx:144 10664 + #: src/screens/Signup/index.tsx:146 10716 10665 msgid "You'll follow the suggested users once you finish creating your account!" 10717 10666 msgstr "" 10718 10667 ··· 10782 10731 msgid "You've run out of videos to watch. Maybe it's a good time to take a break?" 10783 10732 msgstr "" 10784 10733 10785 - #: src/screens/Signup/index.tsx:173 10734 + #: src/screens/Signup/index.tsx:178 10786 10735 msgid "Your account" 10787 10736 msgstr "" 10788 10737 ··· 10810 10759 msgid "Your appeal has been submitted. If your appeal succeeds, you will receive an email." 10811 10760 msgstr "" 10812 10761 10813 - #: src/screens/Signup/StepInfo/index.tsx:261 10762 + #: src/screens/Signup/StepInfo/index.tsx:260 10814 10763 msgid "Your birth date" 10815 10764 msgstr "" 10816 10765 10817 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:229 10766 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:241 10818 10767 msgid "Your browser does not support the video format. Please try a different browser." 10819 10768 msgstr "" 10820 10769 ··· 10842 10791 10843 10792 #: src/screens/Login/ForgotPasswordForm.tsx:51 10844 10793 #: src/screens/Settings/components/ChangePasswordDialog.tsx:81 10845 - #: src/screens/Signup/state.ts:271 10846 - #: src/screens/Signup/StepInfo/index.tsx:101 10794 + #: src/screens/Signup/state.ts:273 10795 + #: src/screens/Signup/StepInfo/index.tsx:100 10847 10796 msgid "Your email appears to be invalid." 10848 10797 msgstr "" 10849 10798 ··· 10896 10845 msgid "Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on." 10897 10846 msgstr "" 10898 10847 10899 - #: src/screens/Signup/StepInfo/index.tsx:130 10848 + #: src/screens/Signup/StepInfo/index.tsx:129 10900 10849 msgid "Your password must be at least 8 characters long." 10901 10850 msgstr "" 10902 10851
-17
src/screens/Login/ScreenTransition.tsx
··· 1 - import {type StyleProp, type ViewStyle} from 'react-native' 2 - import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' 3 - import type React from 'react' 4 - 5 - export function ScreenTransition({ 6 - style, 7 - children, 8 - }: { 9 - style?: StyleProp<ViewStyle> 10 - children: React.ReactNode 11 - }) { 12 - return ( 13 - <Animated.View style={style} entering={FadeInRight} exiting={FadeOutLeft}> 14 - {children} 15 - </Animated.View> 16 - ) 17 - }
-1
src/screens/Login/ScreenTransition.web.tsx
··· 1 - export {Fragment as ScreenTransition} from 'react'
+43 -23
src/screens/Login/index.tsx
··· 1 - import React, {useRef} from 'react' 1 + import {useEffect, useRef, useState} from 'react' 2 2 import {KeyboardAvoidingView} from 'react-native' 3 - import {LayoutAnimationConfig} from 'react-native-reanimated' 3 + import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 ··· 15 15 import {LoginForm} from '#/screens/Login/LoginForm' 16 16 import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 17 17 import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 18 - import {atoms as a} from '#/alf' 18 + import {atoms as a, native} from '#/alf' 19 + import {ScreenTransition} from '#/components/ScreenTransition' 19 20 import {ChooseAccountForm} from './ChooseAccountForm' 20 - import {ScreenTransition} from './ScreenTransition' 21 21 22 22 enum Forms { 23 23 Login, ··· 27 27 PasswordUpdated, 28 28 } 29 29 30 + const OrderedForms = [ 31 + Forms.ChooseAccount, 32 + Forms.Login, 33 + Forms.ForgotPassword, 34 + Forms.SetNewPassword, 35 + Forms.PasswordUpdated, 36 + ] as const 37 + 30 38 export const Login = ({onPressBack}: {onPressBack: () => void}) => { 31 39 const {_} = useLingui() 32 40 const failedAttemptCountRef = useRef(0) ··· 38 46 acc => acc.did === requestedAccountSwitchTo, 39 47 ) 40 48 41 - const [error, setError] = React.useState<string>('') 42 - const [serviceUrl, setServiceUrl] = React.useState<string>( 49 + const [error, setError] = useState('') 50 + const [serviceUrl, setServiceUrl] = useState( 43 51 requestedAccount?.service || DEFAULT_SERVICE, 44 52 ) 45 - const [initialHandle, setInitialHandle] = React.useState<string>( 53 + const [initialHandle, setInitialHandle] = useState( 46 54 requestedAccount?.handle || '', 47 55 ) 48 - const [currentForm, setCurrentForm] = React.useState<Forms>( 56 + const [currentForm, setCurrentForm] = useState<Forms>( 49 57 requestedAccount 50 58 ? Forms.Login 51 59 : accounts.length 52 60 ? Forms.ChooseAccount 53 61 : Forms.Login, 54 62 ) 63 + const [screenTransitionDirection, setScreenTransitionDirection] = useState< 64 + 'Forward' | 'Backward' 65 + >('Forward') 55 66 56 67 const { 57 68 data: serviceDescription, ··· 64 75 setServiceUrl(account.service) 65 76 } 66 77 setInitialHandle(account?.handle || '') 67 - setCurrentForm(Forms.Login) 78 + gotoForm(Forms.Login) 68 79 } 69 80 70 81 const gotoForm = (form: Forms) => { 71 82 setError('') 83 + const index = OrderedForms.indexOf(currentForm) 84 + const nextIndex = OrderedForms.indexOf(form) 85 + setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward') 72 86 setCurrentForm(form) 73 87 } 74 88 75 - React.useEffect(() => { 89 + useEffect(() => { 76 90 if (serviceError) { 77 91 setError( 78 92 _( ··· 89 103 }, [serviceError, serviceUrl, _]) 90 104 91 105 const onPressForgotPassword = () => { 92 - setCurrentForm(Forms.ForgotPassword) 106 + gotoForm(Forms.ForgotPassword) 93 107 logEvent('signin:forgotPasswordPressed', {}) 94 108 } 95 109 96 110 const handlePressBack = () => { 97 111 onPressBack() 112 + setScreenTransitionDirection('Backward') 98 113 logEvent('signin:backPressed', { 99 114 failedAttemptsCount: failedAttemptCountRef.current, 100 115 }) ··· 106 121 timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), 107 122 failedAttemptsCount: failedAttemptCountRef.current, 108 123 }) 109 - setCurrentForm(Forms.Login) 110 124 } 111 125 112 126 const onAttemptFailed = () => { ··· 187 201 } 188 202 189 203 return ( 190 - <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> 191 - <LoggedOutLayout 192 - leadin="" 193 - title={title} 194 - description={description} 195 - scrollable> 196 - <LayoutAnimationConfig skipEntering skipExiting> 197 - <ScreenTransition key={currentForm}>{content}</ScreenTransition> 198 - </LayoutAnimationConfig> 199 - </LoggedOutLayout> 200 - </KeyboardAvoidingView> 204 + <Animated.View style={a.flex_1} entering={native(FadeIn.duration(90))}> 205 + <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> 206 + <LoggedOutLayout 207 + leadin="" 208 + title={title} 209 + description={description} 210 + scrollable> 211 + <LayoutAnimationConfig skipEntering> 212 + <ScreenTransition 213 + key={currentForm} 214 + direction={screenTransitionDirection}> 215 + {content} 216 + </ScreenTransition> 217 + </LayoutAnimationConfig> 218 + </LoggedOutLayout> 219 + </KeyboardAvoidingView> 220 + </Animated.View> 201 221 ) 202 222 }
+5 -7
src/screens/Profile/Header/EditProfileDialog.tsx
··· 1 1 import {useCallback, useEffect, useState} from 'react' 2 - import {Dimensions, View} from 'react-native' 2 + import {useWindowDimensions, View} from 'react-native' 3 3 import {type AppBskyActorDefs} from '@atproto/api' 4 4 import {msg, Plural, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' ··· 28 28 const DISPLAY_NAME_MAX_GRAPHEMES = 64 29 29 const DESCRIPTION_MAX_GRAPHEMES = 256 30 30 31 - const SCREEN_HEIGHT = Dimensions.get('window').height 32 - 33 31 export function EditProfileDialog({ 34 32 profile, 35 33 control, ··· 42 40 const {_} = useLingui() 43 41 const cancelControl = Dialog.useDialogControl() 44 42 const [dirty, setDirty] = useState(false) 43 + const {height} = useWindowDimensions() 45 44 46 45 const onPressCancel = useCallback(() => { 47 46 if (dirty) { ··· 56 55 control={control} 57 56 nativeOptions={{ 58 57 preventDismiss: dirty, 59 - minHeight: SCREEN_HEIGHT, 58 + minHeight: height, 60 59 }} 61 60 webOptions={{ 62 61 onBackgroundPress: () => { ··· 186 185 newUserAvatar, 187 186 newUserBanner, 188 187 }) 189 - onUpdate?.() 190 - control.close() 188 + control.close(() => onUpdate?.()) 191 189 Toast.show(_(msg({message: 'Profile updated', context: 'toast'}))) 192 190 } catch (e: any) { 193 191 logger.error('Failed to update user profile', {message: String(e)}) ··· 369 367 defaultValue={description} 370 368 onChangeText={setDescription} 371 369 multiline 372 - label={_(msg`Display name`)} 370 + label={_(msg`Description`)} 373 371 placeholder={_(msg`Tell us a bit about yourself`)} 374 372 testID="editProfileDescriptionInput" 375 373 />
+5 -10
src/screens/ProfileList/components/MoreOptionsMenu.tsx
··· 8 8 import {toShareUrl} from '#/lib/strings/url-helpers' 9 9 import {logger} from '#/logger' 10 10 import {isWeb} from '#/platform/detection' 11 - import {useModalControls} from '#/state/modals' 12 11 import { 13 12 useListBlockMutation, 14 13 useListDeleteMutation, ··· 18 17 import {useSession} from '#/state/session' 19 18 import {Button, ButtonIcon} from '#/components/Button' 20 19 import {useDialogControl} from '#/components/Dialog' 20 + import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' 21 21 import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' 22 22 import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink' 23 23 import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid' ··· 44 44 }) { 45 45 const {_} = useLingui() 46 46 const {currentAccount} = useSession() 47 - const {openModal} = useModalControls() 47 + const editListDialogControl = useDialogControl() 48 48 const deleteListPromptControl = useDialogControl() 49 49 const reportDialogControl = useReportDialogControl() 50 50 const navigation = useNavigation<NavigationProp>() ··· 80 80 } 81 81 } 82 82 83 - const onPressEdit = () => { 84 - openModal({ 85 - name: 'create-or-edit-list', 86 - list, 87 - }) 88 - } 89 - 90 83 const onPressDelete = async () => { 91 84 await deleteList({uri: list.uri}) 92 85 ··· 201 194 <Menu.Group> 202 195 <Menu.Item 203 196 label={_(msg`Edit list details`)} 204 - onPress={onPressEdit}> 197 + onPress={editListDialogControl.open}> 205 198 <Menu.ItemText> 206 199 <Trans>Edit list details</Trans> 207 200 </Menu.ItemText> ··· 274 267 )} 275 268 </Menu.Outer> 276 269 </Menu.Root> 270 + 271 + <CreateOrEditListDialog control={editListDialogControl} list={list} /> 277 272 278 273 <Prompt.Basic 279 274 control={deleteListPromptControl}
+2 -3
src/screens/Signup/StepCaptcha/index.tsx
··· 8 8 import {createFullHandle} from '#/lib/strings/handles' 9 9 import {logger} from '#/logger' 10 10 import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 11 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 12 11 import {useSignupContext} from '#/screens/Signup/state' 13 12 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 14 13 import {atoms as a, useTheme} from '#/alf' ··· 143 142 }, [dispatch, state.handle]) 144 143 145 144 return ( 146 - <ScreenTransition> 145 + <> 147 146 <View style={[a.gap_lg, a.pt_lg]}> 148 147 <View 149 148 style={[ ··· 171 170 isLoading={state.isLoading} 172 171 onBackPress={onBackPress} 173 172 /> 174 - </ScreenTransition> 173 + </> 175 174 ) 176 175 } 177 176
+2 -3
src/screens/Signup/StepHandle/index.tsx
··· 19 19 checkHandleAvailability, 20 20 useHandleAvailabilityQuery, 21 21 } from '#/state/queries/handle-availability' 22 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 23 22 import {useSignupContext} from '#/screens/Signup/state' 24 23 import {atoms as a, native, useTheme} from '#/alf' 25 24 import * as TextField from '#/components/forms/TextField' ··· 141 140 !validCheck.totalLength 142 141 143 142 return ( 144 - <ScreenTransition> 143 + <> 145 144 <View style={[a.gap_sm, a.pt_lg, a.z_10]}> 146 145 <View> 147 146 <TextField.Root isInvalid={textFieldInvalid}> ··· 252 251 onNextPress={onNextPress} 253 252 /> 254 253 </Animated.View> 255 - </ScreenTransition> 254 + </> 256 255 ) 257 256 } 258 257
+2 -2
src/screens/Signup/StepInfo/index.tsx
··· 9 9 import {isEmailMaybeInvalid} from '#/lib/strings/email' 10 10 import {logger} from '#/logger' 11 11 import {isWeb} from '#/platform/detection' 12 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 13 12 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 14 13 import {Policies} from '#/screens/Signup/StepInfo/Policies' 15 14 import {atoms as a, native} from '#/alf' ··· 26 25 import {InlineLinkText} from '#/components/Link' 27 26 import {Loader} from '#/components/Loader' 28 27 import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate' 28 + import {ScreenTransition} from '#/components/ScreenTransition' 29 29 import {Text} from '#/components/Typography' 30 30 import {BackNextButtons} from '../BackNextButtons' 31 31 ··· 164 164 } 165 165 166 166 return ( 167 - <ScreenTransition> 167 + <ScreenTransition direction={state.screenTransitionDirection}> 168 168 <View style={[a.gap_md]}> 169 169 {state.serviceUrl === DEFAULT_SERVICE && ( 170 170 <View style={[a.gap_xl]}>
+116 -102
src/screens/Signup/index.tsx
··· 23 23 import {StepCaptcha} from '#/screens/Signup/StepCaptcha' 24 24 import {StepHandle} from '#/screens/Signup/StepHandle' 25 25 import {StepInfo} from '#/screens/Signup/StepInfo' 26 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 26 + import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' 27 27 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 28 28 import {Divider} from '#/components/Divider' 29 29 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 30 30 import {InlineLinkText} from '#/components/Link' 31 + import {ScreenTransition} from '#/components/ScreenTransition' 31 32 import {Text} from '#/components/Typography' 32 33 import {GCP_PROJECT_ID} from '#/env' 33 34 import * as bsky from '#/types/bsky' ··· 122 123 }, []) 123 124 124 125 return ( 125 - <SignupContext.Provider value={{state, dispatch}}> 126 - <LoggedOutLayout 127 - leadin="" 128 - title={_(msg`Create Account`)} 129 - description={_(msg`Welcome to the ATmosphere!`)} 130 - scrollable> 131 - <View testID="createAccount" style={a.flex_1}> 132 - {showStarterPackCard && 133 - bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 134 - starterPack.record, 135 - AppBskyGraphStarterpack.isRecord, 136 - ) ? ( 137 - <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}> 138 - <LinearGradientBackground 139 - style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}> 140 - <Text style={[a.font_bold, a.text_xl, {color: 'white'}]}> 141 - {starterPack.record.name} 142 - </Text> 143 - <Text style={[{color: 'white'}]}> 144 - {starterPack.feeds?.length ? ( 145 - <Trans> 146 - You'll follow the suggested users and feeds once you 147 - finish creating your account! 148 - </Trans> 149 - ) : ( 150 - <Trans> 151 - You'll follow the suggested users once you finish creating 152 - your account! 153 - </Trans> 154 - )} 155 - </Text> 156 - </LinearGradientBackground> 157 - </Animated.View> 158 - ) : null} 159 - <View 160 - style={[ 161 - a.flex_1, 162 - a.px_xl, 163 - a.pt_2xl, 164 - !gtMobile && {paddingBottom: 100}, 165 - ]}> 166 - <View style={[a.gap_sm, a.pb_sm]}> 167 - <Text 168 - style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}> 169 - <Trans> 170 - Step {state.activeStep + 1} of{' '} 171 - {state.serviceDescription && 172 - !state.serviceDescription.phoneVerificationRequired 173 - ? '2' 174 - : '3'} 175 - </Trans> 176 - </Text> 177 - <Text style={[a.text_3xl, a.font_heavy]}> 178 - {state.activeStep === SignupStep.INFO ? ( 179 - <Trans>The ATmosphere ✨</Trans> 180 - ) : state.activeStep === SignupStep.HANDLE ? ( 181 - <Trans>Choose your username</Trans> 182 - ) : ( 183 - <Trans>Complete the challenge</Trans> 184 - )} 185 - </Text> 186 - </View> 126 + <Animated.View exiting={native(FadeIn.duration(90))} style={a.flex_1}> 127 + <SignupContext.Provider value={{state, dispatch}}> 128 + <LoggedOutLayout 129 + leadin="" 130 + title={_(msg`Create Account`)} 131 + description={_(msg`Welcome to the ATmosphere!`)} 132 + scrollable> 133 + <View testID="createAccount" style={a.flex_1}> 134 + {showStarterPackCard && 135 + bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 136 + starterPack.record, 137 + AppBskyGraphStarterpack.isRecord, 138 + ) ? ( 139 + <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}> 140 + <LinearGradientBackground 141 + style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}> 142 + <Text style={[a.font_bold, a.text_xl, {color: 'white'}]}> 143 + {starterPack.record.name} 144 + </Text> 145 + <Text style={[{color: 'white'}]}> 146 + {starterPack.feeds?.length ? ( 147 + <Trans> 148 + You'll follow the suggested users and feeds once you 149 + finish creating your account! 150 + </Trans> 151 + ) : ( 152 + <Trans> 153 + You'll follow the suggested users once you finish 154 + creating your account! 155 + </Trans> 156 + )} 157 + </Text> 158 + </LinearGradientBackground> 159 + </Animated.View> 160 + ) : null} 161 + <LayoutAnimationConfig skipEntering> 162 + <ScreenTransition 163 + key={state.activeStep} 164 + direction={state.screenTransitionDirection}> 165 + <View 166 + style={[ 167 + a.flex_1, 168 + a.px_xl, 169 + a.pt_2xl, 170 + !gtMobile && {paddingBottom: 100}, 171 + ]}> 172 + <View style={[a.gap_sm, a.pb_3xl]}> 173 + <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> 174 + <Trans> 175 + Step {state.activeStep + 1} of{' '} 176 + {state.serviceDescription && 177 + !state.serviceDescription.phoneVerificationRequired 178 + ? '2' 179 + : '3'} 180 + </Trans> 181 + </Text> 182 + <Text style={[a.text_3xl, a.font_bold]}> 183 + {state.activeStep === SignupStep.INFO ? ( 184 + <Trans>The ATmosphere ✨</Trans> 185 + ) : state.activeStep === SignupStep.HANDLE ? ( 186 + <Trans>Choose your username</Trans> 187 + ) : ( 188 + <Trans>Complete the challenge</Trans> 189 + )} 190 + </Text> 191 + </View> 187 192 188 - <LayoutAnimationConfig skipEntering skipExiting> 189 - {state.activeStep === SignupStep.INFO ? ( 190 - <StepInfo 191 - onPressBack={onPressBack} 192 - onPressSignIn={onPressSignIn} 193 - isLoadingStarterPack={ 194 - isFetchingStarterPack && !isErrorStarterPack 195 - } 196 - isServerError={isError} 197 - refetchServer={refetch} 198 - /> 199 - ) : state.activeStep === SignupStep.HANDLE ? ( 200 - <StepHandle /> 201 - ) : ( 202 - <StepCaptcha /> 203 - )} 204 - </LayoutAnimationConfig> 193 + <LayoutAnimationConfig skipEntering skipExiting> 194 + {state.activeStep === SignupStep.INFO ? ( 195 + <StepInfo 196 + onPressBack={onPressBack} 197 + onPressSignIn={onPressSignIn} 198 + isLoadingStarterPack={ 199 + isFetchingStarterPack && !isErrorStarterPack 200 + } 201 + isServerError={isError} 202 + refetchServer={refetch} 203 + /> 204 + ) : state.activeStep === SignupStep.HANDLE ? ( 205 + <StepHandle /> 206 + ) : ( 207 + <StepCaptcha /> 208 + )} 209 + </LayoutAnimationConfig> 205 210 206 - <Divider /> 211 + <Divider /> 207 212 208 - <View 209 - style={[a.w_full, a.py_lg, a.flex_row, a.gap_md, a.align_center]}> 210 - <AppLanguageDropdown /> 211 - <Text 212 - style={[ 213 - a.flex_1, 214 - t.atoms.text_contrast_medium, 215 - !gtMobile && a.text_md, 216 - ]}> 217 - <Trans>Having trouble?</Trans>{' '} 218 - <InlineLinkText 219 - label={_(msg`Contact support`)} 220 - to={FEEDBACK_FORM_URL({email: state.email})} 221 - style={[!gtMobile && a.text_md]}> 222 - <Trans>Open a Github Issue</Trans> 223 - </InlineLinkText> 224 - </Text> 225 - </View> 213 + <View 214 + style={[ 215 + a.w_full, 216 + a.py_lg, 217 + a.flex_row, 218 + a.gap_md, 219 + a.align_center, 220 + ]}> 221 + <AppLanguageDropdown /> 222 + <Text 223 + style={[ 224 + a.flex_1, 225 + t.atoms.text_contrast_medium, 226 + !gtMobile && a.text_md, 227 + ]}> 228 + <Trans>Having trouble?</Trans>{' '} 229 + <InlineLinkText 230 + label={_(msg`Contact support`)} 231 + to={FEEDBACK_FORM_URL({email: state.email})} 232 + style={[!gtMobile && a.text_md]}> 233 + <Trans>Open a Github Issue</Trans> 234 + </InlineLinkText> 235 + </Text> 236 + </View> 237 + </View> 238 + </ScreenTransition> 239 + </LayoutAnimationConfig> 226 240 </View> 227 - </View> 228 - </LoggedOutLayout> 229 - </SignupContext.Provider> 241 + </LoggedOutLayout> 242 + </SignupContext.Provider> 243 + </Animated.View> 230 244 ) 231 245 }
+4 -2
src/screens/Signup/state.ts
··· 41 41 export type SignupState = { 42 42 hasPrev: boolean 43 43 activeStep: SignupStep 44 + screenTransitionDirection: 'Forward' | 'Backward' 44 45 45 46 serviceUrl: string 46 47 serviceDescription?: ServiceDescription ··· 84 85 export const initialState: SignupState = { 85 86 hasPrev: false, 86 87 activeStep: SignupStep.INFO, 88 + screenTransitionDirection: 'Forward', 87 89 88 90 serviceUrl: DEFAULT_SERVICE, 89 91 serviceDescription: undefined, ··· 126 128 switch (a.type) { 127 129 case 'prev': { 128 130 if (s.activeStep !== SignupStep.INFO) { 129 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 131 + next.screenTransitionDirection = 'Backward' 130 132 next.activeStep-- 131 133 next.error = '' 132 134 next.errorField = undefined ··· 135 137 } 136 138 case 'next': { 137 139 if (s.activeStep !== SignupStep.CAPTCHA) { 138 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 140 + next.screenTransitionDirection = 'Forward' 139 141 next.activeStep++ 140 142 next.error = '' 141 143 next.errorField = undefined
+2 -2
src/screens/StarterPack/Wizard/StepDetails.tsx
··· 8 8 import {atoms as a, useTheme} from '#/alf' 9 9 import * as TextField from '#/components/forms/TextField' 10 10 import {StarterPack} from '#/components/icons/StarterPack' 11 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 11 + import {ScreenTransition} from '#/components/ScreenTransition' 12 12 import {Text} from '#/components/Typography' 13 13 14 14 export function StepDetails() { ··· 23 23 }) 24 24 25 25 return ( 26 - <ScreenTransition direction={state.transitionDirection}> 26 + <ScreenTransition direction={state.transitionDirection} enabledWeb> 27 27 <View style={[a.px_xl, a.gap_xl, a.mt_4xl]}> 28 28 <View style={[a.gap_md, a.align_center, a.px_md, a.mb_md]}> 29 29 <StarterPack width={90} gradient="sky" />
+5 -2
src/screens/StarterPack/Wizard/StepFeeds.tsx
··· 17 17 import {SearchInput} from '#/components/forms/SearchInput' 18 18 import {useThrottledValue} from '#/components/hooks/useThrottledValue' 19 19 import {Loader} from '#/components/Loader' 20 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 20 + import {ScreenTransition} from '#/components/ScreenTransition' 21 21 import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' 22 22 import {Text} from '#/components/Typography' 23 23 ··· 79 79 } 80 80 81 81 return ( 82 - <ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}> 82 + <ScreenTransition 83 + style={[a.flex_1]} 84 + direction={state.transitionDirection} 85 + enabledWeb> 83 86 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 84 87 <View style={[a.py_sm, a.px_md, {height: 60}]}> 85 88 <SearchInput
+5 -2
src/screens/StarterPack/Wizard/StepProfiles.tsx
··· 13 13 import {atoms as a, useTheme} from '#/alf' 14 14 import {SearchInput} from '#/components/forms/SearchInput' 15 15 import {Loader} from '#/components/Loader' 16 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 16 + import {ScreenTransition} from '#/components/ScreenTransition' 17 17 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' 18 18 import {Text} from '#/components/Typography' 19 19 import type * as bsky from '#/types/bsky' ··· 64 64 } 65 65 66 66 return ( 67 - <ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}> 67 + <ScreenTransition 68 + style={[a.flex_1]} 69 + direction={state.transitionDirection} 70 + enabledWeb> 68 71 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 69 72 <View style={[a.py_sm, a.px_md, {height: 60}]}> 70 73 <SearchInput
-59
src/state/invites.tsx
··· 1 - import React from 'react' 2 - 3 - import * as persisted from '#/state/persisted' 4 - 5 - type StateContext = persisted.Schema['invites'] 6 - type ApiContext = { 7 - setInviteCopied: (code: string) => void 8 - } 9 - 10 - const stateContext = React.createContext<StateContext>( 11 - persisted.defaults.invites, 12 - ) 13 - stateContext.displayName = 'InvitesStateContext' 14 - const apiContext = React.createContext<ApiContext>({ 15 - setInviteCopied(_: string) {}, 16 - }) 17 - apiContext.displayName = 'InvitesApiContext' 18 - 19 - export function Provider({children}: React.PropsWithChildren<{}>) { 20 - const [state, setState] = React.useState(persisted.get('invites')) 21 - 22 - const api = React.useMemo( 23 - () => ({ 24 - setInviteCopied(code: string) { 25 - setState(state => { 26 - state = { 27 - ...state, 28 - copiedInvites: state.copiedInvites.includes(code) 29 - ? state.copiedInvites 30 - : state.copiedInvites.concat([code]), 31 - } 32 - persisted.write('invites', state) 33 - return state 34 - }) 35 - }, 36 - }), 37 - [setState], 38 - ) 39 - 40 - React.useEffect(() => { 41 - return persisted.onUpdate('invites', nextInvites => { 42 - setState(nextInvites) 43 - }) 44 - }, [setState]) 45 - 46 - return ( 47 - <stateContext.Provider value={state}> 48 - <apiContext.Provider value={api}>{children}</apiContext.Provider> 49 - </stateContext.Provider> 50 - ) 51 - } 52 - 53 - export function useInvitesState() { 54 - return React.useContext(stateContext) 55 - } 56 - 57 - export function useInvitesAPI() { 58 - return React.useContext(apiContext) 59 - }
-21
src/state/modals/index.tsx
··· 1 1 import React from 'react' 2 - import {type AppBskyGraphDefs} from '@atproto/api' 3 2 4 3 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 5 - 6 - export interface CreateOrEditListModal { 7 - name: 'create-or-edit-list' 8 - purpose?: string 9 - list?: AppBskyGraphDefs.ListView 10 - onSave?: (uri: string) => void 11 - } 12 4 13 5 export interface UserAddRemoveListsModal { 14 6 name: 'user-add-remove-lists' ··· 23 15 name: 'delete-account' 24 16 } 25 17 26 - export interface WaitlistModal { 27 - name: 'waitlist' 28 - } 29 - 30 - export interface InviteCodesModal { 31 - name: 'invite-codes' 32 - } 33 - 34 18 export interface ContentLanguagesSettingsModal { 35 19 name: 'content-languages-settings' 36 20 } ··· 46 30 | ContentLanguagesSettingsModal 47 31 48 32 // Lists 49 - | CreateOrEditListModal 50 33 | UserAddRemoveListsModal 51 - 52 - // Bluesky access 53 - | WaitlistModal 54 - | InviteCodesModal 55 34 56 35 const ModalContext = React.createContext<{ 57 36 isModalActive: boolean
-65
src/state/queries/invites.ts
··· 1 - import {type ComAtprotoServerDefs} from '@atproto/api' 2 - import {useQuery} from '@tanstack/react-query' 3 - 4 - import {cleanError} from '#/lib/strings/errors' 5 - import {STALE} from '#/state/queries' 6 - import {useAgent} from '#/state/session' 7 - 8 - function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { 9 - return invite.available - invite.uses.length > 0 && !invite.disabled 10 - } 11 - 12 - const inviteCodesQueryKeyRoot = 'inviteCodes' 13 - 14 - export type InviteCodesQueryResponse = Exclude< 15 - ReturnType<typeof useInviteCodesQuery>['data'], 16 - undefined 17 - > 18 - export function useInviteCodesQuery() { 19 - const agent = useAgent() 20 - return useQuery({ 21 - staleTime: STALE.MINUTES.FIVE, 22 - queryKey: [inviteCodesQueryKeyRoot], 23 - queryFn: async () => { 24 - const res = await agent.com.atproto.server 25 - .getAccountInviteCodes({}) 26 - .catch(e => { 27 - if (cleanError(e) === 'Bad token scope') { 28 - return null 29 - } else { 30 - throw e 31 - } 32 - }) 33 - 34 - if (res === null) { 35 - return { 36 - disabled: true, 37 - all: [], 38 - available: [], 39 - used: [], 40 - } 41 - } 42 - 43 - if (!res.data?.codes) { 44 - throw new Error(`useInviteCodesQuery: no codes returned`) 45 - } 46 - 47 - const available = res.data.codes.filter(isInviteAvailable) 48 - const used = res.data.codes 49 - .filter(code => !isInviteAvailable(code)) 50 - .sort((a, b) => { 51 - return ( 52 - new Date(b.uses[0].usedAt).getTime() - 53 - new Date(a.uses[0].usedAt).getTime() 54 - ) 55 - }) 56 - 57 - return { 58 - disabled: false, 59 - all: [...available, ...used], 60 - available, 61 - used, 62 - } 63 - }, 64 - }) 65 - }
+76 -55
src/view/com/auth/SplashScreen.tsx
··· 1 1 import {View} from 'react-native' 2 + import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 2 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 3 4 import {msg, Trans} from '@lingui/macro' 4 5 import {useLingui} from '@lingui/react' 5 6 7 + import {useHaptics} from '#/lib/haptics' 6 8 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 9 + import {CenteredView} from '#/view/com/util/Views' 7 10 import {Logo} from '#/view/icons/Logo' 8 11 import {Logotype} from '#/view/icons/Logotype' 9 12 import {atoms as a, useTheme} from '#/alf' 10 13 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 11 14 import {Button, ButtonText} from '#/components/Button' 12 15 import {Text} from '#/components/Typography' 13 - import {CenteredView} from '../util/Views' 14 16 15 17 export const SplashScreen = ({ 16 18 onPressSignin, ··· 22 24 const t = useTheme() 23 25 const {_} = useLingui() 24 26 27 + const playHaptic = useHaptics() 25 28 const insets = useSafeAreaInsets() 26 29 27 30 return ( 28 31 <CenteredView style={[a.h_full, a.flex_1]}> 29 - <ErrorBoundary> 30 - <View style={[{flex: 1}, a.justify_center, a.align_center]}> 31 - <Logo width={92} fill="sky" /> 32 + <Animated.View 33 + entering={FadeIn.duration(90)} 34 + exiting={FadeOut.duration(90)} 35 + style={[a.flex_1]}> 36 + <ErrorBoundary> 37 + <View style={[a.flex_1, a.justify_center, a.align_center]}> 38 + <Logo width={92} fill="sky" /> 39 + 40 + <View style={[a.pb_sm, a.pt_5xl]}> 41 + <Logotype width={161} fill={t.atoms.text.color} /> 42 + </View> 32 43 33 - <View style={[a.pb_sm, a.pt_5xl]}> 34 - <Logotype width={161} fill={t.atoms.text.color} /> 44 + <Text 45 + style={[ 46 + a.text_md, 47 + a.font_bold, 48 + t.atoms.text_contrast_medium, 49 + a.text_center, 50 + ]}> 51 + <Trans>What's up?</Trans> 52 + </Text> 35 53 </View> 36 54 37 - <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> 38 - <Trans>What's up?</Trans> 39 - </Text> 40 - </View> 41 - <View 42 - testID="signinOrCreateAccount" 43 - style={[a.px_xl, a.gap_md, a.pb_2xl]}> 44 - <Button 45 - testID="createAccountButton" 46 - onPress={onPressCreateAccount} 47 - label={_(msg`Create new account`)} 48 - accessibilityHint={_( 49 - msg`Opens flow to create a new Bluesky account`, 50 - )} 51 - size="large" 52 - variant="solid" 53 - color="primary"> 54 - <ButtonText> 55 - <Trans>Create account</Trans> 56 - </ButtonText> 57 - </Button> 58 - <Button 59 - testID="signInButton" 60 - onPress={onPressSignin} 61 - label={_(msg`Sign in`)} 62 - accessibilityHint={_( 63 - msg`Opens flow to sign in to your existing Bluesky account`, 64 - )} 65 - size="large" 66 - variant="solid" 67 - color="secondary"> 68 - <ButtonText> 69 - <Trans>Sign in</Trans> 70 - </ButtonText> 71 - </Button> 72 - </View> 73 - <View 74 - style={[ 75 - a.px_lg, 76 - a.pt_md, 77 - a.pb_2xl, 78 - a.justify_center, 79 - a.align_center, 80 - ]}> 81 - <View> 82 - <AppLanguageDropdown /> 55 + <View 56 + testID="signinOrCreateAccount" 57 + style={[a.px_xl, a.gap_md, a.pb_2xl]}> 58 + <Button 59 + testID="createAccountButton" 60 + onPress={() => { 61 + onPressCreateAccount() 62 + playHaptic('Light') 63 + }} 64 + label={_(msg`Create new account`)} 65 + accessibilityHint={_( 66 + msg`Opens flow to create a new Bluesky account`, 67 + )} 68 + size="large" 69 + variant="solid" 70 + color="primary"> 71 + <ButtonText> 72 + <Trans>Create account</Trans> 73 + </ButtonText> 74 + </Button> 75 + <Button 76 + testID="signInButton" 77 + onPress={() => { 78 + onPressSignin() 79 + playHaptic('Light') 80 + }} 81 + label={_(msg`Sign in`)} 82 + accessibilityHint={_( 83 + msg`Opens flow to sign in to your existing Bluesky account`, 84 + )} 85 + size="large" 86 + variant="solid" 87 + color="secondary"> 88 + <ButtonText> 89 + <Trans>Sign in</Trans> 90 + </ButtonText> 91 + </Button> 92 + </View> 93 + <View 94 + style={[ 95 + a.px_lg, 96 + a.pt_md, 97 + a.pb_2xl, 98 + a.justify_center, 99 + a.align_center, 100 + ]}> 101 + <View> 102 + <AppLanguageDropdown /> 103 + </View> 83 104 </View> 84 - </View> 85 - <View style={{height: insets.bottom}} /> 86 - </ErrorBoundary> 105 + <View style={{height: insets.bottom}} /> 106 + </ErrorBoundary> 107 + </Animated.View> 87 108 </CenteredView> 88 109 ) 89 110 }
+1 -1
src/view/com/composer/photos/EditImageDialog.web.tsx
··· 19 19 20 20 export function EditImageDialog(props: EditImageDialogProps) { 21 21 return ( 22 - <Dialog.Outer control={props.control}> 22 + <Dialog.Outer control={props.control} webOptions={{alignCenter: true}}> 23 23 <Dialog.Handle /> 24 24 <DialogInner {...props} /> 25 25 </Dialog.Outer>
-403
src/view/com/modals/CreateOrEditList.tsx
··· 1 - import {useCallback, useMemo, useState} from 'react' 2 - import { 3 - ActivityIndicator, 4 - KeyboardAvoidingView, 5 - ScrollView, 6 - StyleSheet, 7 - TextInput, 8 - TouchableOpacity, 9 - View, 10 - } from 'react-native' 11 - import {LinearGradient} from 'expo-linear-gradient' 12 - import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' 13 - import {msg, Trans} from '@lingui/macro' 14 - import {useLingui} from '@lingui/react' 15 - 16 - import {usePalette} from '#/lib/hooks/usePalette' 17 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 18 - import {cleanError, isNetworkError} from '#/lib/strings/errors' 19 - import {enforceLen} from '#/lib/strings/helpers' 20 - import {richTextToString} from '#/lib/strings/rich-text-helpers' 21 - import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 22 - import {colors, gradients, s} from '#/lib/styles' 23 - import {useTheme} from '#/lib/ThemeContext' 24 - import {type ImageMeta} from '#/state/gallery' 25 - import {useModalControls} from '#/state/modals' 26 - import { 27 - useListCreateMutation, 28 - useListMetadataMutation, 29 - } from '#/state/queries/list' 30 - import {useAgent} from '#/state/session' 31 - import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 32 - import {Text} from '#/view/com/util/text/Text' 33 - import * as Toast from '#/view/com/util/Toast' 34 - import {EditableUserAvatar} from '#/view/com/util/UserAvatar' 35 - 36 - const MAX_NAME = 64 // todo 37 - const MAX_DESCRIPTION = 300 // todo 38 - 39 - export const snapPoints = ['fullscreen'] 40 - 41 - export function Component({ 42 - purpose, 43 - onSave, 44 - list, 45 - }: { 46 - purpose?: string 47 - onSave?: (uri: string) => void 48 - list?: AppBskyGraphDefs.ListView 49 - }) { 50 - const {closeModal} = useModalControls() 51 - const {isMobile} = useWebMediaQueries() 52 - const [error, setError] = useState<string>('') 53 - const pal = usePalette('default') 54 - const theme = useTheme() 55 - const {_} = useLingui() 56 - const listCreateMutation = useListCreateMutation() 57 - const listMetadataMutation = useListMetadataMutation() 58 - const agent = useAgent() 59 - 60 - const activePurpose = useMemo(() => { 61 - if (list?.purpose) { 62 - return list.purpose 63 - } 64 - if (purpose) { 65 - return purpose 66 - } 67 - return 'app.bsky.graph.defs#curatelist' 68 - }, [list, purpose]) 69 - const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' 70 - 71 - const [isProcessing, setProcessing] = useState<boolean>(false) 72 - const [name, setName] = useState<string>(list?.name || '') 73 - 74 - const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => { 75 - const text = list?.description 76 - const facets = list?.descriptionFacets 77 - 78 - if (!text || !facets) { 79 - return new RichTextAPI({text: text || ''}) 80 - } 81 - 82 - // We want to be working with a blank state here, so let's get the 83 - // serialized version and turn it back into a RichText 84 - const serialized = richTextToString(new RichTextAPI({text, facets}), false) 85 - 86 - const richText = new RichTextAPI({text: serialized}) 87 - richText.detectFacetsWithoutResolution() 88 - 89 - return richText 90 - }) 91 - const graphemeLength = useMemo(() => { 92 - return shortenLinks(descriptionRt).graphemeLength 93 - }, [descriptionRt]) 94 - const isDescriptionOver = graphemeLength > MAX_DESCRIPTION 95 - 96 - const [avatar, setAvatar] = useState<string | undefined>(list?.avatar) 97 - const [newAvatar, setNewAvatar] = useState<ImageMeta | undefined | null>() 98 - 99 - const onDescriptionChange = useCallback( 100 - (newText: string) => { 101 - const richText = new RichTextAPI({text: newText}) 102 - richText.detectFacetsWithoutResolution() 103 - 104 - setDescriptionRt(richText) 105 - }, 106 - [setDescriptionRt], 107 - ) 108 - 109 - const onPressCancel = useCallback(() => { 110 - closeModal() 111 - }, [closeModal]) 112 - 113 - const onSelectNewAvatar = useCallback( 114 - (img: ImageMeta | null) => { 115 - if (!img) { 116 - setNewAvatar(null) 117 - setAvatar(undefined) 118 - return 119 - } 120 - try { 121 - setNewAvatar(img) 122 - setAvatar(img.path) 123 - } catch (e: any) { 124 - setError(cleanError(e)) 125 - } 126 - }, 127 - [setNewAvatar, setAvatar, setError], 128 - ) 129 - 130 - const onPressSave = useCallback(async () => { 131 - const nameTrimmed = name.trim() 132 - if (!nameTrimmed) { 133 - setError(_(msg`Name is required`)) 134 - return 135 - } 136 - setProcessing(true) 137 - if (error) { 138 - setError('') 139 - } 140 - try { 141 - let richText = new RichTextAPI( 142 - {text: descriptionRt.text.trimEnd()}, 143 - {cleanNewlines: true}, 144 - ) 145 - 146 - await richText.detectFacets(agent) 147 - richText = shortenLinks(richText) 148 - richText = stripInvalidMentions(richText) 149 - 150 - if (list) { 151 - await listMetadataMutation.mutateAsync({ 152 - uri: list.uri, 153 - name: nameTrimmed, 154 - description: richText.text, 155 - descriptionFacets: richText.facets, 156 - avatar: newAvatar, 157 - }) 158 - Toast.show( 159 - isCurateList 160 - ? _(msg({message: 'User list updated', context: 'toast'})) 161 - : _(msg({message: 'Moderation list updated', context: 'toast'})), 162 - ) 163 - onSave?.(list.uri) 164 - } else { 165 - const res = await listCreateMutation.mutateAsync({ 166 - purpose: activePurpose, 167 - name, 168 - description: richText.text, 169 - descriptionFacets: richText.facets, 170 - avatar: newAvatar, 171 - }) 172 - Toast.show( 173 - isCurateList 174 - ? _(msg({message: 'User list created', context: 'toast'})) 175 - : _(msg({message: 'Moderation list created', context: 'toast'})), 176 - ) 177 - onSave?.(res.uri) 178 - } 179 - closeModal() 180 - } catch (e: any) { 181 - if (isNetworkError(e)) { 182 - setError( 183 - _( 184 - msg`Failed to create the list. Check your internet connection and try again.`, 185 - ), 186 - ) 187 - } else { 188 - setError(cleanError(e)) 189 - } 190 - } 191 - setProcessing(false) 192 - }, [ 193 - setProcessing, 194 - setError, 195 - error, 196 - onSave, 197 - closeModal, 198 - activePurpose, 199 - isCurateList, 200 - name, 201 - descriptionRt, 202 - newAvatar, 203 - list, 204 - listMetadataMutation, 205 - listCreateMutation, 206 - _, 207 - agent, 208 - ]) 209 - 210 - return ( 211 - <KeyboardAvoidingView behavior="height"> 212 - <ScrollView 213 - style={[ 214 - pal.view, 215 - { 216 - paddingHorizontal: isMobile ? 16 : 0, 217 - }, 218 - ]} 219 - testID="createOrEditListModal"> 220 - <Text style={[styles.title, pal.text]}> 221 - {isCurateList ? ( 222 - list ? ( 223 - <Trans>Edit User List</Trans> 224 - ) : ( 225 - <Trans>New User List</Trans> 226 - ) 227 - ) : list ? ( 228 - <Trans>Edit Moderation List</Trans> 229 - ) : ( 230 - <Trans>New Moderation List</Trans> 231 - )} 232 - </Text> 233 - {error !== '' && ( 234 - <View style={styles.errorContainer}> 235 - <ErrorMessage message={error} /> 236 - </View> 237 - )} 238 - <Text style={[styles.label, pal.text]}> 239 - <Trans>List Avatar</Trans> 240 - </Text> 241 - <View style={[styles.avi, {borderColor: pal.colors.background}]}> 242 - <EditableUserAvatar 243 - type="list" 244 - size={80} 245 - avatar={avatar} 246 - onSelectNewAvatar={onSelectNewAvatar} 247 - /> 248 - </View> 249 - <View style={styles.form}> 250 - <View> 251 - <View style={styles.labelWrapper}> 252 - <Text style={[styles.label, pal.text]} nativeID="list-name"> 253 - <Trans>List Name</Trans> 254 - </Text> 255 - </View> 256 - <TextInput 257 - testID="editNameInput" 258 - style={[styles.textInput, pal.border, pal.text]} 259 - placeholder={ 260 - isCurateList 261 - ? _(msg`e.g. Great Posters`) 262 - : _(msg`e.g. Spammers`) 263 - } 264 - placeholderTextColor={colors.gray4} 265 - value={name} 266 - onChangeText={v => setName(enforceLen(v, MAX_NAME))} 267 - accessible={true} 268 - accessibilityLabel={_(msg`Name`)} 269 - accessibilityHint="" 270 - accessibilityLabelledBy="list-name" 271 - /> 272 - </View> 273 - <View style={s.pb10}> 274 - <View style={styles.labelWrapper}> 275 - <Text 276 - style={[styles.label, pal.text]} 277 - nativeID="list-description"> 278 - <Trans>Description</Trans> 279 - </Text> 280 - <Text 281 - style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}> 282 - {graphemeLength}/{MAX_DESCRIPTION} 283 - </Text> 284 - </View> 285 - <TextInput 286 - testID="editDescriptionInput" 287 - style={[styles.textArea, pal.border, pal.text]} 288 - placeholder={ 289 - isCurateList 290 - ? _(msg`e.g. The posters who never miss.`) 291 - : _(msg`e.g. Users that repeatedly reply with ads.`) 292 - } 293 - placeholderTextColor={colors.gray4} 294 - keyboardAppearance={theme.colorScheme} 295 - multiline 296 - value={descriptionRt.text} 297 - onChangeText={onDescriptionChange} 298 - accessible={true} 299 - accessibilityLabel={_(msg`Description`)} 300 - accessibilityHint="" 301 - accessibilityLabelledBy="list-description" 302 - /> 303 - </View> 304 - {isProcessing ? ( 305 - <View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}> 306 - <ActivityIndicator /> 307 - </View> 308 - ) : ( 309 - <TouchableOpacity 310 - testID="saveBtn" 311 - style={[s.mt10, isDescriptionOver && s.dimmed]} 312 - disabled={isDescriptionOver} 313 - onPress={onPressSave} 314 - accessibilityRole="button" 315 - accessibilityLabel={_(msg`Save`)} 316 - accessibilityHint=""> 317 - <LinearGradient 318 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 319 - start={{x: 0, y: 0}} 320 - end={{x: 1, y: 1}} 321 - style={styles.btn}> 322 - <Text style={[s.white, s.bold]}> 323 - <Trans context="action">Save</Trans> 324 - </Text> 325 - </LinearGradient> 326 - </TouchableOpacity> 327 - )} 328 - <TouchableOpacity 329 - testID="cancelBtn" 330 - style={s.mt5} 331 - onPress={onPressCancel} 332 - accessibilityRole="button" 333 - accessibilityLabel={_(msg`Cancel`)} 334 - accessibilityHint="" 335 - onAccessibilityEscape={onPressCancel}> 336 - <View style={[styles.btn]}> 337 - <Text style={[s.black, s.bold, pal.text]}> 338 - <Trans context="action">Cancel</Trans> 339 - </Text> 340 - </View> 341 - </TouchableOpacity> 342 - </View> 343 - </ScrollView> 344 - </KeyboardAvoidingView> 345 - ) 346 - } 347 - 348 - const styles = StyleSheet.create({ 349 - title: { 350 - textAlign: 'center', 351 - fontWeight: '600', 352 - fontSize: 24, 353 - marginBottom: 18, 354 - }, 355 - labelWrapper: { 356 - flexDirection: 'row', 357 - gap: 8, 358 - alignItems: 'center', 359 - justifyContent: 'space-between', 360 - paddingHorizontal: 4, 361 - paddingBottom: 4, 362 - marginTop: 20, 363 - }, 364 - label: { 365 - fontWeight: '600', 366 - }, 367 - form: { 368 - paddingHorizontal: 6, 369 - }, 370 - textInput: { 371 - borderWidth: 1, 372 - borderRadius: 6, 373 - paddingHorizontal: 14, 374 - paddingVertical: 10, 375 - fontSize: 16, 376 - }, 377 - textArea: { 378 - borderWidth: 1, 379 - borderRadius: 6, 380 - paddingHorizontal: 12, 381 - paddingTop: 10, 382 - fontSize: 16, 383 - height: 100, 384 - textAlignVertical: 'top', 385 - }, 386 - btn: { 387 - flexDirection: 'row', 388 - alignItems: 'center', 389 - justifyContent: 'center', 390 - width: '100%', 391 - borderRadius: 32, 392 - padding: 10, 393 - marginBottom: 10, 394 - }, 395 - avi: { 396 - width: 84, 397 - height: 84, 398 - borderWidth: 2, 399 - borderRadius: 42, 400 - marginTop: 4, 401 - }, 402 - errorContainer: {marginTop: 20}, 403 - })
-145
src/view/com/modals/CropImage.web.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' 4 - import {LinearGradient} from 'expo-linear-gradient' 5 - import {msg, Trans} from '@lingui/macro' 6 - import {useLingui} from '@lingui/react' 7 - import ReactCrop, {type PercentCrop} from 'react-image-crop' 8 - 9 - import {usePalette} from '#/lib/hooks/usePalette' 10 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 - import {type PickerImage} from '#/lib/media/picker.shared' 12 - import {getDataUriSize} from '#/lib/media/util' 13 - import {gradients, s} from '#/lib/styles' 14 - import {useModalControls} from '#/state/modals' 15 - import {Text} from '#/view/com/util/text/Text' 16 - 17 - export const snapPoints = ['0%'] 18 - 19 - export function Component({ 20 - uri, 21 - aspect, 22 - circular, 23 - onSelect, 24 - }: { 25 - uri: string 26 - aspect?: number 27 - circular?: boolean 28 - onSelect: (img?: PickerImage) => void 29 - }) { 30 - const pal = usePalette('default') 31 - const {_} = useLingui() 32 - 33 - const {closeModal} = useModalControls() 34 - const {isMobile} = useWebMediaQueries() 35 - 36 - const imageRef = React.useRef<HTMLImageElement>(null) 37 - const [crop, setCrop] = React.useState<PercentCrop>() 38 - 39 - const isEmpty = !crop || (crop.width || crop.height) === 0 40 - 41 - const onPressCancel = () => { 42 - onSelect(undefined) 43 - closeModal() 44 - } 45 - const onPressDone = async () => { 46 - const img = imageRef.current! 47 - 48 - const result = await manipulateAsync( 49 - uri, 50 - isEmpty 51 - ? [] 52 - : [ 53 - { 54 - crop: { 55 - originX: (crop.x * img.naturalWidth) / 100, 56 - originY: (crop.y * img.naturalHeight) / 100, 57 - width: (crop.width * img.naturalWidth) / 100, 58 - height: (crop.height * img.naturalHeight) / 100, 59 - }, 60 - }, 61 - ], 62 - { 63 - base64: true, 64 - format: SaveFormat.JPEG, 65 - }, 66 - ) 67 - 68 - onSelect({ 69 - path: result.uri, 70 - mime: 'image/jpeg', 71 - size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0, 72 - width: result.width, 73 - height: result.height, 74 - }) 75 - 76 - closeModal() 77 - } 78 - 79 - return ( 80 - <View> 81 - <View style={[styles.cropper, pal.borderDark]}> 82 - <ReactCrop 83 - aspect={aspect} 84 - crop={crop} 85 - onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)} 86 - circularCrop={circular}> 87 - <img ref={imageRef} src={uri} style={{maxHeight: '75vh'}} /> 88 - </ReactCrop> 89 - </View> 90 - <View style={[styles.btns, isMobile && {paddingHorizontal: 16}]}> 91 - <TouchableOpacity 92 - onPress={onPressCancel} 93 - accessibilityRole="button" 94 - accessibilityLabel={_(msg`Cancel image crop`)} 95 - accessibilityHint={_(msg`Exits image cropping process`)}> 96 - <Text type="xl" style={pal.link}> 97 - <Trans>Cancel</Trans> 98 - </Text> 99 - </TouchableOpacity> 100 - <View style={s.flex1} /> 101 - <TouchableOpacity 102 - onPress={onPressDone} 103 - accessibilityRole="button" 104 - accessibilityLabel={_(msg`Save image crop`)} 105 - accessibilityHint={_(msg`Saves image crop settings`)}> 106 - <LinearGradient 107 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 108 - start={{x: 0, y: 0}} 109 - end={{x: 1, y: 1}} 110 - style={[styles.btn]}> 111 - <Text type="xl-medium" style={s.white}> 112 - <Trans>Done</Trans> 113 - </Text> 114 - </LinearGradient> 115 - </TouchableOpacity> 116 - </View> 117 - </View> 118 - ) 119 - } 120 - 121 - const styles = StyleSheet.create({ 122 - cropper: { 123 - marginLeft: 'auto', 124 - marginRight: 'auto', 125 - borderWidth: 1, 126 - borderRadius: 4, 127 - overflow: 'hidden', 128 - alignItems: 'center', 129 - }, 130 - ctrls: { 131 - flexDirection: 'row', 132 - alignItems: 'center', 133 - marginTop: 10, 134 - }, 135 - btns: { 136 - flexDirection: 'row', 137 - alignItems: 'center', 138 - marginTop: 10, 139 - }, 140 - btn: { 141 - borderRadius: 4, 142 - paddingVertical: 8, 143 - paddingHorizontal: 24, 144 - }, 145 - })
-287
src/view/com/modals/InviteCodes.tsx
··· 1 - import React from 'react' 2 - import { 3 - ActivityIndicator, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 8 - import {setStringAsync} from 'expo-clipboard' 9 - import {type ComAtprotoServerDefs} from '@atproto/api' 10 - import { 11 - FontAwesomeIcon, 12 - type FontAwesomeIconStyle, 13 - } from '@fortawesome/react-native-fontawesome' 14 - import {msg, Trans} from '@lingui/macro' 15 - import {useLingui} from '@lingui/react' 16 - 17 - import {usePalette} from '#/lib/hooks/usePalette' 18 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 19 - import {makeProfileLink} from '#/lib/routes/links' 20 - import {cleanError} from '#/lib/strings/errors' 21 - import {isWeb} from '#/platform/detection' 22 - import {useInvitesAPI, useInvitesState} from '#/state/invites' 23 - import {useModalControls} from '#/state/modals' 24 - import { 25 - type InviteCodesQueryResponse, 26 - useInviteCodesQuery, 27 - } from '#/state/queries/invites' 28 - import {ErrorMessage} from '../util/error/ErrorMessage' 29 - import {Button} from '../util/forms/Button' 30 - import {Link} from '../util/Link' 31 - import {Text} from '../util/text/Text' 32 - import * as Toast from '../util/Toast' 33 - import {UserInfoText} from '../util/UserInfoText' 34 - import {ScrollView} from './util' 35 - 36 - export const snapPoints = ['70%'] 37 - 38 - export function Component() { 39 - const {isLoading, data: invites, error} = useInviteCodesQuery() 40 - 41 - return error ? ( 42 - <ErrorMessage message={cleanError(error)} /> 43 - ) : isLoading || !invites ? ( 44 - <View style={{padding: 18}}> 45 - <ActivityIndicator /> 46 - </View> 47 - ) : ( 48 - <Inner invites={invites} /> 49 - ) 50 - } 51 - 52 - export function Inner({invites}: {invites: InviteCodesQueryResponse}) { 53 - const pal = usePalette('default') 54 - const {_} = useLingui() 55 - const {closeModal} = useModalControls() 56 - const {isTabletOrDesktop} = useWebMediaQueries() 57 - 58 - const onClose = React.useCallback(() => { 59 - closeModal() 60 - }, [closeModal]) 61 - 62 - if (invites.all.length === 0) { 63 - return ( 64 - <View style={[styles.container, pal.view]} testID="inviteCodesModal"> 65 - <View style={[styles.empty, pal.viewLight]}> 66 - <Text type="lg" style={[pal.text, styles.emptyText]}> 67 - <Trans> 68 - You don't have any invite codes yet! We'll send you some when 69 - you've been on Bluesky for a little longer. 70 - </Trans> 71 - </Text> 72 - </View> 73 - <View style={styles.flex1} /> 74 - <View 75 - style={[ 76 - styles.btnContainer, 77 - isTabletOrDesktop && styles.btnContainerDesktop, 78 - ]}> 79 - <Button 80 - type="primary" 81 - label={_(msg`Done`)} 82 - style={styles.btn} 83 - labelStyle={styles.btnLabel} 84 - onPress={onClose} 85 - /> 86 - </View> 87 - </View> 88 - ) 89 - } 90 - 91 - return ( 92 - <View style={[styles.container, pal.view]} testID="inviteCodesModal"> 93 - <Text type="title-xl" style={[styles.title, pal.text]}> 94 - <Trans>Invite a Friend</Trans> 95 - </Text> 96 - <Text type="lg" style={[styles.description, pal.text]}> 97 - <Trans> 98 - Each code works once. You'll receive more invite codes periodically. 99 - </Trans> 100 - </Text> 101 - <ScrollView style={[styles.scrollContainer, pal.border]}> 102 - {invites.available.map((invite, i) => ( 103 - <InviteCode 104 - testID={`inviteCode-${i}`} 105 - key={invite.code} 106 - invite={invite} 107 - invites={invites} 108 - /> 109 - ))} 110 - {invites.used.map((invite, i) => ( 111 - <InviteCode 112 - used 113 - testID={`inviteCode-${i}`} 114 - key={invite.code} 115 - invite={invite} 116 - invites={invites} 117 - /> 118 - ))} 119 - </ScrollView> 120 - <View style={styles.btnContainer}> 121 - <Button 122 - testID="closeBtn" 123 - type="primary" 124 - label={_(msg`Done`)} 125 - style={styles.btn} 126 - labelStyle={styles.btnLabel} 127 - onPress={onClose} 128 - /> 129 - </View> 130 - </View> 131 - ) 132 - } 133 - 134 - function InviteCode({ 135 - testID, 136 - invite, 137 - used, 138 - invites, 139 - }: { 140 - testID: string 141 - invite: ComAtprotoServerDefs.InviteCode 142 - used?: boolean 143 - invites: InviteCodesQueryResponse 144 - }) { 145 - const pal = usePalette('default') 146 - const {_} = useLingui() 147 - const invitesState = useInvitesState() 148 - const {setInviteCopied} = useInvitesAPI() 149 - const uses = invite.uses 150 - 151 - const onPress = React.useCallback(() => { 152 - setStringAsync(invite.code) 153 - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 154 - setInviteCopied(invite.code) 155 - }, [setInviteCopied, invite, _]) 156 - 157 - return ( 158 - <View 159 - style={[ 160 - pal.border, 161 - {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14}, 162 - ]}> 163 - <TouchableOpacity 164 - testID={testID} 165 - style={[styles.inviteCode]} 166 - onPress={onPress} 167 - accessibilityRole="button" 168 - accessibilityLabel={ 169 - invites.available.length === 1 170 - ? _(msg`Invite codes: 1 available`) 171 - : _(msg`Invite codes: ${invites.available.length} available`) 172 - } 173 - accessibilityHint={_(msg`Opens list of invite codes`)}> 174 - <Text 175 - testID={`${testID}-code`} 176 - type={used ? 'md' : 'md-bold'} 177 - style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> 178 - {invite.code} 179 - </Text> 180 - <View style={styles.flex1} /> 181 - {!used && invitesState.copiedInvites.includes(invite.code) && ( 182 - <Text style={[pal.textLight, styles.codeCopied]}> 183 - <Trans>Copied</Trans> 184 - </Text> 185 - )} 186 - {!used && ( 187 - <FontAwesomeIcon 188 - icon={['far', 'clone']} 189 - style={pal.text as FontAwesomeIconStyle} 190 - /> 191 - )} 192 - </TouchableOpacity> 193 - {uses.length > 0 ? ( 194 - <View 195 - style={{ 196 - flexDirection: 'column', 197 - gap: 8, 198 - paddingTop: 6, 199 - }}> 200 - <Text style={pal.text}> 201 - <Trans>Used by:</Trans>{' '} 202 - {uses.map((use, i) => ( 203 - <Link 204 - key={use.usedBy} 205 - href={makeProfileLink({handle: use.usedBy, did: ''})} 206 - style={{ 207 - flexDirection: 'row', 208 - }}> 209 - <UserInfoText did={use.usedBy} style={pal.link} /> 210 - {i !== uses.length - 1 && <Text style={pal.text}>, </Text>} 211 - </Link> 212 - ))} 213 - </Text> 214 - </View> 215 - ) : null} 216 - </View> 217 - ) 218 - } 219 - 220 - const styles = StyleSheet.create({ 221 - container: { 222 - flex: 1, 223 - paddingBottom: isWeb ? 0 : 50, 224 - }, 225 - title: { 226 - textAlign: 'center', 227 - marginTop: 12, 228 - marginBottom: 12, 229 - }, 230 - description: { 231 - textAlign: 'center', 232 - paddingHorizontal: 42, 233 - marginBottom: 14, 234 - }, 235 - 236 - scrollContainer: { 237 - flex: 1, 238 - borderTopWidth: 1, 239 - marginTop: 4, 240 - marginBottom: 16, 241 - }, 242 - 243 - flex1: { 244 - flex: 1, 245 - }, 246 - empty: { 247 - paddingHorizontal: 20, 248 - paddingVertical: 20, 249 - borderRadius: 16, 250 - marginHorizontal: 24, 251 - marginTop: 10, 252 - }, 253 - emptyText: { 254 - textAlign: 'center', 255 - }, 256 - 257 - inviteCode: { 258 - flexDirection: 'row', 259 - alignItems: 'center', 260 - }, 261 - codeCopied: { 262 - marginRight: 8, 263 - }, 264 - strikeThrough: { 265 - textDecorationLine: 'line-through', 266 - textDecorationStyle: 'solid', 267 - }, 268 - 269 - btnContainer: { 270 - flexDirection: 'row', 271 - justifyContent: 'center', 272 - }, 273 - btnContainerDesktop: { 274 - marginTop: 14, 275 - }, 276 - btn: { 277 - flexDirection: 'row', 278 - alignItems: 'center', 279 - justifyContent: 'center', 280 - borderRadius: 32, 281 - paddingHorizontal: 60, 282 - paddingVertical: 14, 283 - }, 284 - btnLabel: { 285 - fontSize: 18, 286 - }, 287 - })
+1 -9
src/view/com/modals/Modal.tsx
··· 7 7 import {useModalControls, useModals} from '#/state/modals' 8 8 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 9 9 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 10 - import * as CreateOrEditListModal from './CreateOrEditList' 11 10 import * as DeleteAccountModal from './DeleteAccount' 12 - import * as InviteCodesModal from './InviteCodes' 13 11 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 14 12 import * as UserAddRemoveListsModal from './UserAddRemoveLists' 15 13 ··· 44 42 45 43 let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS 46 44 let element 47 - if (activeModal?.name === 'create-or-edit-list') { 48 - snapPoints = CreateOrEditListModal.snapPoints 49 - element = <CreateOrEditListModal.Component {...activeModal} /> 50 - } else if (activeModal?.name === 'user-add-remove-lists') { 45 + if (activeModal?.name === 'user-add-remove-lists') { 51 46 snapPoints = UserAddRemoveListsModal.snapPoints 52 47 element = <UserAddRemoveListsModal.Component {...activeModal} /> 53 48 } else if (activeModal?.name === 'delete-account') { 54 49 snapPoints = DeleteAccountModal.snapPoints 55 50 element = <DeleteAccountModal.Component /> 56 - } else if (activeModal?.name === 'invite-codes') { 57 - snapPoints = InviteCodesModal.snapPoints 58 - element = <InviteCodesModal.Component /> 59 51 } else if (activeModal?.name === 'content-languages-settings') { 60 52 snapPoints = ContentLanguagesSettingsModal.snapPoints 61 53 element = <ContentLanguagesSettingsModal.Component />
+1 -7
src/view/com/modals/Modal.web.tsx
··· 6 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 7 import {type Modal as ModalIface} from '#/state/modals' 8 8 import {useModalControls, useModals} from '#/state/modals' 9 - import * as CreateOrEditListModal from './CreateOrEditList' 10 9 import * as DeleteAccountModal from './DeleteAccount' 11 - import * as InviteCodesModal from './InviteCodes' 12 10 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 13 11 import * as UserAddRemoveLists from './UserAddRemoveLists' 14 12 ··· 48 46 } 49 47 50 48 let element 51 - if (modal.name === 'create-or-edit-list') { 52 - element = <CreateOrEditListModal.Component {...modal} /> 53 - } else if (modal.name === 'user-add-remove-lists') { 49 + if (modal.name === 'user-add-remove-lists') { 54 50 element = <UserAddRemoveLists.Component {...modal} /> 55 51 } else if (modal.name === 'delete-account') { 56 52 element = <DeleteAccountModal.Component /> 57 - } else if (modal.name === 'invite-codes') { 58 - element = <InviteCodesModal.Component /> 59 53 } else if (modal.name === 'content-languages-settings') { 60 54 element = <ContentLanguagesSettingsModal.Component /> 61 55 } else {
-8
src/view/com/testing/TestCtrls.e2e.tsx
··· 3 3 import {useQueryClient} from '@tanstack/react-query' 4 4 5 5 import {BLUESKY_PROXY_HEADER} from '#/lib/constants' 6 - import {useModalControls} from '#/state/modals' 7 6 import {useSessionApi, useAgent} from '#/state/session' 8 7 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 9 8 import {useOnboardingDispatch} from '#/state/shell/onboarding' ··· 23 22 const agent = useAgent() 24 23 const queryClient = useQueryClient() 25 24 const {logoutEveryAccount, login} = useSessionApi() 26 - const {openModal} = useModalControls() 27 25 const onboardingDispatch = useOnboardingDispatch() 28 26 const {setShowLoggedOut} = useLoggedOutViewControls() 29 27 const onPressSignInAlice = async () => { ··· 118 116 <Pressable 119 117 testID="e2eRefreshHome" 120 118 onPress={() => queryClient.invalidateQueries({queryKey: ['post-feed']})} 121 - accessibilityRole="button" 122 - style={BTN} 123 - /> 124 - <Pressable 125 - testID="e2eOpenInviteCodesModal" 126 - onPress={() => openModal({name: 'invite-codes'})} 127 119 accessibilityRole="button" 128 120 style={BTN} 129 121 />
+28 -19
src/view/screens/Lists.tsx
··· 1 - import React from 'react' 1 + import {useCallback} from 'react' 2 2 import {AtUri} from '@atproto/api' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' ··· 10 10 type NativeStackScreenProps, 11 11 } from '#/lib/routes/types' 12 12 import {type NavigationProp} from '#/lib/routes/types' 13 - import {useModalControls} from '#/state/modals' 14 13 import {useSetMinimalShellMode} from '#/state/shell' 15 14 import {MyLists} from '#/view/com/lists/MyLists' 16 15 import {atoms as a} from '#/alf' 17 16 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17 + import {useDialogControl} from '#/components/Dialog' 18 + import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' 18 19 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19 20 import * as Layout from '#/components/Layout' 20 21 ··· 23 24 const {_} = useLingui() 24 25 const setMinimalShellMode = useSetMinimalShellMode() 25 26 const navigation = useNavigation<NavigationProp>() 26 - const {openModal} = useModalControls() 27 27 const requireEmailVerification = useRequireEmailVerification() 28 + const createListDialogControl = useDialogControl() 28 29 29 30 useFocusEffect( 30 - React.useCallback(() => { 31 + useCallback(() => { 31 32 setMinimalShellMode(false) 32 33 }, [setMinimalShellMode]), 33 34 ) 34 35 35 - const onPressNewList = React.useCallback(() => { 36 - openModal({ 37 - name: 'create-or-edit-list', 38 - purpose: 'app.bsky.graph.defs#curatelist', 39 - onSave: (uri: string) => { 40 - try { 41 - const urip = new AtUri(uri) 42 - navigation.navigate('ProfileList', { 43 - name: urip.hostname, 44 - rkey: urip.rkey, 45 - }) 46 - } catch {} 47 - }, 48 - }) 49 - }, [openModal, navigation]) 36 + const onPressNewList = useCallback(() => { 37 + createListDialogControl.open() 38 + }, [createListDialogControl]) 50 39 51 40 const wrappedOnPressNewList = requireEmailVerification(onPressNewList, { 52 41 instructions: [ ··· 56 45 ], 57 46 }) 58 47 48 + const onCreateList = useCallback( 49 + (uri: string) => { 50 + try { 51 + const urip = new AtUri(uri) 52 + navigation.navigate('ProfileList', { 53 + name: urip.hostname, 54 + rkey: urip.rkey, 55 + }) 56 + } catch {} 57 + }, 58 + [navigation], 59 + ) 60 + 59 61 return ( 60 62 <Layout.Screen testID="listsScreen"> 61 63 <Layout.Header.Outer> ··· 78 80 </ButtonText> 79 81 </Button> 80 82 </Layout.Header.Outer> 83 + 81 84 <MyLists filter="curate" style={a.flex_grow} /> 85 + 86 + <CreateOrEditListDialog 87 + purpose="app.bsky.graph.defs#curatelist" 88 + control={createListDialogControl} 89 + onSave={onCreateList} 90 + /> 82 91 </Layout.Screen> 83 92 ) 84 93 }
+28 -19
src/view/screens/ModerationModlists.tsx
··· 1 - import React from 'react' 1 + import {useCallback} from 'react' 2 2 import {AtUri} from '@atproto/api' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' ··· 10 10 type NativeStackScreenProps, 11 11 } from '#/lib/routes/types' 12 12 import {type NavigationProp} from '#/lib/routes/types' 13 - import {useModalControls} from '#/state/modals' 14 13 import {useSetMinimalShellMode} from '#/state/shell' 15 14 import {MyLists} from '#/view/com/lists/MyLists' 16 15 import {atoms as a} from '#/alf' 17 16 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17 + import {useDialogControl} from '#/components/Dialog' 18 + import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' 18 19 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19 20 import * as Layout from '#/components/Layout' 20 21 ··· 23 24 const {_} = useLingui() 24 25 const setMinimalShellMode = useSetMinimalShellMode() 25 26 const navigation = useNavigation<NavigationProp>() 26 - const {openModal} = useModalControls() 27 27 const requireEmailVerification = useRequireEmailVerification() 28 + const createListDialogControl = useDialogControl() 28 29 29 30 useFocusEffect( 30 - React.useCallback(() => { 31 + useCallback(() => { 31 32 setMinimalShellMode(false) 32 33 }, [setMinimalShellMode]), 33 34 ) 34 35 35 - const onPressNewList = React.useCallback(() => { 36 - openModal({ 37 - name: 'create-or-edit-list', 38 - purpose: 'app.bsky.graph.defs#modlist', 39 - onSave: (uri: string) => { 40 - try { 41 - const urip = new AtUri(uri) 42 - navigation.navigate('ProfileList', { 43 - name: urip.hostname, 44 - rkey: urip.rkey, 45 - }) 46 - } catch {} 47 - }, 48 - }) 49 - }, [openModal, navigation]) 36 + const onPressNewList = useCallback(() => { 37 + createListDialogControl.open() 38 + }, [createListDialogControl]) 50 39 51 40 const wrappedOnPressNewList = requireEmailVerification(onPressNewList, { 52 41 instructions: [ ··· 56 45 ], 57 46 }) 58 47 48 + const onCreateList = useCallback( 49 + (uri: string) => { 50 + try { 51 + const urip = new AtUri(uri) 52 + navigation.navigate('ProfileList', { 53 + name: urip.hostname, 54 + rkey: urip.rkey, 55 + }) 56 + } catch {} 57 + }, 58 + [navigation], 59 + ) 60 + 59 61 return ( 60 62 <Layout.Screen testID="moderationModlistsScreen"> 61 63 <Layout.Header.Outer> ··· 78 80 </ButtonText> 79 81 </Button> 80 82 </Layout.Header.Outer> 83 + 81 84 <MyLists filter="mod" style={a.flex_grow} /> 85 + 86 + <CreateOrEditListDialog 87 + purpose="app.bsky.graph.defs#modlist" 88 + control={createListDialogControl} 89 + onSave={onCreateList} 90 + /> 82 91 </Layout.Screen> 83 92 ) 84 93 }
+13 -9
src/view/screens/ProfileFeedLikedBy.tsx
··· 1 - import React from 'react' 2 - import {msg} from '@lingui/macro' 1 + import {useCallback} from 'react' 2 + import {Trans} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 import {useFocusEffect} from '@react-navigation/native' 5 5 ··· 10 10 import {makeRecordUri} from '#/lib/strings/url-helpers' 11 11 import {useSetMinimalShellMode} from '#/state/shell' 12 12 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 13 - import {ViewHeader} from '#/view/com/util/ViewHeader' 14 - import {CenteredView} from '#/view/com/util/Views' 15 13 import * as Layout from '#/components/Layout' 16 14 17 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'> ··· 22 20 const {_} = useLingui() 23 21 24 22 useFocusEffect( 25 - React.useCallback(() => { 23 + useCallback(() => { 26 24 setMinimalShellMode(false) 27 25 }, [setMinimalShellMode]), 28 26 ) 29 27 30 28 return ( 31 29 <Layout.Screen testID="postLikedByScreen"> 32 - <CenteredView sideBorders={true}> 33 - <ViewHeader title={_(msg`Liked By`)} /> 34 - <PostLikedByComponent uri={uri} /> 35 - </CenteredView> 30 + <Layout.Header.Outer> 31 + <Layout.Header.BackButton /> 32 + <Layout.Header.Content> 33 + <Layout.Header.TitleText> 34 + <Trans>Liked By</Trans> 35 + </Layout.Header.TitleText> 36 + </Layout.Header.Content> 37 + <Layout.Header.Slot /> 38 + </Layout.Header.Outer> 39 + <PostLikedByComponent uri={uri} /> 36 40 </Layout.Screen> 37 41 ) 38 42 }