Compare commits

..

12 Commits

Author SHA1 Message Date
Eric Bailey d272589bdc Add StackedButton component 2025-09-30 16:08:56 -05:00
dan 5df6036c2c [Web] Fix thread jumps (#9111)
* [Web] Fix thread jumps

* Comment formatting

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-09-30 10:46:31 -05:00
Eric Bailey 3e55e52858 Button tweaks (#9106)
* Tweak button colors

* Semi bold only for tiny buttons
2025-09-30 08:40:21 -05:00
Chenyu f82a6188e6 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>
2025-09-30 05:27:19 -07:00
Samuel Newman c7ce827bfe fix gap on profile (#9081) 2025-09-30 07:28:50 +03:00
pfrazee 9e0f192546 Nightly source-language update 2025-09-30 02:34:39 +00:00
Samuel Newman d3dbb94689 Catch errors on geolocation request, reduce Sentry logs (#9098) 2025-09-29 23:12:50 +03:00
Samuel Newman 031fa95715 remove root sibling library (#9097) 2025-09-29 14:31:55 -05:00
Samuel Newman ae2c9a832f don't send some "activity no longer available" errors (#9100) 2025-09-29 14:15:38 -05:00
Samuel Newman 02a25d2aa2 Log OTA errors properly (#9101)
* Log OTA errors properly

* filter out network errors
2025-09-29 14:14:39 -05:00
Samuel Newman 2b32fff1d0 Fix link crash (#9102)
* fix link crash

* fix link crash
2025-09-29 14:14:13 -05:00
dan 22bb9c599f Send inferrable interactions to third-party feeds (#9094) 2025-09-29 14:07:21 -05:00
27 changed files with 459 additions and 299 deletions
+1
View File
@@ -37,6 +37,7 @@ module.exports = {
'Toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
],
impliedTextProps: [],
suggestedTextWrappers: {
+8 -12
View File
@@ -1,22 +1,18 @@
import React from 'react'
import {render} from '@testing-library/react-native'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootStoreProvider, RootStoreModel} from '../src/state'
import {render} from '@testing-library/react-native'
import {ThemeProvider} from '../src/lib/ThemeContext'
import {type RootStoreModel, RootStoreProvider} from '../src/state'
const customRender = (ui: any, rootStore: RootStoreModel) =>
render(
// eslint-disable-next-line react-native/no-inline-styles
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</RootSiblingParent>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</GestureHandlerRootView>,
)
+1 -2
View File
@@ -71,7 +71,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.17.0",
"@atproto/api": "^0.16.7",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.2",
@@ -197,7 +197,6 @@
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "^3.19.1",
"react-native-root-siblings": "^5.0.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
+61 -60
View File
@@ -4,7 +4,6 @@ import '#/view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
SafeAreaProvider,
@@ -84,7 +83,11 @@ if (isIOS) {
}
if (isAndroid) {
// iOS is handled by the config plugin -sfn
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
).catch(error =>
logger.debug('Could not lock orientation', {safeMessage: error}),
)
}
/**
@@ -133,64 +136,62 @@ function InnerApp() {
<ThemeProvider theme={theme}>
<ContextMenuProvider>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</Splash>
</ContextMenuProvider>
</ThemeProvider>
+54 -57
View File
@@ -3,7 +3,6 @@ import '#/view/icons'
import './style.css'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -111,62 +110,60 @@ function InnerApp() {
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<ContextMenuProvider>
<RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
</ContextMenuProvider>
</ThemeProvider>
</Alf>
+10 -4
View File
@@ -1,14 +1,20 @@
import * as SystemUI from 'expo-system-ui'
import {type Theme} from '@bsky.app/alf'
import {logger} from '#/logger'
import {isAndroid} from '#/platform/detection'
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
if (isAndroid) {
if (themeType === 'theme') {
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
} else {
SystemUI.setBackgroundColorAsync('black')
try {
if (themeType === 'theme') {
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
} else {
SystemUI.setBackgroundColorAsync('black')
}
} catch (error) {
// Can reject with 'The current activity is no longer available' - no big deal
logger.debug('Could not set system UI theme', {safeMessage: error})
}
}
}
+44 -24
View File
@@ -3,17 +3,13 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button as BaseButton, type ButtonProps} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Text as BaseText, type TextProps} from '#/components/Typography'
export const colors = {
warning: {
light: '#DFBC00',
dark: '#BFAF1F',
},
warning: '#FFC404',
}
type Context = {
@@ -29,29 +25,44 @@ export function Icon() {
const t = useTheme()
const {type} = useContext(Context)
const Icon = {
info: InfoIcon,
tip: TipIcon,
info: CircleInfoIcon,
tip: CircleInfoIcon,
warning: WarningIcon,
error: ErrorIcon,
error: CircleXIcon,
}[type]
const fill = {
info: t.atoms.text_contrast_medium.color,
tip: t.palette.primary_500,
warning: colors.warning.light,
warning: colors.warning,
error: t.palette.negative_500,
}[type]
return <Icon fill={fill} size="md" />
}
export function Content({
children,
style,
...rest
}: {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
return (
<View
style={[a.gap_sm, a.flex_1, {minHeight: 20}, a.justify_center, style]}
{...rest}>
{children}
</View>
)
}
export function Text({
children,
style,
...rest
}: Pick<TextProps, 'children' | 'style'>) {
return (
<BaseText
{...rest}
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
<BaseText {...rest} style={[a.text_sm, a.leading_snug, a.pr_md, style]}>
{children}
</BaseText>
)
@@ -60,17 +71,23 @@ export function Text({
export function Button({
children,
...props
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
}: Omit<ButtonProps, 'size' | 'variant'>) {
return (
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
<BaseButton size="tiny" {...props}>
{children}
</BaseButton>
)
}
export function Row({children}: {children: React.ReactNode}) {
export function Row({
children,
style,
}: {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<View style={[a.flex_1, a.flex_row, a.align_start, a.gap_sm, style]}>
{children}
</View>
)
@@ -88,19 +105,20 @@ export function Outer({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const borderColor = {
info: t.atoms.border_contrast_low.borderColor,
tip: t.atoms.border_contrast_low.borderColor,
warning: t.atoms.border_contrast_low.borderColor,
error: t.atoms.border_contrast_low.borderColor,
info: t.atoms.border_contrast_high.borderColor,
tip: t.palette.primary_500,
warning: colors.warning,
error: t.palette.negative_500,
}[type]
return (
<Context.Provider value={{type}}>
<View
style={[
gtMobile ? a.p_md : a.p_sm,
a.p_md,
a.rounded_sm,
a.border,
t.atoms.bg_contrast_25,
t.atoms.bg,
{borderColor},
style,
]}>
@@ -123,7 +141,9 @@ export function Admonition({
<Outer type={type} style={style}>
<Row>
<Icon />
<Text>{children}</Text>
<Content>
<Text>{children}</Text>
</Content>
</Row>
</Outer>
)
+53 -41
View File
@@ -274,18 +274,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_50,
dim: t.palette.primary_100,
dark: t.palette.primary_100,
}),
backgroundColor: t.palette.primary_50,
})
hoverStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
backgroundColor: t.palette.primary_100,
})
} else {
baseStyles.push({
@@ -295,18 +287,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_50,
dim: t.palette.negative_100,
dark: t.palette.negative_100,
}),
backgroundColor: t.palette.negative_50,
})
hoverStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_100,
dim: t.palette.negative_200,
dark: t.palette.negative_200,
}),
backgroundColor: t.palette.negative_100,
})
} else {
baseStyles.push({
@@ -618,37 +602,21 @@ export function useSharedButtonTextStyles() {
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
light: t.palette.primary_600,
dim: t.palette.primary_800,
dark: t.palette.primary_800,
}),
color: t.palette.primary_600,
})
} else {
baseStyles.push({
color: select(t.name, {
light: t.palette.primary_200,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
color: t.palette.primary_200,
})
}
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_800,
dark: t.palette.negative_800,
}),
color: t.palette.negative_600,
})
} else {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_200,
dark: t.palette.negative_200,
}),
color: t.palette.negative_200,
})
}
}
@@ -755,7 +723,7 @@ export function useSharedButtonTextStyles() {
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold)
}
return StyleSheet.flatten(baseStyles)
@@ -869,3 +837,47 @@ export function ButtonIcon({
</View>
)
}
export type StackedButtonProps = Omit<
ButtonProps,
keyof VariantProps | 'children'
> &
Pick<VariantProps, 'color'> & {
children: React.ReactNode
icon: React.ComponentType<SVGIconProps>
}
export function StackedButton({children, ...props}: StackedButtonProps) {
return (
<Button
{...props}
size="tiny"
style={[
a.flex_col,
{
height: 72,
paddingHorizontal: 16,
borderRadius: 20,
gap: 4,
},
props.style,
]}>
<StackedButtonInnerText icon={props.icon}>
{children}
</StackedButtonInnerText>
</Button>
)
}
function StackedButtonInnerText({
children,
icon: Icon,
}: Pick<StackedButtonProps, 'icon' | 'children'>) {
const textStyles = useSharedButtonTextStyles()
return (
<>
<Icon width={24} fill={textStyles.color} />
<ButtonText>{children}</ButtonText>
</>
)
}
+2 -2
View File
@@ -165,8 +165,8 @@ export function useLink({
if (isNative && screen !== 'NotFound') {
const state = navigation.getState()
// if screen is not in the current navigator, it means it's
// most likely a tab screen
if (!state.routeNames.includes(screen)) {
// most likely a tab screen. note: state can be undefined
if (!state?.routeNames.includes(screen)) {
const parent = navigation.getParent()
if (
parent &&
@@ -32,7 +32,9 @@ export interface LabelsOnMeDialogProps {
export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Outer
control={props.control}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<LabelsOnMeDialogInner {...props} />
</Dialog.Outer>
@@ -24,7 +24,9 @@ export interface ModerationDetailsDialogProps {
export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Outer
control={props.control}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<ModerationDetailsDialogInner {...props} />
</Dialog.Outer>
@@ -6,6 +6,7 @@ import * as Pills from '#/components/Pills'
export function ProfileHeaderAlerts({
moderation,
style,
}: {
moderation: ModerationDecision
style?: StyleProp<ViewStyle>
@@ -16,7 +17,7 @@ export function ProfileHeaderAlerts({
}
return (
<Pills.Row size="lg">
<Pills.Row size="lg" style={style}>
{modui.alerts.filter(unique).map(cause => (
<Pills.Label
size="lg"
@@ -219,10 +219,13 @@ function Inner(props: ReportDialogProps) {
<Admonition.Outer type="error">
<Admonition.Row>
<Admonition.Icon />
<Admonition.Text>
<Trans>Something went wrong, please try again</Trans>
</Admonition.Text>
<Admonition.Content>
<Admonition.Text>
<Trans>Something went wrong, please try again</Trans>
</Admonition.Text>
</Admonition.Content>
<Admonition.Button
color="negative_subtle"
label={_(msg`Retry loading report options`)}
onPress={() => refetchLabelers()}>
<ButtonText>
+9 -4
View File
@@ -10,6 +10,7 @@ import {
useUpdates,
} from 'expo-updates'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
@@ -145,8 +146,10 @@ export function useOTAUpdates() {
} else {
logger.debug('No update available.')
}
} catch (e) {
logger.error('OTA Update Error', {error: `${e}`})
} catch (err) {
if (!isNetworkError(err)) {
logger.error('OTA Update Error', {safeMessage: err})
}
}
}, 10e3)
}, [])
@@ -154,8 +157,10 @@ export function useOTAUpdates() {
const onIsTestFlight = React.useCallback(async () => {
try {
await updateTestflight()
} catch (e: any) {
logger.error('Internal OTA Update Error', {error: `${e}`})
} catch (err: any) {
if (!isNetworkError(err)) {
logger.error('Internal OTA Update Error', {safeMessage: err})
}
}
}, [])
+2 -2
View File
@@ -8298,8 +8298,8 @@ msgstr ""
msgid "Something wrong? Let us know."
msgstr ""
#: src/App.native.tsx:125
#: src/App.web.tsx:101
#: src/App.native.tsx:128
#: src/App.web.tsx:100
msgid "Sorry! Your session expired. Please sign in again."
msgstr ""
@@ -621,7 +621,7 @@ function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
if (!isBackdated) return null
const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
const orange = colors.warning
return (
<>
+13 -12
View File
@@ -148,7 +148,9 @@ export function PostThread({uri}: {uri: string}) {
*/
const shouldHandleScroll = useRef(true)
/**
* Called any time the content size of the list changes, _just_ before paint.
* Called any time the content size of the list changes. Could be a fresh
* render, items being added to the list, or any resize that changes the
* scrollable size of the content.
*
* We want this to fire every time we change params (which will reset
* `deferParents` via `onLayout` on the anchor post, due to the key change),
@@ -193,24 +195,23 @@ export function PostThread({uri}: {uri: string}) {
* will give us a _positive_ offset, which will scroll the anchor post
* back _up_ to the top of the screen.
*/
list.scrollToOffset({
offset: anchorOffsetTop - headerHeight,
})
const offset = anchorOffsetTop - headerHeight
list.scrollToOffset({offset})
/*
* After the second pass, `deferParents` will be `false`, and we need
* to ensure this doesn't run again until scroll handling is requested
* again via `shouldHandleScroll.current === true` and a params
* change via `prepareForParamsUpdate`.
* After we manage to do a positive adjustment, we need to ensure this
* doesn't run again until scroll handling is requested again via
* `shouldHandleScroll.current === true` and a params change via
* `prepareForParamsUpdate`.
*
* The `isRoot` here is needed because if we're looking at the anchor
* post, this handler will not fire after `deferParents` is set to
* `false`, since there are no parents to render above it. In this case,
* we want to make sure `shouldHandleScroll` is set to `false` so that
* subsequent size changes unrelated to a params change (like pagination)
* do not affect scroll.
* we want to make sure `shouldHandleScroll` is set to `false` right away
* so that subsequent size changes unrelated to a params change (like
* pagination) do not affect scroll.
*/
if (!deferParents || isRoot) shouldHandleScroll.current = false
if (offset > 0 || isRoot) shouldHandleScroll.current = false
}
})
+23 -11
View File
@@ -209,17 +209,29 @@ let ProfileHeaderShell = ({
{children}
{!isPlaceholderProfile && (
<View
style={[a.px_lg, a.pt_xs, a.pb_sm]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
{isMe ? (
<LabelsOnMe type="account" labels={profile.labels} />
) : (
<ProfileHeaderAlerts moderation={moderation} />
)}
</View>
)}
{!isPlaceholderProfile &&
(isMe ? (
<LabelsOnMe
type="account"
labels={profile.labels}
style={[
a.px_lg,
a.pt_xs,
a.pb_sm,
isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
]}
/>
) : (
<ProfileHeaderAlerts
moderation={moderation}
style={[
a.px_lg,
a.pt_xs,
a.pb_sm,
isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
]}
/>
))}
<GrowableAvatar style={[a.absolute, {top: 104, left: 10}]}>
<TouchableWithoutFeedback
+1 -1
View File
@@ -195,7 +195,7 @@ function AppPasswordCard({
</View>
{appPassword.privileged && (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
<WarningIcon style={[{color: colors.warning[t.scheme]}]} />
<WarningIcon style={[{color: colors.warning}]} />
<Text style={t.atoms.text_contrast_high}>
<Trans>Allows access to direct messages</Trans>
</Text>
@@ -134,7 +134,7 @@ export function ActivityNotificationSettingsScreen({}: Props) {
<Admonition.Outer type="tip">
<Admonition.Row>
<Admonition.Icon />
<View style={[a.flex_1, a.gap_sm]}>
<Admonition.Content>
<Admonition.Text>
<Trans>
Enable notifications for an account by visiting their
@@ -166,7 +166,7 @@ export function ActivityNotificationSettingsScreen({}: Props) {
.
</Trans>
</Admonition.Text>
</View>
</Admonition.Content>
</Admonition.Row>
</Admonition.Outer>
) : (
@@ -1,4 +1,3 @@
import {View} from 'react-native'
import {type AppBskyNotificationDeclaration} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -112,7 +111,7 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
<Admonition.Outer type="tip" style={[a.flex_1]}>
<Admonition.Row>
<Admonition.Icon />
<View style={[a.flex_1, a.gap_sm]}>
<Admonition.Content>
<Admonition.Text>
<Trans>
Note: Bluesky is an open and public network. This setting
@@ -131,7 +130,7 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
<Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLinkText>
</Admonition.Text>
</View>
</Admonition.Content>
</Admonition.Row>
</Admonition.Outer>
</SettingsList.Item>
+15 -3
View File
@@ -28,9 +28,21 @@ import {useAgent} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
export const DIRECT_FEEDBACK_INTERACTIONS = new Set<
export const THIRD_PARTY_ALLOWED_INTERACTIONS = new Set<
AppBskyFeedDefs.Interaction['event']
>(['app.bsky.feed.defs#requestLess', 'app.bsky.feed.defs#requestMore'])
>([
// These are explicit actions and are therefore fine to send.
'app.bsky.feed.defs#requestLess',
'app.bsky.feed.defs#requestMore',
// These can be inferred from the firehose and are therefore fine to send.
'app.bsky.feed.defs#interactionLike',
'app.bsky.feed.defs#interactionQuote',
'app.bsky.feed.defs#interactionReply',
'app.bsky.feed.defs#interactionRepost',
// This can be inferred from pagination requests for everything except the very last page
// so it is fine to send. It is crucial for third party algorithmic feeds to receive these.
'app.bsky.feed.defs#interactionSeen',
])
const logger = Logger.create(Logger.Context.FeedFeedback)
@@ -228,7 +240,7 @@ function isInteractionAllowed(
return false
}
const isDiscover = isDiscoverFeed(feed.feedDescriptor)
return isDiscover ? true : DIRECT_FEEDBACK_INTERACTIONS.has(interaction)
return isDiscover ? true : THIRD_PARTY_ALLOWED_INTERACTIONS.has(interaction)
}
function toString(interaction: AppBskyFeedDefs.Interaction): string {
@@ -1,10 +1,45 @@
import {useEffect, useRef} from 'react'
import * as Location from 'expo-location'
import {createPermissionHook} from 'expo-modules-core'
import {logger} from '#/state/geolocation/logger'
import {getDeviceGeolocation} from '#/state/geolocation/util'
import {device, useStorage} from '#/storage'
/**
* Location.useForegroundPermissions on web just errors if the navigator.permissions API is not available.
* We need to catch and ignore it, since it's effectively denied.
* @see https://github.com/expo/expo/blob/72f1562ed9cce5ff6dfe04aa415b71632a3d4b87/packages/expo-location/src/Location.ts#L290-L293
*/
const useForegroundPermissions = createPermissionHook({
getMethod: () =>
Location.getForegroundPermissionsAsync().catch(error => {
logger.debug(
'useForegroundPermission: error getting location permissions',
{safeMessage: error},
)
return {
status: Location.PermissionStatus.DENIED,
granted: false,
canAskAgain: false,
expires: 0,
}
}),
requestMethod: () =>
Location.requestForegroundPermissionsAsync().catch(error => {
logger.debug(
'useForegroundPermission: error requesting location permissions',
{safeMessage: error},
)
return {
status: Location.PermissionStatus.DENIED,
granted: false,
canAskAgain: false,
expires: 0,
}
}),
})
/**
* Hook to get and sync the device geolocation from the device GPS and store it
* using device storage. If permissions are not granted, it will clear any cached
@@ -12,7 +47,7 @@ import {device, useStorage} from '#/storage'
*/
export function useSyncedDeviceGeolocation() {
const synced = useRef(false)
const [status] = Location.useForegroundPermissions()
const [status] = useForegroundPermissions()
const [deviceGeolocation, setDeviceGeolocation] = useStorage(device, [
'deviceGeolocation',
])
+8
View File
@@ -321,7 +321,15 @@ class BskyAppAgent extends BskyAgent {
// Now the agent is ready.
const account = agentToSessionAccountOrThrow(this)
let lastSession = this.sessionManager.session
this.persistSessionHandler = event => {
if (this.sessionManager.session) {
lastSession = this.sessionManager.session
} else if (event === 'network-error') {
// Put it back, we'll try again later.
this.sessionManager.session = lastSession
}
onSessionChange(this, account.did, event)
if (event !== 'create' && event !== 'update') {
addSessionErrorLog(account.did, event)
+74 -3
View File
@@ -1,11 +1,27 @@
import {View} from 'react-native'
import {Text as RNText, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {atoms as a, useTheme} from '#/alf'
import {
Admonition,
Button as AdmonitionButton,
Content as AdmonitionContent,
Icon as AdmonitionIcon,
Outer as AdmonitionOuter,
Row as AdmonitionRow,
Text as AdmonitionText,
} from '#/components/Admonition'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotateCounterClockwise'
import {BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon} from '#/components/icons/BellRinging'
import {InlineLinkText} from '#/components/Link'
import {H1} from '#/components/Typography'
export function Admonitions() {
const {_} = useLingui()
const t = useTheme()
return (
<View style={[a.gap_md]}>
<H1>Admonitions</H1>
@@ -30,6 +46,61 @@ export function Admonitions() {
<Admonition type="error">
The quick brown fox jumps over the lazy dog.
</Admonition>
<AdmonitionOuter type="error">
<AdmonitionRow>
<AdmonitionIcon />
<AdmonitionContent>
<AdmonitionText>
<Trans>Something went wrong, please try again</Trans>
</AdmonitionText>
</AdmonitionContent>
<AdmonitionButton
color="negative_subtle"
label={_(msg`Retry loading report options`)}
onPress={() => {}}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
<ButtonIcon icon={Retry} />
</AdmonitionButton>
</AdmonitionRow>
</AdmonitionOuter>
<AdmonitionOuter type="tip">
<AdmonitionRow>
<AdmonitionIcon />
<AdmonitionContent>
<AdmonitionText>
<Trans>
Enable notifications for an account by visiting their profile
and pressing the{' '}
<RNText style={[a.font_bold, t.atoms.text_contrast_high]}>
bell icon
</RNText>{' '}
<BellRingingFilledIcon
size="xs"
style={t.atoms.text_contrast_high}
/>
.
</Trans>
</AdmonitionText>
<AdmonitionText>
<Trans>
If you want to restrict who can receive notifications for your
account's activity, you can change this in{' '}
<InlineLinkText
label={_(msg`Privacy and Security settings`)}
to={{screen: 'ActivityPrivacySettings'}}
style={[a.font_bold]}>
Settings &rarr; Privacy and Security
</InlineLinkText>
.
</Trans>
</AdmonitionText>
</AdmonitionContent>
</AdmonitionRow>
</AdmonitionOuter>
</View>
)
}
+25
View File
@@ -8,6 +8,7 @@ import {
ButtonIcon,
type ButtonSize,
ButtonText,
StackedButton,
} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
@@ -18,6 +19,30 @@ export function Buttons() {
<View style={[a.gap_md]}>
<Text style={[a.font_bold, a.text_5xl]}>Buttons</Text>
<View style={[a.flex_row, a.gap_md, a.align_start, {maxWidth: 350}]}>
<StackedButton
label="stacked"
icon={Globe}
color="secondary"
style={[a.flex_1]}>
Bop it
</StackedButton>
<StackedButton
label="stacked"
icon={Globe}
color="negative_subtle"
style={[a.flex_1]}>
Twist it
</StackedButton>
<StackedButton
label="stacked"
icon={Globe}
color="primary"
style={[a.flex_1]}>
Pull it
</StackedButton>
</View>
{[
'primary',
'secondary',
-48
View File
@@ -77,20 +77,6 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.17.0.tgz#1fe87ef703f8020dbe00bb5e5cc18622b8b91f4a"
integrity sha512-FNS9SW7/3kslAnJH7F4fO9/jPjXzC0NMD6u9NjJ/h4EnaIEpWHZQPkmD9Q2hvAwD6+Uo2boYZEPKkOa55Lr5Dg==
dependencies:
"@atproto/common-web" "^0.4.3"
"@atproto/lexicon" "^0.5.1"
"@atproto/syntax" "^0.4.1"
"@atproto/xrpc" "^0.7.5"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/aws@^0.2.28":
version "0.2.28"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.28.tgz#17bd88a6276e323ebb094a3f01bd94b1173a29a4"
@@ -184,16 +170,6 @@
uint8arrays "3.0.0"
zod "^3.23.8"
"@atproto/common-web@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.3.tgz#b4480220b5682db09da45f4ef906eb7619c838b5"
integrity sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==
dependencies:
graphemer "^1.4.0"
multiformats "^9.9.0"
uint8arrays "3.0.0"
zod "^3.23.8"
"@atproto/common@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
@@ -327,17 +303,6 @@
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/lexicon@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.1.tgz#e9b7d5c70dc5a38518a8069cd80fea77ab526947"
integrity sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==
dependencies:
"@atproto/common-web" "^0.4.3"
"@atproto/syntax" "^0.4.1"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/oauth-provider-api@0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.0.tgz#c53a6f2584e6e53746b6cdf233be591fdf7d4355"
@@ -551,14 +516,6 @@
"@atproto/lexicon" "^0.5.0"
zod "^3.23.8"
"@atproto/xrpc@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.5.tgz#40cef1a657b5f28af8ebec9e3dac5872e58e88ea"
integrity sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==
dependencies:
"@atproto/lexicon" "^0.5.1"
zod "^3.23.8"
"@aws-crypto/crc32@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
@@ -17114,11 +17071,6 @@ react-native-reanimated@^3.19.1:
invariant "^2.2.4"
react-native-is-edge-to-edge "1.1.7"
react-native-root-siblings@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-native-root-siblings/-/react-native-root-siblings-5.0.1.tgz#97e050e5155228f65810fb1c466ff8e769c5272c"
integrity sha512-Ay3k/fBj6ReUkWX5WNS+oEAcgPLEGOK8n7K/L7D85mf3xvd8rm/b4spsv26E4HlFzluVx5HKbxEt9cl0wQ1u3g==
react-native-safe-area-context@~5.6.0:
version "5.6.1"
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz#cb4d249ef1a6f7e8fd0cfdfa9764838dffda26b6"