Bluesky app fork with some witchin' additions 💫

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

+188 -25
+1 -1
package.json
··· 72 "icons:optimize": "svgo -f ./assets/icons" 73 }, 74 "dependencies": { 75 - "@atproto/api": "^0.17.1", 76 "@bitdrift/react-native": "^0.6.8", 77 "@braintree/sanitize-url": "^6.0.2", 78 "@bsky.app/alf": "^0.1.5",
··· 72 "icons:optimize": "svgo -f ./assets/icons" 73 }, 74 "dependencies": { 75 + "@atproto/api": "^0.17.6", 76 "@bitdrift/react-native": "^0.6.8", 77 "@braintree/sanitize-url": "^6.0.2", 78 "@bsky.app/alf": "^0.1.5",
+74
src/components/DebugFieldDisplay.tsx
···
··· 1 + import {TouchableWithoutFeedback, View} from 'react-native' 2 + import * as Clipboard from 'expo-clipboard' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import * as Prompt from '#/components/Prompt' 6 + import * as Toast from '#/components/Toast' 7 + import {Text} from '#/components/Typography' 8 + import {useDevMode} from '#/storage/hooks/dev-mode' 9 + 10 + /** 11 + * Internal-use component to display debug information supplied by the appview. 12 + * The `debug` field only exists on some API views, and is only visible for 13 + * internal users in dev mode. As such, none of these strings need to be 14 + * translated. 15 + * 16 + * This component can be removed at any time if we don't find it useful. 17 + */ 18 + export function DebugFieldDisplay<T extends {debug?: {[x: string]: unknown}}>({ 19 + subject, 20 + }: { 21 + subject: T 22 + }) { 23 + const t = useTheme() 24 + const [devMode] = useDevMode() 25 + const prompt = Prompt.usePromptControl() 26 + 27 + if (!devMode) return 28 + if (!subject.debug) return 29 + 30 + return ( 31 + <> 32 + <Prompt.Basic 33 + control={prompt} 34 + title="Debug" 35 + description={JSON.stringify(subject.debug, null, 2)} 36 + cancelButtonCta="Close" 37 + confirmButtonCta="Copy" 38 + onConfirm={() => { 39 + Clipboard.setStringAsync(JSON.stringify(subject.debug, null, 2)) 40 + Toast.show('Copied to clipboard', {type: 'success'}) 41 + }} 42 + /> 43 + <TouchableWithoutFeedback 44 + accessibilityRole="button" 45 + onPress={e => { 46 + e.preventDefault() 47 + e.stopPropagation() 48 + prompt.open() 49 + return false 50 + }}> 51 + <View style={[a.flex_row, a.align_center, a.gap_xs, a.pt_sm, a.pb_xs]}> 52 + <View 53 + style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_25]}> 54 + <Text 55 + style={[a.font_bold, a.text_xs, t.atoms.text_contrast_medium]}> 56 + Debug 57 + </Text> 58 + </View> 59 + <Text 60 + numberOfLines={1} 61 + style={[ 62 + a.flex_1, 63 + a.text_xs, 64 + a.leading_tight, 65 + {fontFamily: 'monospace'}, 66 + t.atoms.text_contrast_low, 67 + ]}> 68 + {JSON.stringify(subject.debug)} 69 + </Text> 70 + </View> 71 + </TouchableWithoutFeedback> 72 + </> 73 + ) 74 + }
-1
src/components/PostControls/ShareMenu/RecentChats.tsx
··· 50 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]} 51 contentContainerStyle={[a.gap_sm, a.px_md]} 52 showsHorizontalScrollIndicator={false} 53 - fadingEdgeLength={64} 54 nestedScrollEnabled> 55 {convos && convos.length > 0 ? ( 56 convos.map(convo => {
··· 50 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]} 51 contentContainerStyle={[a.gap_sm, a.px_md]} 52 showsHorizontalScrollIndicator={false} 53 nestedScrollEnabled> 54 {convos && convos.length > 0 ? ( 55 convos.map(convo => {
+6 -1
src/components/ProfileHoverCard/index.web.tsx
··· 515 <View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}> 516 <Text 517 numberOfLines={1} 518 - style={[a.text_lg, a.font_semi_bold, a.self_start]}> 519 {sanitizeDisplayName( 520 profile.displayName || sanitizeHandle(profile.handle), 521 moderation.ui('displayName'),
··· 515 <View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}> 516 <Text 517 numberOfLines={1} 518 + style={[ 519 + a.text_lg, 520 + a.leading_snug, 521 + a.font_semi_bold, 522 + a.self_start, 523 + ]}> 524 {sanitizeDisplayName( 525 profile.displayName || sanitizeHandle(profile.handle), 526 moderation.ui('displayName'),
+2 -1
src/components/RichTextTag.tsx
··· 96 onLongPress={createStaticClick(menuProps.onPress).onPress} 97 accessibilityHint={hint} 98 label={label} 99 - style={textStyle}> 100 {isNative ? ( 101 display 102 ) : (
··· 96 onLongPress={createStaticClick(menuProps.onPress).onPress} 97 accessibilityHint={hint} 98 label={label} 99 + style={textStyle} 100 + emoji> 101 {isNative ? ( 102 display 103 ) : (
+18 -2
src/components/moderation/LabelsOnMeDialog.tsx
··· 1 - import React from 'react' 2 import {View} from 'react-native' 3 import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import {useMutation} from '@tanstack/react-query' ··· 19 import * as Dialog from '#/components/Dialog' 20 import {InlineLinkText} from '#/components/Link' 21 import {Text} from '#/components/Typography' 22 import {Divider} from '../Divider' 23 import {Loader} from '../Loader' 24 ··· 228 const sourceName = labeler 229 ? sanitizeHandle(labeler.creator.handle, '@') 230 : label.src 231 232 const {mutate, isPending} = useMutation({ 233 mutationFn: async () => { ··· 252 ) 253 }, 254 onError: err => { 255 logger.error('Failed to submit label appeal', {message: err}) 256 - Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark') 257 }, 258 onSuccess: () => { 259 control.close() ··· 285 </Trans> 286 </Text> 287 </View> 288 <View style={[a.my_md]}> 289 <Dialog.Input 290 label={_(msg`Text input field`)}
··· 1 + import React, {useState} from 'react' 2 import {View} from 'react-native' 3 import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' 4 + import {XRPCError} from '@atproto/xrpc' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 import {useMutation} from '@tanstack/react-query' ··· 20 import * as Dialog from '#/components/Dialog' 21 import {InlineLinkText} from '#/components/Link' 22 import {Text} from '#/components/Typography' 23 + import {Admonition} from '../Admonition' 24 import {Divider} from '../Divider' 25 import {Loader} from '../Loader' 26 ··· 230 const sourceName = labeler 231 ? sanitizeHandle(labeler.creator.handle, '@') 232 : label.src 233 + const [error, setError] = useState<string | null>(null) 234 235 const {mutate, isPending} = useMutation({ 236 mutationFn: async () => { ··· 255 ) 256 }, 257 onError: err => { 258 + if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { 259 + setError( 260 + _( 261 + msg`You've already appealed this label and it's being reviewed by our moderation team.`, 262 + ), 263 + ) 264 + } else { 265 + setError(_(msg`Failed to submit appeal, please try again.`)) 266 + } 267 logger.error('Failed to submit label appeal', {message: err}) 268 }, 269 onSuccess: () => { 270 control.close() ··· 296 </Trans> 297 </Text> 298 </View> 299 + {error && ( 300 + <Admonition type="error" style={[a.mt_sm]}> 301 + {error} 302 + </Admonition> 303 + )} 304 <View style={[a.my_md]}> 305 <Dialog.Input 306 label={_(msg`Text input field`)}
+34 -2
src/lib/media/manip.ts
··· 115 // as the starting image, or put it directly into the album 116 const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME) 117 if (album) { 118 - // if album exists, put the image straight in there 119 - await MediaLibrary.createAssetAsync(imagePath, album) 120 } else { 121 // otherwise, create album with asset (albums must always have at least one asset) 122 await MediaLibrary.createAlbumAsync( ··· 133 logger.error(err instanceof Error ? err : String(err), { 134 message: 'Failed to save image to media library', 135 }) 136 } finally { 137 safeDeleteAsync(imagePath) 138 }
··· 115 // as the starting image, or put it directly into the album 116 const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME) 117 if (album) { 118 + // try and migrate if needed 119 + try { 120 + if (await MediaLibrary.albumNeedsMigrationAsync(album)) { 121 + await MediaLibrary.migrateAlbumIfNeededAsync(album) 122 + } 123 + } catch (err) { 124 + logger.info('Attempted and failed to migrate album', { 125 + safeMessage: err, 126 + }) 127 + } 128 + 129 + try { 130 + // if album exists, put the image straight in there 131 + await MediaLibrary.createAssetAsync(imagePath, album) 132 + } catch (err) { 133 + logger.info('Failed to create asset', {safeMessage: err}) 134 + // however, it's possible that we don't have write permission to the album 135 + // try making a new one! 136 + try { 137 + await MediaLibrary.createAlbumAsync( 138 + ALBUM_NAME, 139 + undefined, 140 + undefined, 141 + imagePath, 142 + ) 143 + } catch (err2) { 144 + logger.info('Failed to create asset in a fresh album', { 145 + safeMessage: err2, 146 + }) 147 + // ... and if all else fails, just put it in DCIM 148 + await MediaLibrary.createAssetAsync(imagePath) 149 + } 150 + } 151 } else { 152 // otherwise, create album with asset (albums must always have at least one asset) 153 await MediaLibrary.createAlbumAsync( ··· 164 logger.error(err instanceof Error ? err : String(err), { 165 message: 'Failed to save image to media library', 166 }) 167 + throw err 168 } finally { 169 safeDeleteAsync(imagePath) 170 }
+2 -2
src/locale/locales/en/messages.po
··· 7429 msgid "Save" 7430 msgstr "" 7431 7432 - #: src/view/com/lightbox/ImageViewing/index.tsx:610 7433 msgctxt "action" 7434 msgid "Save" 7435 msgstr "" ··· 7952 msgid "Share" 7953 msgstr "" 7954 7955 - #: src/view/com/lightbox/ImageViewing/index.tsx:619 7956 msgctxt "action" 7957 msgid "Share" 7958 msgstr ""
··· 7429 msgid "Save" 7430 msgstr "" 7431 7432 + #: src/view/com/lightbox/ImageViewing/index.tsx:613 7433 msgctxt "action" 7434 msgid "Save" 7435 msgstr "" ··· 7952 msgid "Share" 7953 msgstr "" 7954 7955 + #: src/view/com/lightbox/ImageViewing/index.tsx:622 7956 msgctxt "action" 7957 msgid "Share" 7958 msgstr ""
+2
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 46 import {atoms as a, useTheme} from '#/alf' 47 import {colors} from '#/components/Admonition' 48 import {Button} from '#/components/Button' 49 import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 50 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 51 import {InlineLinkText, Link} from '#/components/Link' ··· 529 /> 530 </FeedFeedbackProvider> 531 </View> 532 </View> 533 </View> 534 </>
··· 46 import {atoms as a, useTheme} from '#/alf' 47 import {colors} from '#/components/Admonition' 48 import {Button} from '#/components/Button' 49 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 50 import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 51 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 52 import {InlineLinkText, Link} from '#/components/Link' ··· 530 /> 531 </FeedFeedbackProvider> 532 </View> 533 + <DebugFieldDisplay subject={post} /> 534 </View> 535 </View> 536 </>
+2
src/screens/PostThread/components/ThreadItemPost.tsx
··· 30 REPLY_LINE_WIDTH, 31 } from '#/screens/PostThread/const' 32 import {atoms as a, useTheme} from '#/alf' 33 import {useInteractionState} from '#/components/hooks/useInteractionState' 34 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 35 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 335 logContext="PostThreadItem" 336 threadgateRecord={threadgateRecord} 337 /> 338 </View> 339 </View> 340 </PostHider>
··· 30 REPLY_LINE_WIDTH, 31 } from '#/screens/PostThread/const' 32 import {atoms as a, useTheme} from '#/alf' 33 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 34 import {useInteractionState} from '#/components/hooks/useInteractionState' 35 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 36 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 336 logContext="PostThreadItem" 337 threadgateRecord={threadgateRecord} 338 /> 339 + <DebugFieldDisplay subject={post} /> 340 </View> 341 </View> 342 </PostHider>
+2
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 29 TREE_INDENT, 30 } from '#/screens/PostThread/const' 31 import {atoms as a, useTheme} from '#/alf' 32 import {useInteractionState} from '#/components/hooks/useInteractionState' 33 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 34 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 376 logContext="PostThreadItem" 377 threadgateRecord={threadgateRecord} 378 /> 379 </View> 380 </View> 381 </View>
··· 29 TREE_INDENT, 30 } from '#/screens/PostThread/const' 31 import {atoms as a, useTheme} from '#/alf' 32 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 33 import {useInteractionState} from '#/components/hooks/useInteractionState' 34 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 35 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 377 logContext="PostThreadItem" 378 threadgateRecord={threadgateRecord} 379 /> 380 + <DebugFieldDisplay subject={post} /> 381 </View> 382 </View> 383 </View>
+3
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 26 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 27 import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' 28 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 import {useDialogControl} from '#/components/Dialog' 30 import {MessageProfileButton} from '#/components/dms/MessageProfileButton' 31 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' ··· 320 )} 321 </View> 322 )} 323 </View> 324 325 <Prompt.Basic
··· 26 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 27 import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' 28 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 30 import {useDialogControl} from '#/components/Dialog' 31 import {MessageProfileButton} from '#/components/dms/MessageProfileButton' 32 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' ··· 321 )} 322 </View> 323 )} 324 + 325 + <DebugFieldDisplay subject={profile} /> 326 </View> 327 328 <Prompt.Basic
+12 -1
src/screens/Profile/Header/SuggestedFollows.tsx
··· 28 actorDid: string 29 }) { 30 const gate = useGate() 31 32 /* NOTE (caidanw): 33 * Android does not work well with this feature yet. ··· 40 41 return ( 42 <AccordionAnimation isExpanded={isExpanded}> 43 - <ProfileHeaderSuggestedFollows actorDid={actorDid} /> 44 </AccordionAnimation> 45 ) 46 }
··· 28 actorDid: string 29 }) { 30 const gate = useGate() 31 + const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 32 + did: actorDid, 33 + }) 34 + 35 + if (!data?.suggestions?.length) return null 36 37 /* NOTE (caidanw): 38 * Android does not work well with this feature yet. ··· 45 46 return ( 47 <AccordionAnimation isExpanded={isExpanded}> 48 + <ProfileGrid 49 + isSuggestionsLoading={isLoading} 50 + profiles={data.suggestions} 51 + recId={data.recId} 52 + error={error} 53 + viewContext="profileHeader" 54 + /> 55 </AccordionAnimation> 56 ) 57 }
+1 -5
src/screens/VideoFeed/components/Scrubber.tsx
··· 6 type NativeGesture, 7 } from 'react-native-gesture-handler' 8 import Animated, { 9 interpolate, 10 runOnJS, 11 runOnUI, ··· 258 259 return null 260 } 261 - 262 - function clamp(num: number, min: number, max: number) { 263 - 'worklet' 264 - return Math.min(Math.max(num, min), max) 265 - }
··· 6 type NativeGesture, 7 } from 'react-native-gesture-handler' 8 import Animated, { 9 + clamp, 10 interpolate, 11 runOnJS, 12 runOnUI, ··· 259 260 return null 261 }
+5 -1
src/view/com/lightbox/ImageViewing/index.tsx
··· 18 cancelAnimation, 19 interpolate, 20 measure, 21 runOnJS, 22 type SharedValue, 23 useAnimatedReaction, ··· 516 velocity: e.velocityY, 517 velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. 518 deceleration: 1, // Danger! This relies on the reaction below stopping it. 519 }) 520 }) 521 } else { ··· 524 return withSpring(0, { 525 stiffness: 700, 526 damping: 50, 527 }) 528 }) 529 } ··· 595 }) 596 toggleAltExpanded() 597 }} 598 - onLongPress={() => {}}> 599 {altText} 600 </Text> 601 </View>
··· 18 cancelAnimation, 19 interpolate, 20 measure, 21 + ReduceMotion, 22 runOnJS, 23 type SharedValue, 24 useAnimatedReaction, ··· 517 velocity: e.velocityY, 518 velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. 519 deceleration: 1, // Danger! This relies on the reaction below stopping it. 520 + reduceMotion: ReduceMotion.Never, // If this animation doesn't run, the image gets stuck - therefore override Reduce Motion 521 }) 522 }) 523 } else { ··· 526 return withSpring(0, { 527 stiffness: 700, 528 damping: 50, 529 + reduceMotion: ReduceMotion.Never, 530 }) 531 }) 532 } ··· 598 }) 599 toggleAltExpanded() 600 }} 601 + onLongPress={() => {}} 602 + emoji> 603 {altText} 604 </Text> 605 </View>
+2 -1
src/view/com/posts/PostFeedReason.tsx
··· 100 t.atoms.text_contrast_medium, 101 a.font_medium, 102 a.leading_snug, 103 - ]}> 104 {reposter} 105 </WebOnlyInlineLinkText> 106 </ProfileHoverCard>
··· 100 t.atoms.text_contrast_medium, 101 a.font_medium, 102 a.leading_snug, 103 + ]} 104 + emoji> 105 {reposter} 106 </WebOnlyInlineLinkText> 107 </ProfileHoverCard>
+2 -1
src/view/com/util/FeedInfoText.tsx
··· 27 to={href} 28 label={displayName} 29 style={style} 30 - numberOfLines={numberOfLines}> 31 {sanitizeDisplayName(displayName)} 32 </WebOnlyInlineLinkText> 33 )
··· 27 to={href} 28 label={displayName} 29 style={style} 30 + numberOfLines={numberOfLines} 31 + emoji> 32 {sanitizeDisplayName(displayName)} 33 </WebOnlyInlineLinkText> 34 )
+6 -6
src/view/com/util/MainScrollProvider.tsx
··· 1 import React, {useCallback, useEffect} from 'react' 2 import {type NativeScrollEvent} from 'react-native' 3 - import {interpolate, useSharedValue, withSpring} from 'react-native-reanimated' 4 import EventEmitter from 'eventemitter3' 5 6 import {ScrollProvider} from '#/lib/ScrollContext' ··· 9 import {useShellLayout} from '#/state/shell/shell-layout' 10 11 const WEB_HIDE_SHELL_THRESHOLD = 200 12 - 13 - function clamp(num: number, min: number, max: number) { 14 - 'worklet' 15 - return Math.min(Math.max(num, min), max) 16 - } 17 18 export function MainScrollProvider({children}: {children: React.ReactNode}) { 19 const {headerHeight} = useShellLayout()
··· 1 import React, {useCallback, useEffect} from 'react' 2 import {type NativeScrollEvent} from 'react-native' 3 + import { 4 + clamp, 5 + interpolate, 6 + useSharedValue, 7 + withSpring, 8 + } from 'react-native-reanimated' 9 import EventEmitter from 'eventemitter3' 10 11 import {ScrollProvider} from '#/lib/ScrollContext' ··· 14 import {useShellLayout} from '#/state/shell/shell-layout' 15 16 const WEB_HIDE_SHELL_THRESHOLD = 200 17 18 export function MainScrollProvider({children}: {children: React.ReactNode}) { 19 const {headerHeight} = useShellLayout()
+14
yarn.lock
··· 84 tlds "^1.234.0" 85 zod "^3.23.8" 86 87 "@atproto/aws@^0.2.30": 88 version "0.2.30" 89 resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.30.tgz#17c882a2ec838fc6ff2a6c76f66a12e5f29d227e"
··· 84 tlds "^1.234.0" 85 zod "^3.23.8" 86 87 + "@atproto/api@^0.17.6": 88 + version "0.17.6" 89 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.17.6.tgz#1fccd939f5f1010397c4d57110b1a0d8673058a6" 90 + integrity sha512-0iYCD8+LOsHjHjwJcqGPfJN/h4b+IpU3GjOV0TSLk0XdCaxpHBKNu3wgCJVst4DhVjXcgsr2qQoRZ3Jja2LupA== 91 + dependencies: 92 + "@atproto/common-web" "^0.4.3" 93 + "@atproto/lexicon" "^0.5.1" 94 + "@atproto/syntax" "^0.4.1" 95 + "@atproto/xrpc" "^0.7.5" 96 + await-lock "^2.2.2" 97 + multiformats "^9.9.0" 98 + tlds "^1.234.0" 99 + zod "^3.23.8" 100 + 101 "@atproto/aws@^0.2.30": 102 version "0.2.30" 103 resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.30.tgz#17c882a2ec838fc6ff2a6c76f66a12e5f29d227e"