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 72 "icons:optimize": "svgo -f ./assets/icons" 73 73 }, 74 74 "dependencies": { 75 - "@atproto/api": "^0.17.1", 75 + "@atproto/api": "^0.17.6", 76 76 "@bitdrift/react-native": "^0.6.8", 77 77 "@braintree/sanitize-url": "^6.0.2", 78 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 50 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]} 51 51 contentContainerStyle={[a.gap_sm, a.px_md]} 52 52 showsHorizontalScrollIndicator={false} 53 - fadingEdgeLength={64} 54 53 nestedScrollEnabled> 55 54 {convos && convos.length > 0 ? ( 56 55 convos.map(convo => {
+6 -1
src/components/ProfileHoverCard/index.web.tsx
··· 515 515 <View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}> 516 516 <Text 517 517 numberOfLines={1} 518 - style={[a.text_lg, a.font_semi_bold, a.self_start]}> 518 + style={[ 519 + a.text_lg, 520 + a.leading_snug, 521 + a.font_semi_bold, 522 + a.self_start, 523 + ]}> 519 524 {sanitizeDisplayName( 520 525 profile.displayName || sanitizeHandle(profile.handle), 521 526 moderation.ui('displayName'),
+2 -1
src/components/RichTextTag.tsx
··· 96 96 onLongPress={createStaticClick(menuProps.onPress).onPress} 97 97 accessibilityHint={hint} 98 98 label={label} 99 - style={textStyle}> 99 + style={textStyle} 100 + emoji> 100 101 {isNative ? ( 101 102 display 102 103 ) : (
+18 -2
src/components/moderation/LabelsOnMeDialog.tsx
··· 1 - import React from 'react' 1 + import React, {useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' 4 + import {XRPCError} from '@atproto/xrpc' 4 5 import {msg, Trans} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' 6 7 import {useMutation} from '@tanstack/react-query' ··· 19 20 import * as Dialog from '#/components/Dialog' 20 21 import {InlineLinkText} from '#/components/Link' 21 22 import {Text} from '#/components/Typography' 23 + import {Admonition} from '../Admonition' 22 24 import {Divider} from '../Divider' 23 25 import {Loader} from '../Loader' 24 26 ··· 228 230 const sourceName = labeler 229 231 ? sanitizeHandle(labeler.creator.handle, '@') 230 232 : label.src 233 + const [error, setError] = useState<string | null>(null) 231 234 232 235 const {mutate, isPending} = useMutation({ 233 236 mutationFn: async () => { ··· 252 255 ) 253 256 }, 254 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 + } 255 267 logger.error('Failed to submit label appeal', {message: err}) 256 - Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark') 257 268 }, 258 269 onSuccess: () => { 259 270 control.close() ··· 285 296 </Trans> 286 297 </Text> 287 298 </View> 299 + {error && ( 300 + <Admonition type="error" style={[a.mt_sm]}> 301 + {error} 302 + </Admonition> 303 + )} 288 304 <View style={[a.my_md]}> 289 305 <Dialog.Input 290 306 label={_(msg`Text input field`)}
+34 -2
src/lib/media/manip.ts
··· 115 115 // as the starting image, or put it directly into the album 116 116 const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME) 117 117 if (album) { 118 - // if album exists, put the image straight in there 119 - await MediaLibrary.createAssetAsync(imagePath, 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 + } 120 151 } else { 121 152 // otherwise, create album with asset (albums must always have at least one asset) 122 153 await MediaLibrary.createAlbumAsync( ··· 133 164 logger.error(err instanceof Error ? err : String(err), { 134 165 message: 'Failed to save image to media library', 135 166 }) 167 + throw err 136 168 } finally { 137 169 safeDeleteAsync(imagePath) 138 170 }
+2 -2
src/locale/locales/en/messages.po
··· 7429 7429 msgid "Save" 7430 7430 msgstr "" 7431 7431 7432 - #: src/view/com/lightbox/ImageViewing/index.tsx:610 7432 + #: src/view/com/lightbox/ImageViewing/index.tsx:613 7433 7433 msgctxt "action" 7434 7434 msgid "Save" 7435 7435 msgstr "" ··· 7952 7952 msgid "Share" 7953 7953 msgstr "" 7954 7954 7955 - #: src/view/com/lightbox/ImageViewing/index.tsx:619 7955 + #: src/view/com/lightbox/ImageViewing/index.tsx:622 7956 7956 msgctxt "action" 7957 7957 msgid "Share" 7958 7958 msgstr ""
+2
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 46 46 import {atoms as a, useTheme} from '#/alf' 47 47 import {colors} from '#/components/Admonition' 48 48 import {Button} from '#/components/Button' 49 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 49 50 import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 50 51 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 51 52 import {InlineLinkText, Link} from '#/components/Link' ··· 529 530 /> 530 531 </FeedFeedbackProvider> 531 532 </View> 533 + <DebugFieldDisplay subject={post} /> 532 534 </View> 533 535 </View> 534 536 </>
+2
src/screens/PostThread/components/ThreadItemPost.tsx
··· 30 30 REPLY_LINE_WIDTH, 31 31 } from '#/screens/PostThread/const' 32 32 import {atoms as a, useTheme} from '#/alf' 33 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 33 34 import {useInteractionState} from '#/components/hooks/useInteractionState' 34 35 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 35 36 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 335 336 logContext="PostThreadItem" 336 337 threadgateRecord={threadgateRecord} 337 338 /> 339 + <DebugFieldDisplay subject={post} /> 338 340 </View> 339 341 </View> 340 342 </PostHider>
+2
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 29 29 TREE_INDENT, 30 30 } from '#/screens/PostThread/const' 31 31 import {atoms as a, useTheme} from '#/alf' 32 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 32 33 import {useInteractionState} from '#/components/hooks/useInteractionState' 33 34 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 34 35 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' ··· 376 377 logContext="PostThreadItem" 377 378 threadgateRecord={threadgateRecord} 378 379 /> 380 + <DebugFieldDisplay subject={post} /> 379 381 </View> 380 382 </View> 381 383 </View>
+3
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 26 26 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 27 27 import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' 28 28 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 + import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 29 30 import {useDialogControl} from '#/components/Dialog' 30 31 import {MessageProfileButton} from '#/components/dms/MessageProfileButton' 31 32 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' ··· 320 321 )} 321 322 </View> 322 323 )} 324 + 325 + <DebugFieldDisplay subject={profile} /> 323 326 </View> 324 327 325 328 <Prompt.Basic
+12 -1
src/screens/Profile/Header/SuggestedFollows.tsx
··· 28 28 actorDid: string 29 29 }) { 30 30 const gate = useGate() 31 + const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 32 + did: actorDid, 33 + }) 34 + 35 + if (!data?.suggestions?.length) return null 31 36 32 37 /* NOTE (caidanw): 33 38 * Android does not work well with this feature yet. ··· 40 45 41 46 return ( 42 47 <AccordionAnimation isExpanded={isExpanded}> 43 - <ProfileHeaderSuggestedFollows actorDid={actorDid} /> 48 + <ProfileGrid 49 + isSuggestionsLoading={isLoading} 50 + profiles={data.suggestions} 51 + recId={data.recId} 52 + error={error} 53 + viewContext="profileHeader" 54 + /> 44 55 </AccordionAnimation> 45 56 ) 46 57 }
+1 -5
src/screens/VideoFeed/components/Scrubber.tsx
··· 6 6 type NativeGesture, 7 7 } from 'react-native-gesture-handler' 8 8 import Animated, { 9 + clamp, 9 10 interpolate, 10 11 runOnJS, 11 12 runOnUI, ··· 258 259 259 260 return null 260 261 } 261 - 262 - function clamp(num: number, min: number, max: number) { 263 - 'worklet' 264 - return Math.min(Math.max(num, min), max) 265 - }
+5 -1
src/view/com/lightbox/ImageViewing/index.tsx
··· 18 18 cancelAnimation, 19 19 interpolate, 20 20 measure, 21 + ReduceMotion, 21 22 runOnJS, 22 23 type SharedValue, 23 24 useAnimatedReaction, ··· 516 517 velocity: e.velocityY, 517 518 velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. 518 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 519 521 }) 520 522 }) 521 523 } else { ··· 524 526 return withSpring(0, { 525 527 stiffness: 700, 526 528 damping: 50, 529 + reduceMotion: ReduceMotion.Never, 527 530 }) 528 531 }) 529 532 } ··· 595 598 }) 596 599 toggleAltExpanded() 597 600 }} 598 - onLongPress={() => {}}> 601 + onLongPress={() => {}} 602 + emoji> 599 603 {altText} 600 604 </Text> 601 605 </View>
+2 -1
src/view/com/posts/PostFeedReason.tsx
··· 100 100 t.atoms.text_contrast_medium, 101 101 a.font_medium, 102 102 a.leading_snug, 103 - ]}> 103 + ]} 104 + emoji> 104 105 {reposter} 105 106 </WebOnlyInlineLinkText> 106 107 </ProfileHoverCard>
+2 -1
src/view/com/util/FeedInfoText.tsx
··· 27 27 to={href} 28 28 label={displayName} 29 29 style={style} 30 - numberOfLines={numberOfLines}> 30 + numberOfLines={numberOfLines} 31 + emoji> 31 32 {sanitizeDisplayName(displayName)} 32 33 </WebOnlyInlineLinkText> 33 34 )
+6 -6
src/view/com/util/MainScrollProvider.tsx
··· 1 1 import React, {useCallback, useEffect} from 'react' 2 2 import {type NativeScrollEvent} from 'react-native' 3 - import {interpolate, useSharedValue, withSpring} from 'react-native-reanimated' 3 + import { 4 + clamp, 5 + interpolate, 6 + useSharedValue, 7 + withSpring, 8 + } from 'react-native-reanimated' 4 9 import EventEmitter from 'eventemitter3' 5 10 6 11 import {ScrollProvider} from '#/lib/ScrollContext' ··· 9 14 import {useShellLayout} from '#/state/shell/shell-layout' 10 15 11 16 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 17 18 18 export function MainScrollProvider({children}: {children: React.ReactNode}) { 19 19 const {headerHeight} = useShellLayout()
+14
yarn.lock
··· 84 84 tlds "^1.234.0" 85 85 zod "^3.23.8" 86 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 + 87 101 "@atproto/aws@^0.2.30": 88 102 version "0.2.30" 89 103 resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.30.tgz#17c882a2ec838fc6ff2a6c76f66a12e5f29d227e"