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