An ATproto social media client -- with an independent Appview.

chore: sync with upstream (#57)

* Send inferrable interactions to third-party feeds (#9094)

* Fix link crash (#9102)

* fix link crash

* fix link crash

* Log OTA errors properly (#9101)

* Log OTA errors properly

* filter out network errors

* don't send some "activity no longer available" errors (#9100)

* remove root sibling library (#9097)

* Catch errors on geolocation request, reduce Sentry logs (#9098)

* Nightly source-language update

* fix gap on profile (#9081)

* Update admonition component (#9068)

* update admonition component

* fix linting, adominition alighment

* design tweak

* edge cases for admonition, update storybook

* Update src/components/Admonition.tsx

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* fix mobile version

* change button style

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Button tweaks (#9106)

* Tweak button colors

* Semi bold only for tiny buttons

* [Web] Fix thread jumps (#9111)

* [Web] Fix thread jumps

* Comment formatting

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Add StackedButton component (#9086)

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: pfrazee <1270099+pfrazee@users.noreply.github.com>
Co-authored-by: Chenyu <10610892+BinaryFiddler@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

+253 -104
+1
.eslintrc.js
··· 37 37 'Toast.Action', 38 38 'AgeAssuranceAdmonition', 39 39 'Span', 40 + 'StackedButton', 40 41 ], 41 42 impliedTextProps: [], 42 43 suggestedTextWrappers: {
+44 -24
src/components/Admonition.tsx
··· 3 3 4 4 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 5 5 import {Button as BaseButton, type ButtonProps} from '#/components/Button' 6 - import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 7 - import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye' 8 - import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf' 6 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 7 + import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' 9 8 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 10 9 import {Text as BaseText, type TextProps} from '#/components/Typography' 11 10 12 11 export const colors = { 13 - warning: { 14 - light: '#DFBC00', 15 - dark: '#BFAF1F', 16 - }, 12 + warning: '#FFC404', 17 13 } 18 14 19 15 type Context = { ··· 29 25 const t = useTheme() 30 26 const {type} = useContext(Context) 31 27 const Icon = { 32 - info: InfoIcon, 33 - tip: TipIcon, 28 + info: CircleInfoIcon, 29 + tip: CircleInfoIcon, 34 30 warning: WarningIcon, 35 - error: ErrorIcon, 31 + error: CircleXIcon, 36 32 }[type] 37 33 const fill = { 38 34 info: t.atoms.text_contrast_medium.color, 39 35 tip: t.palette.primary_500, 40 - warning: colors.warning.light, 36 + warning: colors.warning, 41 37 error: t.palette.negative_500, 42 38 }[type] 43 39 return <Icon fill={fill} size="md" /> 44 40 } 45 41 42 + export function Content({ 43 + children, 44 + style, 45 + ...rest 46 + }: { 47 + children: React.ReactNode 48 + style?: StyleProp<ViewStyle> 49 + }) { 50 + return ( 51 + <View 52 + style={[a.gap_sm, a.flex_1, {minHeight: 20}, a.justify_center, style]} 53 + {...rest}> 54 + {children} 55 + </View> 56 + ) 57 + } 58 + 46 59 export function Text({ 47 60 children, 48 61 style, 49 62 ...rest 50 63 }: Pick<TextProps, 'children' | 'style'>) { 51 64 return ( 52 - <BaseText 53 - {...rest} 54 - style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}> 65 + <BaseText {...rest} style={[a.text_sm, a.leading_snug, a.pr_md, style]}> 55 66 {children} 56 67 </BaseText> 57 68 ) ··· 60 71 export function Button({ 61 72 children, 62 73 ...props 63 - }: Omit<ButtonProps, 'size' | 'variant' | 'color'>) { 74 + }: Omit<ButtonProps, 'size' | 'variant'>) { 64 75 return ( 65 - <BaseButton size="tiny" variant="outline" color="secondary" {...props}> 76 + <BaseButton size="tiny" {...props}> 66 77 {children} 67 78 </BaseButton> 68 79 ) 69 80 } 70 81 71 - export function Row({children}: {children: React.ReactNode}) { 82 + export function Row({ 83 + children, 84 + style, 85 + }: { 86 + children: React.ReactNode 87 + style?: StyleProp<ViewStyle> 88 + }) { 72 89 return ( 73 - <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 90 + <View style={[a.flex_1, a.flex_row, a.align_start, a.gap_sm, style]}> 74 91 {children} 75 92 </View> 76 93 ) ··· 88 105 const t = useTheme() 89 106 const {gtMobile} = useBreakpoints() 90 107 const borderColor = { 91 - info: t.atoms.border_contrast_low.borderColor, 92 - tip: t.atoms.border_contrast_low.borderColor, 93 - warning: t.atoms.border_contrast_low.borderColor, 94 - error: t.atoms.border_contrast_low.borderColor, 108 + info: t.atoms.border_contrast_high.borderColor, 109 + tip: t.palette.primary_500, 110 + warning: colors.warning, 111 + error: t.palette.negative_500, 95 112 }[type] 96 113 return ( 97 114 <Context.Provider value={{type}}> 98 115 <View 99 116 style={[ 100 117 gtMobile ? a.p_md : a.p_sm, 118 + a.p_md, 101 119 a.rounded_sm, 102 120 a.border, 103 - t.atoms.bg_contrast_25, 121 + t.atoms.bg, 104 122 {borderColor}, 105 123 style, 106 124 ]}> ··· 123 141 <Outer type={type} style={style}> 124 142 <Row> 125 143 <Icon /> 126 - <Text>{children}</Text> 144 + <Content> 145 + <Text>{children}</Text> 146 + </Content> 127 147 </Row> 128 148 </Outer> 129 149 )
+53 -41
src/components/Button.tsx
··· 274 274 } else if (color === 'primary_subtle') { 275 275 if (!disabled) { 276 276 baseStyles.push({ 277 - backgroundColor: select(t.name, { 278 - light: t.palette.primary_50, 279 - dim: t.palette.primary_100, 280 - dark: t.palette.primary_100, 281 - }), 277 + backgroundColor: t.palette.primary_50, 282 278 }) 283 279 hoverStyles.push({ 284 - backgroundColor: select(t.name, { 285 - light: t.palette.primary_100, 286 - dim: t.palette.primary_200, 287 - dark: t.palette.primary_200, 288 - }), 280 + backgroundColor: t.palette.primary_100, 289 281 }) 290 282 } else { 291 283 baseStyles.push({ ··· 299 291 } else if (color === 'negative_subtle') { 300 292 if (!disabled) { 301 293 baseStyles.push({ 302 - backgroundColor: select(t.name, { 303 - light: t.palette.negative_50, 304 - dim: t.palette.negative_100, 305 - dark: t.palette.negative_100, 306 - }), 294 + backgroundColor: t.palette.negative_50, 307 295 }) 308 296 hoverStyles.push({ 309 - backgroundColor: select(t.name, { 310 - light: t.palette.negative_100, 311 - dim: t.palette.negative_200, 312 - dark: t.palette.negative_200, 313 - }), 297 + backgroundColor: t.palette.negative_100, 314 298 }) 315 299 } else { 316 300 baseStyles.push({ ··· 626 610 } else if (color === 'primary_subtle') { 627 611 if (!disabled) { 628 612 baseStyles.push({ 629 - color: select(t.name, { 630 - light: t.palette.primary_600, 631 - dim: t.palette.primary_800, 632 - dark: t.palette.primary_800, 633 - }), 613 + color: t.palette.primary_600, 634 614 }) 635 615 } else { 636 616 baseStyles.push({ 637 - color: select(t.name, { 638 - light: t.palette.primary_200, 639 - dim: t.palette.primary_200, 640 - dark: t.palette.primary_200, 641 - }), 617 + color: t.palette.primary_200, 642 618 }) 643 619 } 644 620 } else if (color === 'negative_subtle') { 645 621 if (!disabled) { 646 622 baseStyles.push({ 647 - color: select(t.name, { 648 - light: t.palette.negative_600, 649 - dim: t.palette.negative_800, 650 - dark: t.palette.negative_800, 651 - }), 623 + color: t.palette.negative_600, 652 624 }) 653 625 } else { 654 626 baseStyles.push({ 655 - color: select(t.name, { 656 - light: t.palette.negative_200, 657 - dim: t.palette.negative_200, 658 - dark: t.palette.negative_200, 659 - }), 627 + color: t.palette.negative_200, 660 628 }) 661 629 } 662 630 } ··· 763 731 } else if (size === 'small') { 764 732 baseStyles.push(a.text_sm, a.leading_snug, a.font_medium) 765 733 } else if (size === 'tiny') { 766 - baseStyles.push(a.text_xs, a.leading_snug, a.font_medium) 734 + baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold) 767 735 } 768 736 769 737 return StyleSheet.flatten(baseStyles) ··· 877 845 </View> 878 846 ) 879 847 } 848 + 849 + export type StackedButtonProps = Omit< 850 + ButtonProps, 851 + keyof VariantProps | 'children' 852 + > & 853 + Pick<VariantProps, 'color'> & { 854 + children: React.ReactNode 855 + icon: React.ComponentType<SVGIconProps> 856 + } 857 + 858 + export function StackedButton({children, ...props}: StackedButtonProps) { 859 + return ( 860 + <Button 861 + {...props} 862 + size="tiny" 863 + style={[ 864 + a.flex_col, 865 + { 866 + height: 72, 867 + paddingHorizontal: 16, 868 + borderRadius: 20, 869 + gap: 4, 870 + }, 871 + props.style, 872 + ]}> 873 + <StackedButtonInnerText icon={props.icon}> 874 + {children} 875 + </StackedButtonInnerText> 876 + </Button> 877 + ) 878 + } 879 + 880 + function StackedButtonInnerText({ 881 + children, 882 + icon: Icon, 883 + }: Pick<StackedButtonProps, 'icon' | 'children'>) { 884 + const textStyles = useSharedButtonTextStyles() 885 + return ( 886 + <> 887 + <Icon width={24} fill={textStyles.color} /> 888 + <ButtonText>{children}</ButtonText> 889 + </> 890 + ) 891 + }
+3 -1
src/components/moderation/LabelsOnMeDialog.tsx
··· 32 32 33 33 export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) { 34 34 return ( 35 - <Dialog.Outer control={props.control}> 35 + <Dialog.Outer 36 + control={props.control} 37 + nativeOptions={{preventExpansion: true}}> 36 38 <Dialog.Handle /> 37 39 <LabelsOnMeDialogInner {...props} /> 38 40 </Dialog.Outer>
+3 -1
src/components/moderation/ModerationDetailsDialog.tsx
··· 24 24 25 25 export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) { 26 26 return ( 27 - <Dialog.Outer control={props.control}> 27 + <Dialog.Outer 28 + control={props.control} 29 + nativeOptions={{preventExpansion: true}}> 28 30 <Dialog.Handle /> 29 31 <ModerationDetailsDialogInner {...props} /> 30 32 </Dialog.Outer>
+2 -1
src/components/moderation/ProfileHeaderAlerts.tsx
··· 6 6 7 7 export function ProfileHeaderAlerts({ 8 8 moderation, 9 + style, 9 10 }: { 10 11 moderation: ModerationDecision 11 12 style?: StyleProp<ViewStyle> ··· 16 17 } 17 18 18 19 return ( 19 - <Pills.Row size="lg"> 20 + <Pills.Row size="lg" style={style}> 20 21 {modui.alerts.filter(unique).map(cause => ( 21 22 <Pills.Label 22 23 size="lg"
+6 -3
src/components/moderation/ReportDialog/index.tsx
··· 219 219 <Admonition.Outer type="error"> 220 220 <Admonition.Row> 221 221 <Admonition.Icon /> 222 - <Admonition.Text> 223 - <Trans>Something went wrong, please try again</Trans> 224 - </Admonition.Text> 222 + <Admonition.Content> 223 + <Admonition.Text> 224 + <Trans>Something went wrong, please try again</Trans> 225 + </Admonition.Text> 226 + </Admonition.Content> 225 227 <Admonition.Button 228 + color="negative_subtle" 226 229 label={_(msg`Retry loading report options`)} 227 230 onPress={() => refetchLabelers()}> 228 231 <ButtonText>
+1 -1
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 620 620 621 621 if (!isBackdated) return null 622 622 623 - const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light 623 + const orange = colors.warning 624 624 625 625 return ( 626 626 <>
+13 -12
src/screens/PostThread/index.tsx
··· 148 148 */ 149 149 const shouldHandleScroll = useRef(true) 150 150 /** 151 - * Called any time the content size of the list changes, _just_ before paint. 151 + * Called any time the content size of the list changes. Could be a fresh 152 + * render, items being added to the list, or any resize that changes the 153 + * scrollable size of the content. 152 154 * 153 155 * We want this to fire every time we change params (which will reset 154 156 * `deferParents` via `onLayout` on the anchor post, due to the key change), ··· 193 195 * will give us a _positive_ offset, which will scroll the anchor post 194 196 * back _up_ to the top of the screen. 195 197 */ 196 - list.scrollToOffset({ 197 - offset: anchorOffsetTop - headerHeight, 198 - }) 198 + const offset = anchorOffsetTop - headerHeight 199 + list.scrollToOffset({offset}) 199 200 200 201 /* 201 - * After the second pass, `deferParents` will be `false`, and we need 202 - * to ensure this doesn't run again until scroll handling is requested 203 - * again via `shouldHandleScroll.current === true` and a params 204 - * change via `prepareForParamsUpdate`. 202 + * After we manage to do a positive adjustment, we need to ensure this 203 + * doesn't run again until scroll handling is requested again via 204 + * `shouldHandleScroll.current === true` and a params change via 205 + * `prepareForParamsUpdate`. 205 206 * 206 207 * The `isRoot` here is needed because if we're looking at the anchor 207 208 * post, this handler will not fire after `deferParents` is set to 208 209 * `false`, since there are no parents to render above it. In this case, 209 - * we want to make sure `shouldHandleScroll` is set to `false` so that 210 - * subsequent size changes unrelated to a params change (like pagination) 211 - * do not affect scroll. 210 + * we want to make sure `shouldHandleScroll` is set to `false` right away 211 + * so that subsequent size changes unrelated to a params change (like 212 + * pagination) do not affect scroll. 212 213 */ 213 - if (!deferParents || isRoot) shouldHandleScroll.current = false 214 + if (offset > 0 || isRoot) shouldHandleScroll.current = false 214 215 } 215 216 }) 216 217
+23 -11
src/screens/Profile/Header/Shell.tsx
··· 209 209 210 210 {children} 211 211 212 - {!isPlaceholderProfile && ( 213 - <View 214 - style={[a.px_lg, a.pt_xs, a.pb_sm]} 215 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 216 - {isMe ? ( 217 - <LabelsOnMe type="account" labels={profile.labels} /> 218 - ) : ( 219 - <ProfileHeaderAlerts moderation={moderation} /> 220 - )} 221 - </View> 222 - )} 212 + {!isPlaceholderProfile && 213 + (isMe ? ( 214 + <LabelsOnMe 215 + type="account" 216 + labels={profile.labels} 217 + style={[ 218 + a.px_lg, 219 + a.pt_xs, 220 + a.pb_sm, 221 + isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, 222 + ]} 223 + /> 224 + ) : ( 225 + <ProfileHeaderAlerts 226 + moderation={moderation} 227 + style={[ 228 + a.px_lg, 229 + a.pt_xs, 230 + a.pb_sm, 231 + isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, 232 + ]} 233 + /> 234 + ))} 223 235 224 236 <GrowableAvatar style={[a.absolute, {top: 104, left: 10}]}> 225 237 <TouchableWithoutFeedback
+1 -1
src/screens/Settings/AppPasswords.tsx
··· 195 195 </View> 196 196 {appPassword.privileged && ( 197 197 <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}> 198 - <WarningIcon style={[{color: colors.warning[t.scheme]}]} /> 198 + <WarningIcon style={[{color: colors.warning}]} /> 199 199 <Text style={t.atoms.text_contrast_high}> 200 200 <Trans>Allows access to direct messages</Trans> 201 201 </Text>
+2 -2
src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx
··· 134 134 <Admonition.Outer type="tip"> 135 135 <Admonition.Row> 136 136 <Admonition.Icon /> 137 - <View style={[a.flex_1, a.gap_sm]}> 137 + <Admonition.Content> 138 138 <Admonition.Text> 139 139 <Trans> 140 140 Enable notifications for an account by visiting their ··· 163 163 . 164 164 </Trans> 165 165 </Admonition.Text> 166 - </View> 166 + </Admonition.Content> 167 167 </Admonition.Row> 168 168 </Admonition.Outer> 169 169 ) : (
+2 -3
src/screens/Settings/PrivacyAndSecuritySettings.tsx
··· 1 - import {View} from 'react-native' 2 1 import {type AppBskyNotificationDeclaration} from '@atproto/api' 3 2 import {msg, Trans} from '@lingui/macro' 4 3 import {useLingui} from '@lingui/react' ··· 112 111 <Admonition.Outer type="tip" style={[a.flex_1]}> 113 112 <Admonition.Row> 114 113 <Admonition.Icon /> 115 - <View style={[a.flex_1, a.gap_sm]}> 114 + <Admonition.Content> 116 115 <Admonition.Text> 117 116 <Trans> 118 117 Note: Bluesky is an open and public network. This setting ··· 131 130 <Trans>Learn more about what is public on Bluesky.</Trans> 132 131 </InlineLinkText> 133 132 </Admonition.Text> 134 - </View> 133 + </Admonition.Content> 135 134 </Admonition.Row> 136 135 </Admonition.Outer> 137 136 </SettingsList.Item>
+74 -3
src/view/screens/Storybook/Admonitions.tsx
··· 1 - import {View} from 'react-native' 1 + import {Text as RNText, View} from 'react-native' 2 + import {msg, Trans} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 2 4 3 - import {atoms as a} from '#/alf' 4 - import {Admonition} from '#/components/Admonition' 5 + import {atoms as a, useTheme} from '#/alf' 6 + import { 7 + Admonition, 8 + Button as AdmonitionButton, 9 + Content as AdmonitionContent, 10 + Icon as AdmonitionIcon, 11 + Outer as AdmonitionOuter, 12 + Row as AdmonitionRow, 13 + Text as AdmonitionText, 14 + } from '#/components/Admonition' 15 + import {ButtonIcon, ButtonText} from '#/components/Button' 16 + import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotateCounterClockwise' 17 + import {BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon} from '#/components/icons/BellRinging' 5 18 import {InlineLinkText} from '#/components/Link' 6 19 import {H1} from '#/components/Typography' 7 20 8 21 export function Admonitions() { 22 + const {_} = useLingui() 23 + const t = useTheme() 24 + 9 25 return ( 10 26 <View style={[a.gap_md]}> 11 27 <H1>Admonitions</H1> ··· 30 46 <Admonition type="error"> 31 47 The quick brown fox jumps over the lazy dog. 32 48 </Admonition> 49 + 50 + <AdmonitionOuter type="error"> 51 + <AdmonitionRow> 52 + <AdmonitionIcon /> 53 + <AdmonitionContent> 54 + <AdmonitionText> 55 + <Trans>Something went wrong, please try again</Trans> 56 + </AdmonitionText> 57 + </AdmonitionContent> 58 + <AdmonitionButton 59 + color="negative_subtle" 60 + label={_(msg`Retry loading report options`)} 61 + onPress={() => {}}> 62 + <ButtonText> 63 + <Trans>Retry</Trans> 64 + </ButtonText> 65 + <ButtonIcon icon={Retry} /> 66 + </AdmonitionButton> 67 + </AdmonitionRow> 68 + </AdmonitionOuter> 69 + 70 + <AdmonitionOuter type="tip"> 71 + <AdmonitionRow> 72 + <AdmonitionIcon /> 73 + <AdmonitionContent> 74 + <AdmonitionText> 75 + <Trans> 76 + Enable notifications for an account by visiting their profile 77 + and pressing the{' '} 78 + <RNText style={[a.font_bold, t.atoms.text_contrast_high]}> 79 + bell icon 80 + </RNText>{' '} 81 + <BellRingingFilledIcon 82 + size="xs" 83 + style={t.atoms.text_contrast_high} 84 + /> 85 + . 86 + </Trans> 87 + </AdmonitionText> 88 + <AdmonitionText> 89 + <Trans> 90 + If you want to restrict who can receive notifications for your 91 + account's activity, you can change this in{' '} 92 + <InlineLinkText 93 + label={_(msg`Privacy and Security settings`)} 94 + to={{screen: 'ActivityPrivacySettings'}} 95 + style={[a.font_bold]}> 96 + Settings &rarr; Privacy and Security 97 + </InlineLinkText> 98 + . 99 + </Trans> 100 + </AdmonitionText> 101 + </AdmonitionContent> 102 + </AdmonitionRow> 103 + </AdmonitionOuter> 33 104 </View> 34 105 ) 35 106 }
+25
src/view/screens/Storybook/Buttons.tsx
··· 8 8 ButtonIcon, 9 9 type ButtonSize, 10 10 ButtonText, 11 + StackedButton, 11 12 } from '#/components/Button' 12 13 import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' 13 14 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' ··· 17 18 return ( 18 19 <View style={[a.gap_md]}> 19 20 <Text style={[a.font_heavy, a.text_5xl]}>Buttons</Text> 21 + 22 + <View style={[a.flex_row, a.gap_md, a.align_start, {maxWidth: 350}]}> 23 + <StackedButton 24 + label="stacked" 25 + icon={Globe} 26 + color="secondary" 27 + style={[a.flex_1]}> 28 + Bop it 29 + </StackedButton> 30 + <StackedButton 31 + label="stacked" 32 + icon={Globe} 33 + color="negative_subtle" 34 + style={[a.flex_1]}> 35 + Twist it 36 + </StackedButton> 37 + <StackedButton 38 + label="stacked" 39 + icon={Globe} 40 + color="primary" 41 + style={[a.flex_1]}> 42 + Pull it 43 + </StackedButton> 44 + </View> 20 45 21 46 {[ 22 47 'primary',