+1
.eslintrc.js
+1
.eslintrc.js
+8
-12
jest/test-utils.tsx
+8
-12
jest/test-utils.tsx
···
1
-
import React from 'react'
2
-
import {render} from '@testing-library/react-native'
3
1
import {GestureHandlerRootView} from 'react-native-gesture-handler'
4
-
import {RootSiblingParent} from 'react-native-root-siblings'
5
2
import {SafeAreaProvider} from 'react-native-safe-area-context'
6
-
import {RootStoreProvider, RootStoreModel} from '../src/state'
3
+
import {render} from '@testing-library/react-native'
4
+
7
5
import {ThemeProvider} from '../src/lib/ThemeContext'
6
+
import {type RootStoreModel, RootStoreProvider} from '../src/state'
8
7
9
8
const customRender = (ui: any, rootStore: RootStoreModel) =>
10
9
render(
11
-
// eslint-disable-next-line react-native/no-inline-styles
12
10
<GestureHandlerRootView style={{flex: 1}}>
13
-
<RootSiblingParent>
14
-
<RootStoreProvider value={rootStore}>
15
-
<ThemeProvider theme="light">
16
-
<SafeAreaProvider>{ui}</SafeAreaProvider>
17
-
</ThemeProvider>
18
-
</RootStoreProvider>
19
-
</RootSiblingParent>
11
+
<RootStoreProvider value={rootStore}>
12
+
<ThemeProvider theme="light">
13
+
<SafeAreaProvider>{ui}</SafeAreaProvider>
14
+
</ThemeProvider>
15
+
</RootStoreProvider>
20
16
</GestureHandlerRootView>,
21
17
)
22
18
-1
package.json
-1
package.json
···
195
195
"react-native-progress": "bluesky-social/react-native-progress",
196
196
"react-native-qrcode-styled": "^0.3.3",
197
197
"react-native-reanimated": "^3.19.1",
198
-
"react-native-root-siblings": "^5.0.1",
199
198
"react-native-safe-area-context": "~5.6.0",
200
199
"react-native-screens": "~4.16.0",
201
200
"react-native-svg": "15.12.1",
+59
-58
src/App.native.tsx
+59
-58
src/App.native.tsx
···
2
2
3
3
import React, {useEffect, useState} from 'react'
4
4
import {GestureHandlerRootView} from 'react-native-gesture-handler'
5
-
import {RootSiblingParent} from 'react-native-root-siblings'
6
5
import {
7
6
initialWindowMetrics,
8
7
SafeAreaProvider,
···
75
74
}
76
75
if (isAndroid) {
77
76
// iOS is handled by the config plugin -sfn
78
-
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
77
+
ScreenOrientation.lockAsync(
78
+
ScreenOrientation.OrientationLock.PORTRAIT_UP,
79
+
).catch(error =>
80
+
logger.debug('Could not lock orientation', {safeMessage: error}),
81
+
)
79
82
}
80
83
81
84
function InnerApp() {
···
120
123
<ThemeProvider theme={theme}>
121
124
<ContextMenuProvider>
122
125
<Splash isReady={isReady && hasCheckedReferrer}>
123
-
<RootSiblingParent>
124
-
<VideoVolumeProvider>
125
-
<React.Fragment
126
-
// Resets the entire tree below when it changes:
127
-
key={currentAccount?.did}>
128
-
<QueryProvider currentDid={currentAccount?.did}>
129
-
<PolicyUpdateOverlayProvider>
130
-
<StatsigProvider>
131
-
<ComposerProvider>
132
-
<MessagesProvider>
133
-
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
134
-
<LabelDefsProvider>
135
-
<ModerationOptsProvider>
136
-
<LoggedOutViewProvider>
137
-
<SelectedFeedProvider>
138
-
<HiddenRepliesProvider>
139
-
<HomeBadgeProvider>
140
-
<UnreadNotifsProvider>
141
-
<BackgroundNotificationPreferencesProvider>
142
-
<MutedThreadsProvider>
143
-
<ProgressGuideProvider>
144
-
<ServiceAccountManager>
145
-
<EmailVerificationProvider>
146
-
<HideBottomBarBorderProvider>
147
-
<GestureHandlerRootView
148
-
style={s.h100pct}>
149
-
<GlobalGestureEventsProvider>
150
-
<IntentDialogProvider>
151
-
<TestCtrls />
152
-
<Shell />
153
-
<NuxDialogs />
154
-
<ToastOutlet />
155
-
</IntentDialogProvider>
156
-
</GlobalGestureEventsProvider>
157
-
</GestureHandlerRootView>
158
-
</HideBottomBarBorderProvider>
159
-
</EmailVerificationProvider>
160
-
</ServiceAccountManager>
161
-
</ProgressGuideProvider>
162
-
</MutedThreadsProvider>
163
-
</BackgroundNotificationPreferencesProvider>
164
-
</UnreadNotifsProvider>
165
-
</HomeBadgeProvider>
166
-
</HiddenRepliesProvider>
167
-
</SelectedFeedProvider>
168
-
</LoggedOutViewProvider>
169
-
</ModerationOptsProvider>
170
-
</LabelDefsProvider>
171
-
</MessagesProvider>
172
-
</ComposerProvider>
173
-
</StatsigProvider>
174
-
</PolicyUpdateOverlayProvider>
175
-
</QueryProvider>
176
-
</React.Fragment>
177
-
</VideoVolumeProvider>
178
-
</RootSiblingParent>
126
+
<VideoVolumeProvider>
127
+
<React.Fragment
128
+
// Resets the entire tree below when it changes:
129
+
key={currentAccount?.did}>
130
+
<QueryProvider currentDid={currentAccount?.did}>
131
+
<PolicyUpdateOverlayProvider>
132
+
<StatsigProvider>
133
+
<ComposerProvider>
134
+
<MessagesProvider>
135
+
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
136
+
<LabelDefsProvider>
137
+
<ModerationOptsProvider>
138
+
<LoggedOutViewProvider>
139
+
<SelectedFeedProvider>
140
+
<HiddenRepliesProvider>
141
+
<HomeBadgeProvider>
142
+
<UnreadNotifsProvider>
143
+
<BackgroundNotificationPreferencesProvider>
144
+
<MutedThreadsProvider>
145
+
<ProgressGuideProvider>
146
+
<ServiceAccountManager>
147
+
<EmailVerificationProvider>
148
+
<HideBottomBarBorderProvider>
149
+
<GestureHandlerRootView
150
+
style={s.h100pct}>
151
+
<GlobalGestureEventsProvider>
152
+
<IntentDialogProvider>
153
+
<TestCtrls />
154
+
<Shell />
155
+
<NuxDialogs />
156
+
<ToastOutlet />
157
+
</IntentDialogProvider>
158
+
</GlobalGestureEventsProvider>
159
+
</GestureHandlerRootView>
160
+
</HideBottomBarBorderProvider>
161
+
</EmailVerificationProvider>
162
+
</ServiceAccountManager>
163
+
</ProgressGuideProvider>
164
+
</MutedThreadsProvider>
165
+
</BackgroundNotificationPreferencesProvider>
166
+
</UnreadNotifsProvider>
167
+
</HomeBadgeProvider>
168
+
</HiddenRepliesProvider>
169
+
</SelectedFeedProvider>
170
+
</LoggedOutViewProvider>
171
+
</ModerationOptsProvider>
172
+
</LabelDefsProvider>
173
+
</MessagesProvider>
174
+
</ComposerProvider>
175
+
</StatsigProvider>
176
+
</PolicyUpdateOverlayProvider>
177
+
</QueryProvider>
178
+
</React.Fragment>
179
+
</VideoVolumeProvider>
179
180
</Splash>
180
181
</ContextMenuProvider>
181
182
</ThemeProvider>
+52
-55
src/App.web.tsx
+52
-55
src/App.web.tsx
···
2
2
import './style.css'
3
3
4
4
import React, {useEffect, useState} from 'react'
5
-
import {RootSiblingParent} from 'react-native-root-siblings'
6
5
import {SafeAreaProvider} from 'react-native-safe-area-context'
7
6
import {msg} from '@lingui/macro'
8
7
import {useLingui} from '@lingui/react'
···
98
97
<Alf theme={theme}>
99
98
<ThemeProvider theme={theme}>
100
99
<ContextMenuProvider>
101
-
<RootSiblingParent>
102
-
<VideoVolumeProvider>
103
-
<ActiveVideoProvider>
104
-
<React.Fragment
105
-
// Resets the entire tree below when it changes:
106
-
key={currentAccount?.did}>
107
-
<QueryProvider currentDid={currentAccount?.did}>
108
-
<PolicyUpdateOverlayProvider>
109
-
<StatsigProvider>
110
-
<ComposerProvider>
111
-
<MessagesProvider>
112
-
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
113
-
<LabelDefsProvider>
114
-
<ModerationOptsProvider>
115
-
<LoggedOutViewProvider>
116
-
<SelectedFeedProvider>
117
-
<HiddenRepliesProvider>
118
-
<HomeBadgeProvider>
119
-
<UnreadNotifsProvider>
120
-
<BackgroundNotificationPreferencesProvider>
121
-
<MutedThreadsProvider>
122
-
<SafeAreaProvider>
123
-
<ProgressGuideProvider>
124
-
<ServiceConfigProvider>
125
-
<EmailVerificationProvider>
126
-
<HideBottomBarBorderProvider>
127
-
<IntentDialogProvider>
128
-
<Shell />
129
-
<NuxDialogs />
130
-
<ToastOutlet />
131
-
</IntentDialogProvider>
132
-
</HideBottomBarBorderProvider>
133
-
</EmailVerificationProvider>
134
-
</ServiceConfigProvider>
135
-
</ProgressGuideProvider>
136
-
</SafeAreaProvider>
137
-
</MutedThreadsProvider>
138
-
</BackgroundNotificationPreferencesProvider>
139
-
</UnreadNotifsProvider>
140
-
</HomeBadgeProvider>
141
-
</HiddenRepliesProvider>
142
-
</SelectedFeedProvider>
143
-
</LoggedOutViewProvider>
144
-
</ModerationOptsProvider>
145
-
</LabelDefsProvider>
146
-
</MessagesProvider>
147
-
</ComposerProvider>
148
-
</StatsigProvider>
149
-
</PolicyUpdateOverlayProvider>
150
-
</QueryProvider>
151
-
</React.Fragment>
152
-
</ActiveVideoProvider>
153
-
</VideoVolumeProvider>
154
-
</RootSiblingParent>
100
+
<VideoVolumeProvider>
101
+
<ActiveVideoProvider>
102
+
<React.Fragment
103
+
// Resets the entire tree below when it changes:
104
+
key={currentAccount?.did}>
105
+
<QueryProvider currentDid={currentAccount?.did}>
106
+
<PolicyUpdateOverlayProvider>
107
+
<StatsigProvider>
108
+
<ComposerProvider>
109
+
<MessagesProvider>
110
+
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
111
+
<LabelDefsProvider>
112
+
<ModerationOptsProvider>
113
+
<LoggedOutViewProvider>
114
+
<SelectedFeedProvider>
115
+
<HiddenRepliesProvider>
116
+
<HomeBadgeProvider>
117
+
<UnreadNotifsProvider>
118
+
<BackgroundNotificationPreferencesProvider>
119
+
<MutedThreadsProvider>
120
+
<SafeAreaProvider>
121
+
<ProgressGuideProvider>
122
+
<ServiceConfigProvider>
123
+
<EmailVerificationProvider>
124
+
<HideBottomBarBorderProvider>
125
+
<IntentDialogProvider>
126
+
<Shell />
127
+
<NuxDialogs />
128
+
<ToastOutlet />
129
+
</IntentDialogProvider>
130
+
</HideBottomBarBorderProvider>
131
+
</EmailVerificationProvider>
132
+
</ServiceConfigProvider>
133
+
</ProgressGuideProvider>
134
+
</SafeAreaProvider>
135
+
</MutedThreadsProvider>
136
+
</BackgroundNotificationPreferencesProvider>
137
+
</UnreadNotifsProvider>
138
+
</HomeBadgeProvider>
139
+
</HiddenRepliesProvider>
140
+
</SelectedFeedProvider>
141
+
</LoggedOutViewProvider>
142
+
</ModerationOptsProvider>
143
+
</LabelDefsProvider>
144
+
</MessagesProvider>
145
+
</ComposerProvider>
146
+
</StatsigProvider>
147
+
</PolicyUpdateOverlayProvider>
148
+
</QueryProvider>
149
+
</React.Fragment>
150
+
</ActiveVideoProvider>
151
+
</VideoVolumeProvider>
155
152
</ContextMenuProvider>
156
153
</ThemeProvider>
157
154
</Alf>
+10
-4
src/alf/util/systemUI.ts
+10
-4
src/alf/util/systemUI.ts
···
1
1
import * as SystemUI from 'expo-system-ui'
2
2
import {type Theme} from '@bsky.app/alf'
3
3
4
+
import {logger} from '#/logger'
4
5
import {isAndroid} from '#/platform/detection'
5
6
6
7
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
7
8
if (isAndroid) {
8
-
if (themeType === 'theme') {
9
-
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
10
-
} else {
11
-
SystemUI.setBackgroundColorAsync('black')
9
+
try {
10
+
if (themeType === 'theme') {
11
+
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
12
+
} else {
13
+
SystemUI.setBackgroundColorAsync('black')
14
+
}
15
+
} catch (error) {
16
+
// Can reject with 'The current activity is no longer available' - no big deal
17
+
logger.debug('Could not set system UI theme', {safeMessage: error})
12
18
}
13
19
}
14
20
}
+44
-24
src/components/Admonition.tsx
+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
+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({
···
295
287
} else if (color === 'negative_subtle') {
296
288
if (!disabled) {
297
289
baseStyles.push({
298
-
backgroundColor: select(t.name, {
299
-
light: t.palette.negative_50,
300
-
dim: t.palette.negative_100,
301
-
dark: t.palette.negative_100,
302
-
}),
290
+
backgroundColor: t.palette.negative_50,
303
291
})
304
292
hoverStyles.push({
305
-
backgroundColor: select(t.name, {
306
-
light: t.palette.negative_100,
307
-
dim: t.palette.negative_200,
308
-
dark: t.palette.negative_200,
309
-
}),
293
+
backgroundColor: t.palette.negative_100,
310
294
})
311
295
} else {
312
296
baseStyles.push({
···
618
602
} else if (color === 'primary_subtle') {
619
603
if (!disabled) {
620
604
baseStyles.push({
621
-
color: select(t.name, {
622
-
light: t.palette.primary_600,
623
-
dim: t.palette.primary_800,
624
-
dark: t.palette.primary_800,
625
-
}),
605
+
color: t.palette.primary_600,
626
606
})
627
607
} else {
628
608
baseStyles.push({
629
-
color: select(t.name, {
630
-
light: t.palette.primary_200,
631
-
dim: t.palette.primary_200,
632
-
dark: t.palette.primary_200,
633
-
}),
609
+
color: t.palette.primary_200,
634
610
})
635
611
}
636
612
} else if (color === 'negative_subtle') {
637
613
if (!disabled) {
638
614
baseStyles.push({
639
-
color: select(t.name, {
640
-
light: t.palette.negative_600,
641
-
dim: t.palette.negative_800,
642
-
dark: t.palette.negative_800,
643
-
}),
615
+
color: t.palette.negative_600,
644
616
})
645
617
} else {
646
618
baseStyles.push({
647
-
color: select(t.name, {
648
-
light: t.palette.negative_200,
649
-
dim: t.palette.negative_200,
650
-
dark: t.palette.negative_200,
651
-
}),
619
+
color: t.palette.negative_200,
652
620
})
653
621
}
654
622
}
···
755
723
} else if (size === 'small') {
756
724
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
757
725
} else if (size === 'tiny') {
758
-
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
726
+
baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold)
759
727
}
760
728
761
729
return StyleSheet.flatten(baseStyles)
···
869
837
</View>
870
838
)
871
839
}
840
+
841
+
export type StackedButtonProps = Omit<
842
+
ButtonProps,
843
+
keyof VariantProps | 'children'
844
+
> &
845
+
Pick<VariantProps, 'color'> & {
846
+
children: React.ReactNode
847
+
icon: React.ComponentType<SVGIconProps>
848
+
}
849
+
850
+
export function StackedButton({children, ...props}: StackedButtonProps) {
851
+
return (
852
+
<Button
853
+
{...props}
854
+
size="tiny"
855
+
style={[
856
+
a.flex_col,
857
+
{
858
+
height: 72,
859
+
paddingHorizontal: 16,
860
+
borderRadius: 20,
861
+
gap: 4,
862
+
},
863
+
props.style,
864
+
]}>
865
+
<StackedButtonInnerText icon={props.icon}>
866
+
{children}
867
+
</StackedButtonInnerText>
868
+
</Button>
869
+
)
870
+
}
871
+
872
+
function StackedButtonInnerText({
873
+
children,
874
+
icon: Icon,
875
+
}: Pick<StackedButtonProps, 'icon' | 'children'>) {
876
+
const textStyles = useSharedButtonTextStyles()
877
+
return (
878
+
<>
879
+
<Icon width={24} fill={textStyles.color} />
880
+
<ButtonText>{children}</ButtonText>
881
+
</>
882
+
)
883
+
}
+2
-2
src/components/Link.tsx
+2
-2
src/components/Link.tsx
···
169
169
if (isNative && screen !== 'NotFound') {
170
170
const state = navigation.getState()
171
171
// if screen is not in the current navigator, it means it's
172
-
// most likely a tab screen
173
-
if (!state.routeNames.includes(screen)) {
172
+
// most likely a tab screen. note: state can be undefined
173
+
if (!state?.routeNames.includes(screen)) {
174
174
const parent = navigation.getParent()
175
175
if (
176
176
parent &&
+3
-1
src/components/moderation/LabelsOnMeDialog.tsx
+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
+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
+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
+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>
+9
-4
src/lib/hooks/useOTAUpdates.ts
+9
-4
src/lib/hooks/useOTAUpdates.ts
···
10
10
useUpdates,
11
11
} from 'expo-updates'
12
12
13
+
import {isNetworkError} from '#/lib/strings/errors'
13
14
import {logger} from '#/logger'
14
15
import {isIOS} from '#/platform/detection'
15
16
import {IS_TESTFLIGHT} from '#/env'
···
145
146
} else {
146
147
logger.debug('No update available.')
147
148
}
148
-
} catch (e) {
149
-
logger.error('OTA Update Error', {error: `${e}`})
149
+
} catch (err) {
150
+
if (!isNetworkError(err)) {
151
+
logger.error('OTA Update Error', {safeMessage: err})
152
+
}
150
153
}
151
154
}, 10e3)
152
155
}, [])
···
154
157
const onIsTestFlight = React.useCallback(async () => {
155
158
try {
156
159
await updateTestflight()
157
-
} catch (e: any) {
158
-
logger.error('Internal OTA Update Error', {error: `${e}`})
160
+
} catch (err: any) {
161
+
if (!isNetworkError(err)) {
162
+
logger.error('Internal OTA Update Error', {safeMessage: err})
163
+
}
159
164
}
160
165
}, [])
161
166
+1
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
+1
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
+13
-12
src/screens/PostThread/index.tsx
+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
+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
+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
+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
···
166
166
.
167
167
</Trans>
168
168
</Admonition.Text>
169
-
</View>
169
+
</Admonition.Content>
170
170
</Admonition.Row>
171
171
</Admonition.Outer>
172
172
) : (
+2
-3
src/screens/Settings/PrivacyAndSecuritySettings.tsx
+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>
+15
-3
src/state/feed-feedback.tsx
+15
-3
src/state/feed-feedback.tsx
···
28
28
29
29
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
30
30
31
-
export const DIRECT_FEEDBACK_INTERACTIONS = new Set<
31
+
export const THIRD_PARTY_ALLOWED_INTERACTIONS = new Set<
32
32
AppBskyFeedDefs.Interaction['event']
33
-
>(['app.bsky.feed.defs#requestLess', 'app.bsky.feed.defs#requestMore'])
33
+
>([
34
+
// These are explicit actions and are therefore fine to send.
35
+
'app.bsky.feed.defs#requestLess',
36
+
'app.bsky.feed.defs#requestMore',
37
+
// These can be inferred from the firehose and are therefore fine to send.
38
+
'app.bsky.feed.defs#interactionLike',
39
+
'app.bsky.feed.defs#interactionQuote',
40
+
'app.bsky.feed.defs#interactionReply',
41
+
'app.bsky.feed.defs#interactionRepost',
42
+
// This can be inferred from pagination requests for everything except the very last page
43
+
// so it is fine to send. It is crucial for third party algorithmic feeds to receive these.
44
+
'app.bsky.feed.defs#interactionSeen',
45
+
])
34
46
35
47
const logger = Logger.create(Logger.Context.FeedFeedback)
36
48
···
228
240
return false
229
241
}
230
242
const isDiscover = isDiscoverFeed(feed.feedDescriptor)
231
-
return isDiscover ? true : DIRECT_FEEDBACK_INTERACTIONS.has(interaction)
243
+
return isDiscover ? true : THIRD_PARTY_ALLOWED_INTERACTIONS.has(interaction)
232
244
}
233
245
234
246
function toString(interaction: AppBskyFeedDefs.Interaction): string {
+74
-3
src/view/screens/Storybook/Admonitions.tsx
+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 → 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
+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_bold, 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',
-5
yarn.lock
-5
yarn.lock
···
17105
17105
invariant "^2.2.4"
17106
17106
react-native-is-edge-to-edge "1.1.7"
17107
17107
17108
-
react-native-root-siblings@^5.0.1:
17109
-
version "5.0.1"
17110
-
resolved "https://registry.yarnpkg.com/react-native-root-siblings/-/react-native-root-siblings-5.0.1.tgz#97e050e5155228f65810fb1c466ff8e769c5272c"
17111
-
integrity sha512-Ay3k/fBj6ReUkWX5WNS+oEAcgPLEGOK8n7K/L7D85mf3xvd8rm/b4spsv26E4HlFzluVx5HKbxEt9cl0wQ1u3g==
17112
-
17113
17108
react-native-safe-area-context@~5.6.0:
17114
17109
version "5.6.1"
17115
17110
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz#cb4d249ef1a6f7e8fd0cfdfa9764838dffda26b6"