Merge branch 'main' into samuel/live-handle-checks
This commit is contained in:
+2
-1
@@ -142,7 +142,7 @@
|
|||||||
"expo-file-system": "~18.1.10",
|
"expo-file-system": "~18.1.10",
|
||||||
"expo-font": "~13.3.1",
|
"expo-font": "~13.3.1",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
"expo-image": "~2.2.1",
|
"expo-image": "^2.4.0",
|
||||||
"expo-image-crop-tool": "^0.1.8",
|
"expo-image-crop-tool": "^0.1.8",
|
||||||
"expo-image-manipulator": "~13.1.7",
|
"expo-image-manipulator": "~13.1.7",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
@@ -186,6 +186,7 @@
|
|||||||
"react-native": "^0.79.3",
|
"react-native": "^0.79.3",
|
||||||
"react-native-compressor": "^1.11.0",
|
"react-native-compressor": "^1.11.0",
|
||||||
"react-native-date-picker": "^5.0.12",
|
"react-native-date-picker": "^5.0.12",
|
||||||
|
"react-native-device-attest": "^0.1.6",
|
||||||
"react-native-drawer-layout": "^4.1.8",
|
"react-native-drawer-layout": "^4.1.8",
|
||||||
"react-native-edge-to-edge": "^1.6.0",
|
"react-native-edge-to-edge": "^1.6.0",
|
||||||
"react-native-gesture-handler": "2.25.0",
|
"react-native-gesture-handler": "2.25.0",
|
||||||
|
|||||||
+46
-43
@@ -70,6 +70,7 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
|||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
|
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
@@ -137,49 +138,51 @@ function InnerApp() {
|
|||||||
// Resets the entire tree below when it changes:
|
// Resets the entire tree below when it changes:
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<AgeAssuranceProvider>
|
<StatsigProvider>
|
||||||
<ComposerProvider>
|
<AgeAssuranceProvider>
|
||||||
<MessagesProvider>
|
<ComposerProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
<MessagesProvider>
|
||||||
<LabelDefsProvider>
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
<ModerationOptsProvider>
|
<LabelDefsProvider>
|
||||||
<LoggedOutViewProvider>
|
<ModerationOptsProvider>
|
||||||
<SelectedFeedProvider>
|
<LoggedOutViewProvider>
|
||||||
<HiddenRepliesProvider>
|
<SelectedFeedProvider>
|
||||||
<HomeBadgeProvider>
|
<HiddenRepliesProvider>
|
||||||
<UnreadNotifsProvider>
|
<HomeBadgeProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<UnreadNotifsProvider>
|
||||||
<MutedThreadsProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<ProgressGuideProvider>
|
<MutedThreadsProvider>
|
||||||
<ServiceAccountManager>
|
<ProgressGuideProvider>
|
||||||
<HideBottomBarBorderProvider>
|
<ServiceAccountManager>
|
||||||
<GestureHandlerRootView
|
<HideBottomBarBorderProvider>
|
||||||
style={s.h100pct}>
|
<GestureHandlerRootView
|
||||||
<GlobalGestureEventsProvider>
|
style={s.h100pct}>
|
||||||
<IntentDialogProvider>
|
<GlobalGestureEventsProvider>
|
||||||
<TestCtrls />
|
<IntentDialogProvider>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
<NuxDialogs />
|
<Shell />
|
||||||
</IntentDialogProvider>
|
<NuxDialogs />
|
||||||
</GlobalGestureEventsProvider>
|
</IntentDialogProvider>
|
||||||
</GestureHandlerRootView>
|
</GlobalGestureEventsProvider>
|
||||||
</HideBottomBarBorderProvider>
|
</GestureHandlerRootView>
|
||||||
</ServiceAccountManager>
|
</HideBottomBarBorderProvider>
|
||||||
</ProgressGuideProvider>
|
</ServiceAccountManager>
|
||||||
</MutedThreadsProvider>
|
</ProgressGuideProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</MutedThreadsProvider>
|
||||||
</UnreadNotifsProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</HomeBadgeProvider>
|
</UnreadNotifsProvider>
|
||||||
</HiddenRepliesProvider>
|
</HomeBadgeProvider>
|
||||||
</SelectedFeedProvider>
|
</HiddenRepliesProvider>
|
||||||
</LoggedOutViewProvider>
|
</SelectedFeedProvider>
|
||||||
</ModerationOptsProvider>
|
</LoggedOutViewProvider>
|
||||||
</LabelDefsProvider>
|
</ModerationOptsProvider>
|
||||||
</MessagesProvider>
|
</LabelDefsProvider>
|
||||||
</ComposerProvider>
|
</MessagesProvider>
|
||||||
</AgeAssuranceProvider>
|
</ComposerProvider>
|
||||||
</StatsigProvider>
|
</AgeAssuranceProvider>
|
||||||
|
</StatsigProvider>
|
||||||
|
</PolicyUpdateOverlayProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
</VideoVolumeProvider>
|
</VideoVolumeProvider>
|
||||||
|
|||||||
+42
-39
@@ -57,6 +57,7 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
|||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
|
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
@@ -117,45 +118,47 @@ function InnerApp() {
|
|||||||
// Resets the entire tree below when it changes:
|
// Resets the entire tree below when it changes:
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<AgeAssuranceProvider>
|
<StatsigProvider>
|
||||||
<ComposerProvider>
|
<AgeAssuranceProvider>
|
||||||
<MessagesProvider>
|
<ComposerProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
<MessagesProvider>
|
||||||
<LabelDefsProvider>
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
<ModerationOptsProvider>
|
<LabelDefsProvider>
|
||||||
<LoggedOutViewProvider>
|
<ModerationOptsProvider>
|
||||||
<SelectedFeedProvider>
|
<LoggedOutViewProvider>
|
||||||
<HiddenRepliesProvider>
|
<SelectedFeedProvider>
|
||||||
<HomeBadgeProvider>
|
<HiddenRepliesProvider>
|
||||||
<UnreadNotifsProvider>
|
<HomeBadgeProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<UnreadNotifsProvider>
|
||||||
<MutedThreadsProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<SafeAreaProvider>
|
<MutedThreadsProvider>
|
||||||
<ProgressGuideProvider>
|
<SafeAreaProvider>
|
||||||
<ServiceConfigProvider>
|
<ProgressGuideProvider>
|
||||||
<HideBottomBarBorderProvider>
|
<ServiceConfigProvider>
|
||||||
<IntentDialogProvider>
|
<HideBottomBarBorderProvider>
|
||||||
<Shell />
|
<IntentDialogProvider>
|
||||||
<NuxDialogs />
|
<Shell />
|
||||||
</IntentDialogProvider>
|
<NuxDialogs />
|
||||||
</HideBottomBarBorderProvider>
|
</IntentDialogProvider>
|
||||||
</ServiceConfigProvider>
|
</HideBottomBarBorderProvider>
|
||||||
</ProgressGuideProvider>
|
</ServiceConfigProvider>
|
||||||
</SafeAreaProvider>
|
</ProgressGuideProvider>
|
||||||
</MutedThreadsProvider>
|
</SafeAreaProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</MutedThreadsProvider>
|
||||||
</UnreadNotifsProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</HomeBadgeProvider>
|
</UnreadNotifsProvider>
|
||||||
</HiddenRepliesProvider>
|
</HomeBadgeProvider>
|
||||||
</SelectedFeedProvider>
|
</HiddenRepliesProvider>
|
||||||
</LoggedOutViewProvider>
|
</SelectedFeedProvider>
|
||||||
</ModerationOptsProvider>
|
</LoggedOutViewProvider>
|
||||||
</LabelDefsProvider>
|
</ModerationOptsProvider>
|
||||||
</MessagesProvider>
|
</LabelDefsProvider>
|
||||||
</ComposerProvider>
|
</MessagesProvider>
|
||||||
</AgeAssuranceProvider>
|
</ComposerProvider>
|
||||||
</StatsigProvider>
|
</AgeAssuranceProvider>
|
||||||
|
</StatsigProvider>
|
||||||
|
</PolicyUpdateOverlayProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -979,6 +979,12 @@ export const atoms = {
|
|||||||
hidden: {
|
hidden: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
|
inline: web({
|
||||||
|
display: 'inline',
|
||||||
|
}),
|
||||||
|
block: web({
|
||||||
|
display: 'block',
|
||||||
|
}),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Transition
|
* Transition
|
||||||
|
|||||||
+96
-59
@@ -37,7 +37,8 @@ export type ButtonColor =
|
|||||||
| 'secondary'
|
| 'secondary'
|
||||||
| 'secondary_inverted'
|
| 'secondary_inverted'
|
||||||
| 'negative'
|
| 'negative'
|
||||||
| 'negative_secondary'
|
| 'primary_subtle'
|
||||||
|
| 'negative_subtle'
|
||||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||||
export type ButtonShape = 'round' | 'square' | 'default'
|
export type ButtonShape = 'round' | 'square' | 'default'
|
||||||
export type VariantProps = {
|
export type VariantProps = {
|
||||||
@@ -233,19 +234,15 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.palette.primary_200,
|
||||||
light: t.palette.primary_700,
|
|
||||||
dim: t.palette.primary_300,
|
|
||||||
dark: t.palette.primary_300,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (color === 'secondary') {
|
} else if (color === 'secondary') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push(t.atoms.bg_contrast_25)
|
baseStyles.push(t.atoms.bg_contrast_25)
|
||||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
hoverStyles.push(t.atoms.bg_contrast_100)
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(t.atoms.bg_contrast_100)
|
baseStyles.push(t.atoms.bg_contrast_50)
|
||||||
}
|
}
|
||||||
} else if (color === 'secondary_inverted') {
|
} else if (color === 'secondary_inverted') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
@@ -253,7 +250,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
backgroundColor: t.palette.contrast_900,
|
backgroundColor: t.palette.contrast_900,
|
||||||
})
|
})
|
||||||
hoverStyles.push({
|
hoverStyles.push({
|
||||||
backgroundColor: t.palette.contrast_950,
|
backgroundColor: t.palette.contrast_975,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
@@ -269,15 +266,36 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
backgroundColor: t.palette.negative_600,
|
backgroundColor: t.palette.negative_600,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
baseStyles.push({
|
||||||
|
backgroundColor: t.palette.negative_700,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (color === 'primary_subtle') {
|
||||||
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: select(t.name, {
|
||||||
light: t.palette.negative_700,
|
light: t.palette.primary_50,
|
||||||
dim: t.palette.negative_300,
|
dim: t.palette.primary_100,
|
||||||
dark: t.palette.negative_300,
|
dark: t.palette.primary_100,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
hoverStyles.push({
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_100,
|
||||||
|
dim: t.palette.primary_200,
|
||||||
|
dark: t.palette.primary_200,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
baseStyles.push({
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_25,
|
||||||
|
dim: t.palette.primary_50,
|
||||||
|
dark: t.palette.primary_50,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (color === 'negative_secondary') {
|
} else if (color === 'negative_subtle') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: select(t.name, {
|
||||||
@@ -296,7 +314,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: select(t.name, {
|
||||||
light: t.palette.negative_100,
|
light: t.palette.negative_25,
|
||||||
dim: t.palette.negative_50,
|
dim: t.palette.negative_50,
|
||||||
dark: t.palette.negative_50,
|
dark: t.palette.negative_50,
|
||||||
}),
|
}),
|
||||||
@@ -407,7 +425,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (color === 'negative_secondary') {
|
} else if (color === 'negative_subtle') {
|
||||||
if (variant === 'outline') {
|
if (variant === 'outline') {
|
||||||
baseStyles.push(a.border, t.atoms.bg, {
|
baseStyles.push(a.border, t.atoms.bg, {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -442,44 +460,48 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
if (shape === 'default') {
|
if (shape === 'default') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
paddingVertical: 13,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 24,
|
||||||
borderRadius: 8,
|
borderRadius: 10,
|
||||||
gap: 8,
|
gap: 4,
|
||||||
})
|
})
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
paddingVertical: 9,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
borderRadius: 6,
|
borderRadius: 8,
|
||||||
gap: 6,
|
gap: 3,
|
||||||
})
|
})
|
||||||
} else if (size === 'tiny') {
|
} else if (size === 'tiny') {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
paddingVertical: 4,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
borderRadius: 4,
|
borderRadius: 6,
|
||||||
gap: 4,
|
gap: 2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (shape === 'round' || shape === 'square') {
|
} else if (shape === 'round' || shape === 'square') {
|
||||||
|
/*
|
||||||
|
* These sizes match the actual rendered size on screen, based on
|
||||||
|
* Chrome's web inspector
|
||||||
|
*/
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
if (shape === 'round') {
|
if (shape === 'round') {
|
||||||
baseStyles.push({height: 46, width: 46})
|
baseStyles.push({height: 45, width: 45})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({height: 44, width: 44})
|
baseStyles.push({height: 45, width: 45})
|
||||||
}
|
}
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
if (shape === 'round') {
|
if (shape === 'round') {
|
||||||
baseStyles.push({height: 34, width: 34})
|
baseStyles.push({height: 33, width: 33})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({height: 34, width: 34})
|
baseStyles.push({height: 33, width: 33})
|
||||||
}
|
}
|
||||||
} else if (size === 'tiny') {
|
} else if (size === 'tiny') {
|
||||||
if (shape === 'round') {
|
if (shape === 'round') {
|
||||||
baseStyles.push({height: 22, width: 22})
|
baseStyles.push({height: 25, width: 25})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({height: 21, width: 21})
|
baseStyles.push({height: 25, width: 25})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +509,9 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
baseStyles.push(a.rounded_full)
|
baseStyles.push(a.rounded_full)
|
||||||
} else if (shape === 'square') {
|
} else if (shape === 'square') {
|
||||||
if (size === 'tiny') {
|
if (size === 'tiny') {
|
||||||
baseStyles.push(a.rounded_xs)
|
baseStyles.push({
|
||||||
|
borderRadius: 6,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(a.rounded_sm)
|
baseStyles.push(a.rounded_sm)
|
||||||
}
|
}
|
||||||
@@ -531,6 +555,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
|
a.curve_continuous,
|
||||||
flattenedBaseStyles,
|
flattenedBaseStyles,
|
||||||
...(state.hovered || state.pressed
|
...(state.hovered || state.pressed
|
||||||
? [hoverStyles, flatten(hoverStyleProp)]
|
? [hoverStyles, flatten(hoverStyleProp)]
|
||||||
@@ -567,51 +592,64 @@ export function useSharedButtonTextStyles() {
|
|||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({color: t.palette.white})
|
baseStyles.push({color: t.palette.white})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
baseStyles.push({color: t.palette.white})
|
||||||
}
|
}
|
||||||
} else if (color === 'secondary') {
|
} else if (color === 'secondary') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push(t.atoms.text_contrast_medium)
|
||||||
color: t.palette.contrast_700,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
color: t.palette.contrast_400,
|
color: t.palette.contrast_300,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (color === 'secondary_inverted') {
|
} else if (color === 'secondary_inverted') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push(t.atoms.text_inverted)
|
||||||
color: t.palette.contrast_50,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
color: t.palette.contrast_400,
|
color: t.palette.contrast_300,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (color === 'negative') {
|
} else if (color === 'negative') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({color: t.palette.white})
|
baseStyles.push({color: t.palette.white})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
baseStyles.push({color: t.palette.negative_300})
|
||||||
}
|
}
|
||||||
} else if (color === 'negative_secondary') {
|
} else if (color === 'primary_subtle') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
color: select(t.name, {
|
color: select(t.name, {
|
||||||
light: t.palette.negative_500,
|
light: t.palette.primary_600,
|
||||||
dim: t.palette.negative_950,
|
dim: t.palette.primary_800,
|
||||||
dark: t.palette.negative_900,
|
dark: t.palette.primary_800,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
color: select(t.name, {
|
color: select(t.name, {
|
||||||
light: t.palette.negative_500,
|
light: t.palette.primary_200,
|
||||||
dim: t.palette.negative_700,
|
dim: t.palette.primary_200,
|
||||||
dark: t.palette.negative_700,
|
dark: 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,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
baseStyles.push({
|
||||||
|
color: select(t.name, {
|
||||||
|
light: t.palette.negative_200,
|
||||||
|
dim: t.palette.negative_200,
|
||||||
|
dark: t.palette.negative_200,
|
||||||
}),
|
}),
|
||||||
opacity: 0.5,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,7 +731,7 @@ export function useSharedButtonTextStyles() {
|
|||||||
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
|
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (color === 'negative_secondary') {
|
} else if (color === 'negative_subtle') {
|
||||||
if (variant === 'outline') {
|
if (variant === 'outline') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({color: t.palette.negative_400})
|
baseStyles.push({color: t.palette.negative_400})
|
||||||
@@ -716,7 +754,7 @@ export function useSharedButtonTextStyles() {
|
|||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push(a.text_md, a.leading_tight)
|
baseStyles.push(a.text_md, a.leading_tight)
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push(a.text_sm, a.leading_tight)
|
baseStyles.push(a.text_md, a.leading_tight)
|
||||||
} else if (size === 'tiny') {
|
} else if (size === 'tiny') {
|
||||||
baseStyles.push(a.text_xs, a.leading_tight)
|
baseStyles.push(a.text_xs, a.leading_tight)
|
||||||
}
|
}
|
||||||
@@ -737,14 +775,16 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
|||||||
|
|
||||||
export function ButtonIcon({
|
export function ButtonIcon({
|
||||||
icon: Comp,
|
icon: Comp,
|
||||||
position,
|
|
||||||
size,
|
size,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
|
/**
|
||||||
|
* @deprecated no longer needed
|
||||||
|
*/
|
||||||
position?: 'left' | 'right'
|
position?: 'left' | 'right'
|
||||||
size?: SVGIconProps['size']
|
size?: SVGIconProps['size']
|
||||||
}) {
|
}) {
|
||||||
const {size: buttonSize, disabled} = useButtonContext()
|
const {size: buttonSize} = useButtonContext()
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
const {iconSize, iconContainerSize} = React.useMemo(() => {
|
const {iconSize, iconContainerSize} = React.useMemo(() => {
|
||||||
/**
|
/**
|
||||||
@@ -779,8 +819,8 @@ export function ButtonIcon({
|
|||||||
* don't increase button size
|
* don't increase button size
|
||||||
*/
|
*/
|
||||||
const iconContainerSize = {
|
const iconContainerSize = {
|
||||||
large: 18,
|
large: 17,
|
||||||
small: 16,
|
small: 17,
|
||||||
tiny: 13,
|
tiny: 13,
|
||||||
}[buttonSize || 'small']
|
}[buttonSize || 'small']
|
||||||
|
|
||||||
@@ -797,9 +837,6 @@ export function ButtonIcon({
|
|||||||
{
|
{
|
||||||
width: iconContainerSize,
|
width: iconContainerSize,
|
||||||
height: iconContainerSize,
|
height: iconContainerSize,
|
||||||
opacity: disabled ? 0.7 : 1,
|
|
||||||
marginLeft: position === 'left' ? -2 : 0,
|
|
||||||
marginRight: position === 'right' ? -2 : 0,
|
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
Children,
|
||||||
|
cloneElement,
|
||||||
|
isValidElement,
|
||||||
|
type ReactElement,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
AccessibilityInfo,
|
||||||
|
findNodeHandle,
|
||||||
|
Pressable,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useA11y} from '#/state/a11y'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally wraps children in a `FocusTrap` component based on whether
|
||||||
|
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||||
|
* no better option is available.
|
||||||
|
*/
|
||||||
|
export function FocusScope({children}: {children: ReactNode}) {
|
||||||
|
const {screenReaderEnabled} = useA11y()
|
||||||
|
|
||||||
|
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `FocusTrap` is intended as a last-ditch effort to ensure that users keep
|
||||||
|
* focus within a certain section of the app, like an overlay.
|
||||||
|
*
|
||||||
|
* It works by placing "guards" at the start and end of the active content.
|
||||||
|
* Then when the user reaches either of those guards, it will announce that
|
||||||
|
* they have reached the start or end of the content and tell them how to
|
||||||
|
* remain within the active content section.
|
||||||
|
*/
|
||||||
|
function FocusTrap({children}: {children: ReactNode}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const child = useRef<View>(null)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Here we add a ref to the first child of this component. This currently
|
||||||
|
* overrides any ref already on that first child, so we throw an error here
|
||||||
|
* to prevent us from ever accidentally doing this.
|
||||||
|
*/
|
||||||
|
const decoratedChildren = useMemo(() => {
|
||||||
|
return Children.toArray(children).map((node, i) => {
|
||||||
|
if (i === 0 && isValidElement(node)) {
|
||||||
|
const n = node as ReactElement<any>
|
||||||
|
if (n.props.ref !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'FocusScope needs to override the ref on its first child.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return cloneElement(n, {
|
||||||
|
...n.props,
|
||||||
|
ref: child,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
})
|
||||||
|
}, [children])
|
||||||
|
|
||||||
|
const focusNode = useCallback((ref: View | null) => {
|
||||||
|
if (!ref) return
|
||||||
|
const node = findNodeHandle(ref)
|
||||||
|
if (node) {
|
||||||
|
AccessibilityInfo.setAccessibilityFocus(node)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
focusNode(child.current)
|
||||||
|
}, 1e3)
|
||||||
|
}, [focusNode])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable
|
||||||
|
accessible
|
||||||
|
accessibilityLabel={_(
|
||||||
|
msg`You've reached the start of the active content.`,
|
||||||
|
)}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||||
|
)}
|
||||||
|
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||||
|
onAccessibilityAction={event => {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focusNode(child.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Noop />
|
||||||
|
</Pressable>
|
||||||
|
<View
|
||||||
|
/**
|
||||||
|
* This property traps focus effectively on iOS, but not on Android.
|
||||||
|
*/
|
||||||
|
accessibilityViewIsModal>
|
||||||
|
{decoratedChildren}
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
accessibilityLabel={_(
|
||||||
|
msg`You've reached the end of the active content.`,
|
||||||
|
)}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||||
|
)}
|
||||||
|
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||||
|
onAccessibilityAction={event => {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focusNode(child.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Noop />
|
||||||
|
</Pressable>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Noop() {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
accessible={false}
|
||||||
|
style={{
|
||||||
|
height: 1,
|
||||||
|
opacity: 0,
|
||||||
|
}}>
|
||||||
|
{' '}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {type ReactNode} from 'react'
|
||||||
|
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The web version of the FocusScope component is a proper implementation, we
|
||||||
|
* use this in Dialogs and such already. It's here as a convenient counterpart
|
||||||
|
* to the hacky native solution.
|
||||||
|
*/
|
||||||
|
export function FocusScope({children}: {children: ReactNode}) {
|
||||||
|
return (
|
||||||
|
<RadixFocusScope.FocusScope loop asChild trapped>
|
||||||
|
{children}
|
||||||
|
</RadixFocusScope.FocusScope>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function LockScroll() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||||
|
|
||||||
|
export const LockScroll = RemoveScrollBar
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {Logo} from '#/view/icons/Logo'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function Badge() {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pl_md,
|
||||||
|
a.pr_lg,
|
||||||
|
a.py_sm,
|
||||||
|
a.rounded_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.primary_25,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Logo fill={t.palette.primary_600} width={14} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.font_bold,
|
||||||
|
{
|
||||||
|
color: t.palette.primary_600,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>Announcement</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import {type ReactNode} from 'react'
|
||||||
|
import {ScrollView, View} from 'react-native'
|
||||||
|
import {
|
||||||
|
useSafeAreaFrame,
|
||||||
|
useSafeAreaInsets,
|
||||||
|
} from 'react-native-safe-area-context'
|
||||||
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
|
import {isAndroid, isNative} from '#/platform/detection'
|
||||||
|
import {useA11y} from '#/state/a11y'
|
||||||
|
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
|
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
||||||
|
import {FocusScope} from '#/components/FocusScope'
|
||||||
|
import {LockScroll} from '#/components/LockScroll'
|
||||||
|
|
||||||
|
const GUTTER = 24
|
||||||
|
|
||||||
|
export function Overlay({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
label: string
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
|
const {reduceMotionEnabled} = useA11y()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const frame = useSafeAreaFrame()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LockScroll />
|
||||||
|
|
||||||
|
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
|
||||||
|
{gtPhone ? (
|
||||||
|
<View style={[a.absolute, a.inset_0, {opacity: 0.8}]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.fixed,
|
||||||
|
a.inset_0,
|
||||||
|
{backgroundColor: t.palette.black},
|
||||||
|
!reduceMotionEnabled && a.fade_in,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||||
|
t.atoms.bg.backgroundColor,
|
||||||
|
t.atoms.bg.backgroundColor,
|
||||||
|
]}
|
||||||
|
start={[0.5, 0]}
|
||||||
|
end={[0.5, 1]}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={[
|
||||||
|
a.z_10,
|
||||||
|
gtPhone &&
|
||||||
|
web({
|
||||||
|
paddingHorizontal: GUTTER,
|
||||||
|
paddingVertical: '10vh',
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
contentContainerStyle={[a.align_center]}>
|
||||||
|
{/**
|
||||||
|
* This is needed to prevent centered dialogs from overflowing
|
||||||
|
* above the screen, and provides a "natural" centering so that
|
||||||
|
* stacked dialogs appear relatively aligned.
|
||||||
|
*/}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.z_20,
|
||||||
|
a.align_center,
|
||||||
|
!gtPhone && [a.justify_end, {minHeight: frame.height}],
|
||||||
|
isNative && [
|
||||||
|
{
|
||||||
|
paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}>
|
||||||
|
{!gtPhone && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.w_full,
|
||||||
|
{
|
||||||
|
minHeight: Math.max(insets.top, a.p_2xl.padding),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||||
|
t.atoms.bg.backgroundColor,
|
||||||
|
]}
|
||||||
|
start={[0.5, 0]}
|
||||||
|
end={[0.5, 1]}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FocusScope>
|
||||||
|
<View
|
||||||
|
accessible={isAndroid}
|
||||||
|
role="dialog"
|
||||||
|
aria-role="dialog"
|
||||||
|
aria-label={label}
|
||||||
|
style={flatten([
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.p_2xl,
|
||||||
|
t.atoms.bg,
|
||||||
|
!reduceMotionEnabled && a.zoom_fade_in,
|
||||||
|
gtPhone && [
|
||||||
|
a.rounded_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.shadow_lg,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
web({
|
||||||
|
maxWidth: 420,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
])}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</FocusScope>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
|
|
||||||
|
const portalGroup = createPortalGroup()
|
||||||
|
|
||||||
|
export const Provider = portalGroup.Provider
|
||||||
|
export const Portal = portalGroup.Portal
|
||||||
|
export const Outlet = portalGroup.Outlet
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import {describe, test} from '@jest/globals'
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeCompletedState,
|
||||||
|
syncCompletedState,
|
||||||
|
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||||
|
|
||||||
|
jest.mock('../../../state/queries/nuxs')
|
||||||
|
|
||||||
|
describe('computeCompletedState', () => {
|
||||||
|
test(`initial state`, () => {
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady: false,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`nux loaded state`, () => {
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completed).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`nux saving state`, () => {
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: true,
|
||||||
|
completedForDevice: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`nux is completed`, () => {
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: true,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`initial state, but already completed for device`, () => {
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady: false,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(completed).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('syncCompletedState', () => {
|
||||||
|
describe('!nuxIsReady', () => {
|
||||||
|
test(`!completedForDevice, no-op`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: false,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: false,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`completedForDevice, no-op`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: false,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: true,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nuxIsReady', () => {
|
||||||
|
describe(`!nuxIsCompleted`, () => {
|
||||||
|
describe(`!nuxIsOptimisticallyCompleted`, () => {
|
||||||
|
test(`!completedForDevice, no-op`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: false,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`completedForDevice, syncs to server`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: true,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches the case where we already called `save` to sync device state
|
||||||
|
* to server, thus `nuxIsOptimisticallyCompleted` is true.
|
||||||
|
*/
|
||||||
|
describe(`nuxIsOptimisticallyCompleted`, () => {
|
||||||
|
test(`completedForDevice, no-op`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: false,
|
||||||
|
nuxIsOptimisticallyCompleted: true,
|
||||||
|
completedForDevice: true,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`nuxIsCompleted`, () => {
|
||||||
|
test(`!completedForDevice, syncs to device`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: true,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: false,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`completedForDevice, no-op`, () => {
|
||||||
|
const save = jest.fn()
|
||||||
|
const setCompletedForDevice = jest.fn()
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady: true,
|
||||||
|
nuxIsCompleted: true,
|
||||||
|
nuxIsOptimisticallyCompleted: false,
|
||||||
|
completedForDevice: true,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(save).not.toHaveBeenCalled()
|
||||||
|
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The singulary active update ID. This is configured here to ensure that
|
||||||
|
* the relationship is clear.
|
||||||
|
*/
|
||||||
|
export const ACTIVE_UPDATE_ID = ID
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal'
|
||||||
|
import {
|
||||||
|
type PolicyUpdateState,
|
||||||
|
usePolicyUpdateState,
|
||||||
|
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||||
|
|
||||||
|
const Context = createContext<{
|
||||||
|
state: PolicyUpdateState
|
||||||
|
setIsReadyToShowOverlay: () => void
|
||||||
|
}>({
|
||||||
|
state: {
|
||||||
|
completed: true,
|
||||||
|
complete: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Although our data will be ready to go when the app shell mounts, we don't
|
||||||
|
* want to show the overlay until we actually render it, which happens after
|
||||||
|
* sigin/signup/onboarding in `createNativeStackNavigatorWithAuth`.
|
||||||
|
*/
|
||||||
|
setIsReadyToShowOverlay: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function usePolicyUpdateContext() {
|
||||||
|
const context = useContext(Context)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'usePolicyUpdateContext must be used within a PolicyUpdateProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({children}: {children?: ReactNode}) {
|
||||||
|
const {hasSession} = useSession()
|
||||||
|
const [isReadyToShowOverlay, setIsReadyToShowOverlay] = useState(false)
|
||||||
|
const state = usePolicyUpdateState({
|
||||||
|
// only enable the policy update overlay in non-test environments
|
||||||
|
enabled:
|
||||||
|
isReadyToShowOverlay && hasSession && process.env.NODE_ENV !== 'test',
|
||||||
|
})
|
||||||
|
|
||||||
|
const ctx = useMemo(
|
||||||
|
() => ({
|
||||||
|
state,
|
||||||
|
setIsReadyToShowOverlay() {
|
||||||
|
if (isReadyToShowOverlay) return
|
||||||
|
setIsReadyToShowOverlay(true)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[state, isReadyToShowOverlay, setIsReadyToShowOverlay],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalProvider>
|
||||||
|
<Context.Provider value={ctx}>{children}</Context.Provider>
|
||||||
|
</PortalProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {useEffect} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||||
|
import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||||
|
import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
|
||||||
|
import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
|
||||||
|
|
||||||
|
export {Provider} from '#/components/PolicyUpdateOverlay/context'
|
||||||
|
export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||||
|
export {Outlet} from '#/components/PolicyUpdateOverlay/Portal'
|
||||||
|
|
||||||
|
export function PolicyUpdateOverlay() {
|
||||||
|
const {state, setIsReadyToShowOverlay} = usePolicyUpdateContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Tell the context that we are ready to show the overlay.
|
||||||
|
*/
|
||||||
|
setIsReadyToShowOverlay()
|
||||||
|
}, [setIsReadyToShowOverlay])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* See `window.clearNux` example in `/state/queries/nuxs` for a way to clear
|
||||||
|
* NUX state for local testing and debugging.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (state.completed) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<FullWindowOverlay>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.fixed,
|
||||||
|
a.inset_0,
|
||||||
|
// setting a zIndex when using FullWindowOverlay on iOS
|
||||||
|
// means the taps pass straight through to the underlying content (???)
|
||||||
|
// so don't set it on iOS. FullWindowOverlay already does the job.
|
||||||
|
!isIOS && {zIndex: 9999},
|
||||||
|
]}>
|
||||||
|
<Content state={state} />
|
||||||
|
</View>
|
||||||
|
</FullWindowOverlay>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import {Logger} from '#/logger'
|
||||||
|
|
||||||
|
export const logger = Logger.create(Logger.Context.PolicyUpdate)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* Keep this file separate to avoid import issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Nux} from '#/state/queries/nuxs'
|
||||||
|
|
||||||
|
export const ID = Nux.PolicyUpdate202508
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
|
import {useA11y} from '#/state/a11y'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
|
import {Badge} from '#/components/PolicyUpdateOverlay/Badge'
|
||||||
|
import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
|
||||||
|
import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function Content({state}: {state: PolicyUpdateState}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {screenReaderEnabled} = useA11y()
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
state.complete()
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
const linkStyle = [a.text_md]
|
||||||
|
const links = {
|
||||||
|
terms: {
|
||||||
|
overridePresentation: false,
|
||||||
|
to: `https://bsky.social/about/support`,
|
||||||
|
label: _(msg`Terms of Service`),
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
overridePresentation: false,
|
||||||
|
to: `https://bsky.social/about/support`,
|
||||||
|
label: _(msg`Privacy Policy`),
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
overridePresentation: false,
|
||||||
|
to: `https://bsky.social/about/support`,
|
||||||
|
label: _(msg`Copyright Policy`),
|
||||||
|
},
|
||||||
|
guidelines: {
|
||||||
|
overridePresentation: false,
|
||||||
|
to: `https://bsky.social/about/support`,
|
||||||
|
label: _(msg`Community Guidelines`),
|
||||||
|
},
|
||||||
|
blog: {
|
||||||
|
overridePresentation: false,
|
||||||
|
to: `https://bsky.social/about/support`,
|
||||||
|
label: _(msg`Our blog post`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const linkButtonStyles = {
|
||||||
|
overridePresentation: false,
|
||||||
|
color: 'secondary',
|
||||||
|
size: 'small',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const label = isAndroid
|
||||||
|
? _(
|
||||||
|
msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
|
||||||
|
)
|
||||||
|
: _(msg`We're updating our policies`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay label={label}>
|
||||||
|
<View style={[a.align_start, a.gap_xl]}>
|
||||||
|
<Badge />
|
||||||
|
|
||||||
|
{screenReaderEnabled ? (
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||||
|
<Trans>Hey there 👋</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
We’re updating our Terms of Service, Privacy Policy, and
|
||||||
|
Copyright Policy, effective September 12th, 2025.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
We're also updating our Community Guidelines, and we want your
|
||||||
|
input! These new guidelines will take effect on October 13th,
|
||||||
|
2025.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
Learn more about these changes and how to share your thoughts
|
||||||
|
with us by reading our blog post.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Link {...links.terms} {...linkButtonStyles}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Terms of Service</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Link>
|
||||||
|
<Link {...links.privacy} {...linkButtonStyles}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Privacy Policy</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Link>
|
||||||
|
<Link {...links.copyright} {...linkButtonStyles}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Copyright Policy</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Link>
|
||||||
|
<Link {...links.blog} {...linkButtonStyles}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Read our blog post</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||||
|
<Trans>Hey there 👋</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
We’re updating our{' '}
|
||||||
|
<InlineLinkText {...links.terms} style={linkStyle}>
|
||||||
|
Terms of Service
|
||||||
|
</InlineLinkText>
|
||||||
|
,{' '}
|
||||||
|
<InlineLinkText {...links.privacy} style={linkStyle}>
|
||||||
|
Privacy Policy
|
||||||
|
</InlineLinkText>
|
||||||
|
, and{' '}
|
||||||
|
<InlineLinkText {...links.copyright} style={linkStyle}>
|
||||||
|
Copyright Policy
|
||||||
|
</InlineLinkText>
|
||||||
|
, effective September 12th, 2025.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
We're also updating our{' '}
|
||||||
|
<InlineLinkText {...links.guidelines} style={linkStyle}>
|
||||||
|
Community Guidelines
|
||||||
|
</InlineLinkText>
|
||||||
|
, and we want your input! These new guidelines will take effect
|
||||||
|
on October 13th, 2025.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.leading_snug, a.text_md]}>
|
||||||
|
<Trans>
|
||||||
|
Learn more about these changes and how to share your thoughts
|
||||||
|
with us by{' '}
|
||||||
|
<InlineLinkText {...links.blog} style={linkStyle}>
|
||||||
|
reading our blog post.
|
||||||
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.gap_md]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Continue`)}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Tap to acknowledge that you understand and agree to these updates and continue using Bluesky`,
|
||||||
|
)}
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onPress={handleClose}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_sm,
|
||||||
|
a.italic,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
By clicking "Continue" you acknowledge that you understand and
|
||||||
|
agree to these updates.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Overlay>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import {useMemo} from 'react'
|
||||||
|
|
||||||
|
import {useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
|
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||||
|
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
import {device, useStorage} from '#/storage'
|
||||||
|
|
||||||
|
export type PolicyUpdateState = {
|
||||||
|
completed: boolean
|
||||||
|
complete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePolicyUpdateState({
|
||||||
|
enabled,
|
||||||
|
}: {
|
||||||
|
/**
|
||||||
|
* Used to skip the policy update overlay until we're actually ready to
|
||||||
|
* show it.
|
||||||
|
*/
|
||||||
|
enabled: boolean
|
||||||
|
}) {
|
||||||
|
const nux = useNux(ACTIVE_UPDATE_ID)
|
||||||
|
const {mutate: save, variables} = useSaveNux()
|
||||||
|
const deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID])
|
||||||
|
const debugOverride =
|
||||||
|
!!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
/**
|
||||||
|
* If not enabled, then just return a completed state so the app functions
|
||||||
|
* as normal.
|
||||||
|
*/
|
||||||
|
if (!enabled) {
|
||||||
|
return {
|
||||||
|
completed: true,
|
||||||
|
complete() {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nuxIsReady = nux.status === 'ready'
|
||||||
|
const nuxIsCompleted = nux.nux?.completed === true
|
||||||
|
const nuxIsOptimisticallyCompleted = !!variables?.completed
|
||||||
|
const [completedForDevice, setCompletedForDevice] = deviceStorage
|
||||||
|
|
||||||
|
const completed = computeCompletedState({
|
||||||
|
nuxIsReady,
|
||||||
|
nuxIsCompleted,
|
||||||
|
nuxIsOptimisticallyCompleted,
|
||||||
|
completedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug(`state`, {
|
||||||
|
completed,
|
||||||
|
nux,
|
||||||
|
completedForDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!debugOverride) {
|
||||||
|
syncCompletedState({
|
||||||
|
nuxIsReady,
|
||||||
|
nuxIsCompleted,
|
||||||
|
nuxIsOptimisticallyCompleted,
|
||||||
|
completedForDevice,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
completed,
|
||||||
|
complete() {
|
||||||
|
logger.debug(`user completed`)
|
||||||
|
save({
|
||||||
|
id: ACTIVE_UPDATE_ID,
|
||||||
|
completed: true,
|
||||||
|
data: undefined,
|
||||||
|
})
|
||||||
|
setCompletedForDevice(true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [enabled, nux, save, variables, deviceStorage, debugOverride])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeCompletedState({
|
||||||
|
nuxIsReady,
|
||||||
|
nuxIsCompleted,
|
||||||
|
nuxIsOptimisticallyCompleted,
|
||||||
|
completedForDevice,
|
||||||
|
}: {
|
||||||
|
nuxIsReady: boolean
|
||||||
|
nuxIsCompleted: boolean
|
||||||
|
nuxIsOptimisticallyCompleted: boolean
|
||||||
|
completedForDevice: boolean | undefined
|
||||||
|
}): boolean {
|
||||||
|
/**
|
||||||
|
* Assume completed to prevent flash
|
||||||
|
*/
|
||||||
|
let completed = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer server state, if available
|
||||||
|
*/
|
||||||
|
if (nuxIsReady) {
|
||||||
|
completed = nuxIsCompleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override with optimistic state or device state
|
||||||
|
*/
|
||||||
|
if (nuxIsOptimisticallyCompleted || !!completedForDevice) {
|
||||||
|
completed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return completed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncCompletedState({
|
||||||
|
nuxIsReady,
|
||||||
|
nuxIsCompleted,
|
||||||
|
nuxIsOptimisticallyCompleted,
|
||||||
|
completedForDevice,
|
||||||
|
save,
|
||||||
|
setCompletedForDevice,
|
||||||
|
}: {
|
||||||
|
nuxIsReady: boolean
|
||||||
|
nuxIsCompleted: boolean
|
||||||
|
nuxIsOptimisticallyCompleted: boolean
|
||||||
|
completedForDevice: boolean | undefined
|
||||||
|
save: ReturnType<typeof useSaveNux>['mutate']
|
||||||
|
setCompletedForDevice: (value: boolean) => void
|
||||||
|
}) {
|
||||||
|
/*
|
||||||
|
* Sync device state to server state for this account
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
nuxIsReady &&
|
||||||
|
!nuxIsCompleted &&
|
||||||
|
!nuxIsOptimisticallyCompleted &&
|
||||||
|
!!completedForDevice
|
||||||
|
) {
|
||||||
|
logger.debug(`syncing device state to server state`)
|
||||||
|
save({
|
||||||
|
id: ACTIVE_UPDATE_ID,
|
||||||
|
completed: true,
|
||||||
|
data: undefined,
|
||||||
|
})
|
||||||
|
} else if (nuxIsReady && nuxIsCompleted && !completedForDevice) {
|
||||||
|
logger.debug(`syncing server state to device state`)
|
||||||
|
/*
|
||||||
|
* Sync server state to device state
|
||||||
|
*/
|
||||||
|
setCompletedForDevice(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
|
||||||
|
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||||
|
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||||
|
import {device, useStorage} from '#/storage'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Marks the active policy update as completed in device storage.
|
||||||
|
* `usePolicyUpdateState` will react to this and replicate this status in the
|
||||||
|
* server NUX state for this account.
|
||||||
|
*/
|
||||||
|
export function usePreemptivelyCompleteActivePolicyUpdate() {
|
||||||
|
const [_completedForDevice, setCompletedForDevice] = useStorage(device, [
|
||||||
|
ACTIVE_UPDATE_ID,
|
||||||
|
])
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
logger.debug(`preemptively completing active policy update`)
|
||||||
|
setCompletedForDevice(true)
|
||||||
|
}, [setCompletedForDevice])
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useRef} from 'react'
|
import {useImperativeHandle, useRef, useState} from 'react'
|
||||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||||
@@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
|||||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
import {TimeIndicator} from './TimeIndicator'
|
import {TimeIndicator} from './TimeIndicator'
|
||||||
|
|
||||||
export const VideoEmbedInnerNative = React.forwardRef(
|
export function VideoEmbedInnerNative({
|
||||||
function VideoEmbedInnerNative(
|
ref,
|
||||||
{
|
embed,
|
||||||
embed,
|
setStatus,
|
||||||
setStatus,
|
setIsLoading,
|
||||||
setIsLoading,
|
setIsActive,
|
||||||
setIsActive,
|
}: {
|
||||||
}: {
|
ref: React.Ref<{togglePlayback: () => void}>
|
||||||
embed: AppBskyEmbedVideo.View
|
embed: AppBskyEmbedVideo.View
|
||||||
setStatus: (status: 'playing' | 'paused') => void
|
setStatus: (status: 'playing' | 'paused') => void
|
||||||
setIsLoading: (isLoading: boolean) => void
|
setIsLoading: (isLoading: boolean) => void
|
||||||
setIsActive: (isActive: boolean) => void
|
setIsActive: (isActive: boolean) => void
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const videoRef = useRef<BlueskyVideoView>(null)
|
||||||
|
const autoplayDisabled = useAutoplayDisabled()
|
||||||
|
const isWithinMessage = useIsWithinMessage()
|
||||||
|
const [muted, setMuted] = useVideoMuteState()
|
||||||
|
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState(0)
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
togglePlayback: () => {
|
||||||
|
videoRef.current?.togglePlayback()
|
||||||
},
|
},
|
||||||
ref: React.Ref<{togglePlayback: () => void}>,
|
}))
|
||||||
) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const videoRef = useRef<BlueskyVideoView>(null)
|
|
||||||
const autoplayDisabled = useAutoplayDisabled()
|
|
||||||
const isWithinMessage = useIsWithinMessage()
|
|
||||||
const [muted, setMuted] = useVideoMuteState()
|
|
||||||
|
|
||||||
const [isPlaying, setIsPlaying] = React.useState(false)
|
if (error) {
|
||||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
throw new Error(error)
|
||||||
const [error, setError] = React.useState<string>()
|
}
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
return (
|
||||||
togglePlayback: () => {
|
<View style={[a.flex_1, a.relative]}>
|
||||||
videoRef.current?.togglePlayback()
|
<BlueskyVideoView
|
||||||
},
|
url={embed.playlist}
|
||||||
}))
|
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||||
|
beginMuted={autoplayDisabled ? false : muted}
|
||||||
if (error) {
|
style={[a.rounded_sm]}
|
||||||
throw new Error(error)
|
onActiveChange={e => {
|
||||||
}
|
setIsActive(e.nativeEvent.isActive)
|
||||||
|
}}
|
||||||
return (
|
onLoadingChange={e => {
|
||||||
<View style={[a.flex_1, a.relative]}>
|
setIsLoading(e.nativeEvent.isLoading)
|
||||||
<BlueskyVideoView
|
}}
|
||||||
url={embed.playlist}
|
onMutedChange={e => {
|
||||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
setMuted(e.nativeEvent.isMuted)
|
||||||
beginMuted={autoplayDisabled ? false : muted}
|
}}
|
||||||
style={[a.rounded_sm]}
|
onStatusChange={e => {
|
||||||
onActiveChange={e => {
|
setStatus(e.nativeEvent.status)
|
||||||
setIsActive(e.nativeEvent.isActive)
|
setIsPlaying(e.nativeEvent.status === 'playing')
|
||||||
}}
|
}}
|
||||||
onLoadingChange={e => {
|
onTimeRemainingChange={e => {
|
||||||
setIsLoading(e.nativeEvent.isLoading)
|
setTimeRemaining(e.nativeEvent.timeRemaining)
|
||||||
}}
|
}}
|
||||||
onMutedChange={e => {
|
onError={e => {
|
||||||
setMuted(e.nativeEvent.isMuted)
|
setError(e.nativeEvent.error)
|
||||||
}}
|
}}
|
||||||
onStatusChange={e => {
|
ref={videoRef}
|
||||||
setStatus(e.nativeEvent.status)
|
accessibilityLabel={
|
||||||
setIsPlaying(e.nativeEvent.status === 'playing')
|
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||||
}}
|
}
|
||||||
onTimeRemainingChange={e => {
|
accessibilityHint=""
|
||||||
setTimeRemaining(e.nativeEvent.timeRemaining)
|
/>
|
||||||
}}
|
<VideoControls
|
||||||
onError={e => {
|
enterFullscreen={() => {
|
||||||
setError(e.nativeEvent.error)
|
videoRef.current?.enterFullscreen(true)
|
||||||
}}
|
}}
|
||||||
ref={videoRef}
|
toggleMuted={() => {
|
||||||
accessibilityLabel={
|
videoRef.current?.toggleMuted()
|
||||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
}}
|
||||||
}
|
togglePlayback={() => {
|
||||||
accessibilityHint=""
|
videoRef.current?.togglePlayback()
|
||||||
/>
|
}}
|
||||||
<VideoControls
|
isPlaying={isPlaying}
|
||||||
enterFullscreen={() => {
|
timeRemaining={timeRemaining}
|
||||||
videoRef.current?.enterFullscreen(true)
|
/>
|
||||||
}}
|
<MediaInsetBorder />
|
||||||
toggleMuted={() => {
|
</View>
|
||||||
videoRef.current?.toggleMuted()
|
)
|
||||||
}}
|
}
|
||||||
togglePlayback={() => {
|
|
||||||
videoRef.current?.togglePlayback()
|
|
||||||
}}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
timeRemaining={timeRemaining}
|
|
||||||
/>
|
|
||||||
<MediaInsetBorder />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function VideoControls({
|
function VideoControls({
|
||||||
enterFullscreen,
|
enterFullscreen,
|
||||||
|
|||||||
@@ -224,15 +224,19 @@ function useHLS({
|
|||||||
throw new HLSUnsupportedError()
|
throw new HLSUnsupportedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latestEstimate = BandwidthEstimate.get()
|
||||||
const hls = new Hls({
|
const hls = new Hls({
|
||||||
maxMaxBufferLength: 10, // only load 10s ahead
|
maxMaxBufferLength: 10, // only load 10s ahead
|
||||||
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize
|
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize
|
||||||
// it will buffer until it is greater than *both* of those values
|
// it will buffer until it is greater than *both* of those values
|
||||||
// so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
|
// so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
|
||||||
|
startLevel:
|
||||||
|
latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel,
|
||||||
|
// the '-1' value makes a test request to estimate bandwidth and quality level
|
||||||
|
// before showing the first fragment
|
||||||
})
|
})
|
||||||
hlsRef.current = hls
|
hlsRef.current = hls
|
||||||
|
|
||||||
const latestEstimate = BandwidthEstimate.get()
|
|
||||||
if (latestEstimate !== undefined) {
|
if (latestEstimate !== undefined) {
|
||||||
hls.bandwidthEstimate = latestEstimate
|
hls.bandwidthEstimate = latestEstimate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useCallback, useState} from 'react'
|
import {useCallback, useRef, useState} from 'react'
|
||||||
import {ActivityIndicator, View} from 'react-native'
|
import {ActivityIndicator, View} from 'react-native'
|
||||||
import {ImageBackground} from 'expo-image'
|
import {ImageBackground} from 'expo-image'
|
||||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
|
|||||||
|
|
||||||
function InnerWrapper({embed}: Props) {
|
function InnerWrapper({embed}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const ref = React.useRef<{togglePlayback: () => void}>(null)
|
const ref = useRef<{togglePlayback: () => void}>(null)
|
||||||
|
|
||||||
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
|
const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
|
||||||
'pending',
|
'pending',
|
||||||
)
|
)
|
||||||
const [isLoading, setIsLoading] = React.useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isActive, setIsActive] = React.useState(false)
|
const [isActive, setIsActive] = useState(false)
|
||||||
const showSpinner = useThrottledValue(isActive && isLoading, 100)
|
const showSpinner = useThrottledValue(isActive && isLoading, 100)
|
||||||
|
|
||||||
const showOverlay =
|
const showOverlay =
|
||||||
@@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) {
|
|||||||
(status === 'paused' && !isActive) ||
|
(status === 'paused' && !isActive) ||
|
||||||
status === 'pending'
|
status === 'pending'
|
||||||
|
|
||||||
React.useEffect(() => {
|
if (!isActive && status !== 'pending') {
|
||||||
if (!isActive && status !== 'pending') {
|
setStatus('pending')
|
||||||
setStatus('pending')
|
}
|
||||||
}
|
|
||||||
}, [isActive, status])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
ref.current?.togglePlayback()
|
ref.current?.togglePlayback()
|
||||||
}}
|
}}
|
||||||
label={_(msg`Play video`)}
|
label={_(msg`Play video`)}>
|
||||||
color="secondary">
|
|
||||||
{showSpinner ? (
|
{showSpinner ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
@@ -83,9 +90,7 @@ export function VideoEmbed({
|
|||||||
style={{display: 'flex', flex: 1, cursor: 'default'}}
|
style={{display: 'flex', flex: 1, cursor: 'default'}}
|
||||||
onClick={evt => evt.stopPropagation()}>
|
onClick={evt => evt.stopPropagation()}>
|
||||||
<ErrorBoundary renderError={renderError} key={key}>
|
<ErrorBoundary renderError={renderError} key={key}>
|
||||||
<ViewportObserver
|
<OnlyNearScreen>
|
||||||
sendPosition={sendPosition}
|
|
||||||
isAnyViewActive={currentActiveView !== null}>
|
|
||||||
<VideoEmbedInnerWeb
|
<VideoEmbedInnerWeb
|
||||||
embed={embed}
|
embed={embed}
|
||||||
active={active}
|
active={active}
|
||||||
@@ -93,31 +98,39 @@ export function VideoEmbed({
|
|||||||
onScreen={onScreen}
|
onScreen={onScreen}
|
||||||
lastKnownTime={lastKnownTime}
|
lastKnownTime={lastKnownTime}
|
||||||
/>
|
/>
|
||||||
</ViewportObserver>
|
</OnlyNearScreen>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.pt_xs]}>
|
<View style={[a.pt_xs]}>
|
||||||
{cropDisabled ? (
|
<ViewportObserver
|
||||||
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
sendPosition={sendPosition}
|
||||||
{contents}
|
isAnyViewActive={currentActiveView !== null}>
|
||||||
</View>
|
{cropDisabled ? (
|
||||||
) : (
|
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
||||||
<ConstrainedImage
|
{contents}
|
||||||
fullBleed={crop === 'square'}
|
</View>
|
||||||
aspectRatio={constrained || 1}>
|
) : (
|
||||||
{contents}
|
<ConstrainedImage
|
||||||
</ConstrainedImage>
|
fullBleed={crop === 'square'}
|
||||||
)}
|
aspectRatio={constrained || 1}>
|
||||||
|
{contents}
|
||||||
|
</ConstrainedImage>
|
||||||
|
)}
|
||||||
|
</ViewportObserver>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NearScreenContext = createContext(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a 100vh tall div and watches it with an IntersectionObserver to
|
* Renders a 100vh tall div and watches it with an IntersectionObserver to
|
||||||
* send the position of the div when it's near the screen.
|
* send the position of the div when it's near the screen.
|
||||||
|
*
|
||||||
|
* IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||||
*/
|
*/
|
||||||
function ViewportObserver({
|
function ViewportObserver({
|
||||||
children,
|
children,
|
||||||
@@ -164,7 +177,9 @@ function ViewportObserver({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1, a.flex_row]}>
|
<View style={[a.flex_1, a.flex_row]}>
|
||||||
{nearScreen && children}
|
<NearScreenContext.Provider value={nearScreen}>
|
||||||
|
{children}
|
||||||
|
</NearScreenContext.Provider>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
@@ -182,6 +197,18 @@ function ViewportObserver({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Awkward data flow here, but we need to hide the video when it's not near the screen.
|
||||||
|
* But also, ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||||
|
* So we put it at the top level of the component tree here, then hide the children of
|
||||||
|
* the auto-resizing container.
|
||||||
|
*/
|
||||||
|
export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => {
|
||||||
|
const nearScreen = useContext(NearScreenContext)
|
||||||
|
|
||||||
|
return nearScreen ? children : null
|
||||||
|
}
|
||||||
|
|
||||||
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ProfileHoverCardProps} from './types'
|
import {type ProfileHoverCardProps} from './types'
|
||||||
|
|
||||||
export function ProfileHoverCard({children}: ProfileHoverCardProps) {
|
export function ProfileHoverCard({children}: ProfileHoverCardProps) {
|
||||||
return children
|
return children
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
|||||||
return props.children
|
return props.children
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<View onPointerMove={onPointerMove} style={[a.flex_shrink, props.style]}>
|
<View
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
style={[a.flex_shrink, props.inline && a.inline, props.style]}>
|
||||||
<ProfileHoverCardInner {...props} />
|
<ProfileHoverCardInner {...props} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -326,7 +328,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
|||||||
onPointerLeave={onPointerLeaveTarget}
|
onPointerLeave={onPointerLeaveTarget}
|
||||||
// @ts-ignore web only prop
|
// @ts-ignore web only prop
|
||||||
onMouseUp={onPress}
|
onMouseUp={onPress}
|
||||||
style={{flexShrink: 1}}>
|
style={[a.flex_shrink, props.inline && a.inline]}>
|
||||||
{props.children}
|
{props.children}
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<Portal>
|
<Portal>
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export type ProfileHoverCardProps = ViewStyleProp & {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
did: string
|
did: string
|
||||||
disable?: boolean
|
disable?: boolean
|
||||||
|
inline?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ export function Content<T>({items, renderItem}: ContentProps<T>) {
|
|||||||
a.border,
|
a.border,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
]}>
|
]}>
|
||||||
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
||||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import {createContext, useContext, useMemo} from 'react'
|
import {createContext, useContext, useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a, select, useTheme} from '#/alf'
|
import {atoms as a, select, useAlf, useTheme} from '#/alf'
|
||||||
import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
|
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import {type ToastType} from '#/components/Toast/types'
|
import {type ToastType} from '#/components/Toast/types'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '../icons/CircleCheck'
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
type: ToastType
|
type: ToastType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
default: SuccessIcon,
|
default: CircleCheck,
|
||||||
success: SuccessIcon,
|
success: CircleCheck,
|
||||||
error: ErrorIcon,
|
error: ErrorIcon,
|
||||||
warning: WarningIcon,
|
warning: WarningIcon,
|
||||||
info: CircleInfo,
|
info: CircleInfo,
|
||||||
@@ -32,9 +32,19 @@ export function Toast({
|
|||||||
type: ToastType
|
type: ToastType
|
||||||
content: React.ReactNode
|
content: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const {fonts} = useAlf()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const styles = useToastStyles({type})
|
const styles = useToastStyles({type})
|
||||||
const Icon = ICONS[type]
|
const Icon = ICONS[type]
|
||||||
|
/**
|
||||||
|
* Vibes-based number, adjusts `top` of `View` that wraps the text to
|
||||||
|
* compensate for different type sizes and keep the first line of text
|
||||||
|
* aligned with the icon. - esb
|
||||||
|
*/
|
||||||
|
const fontScaleCompensation = useMemo(
|
||||||
|
() => parseInt(fonts.scale) * -1 * 0.65,
|
||||||
|
[fonts.scale],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider value={useMemo(() => ({type}), [type])}>
|
<Context.Provider value={useMemo(() => ({type}), [type])}>
|
||||||
@@ -56,7 +66,13 @@ export function Toast({
|
|||||||
]}>
|
]}>
|
||||||
<Icon size="md" fill={styles.iconColor} />
|
<Icon size="md" fill={styles.iconColor} />
|
||||||
|
|
||||||
<View style={[a.flex_1]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
{
|
||||||
|
top: fontScaleCompensation,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
{typeof content === 'string' ? (
|
{typeof content === 'string' ? (
|
||||||
<ToastText>{content}</ToastText>
|
<ToastText>{content}</ToastText>
|
||||||
) : (
|
) : (
|
||||||
@@ -91,114 +107,58 @@ function useToastStyles({type}: {type: ToastType}) {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
iconColor: t.atoms.text.color,
|
||||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
textColor: t.atoms.text.color,
|
||||||
}),
|
|
||||||
borderColor: select(t.name, {
|
|
||||||
light: t.atoms.border_contrast_low.borderColor,
|
|
||||||
dim: t.atoms.border_contrast_high.borderColor,
|
|
||||||
dark: t.atoms.border_contrast_high.borderColor,
|
|
||||||
}),
|
|
||||||
iconColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
textColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.palette.primary_25,
|
||||||
light: t.palette.primary_100,
|
|
||||||
dim: t.palette.primary_100,
|
|
||||||
dark: t.palette.primary_50,
|
|
||||||
}),
|
|
||||||
borderColor: select(t.name, {
|
borderColor: select(t.name, {
|
||||||
light: t.palette.primary_500,
|
light: t.palette.primary_300,
|
||||||
dim: t.palette.primary_500,
|
dim: t.palette.primary_200,
|
||||||
dark: t.palette.primary_500,
|
dark: t.palette.primary_100,
|
||||||
}),
|
}),
|
||||||
iconColor: select(t.name, {
|
iconColor: select(t.name, {
|
||||||
light: t.palette.primary_500,
|
light: t.palette.primary_600,
|
||||||
dim: t.palette.primary_600,
|
dim: t.palette.primary_700,
|
||||||
dark: t.palette.primary_600,
|
dark: t.palette.primary_700,
|
||||||
}),
|
}),
|
||||||
textColor: select(t.name, {
|
textColor: select(t.name, {
|
||||||
light: t.palette.primary_500,
|
light: t.palette.primary_600,
|
||||||
dim: t.palette.primary_600,
|
dim: t.palette.primary_700,
|
||||||
dark: t.palette.primary_600,
|
dark: t.palette.primary_700,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.palette.negative_25,
|
||||||
light: t.palette.negative_200,
|
|
||||||
dim: t.palette.negative_25,
|
|
||||||
dark: t.palette.negative_25,
|
|
||||||
}),
|
|
||||||
borderColor: select(t.name, {
|
borderColor: select(t.name, {
|
||||||
light: t.palette.negative_300,
|
light: t.palette.negative_200,
|
||||||
dim: t.palette.negative_300,
|
dim: t.palette.negative_200,
|
||||||
dark: t.palette.negative_300,
|
dark: t.palette.negative_100,
|
||||||
}),
|
}),
|
||||||
iconColor: select(t.name, {
|
iconColor: select(t.name, {
|
||||||
light: t.palette.negative_600,
|
light: t.palette.negative_700,
|
||||||
dim: t.palette.negative_600,
|
dim: t.palette.negative_900,
|
||||||
dark: t.palette.negative_600,
|
dark: t.palette.negative_900,
|
||||||
}),
|
}),
|
||||||
textColor: select(t.name, {
|
textColor: select(t.name, {
|
||||||
light: t.palette.negative_600,
|
light: t.palette.negative_700,
|
||||||
dim: t.palette.negative_600,
|
dim: t.palette.negative_900,
|
||||||
dark: t.palette.negative_600,
|
dark: t.palette.negative_900,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
iconColor: t.atoms.text.color,
|
||||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
textColor: t.atoms.text.color,
|
||||||
}),
|
|
||||||
borderColor: select(t.name, {
|
|
||||||
light: t.atoms.border_contrast_low.borderColor,
|
|
||||||
dim: t.atoms.border_contrast_high.borderColor,
|
|
||||||
dark: t.atoms.border_contrast_high.borderColor,
|
|
||||||
}),
|
|
||||||
iconColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
textColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
backgroundColor: select(t.name, {
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
iconColor: t.atoms.text.color,
|
||||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
textColor: t.atoms.text.color,
|
||||||
}),
|
|
||||||
borderColor: select(t.name, {
|
|
||||||
light: t.atoms.border_contrast_low.borderColor,
|
|
||||||
dim: t.atoms.border_contrast_high.borderColor,
|
|
||||||
dark: t.atoms.border_contrast_high.borderColor,
|
|
||||||
}),
|
|
||||||
iconColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
textColor: select(t.name, {
|
|
||||||
light: t.atoms.text_contrast_medium.color,
|
|
||||||
dim: t.atoms.text_contrast_medium.color,
|
|
||||||
dark: t.atoms.text_contrast_medium.color,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
}[type]
|
}[type]
|
||||||
}, [t, type])
|
}, [t, type])
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import {type ToastApi} from '#/components/Toast/types'
|
||||||
|
|
||||||
export function ToastContainer() {
|
export function ToastContainer() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function show() {}
|
export const toast: ToastApi = {
|
||||||
|
show() {},
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,12 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
|
|||||||
<Toast content={activeToast.content} type={activeToast.type} />
|
<Toast content={activeToast.content} type={activeToast.type} />
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[a.absolute, a.inset_0]}
|
style={[a.absolute, a.inset_0]}
|
||||||
accessibilityLabel={_(msg`Dismiss toast`)}
|
accessibilityLabel={_(
|
||||||
|
msg({
|
||||||
|
message: `Dismiss message`,
|
||||||
|
comment: `Accessibility label for dismissing a toast notification`,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => setActiveToast(undefined)}
|
onPress={() => setActiveToast(undefined)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ function DialogInner({
|
|||||||
label={_(msg`Remove live status`)}
|
label={_(msg`Remove live status`)}
|
||||||
onPress={() => removeLiveStatus()}
|
onPress={() => removeLiveStatus()}
|
||||||
size={platform({native: 'large', web: 'small'})}
|
size={platform({native: 'large', web: 'small'})}
|
||||||
color="negative_secondary"
|
color="negative_subtle"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
disabled={isRemovingLiveStatus || isGoingLive}>
|
disabled={isRemovingLiveStatus || isGoingLive}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
|
|||||||
Vendored
+7
@@ -78,3 +78,10 @@ export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN
|
|||||||
*/
|
*/
|
||||||
export const BITDRIFT_API_KEY: string | undefined =
|
export const BITDRIFT_API_KEY: string | undefined =
|
||||||
process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
|
process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GCP project ID which is required for device attestation
|
||||||
|
*/
|
||||||
|
export const GCP_PROJECT_ID: number = Number(
|
||||||
|
process.env.EXPO_PUBLIC_GCP_PROJECT_ID,
|
||||||
|
)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
|
|||||||
isIncompleteThread: boolean
|
isIncompleteThread: boolean
|
||||||
isFallbackMarker: boolean
|
isFallbackMarker: boolean
|
||||||
isOrphan: boolean
|
isOrphan: boolean
|
||||||
|
isThreadMuted: boolean
|
||||||
rootUri: string
|
rootUri: string
|
||||||
feedPostUri: string
|
feedPostUri: string
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ export class FeedViewPostsSlice {
|
|||||||
this.isIncompleteThread = false
|
this.isIncompleteThread = false
|
||||||
this.isFallbackMarker = false
|
this.isFallbackMarker = false
|
||||||
this.isOrphan = false
|
this.isOrphan = false
|
||||||
|
this.isThreadMuted = post.viewer?.threadMuted ?? false
|
||||||
this.feedPostUri = post.uri
|
this.feedPostUri = post.uri
|
||||||
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
||||||
this.rootUri = reply.root.uri
|
this.rootUri = reply.root.uri
|
||||||
@@ -361,6 +363,20 @@ export class FeedTuner {
|
|||||||
return slices
|
return slices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static removeMutedThreads(
|
||||||
|
tuner: FeedTuner,
|
||||||
|
slices: FeedViewPostsSlice[],
|
||||||
|
_dryRun: boolean,
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < slices.length; i++) {
|
||||||
|
if (slices[i].isThreadMuted) {
|
||||||
|
slices.splice(i, 1)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slices
|
||||||
|
}
|
||||||
|
|
||||||
static dedupThreads(
|
static dedupThreads(
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
slices: FeedViewPostsSlice[],
|
slices: FeedViewPostsSlice[],
|
||||||
|
|||||||
@@ -208,3 +208,10 @@ export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'
|
|||||||
export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev'
|
export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev'
|
||||||
|
|
||||||
export const DEV_ENV_APPVIEW = `http://localhost:2584` // always the same
|
export const DEV_ENV_APPVIEW = `http://localhost:2584` // always the same
|
||||||
|
|
||||||
|
export const webLinks = {
|
||||||
|
tos: `https://bsky.social/about/support/tos`,
|
||||||
|
privacy: `https://bsky.social/about/support/privacy-policy`,
|
||||||
|
community: `https://bsky.social/about/support/community-guidelines`,
|
||||||
|
communityDeprecated: `https://bsky.social/about/support/community-guidelines-deprecated`,
|
||||||
|
}
|
||||||
|
|||||||
@@ -301,17 +301,6 @@ export function useNotificationsHandler() {
|
|||||||
const payload = getNotificationPayload(e.notification)
|
const payload = getNotificationPayload(e.notification)
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
if (!payload.reason) {
|
|
||||||
notyLogger.error(
|
|
||||||
'useNotificationsHandler: received unknown payload',
|
|
||||||
{
|
|
||||||
payload,
|
|
||||||
identifier: e.notification.request.identifier,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notyLogger.debug(
|
notyLogger.debug(
|
||||||
'User pressed a notification, opening notifications tab',
|
'User pressed a notification, opening notifications tab',
|
||||||
{},
|
{},
|
||||||
@@ -393,16 +382,20 @@ export function getNotificationPayload(
|
|||||||
isIOS ? e.request.trigger.payload : e.request.content.data
|
isIOS ? e.request.trigger.payload : e.request.content.data
|
||||||
) as NotificationPayload
|
) as NotificationPayload
|
||||||
|
|
||||||
if (payload) {
|
if (payload && payload.reason) {
|
||||||
return payload
|
return payload
|
||||||
} else {
|
} else {
|
||||||
|
if (payload) {
|
||||||
|
notyLogger.warn('getNotificationPayload: received unknown payload', {
|
||||||
|
payload,
|
||||||
|
identifier: e.request.identifier,
|
||||||
|
})
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notificationToURL(
|
export function notificationToURL(payload: NotificationPayload): string | null {
|
||||||
payload: NotificationPayload,
|
|
||||||
): string | undefined {
|
|
||||||
switch (payload?.reason) {
|
switch (payload?.reason) {
|
||||||
case 'like':
|
case 'like':
|
||||||
case 'repost':
|
case 'repost':
|
||||||
@@ -433,10 +426,12 @@ export function notificationToURL(
|
|||||||
}
|
}
|
||||||
case 'chat-message':
|
case 'chat-message':
|
||||||
// should be handled separately
|
// should be handled separately
|
||||||
return undefined
|
return null
|
||||||
case 'verified':
|
case 'verified':
|
||||||
case 'unverified':
|
case 'unverified':
|
||||||
default:
|
|
||||||
return '/notifications'
|
return '/notifications'
|
||||||
|
default:
|
||||||
|
// do nothing if we don't know what to do with it
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type Gate =
|
|||||||
| 'alt_share_icon'
|
| 'alt_share_icon'
|
||||||
| 'debug_show_feedcontext'
|
| 'debug_show_feedcontext'
|
||||||
| 'debug_subscriptions'
|
| 'debug_subscriptions'
|
||||||
|
| 'disable_onboarding_policy_update_notice'
|
||||||
| 'explore_show_suggested_feeds'
|
| 'explore_show_suggested_feeds'
|
||||||
| 'handle_suggestions'
|
| 'handle_suggestions'
|
||||||
| 'old_postonboarding'
|
| 'old_postonboarding'
|
||||||
|
|||||||
+195
-103
@@ -518,8 +518,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/Navigation.tsx:523
|
#: src/Navigation.tsx:523
|
||||||
#: src/screens/Settings/AboutSettings.tsx:75
|
#: src/screens/Settings/AboutSettings.tsx:75
|
||||||
#: src/screens/Settings/Settings.tsx:240
|
#: src/screens/Settings/Settings.tsx:244
|
||||||
#: src/screens/Settings/Settings.tsx:243
|
#: src/screens/Settings/Settings.tsx:247
|
||||||
msgid "About"
|
msgid "About"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -538,8 +538,8 @@ msgid "Accept Request"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/AccessibilitySettings.tsx:44
|
#: src/screens/Settings/AccessibilitySettings.tsx:44
|
||||||
#: src/screens/Settings/Settings.tsx:216
|
#: src/screens/Settings/Settings.tsx:220
|
||||||
#: src/screens/Settings/Settings.tsx:219
|
#: src/screens/Settings/Settings.tsx:223
|
||||||
msgid "Accessibility"
|
msgid "Accessibility"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -550,8 +550,8 @@ msgstr ""
|
|||||||
#: src/Navigation.tsx:398
|
#: src/Navigation.tsx:398
|
||||||
#: src/screens/Login/LoginForm.tsx:194
|
#: src/screens/Login/LoginForm.tsx:194
|
||||||
#: src/screens/Settings/AccountSettings.tsx:49
|
#: src/screens/Settings/AccountSettings.tsx:49
|
||||||
#: src/screens/Settings/Settings.tsx:170
|
#: src/screens/Settings/Settings.tsx:174
|
||||||
#: src/screens/Settings/Settings.tsx:173
|
#: src/screens/Settings/Settings.tsx:177
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -582,11 +582,11 @@ msgstr ""
|
|||||||
msgid "Account Muted by List"
|
msgid "Account Muted by List"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:582
|
#: src/screens/Settings/Settings.tsx:624
|
||||||
msgid "Account options"
|
msgid "Account options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:618
|
#: src/screens/Settings/Settings.tsx:660
|
||||||
msgid "Account removed from quick access"
|
msgid "Account removed from quick access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -668,8 +668,8 @@ msgstr ""
|
|||||||
msgid "Add alt text (optional)"
|
msgid "Add alt text (optional)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:522
|
#: src/screens/Settings/Settings.tsx:564
|
||||||
#: src/screens/Settings/Settings.tsx:525
|
#: src/screens/Settings/Settings.tsx:567
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:259
|
#: src/view/shell/desktop/LeftNav.tsx:259
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:263
|
#: src/view/shell/desktop/LeftNav.tsx:263
|
||||||
msgid "Add another account"
|
msgid "Add another account"
|
||||||
@@ -921,11 +921,11 @@ msgstr ""
|
|||||||
msgid "An error occurred while generating your starter pack. Want to try again?"
|
msgid "An error occurred while generating your starter pack. Want to try again?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/index.tsx:160
|
#: src/components/Post/Embed/VideoEmbed/index.tsx:157
|
||||||
msgid "An error occurred while loading the video. Please try again later."
|
msgid "An error occurred while loading the video. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:199
|
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:226
|
||||||
msgid "An error occurred while loading the video. Please try again."
|
msgid "An error occurred while loading the video. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -999,6 +999,10 @@ msgstr ""
|
|||||||
msgid "Animated GIF"
|
msgid "Animated GIF"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/Badge.tsx:33
|
||||||
|
msgid "Announcement"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:37
|
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:37
|
||||||
msgid "Announcing verification on Bluesky"
|
msgid "Announcing verification on Bluesky"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1093,8 +1097,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/Navigation.tsx:390
|
#: src/Navigation.tsx:390
|
||||||
#: src/screens/Settings/AppearanceSettings.tsx:88
|
#: src/screens/Settings/AppearanceSettings.tsx:88
|
||||||
#: src/screens/Settings/Settings.tsx:208
|
#: src/screens/Settings/Settings.tsx:212
|
||||||
#: src/screens/Settings/Settings.tsx:211
|
#: src/screens/Settings/Settings.tsx:215
|
||||||
msgid "Appearance"
|
msgid "Appearance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1103,8 +1107,8 @@ msgstr ""
|
|||||||
msgid "Apply default recommended feeds"
|
msgid "Apply default recommended feeds"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:490
|
#: src/screens/Settings/Settings.tsx:498
|
||||||
#: src/screens/Settings/Settings.tsx:492
|
#: src/screens/Settings/Settings.tsx:500
|
||||||
msgid "Apply Pull Request"
|
msgid "Apply Pull Request"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1402,6 +1406,14 @@ msgstr ""
|
|||||||
msgid "Bluesky will proactively verify notable and authentic accounts."
|
msgid "Bluesky will proactively verify notable and authentic accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/screens/Signup/StepInfo/Policies.tsx:26
|
||||||
|
msgid "Bluesky's Community Guidelines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/screens/Signup/StepInfo/Policies.tsx:32
|
||||||
|
msgid "Bluesky's Updated Community Guidelines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:99
|
#: src/screens/Settings/AppIconSettings/index.tsx:99
|
||||||
msgid "Bluesky+"
|
msgid "Bluesky+"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1480,19 +1492,23 @@ msgstr ""
|
|||||||
msgid "By <0>{0}</0>"
|
msgid "By <0>{0}</0>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:181
|
||||||
|
msgid "By clicking \"Continue\" you acknowledge that you understand and agree to these updates."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:329
|
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:329
|
||||||
msgid "By continuing, you agree to the <0>KWS Terms of Use</0> and acknowledge that KWS will store your verified status with your hashed email address in accordance with the <1>KWS Privacy Policy</1>. This means you won’t need to verify again the next time you use this email for other apps, games, and services powered by KWS technology."
|
msgid "By continuing, you agree to the <0>KWS Terms of Use</0> and acknowledge that KWS will store your verified status with your hashed email address in accordance with the <1>KWS Privacy Policy</1>. This means you won’t need to verify again the next time you use this email for other apps, games, and services powered by KWS technology."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:81
|
#: src/screens/Signup/StepInfo/Policies.tsx:111
|
||||||
msgid "By creating an account you agree to the <0>Privacy Policy</0>."
|
msgid "By creating an account you agree to the <0>Privacy Policy</0>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:48
|
#: src/screens/Signup/StepInfo/Policies.tsx:78
|
||||||
msgid "By creating an account you agree to the <0>Terms of Service</0> and <1>Privacy Policy</1>."
|
msgid "By creating an account you agree to the <0>Terms of Service</0> and <1>Privacy Policy</1>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:68
|
#: src/screens/Signup/StepInfo/Policies.tsx:98
|
||||||
msgid "By creating an account you agree to the <0>Terms of Service</0>."
|
msgid "By creating an account you agree to the <0>Terms of Service</0>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1525,7 +1541,7 @@ msgstr ""
|
|||||||
#: src/screens/Settings/AppIconSettings/index.tsx:225
|
#: src/screens/Settings/AppIconSettings/index.tsx:225
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:78
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:78
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:85
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:85
|
||||||
#: src/screens/Settings/Settings.tsx:285
|
#: src/screens/Settings/Settings.tsx:289
|
||||||
#: src/screens/Takendown.tsx:99
|
#: src/screens/Takendown.tsx:99
|
||||||
#: src/screens/Takendown.tsx:102
|
#: src/screens/Takendown.tsx:102
|
||||||
#: src/view/com/composer/Composer.tsx:965
|
#: src/view/com/composer/Composer.tsx:965
|
||||||
@@ -1748,11 +1764,11 @@ msgstr ""
|
|||||||
msgid "Choose your username"
|
msgid "Choose your username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:482
|
#: src/screens/Settings/Settings.tsx:490
|
||||||
msgid "Clear all storage data"
|
msgid "Clear all storage data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:484
|
#: src/screens/Settings/Settings.tsx:492
|
||||||
msgid "Clear all storage data (restart after this)"
|
msgid "Clear all storage data (restart after this)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1858,7 +1874,7 @@ msgstr ""
|
|||||||
msgid "Close dialog"
|
msgid "Close dialog"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/shell/index.web.tsx:89
|
#: src/view/shell/index.web.tsx:100
|
||||||
msgid "Close drawer menu"
|
msgid "Close drawer menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1933,6 +1949,7 @@ msgstr ""
|
|||||||
msgid "Comics"
|
msgid "Comics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:45
|
||||||
#: src/Navigation.tsx:340
|
#: src/Navigation.tsx:340
|
||||||
#: src/view/screens/CommunityGuidelines.tsx:34
|
#: src/view/screens/CommunityGuidelines.tsx:34
|
||||||
msgid "Community Guidelines"
|
msgid "Community Guidelines"
|
||||||
@@ -2029,8 +2046,8 @@ msgstr ""
|
|||||||
msgid "Content & Media"
|
msgid "Content & Media"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:200
|
#: src/screens/Settings/Settings.tsx:204
|
||||||
#: src/screens/Settings/Settings.tsx:203
|
#: src/screens/Settings/Settings.tsx:207
|
||||||
msgid "Content and media"
|
msgid "Content and media"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2076,6 +2093,8 @@ msgstr ""
|
|||||||
msgid "Context menu backdrop, click to close the menu."
|
msgid "Context menu backdrop, click to close the menu."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:162
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:170
|
||||||
#: src/screens/Onboarding/StepInterests/index.tsx:244
|
#: src/screens/Onboarding/StepInterests/index.tsx:244
|
||||||
#: src/screens/Onboarding/StepProfile/index.tsx:276
|
#: src/screens/Onboarding/StepProfile/index.tsx:276
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
@@ -2230,6 +2249,8 @@ msgstr ""
|
|||||||
msgid "Copy TXT record value"
|
msgid "Copy TXT record value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:40
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:107
|
||||||
#: src/Navigation.tsx:345
|
#: src/Navigation.tsx:345
|
||||||
#: src/view/screens/CopyrightPolicy.tsx:31
|
#: src/view/screens/CopyrightPolicy.tsx:31
|
||||||
msgid "Copyright Policy"
|
msgid "Copyright Policy"
|
||||||
@@ -2398,7 +2419,7 @@ msgstr ""
|
|||||||
msgid "Deactivate account"
|
msgid "Deactivate account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:447
|
#: src/screens/Settings/Settings.tsx:455
|
||||||
msgid "Debug Moderation"
|
msgid "Debug Moderation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2447,7 +2468,7 @@ msgstr ""
|
|||||||
msgid "Delete chat"
|
msgid "Delete chat"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:454
|
#: src/screens/Settings/Settings.tsx:462
|
||||||
msgid "Delete chat declaration record"
|
msgid "Delete chat declaration record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2563,8 +2584,8 @@ msgctxt "toast"
|
|||||||
msgid "Developer mode enabled"
|
msgid "Developer mode enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:267
|
#: src/screens/Settings/Settings.tsx:271
|
||||||
#: src/screens/Settings/Settings.tsx:270
|
#: src/screens/Settings/Settings.tsx:274
|
||||||
msgid "Developer options"
|
msgid "Developer options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2659,12 +2680,13 @@ msgstr ""
|
|||||||
msgid "Dismiss interests"
|
msgid "Dismiss interests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/interstitials/TrendingVideos.tsx:89
|
#. Accessibility label for dismissing a toast notification
|
||||||
msgid "Dismiss this section"
|
#: src/components/Toast/index.web.tsx:82
|
||||||
|
msgid "Dismiss message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Toast/index.web.tsx:81
|
#: src/components/interstitials/TrendingVideos.tsx:89
|
||||||
msgid "Dismiss toast"
|
msgid "Dismiss this section"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/AccessibilitySettings.tsx:69
|
#: src/screens/Settings/AccessibilitySettings.tsx:69
|
||||||
@@ -3101,7 +3123,7 @@ msgstr ""
|
|||||||
msgid "Enter your username and password"
|
msgid "Enter your username and password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:135
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:132
|
||||||
msgid "Enters full screen"
|
msgid "Enters full screen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4033,8 +4055,8 @@ msgstr ""
|
|||||||
msgid "Having trouble?"
|
msgid "Having trouble?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:232
|
|
||||||
#: src/screens/Settings/Settings.tsx:236
|
#: src/screens/Settings/Settings.tsx:236
|
||||||
|
#: src/screens/Settings/Settings.tsx:240
|
||||||
#: src/view/shell/desktop/RightNav.tsx:120
|
#: src/view/shell/desktop/RightNav.tsx:120
|
||||||
#: src/view/shell/desktop/RightNav.tsx:121
|
#: src/view/shell/desktop/RightNav.tsx:121
|
||||||
#: src/view/shell/Drawer.tsx:370
|
#: src/view/shell/Drawer.tsx:370
|
||||||
@@ -4049,6 +4071,11 @@ msgstr ""
|
|||||||
msgid "Here is your app password!"
|
msgid "Here is your app password!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:73
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:119
|
||||||
|
msgid "Hey there 👋"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/VideoPostCard.tsx:178
|
#: src/components/VideoPostCard.tsx:178
|
||||||
#: src/components/VideoPostCard.tsx:460
|
#: src/components/VideoPostCard.tsx:460
|
||||||
msgid "Hidden"
|
msgid "Hidden"
|
||||||
@@ -4225,7 +4252,7 @@ msgstr ""
|
|||||||
msgid "If alt text is long, toggles alt text expanded state"
|
msgid "If alt text is long, toggles alt text expanded state"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:112
|
#: src/screens/Signup/StepInfo/Policies.tsx:142
|
||||||
msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf."
|
msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4404,7 +4431,7 @@ msgstr ""
|
|||||||
msgid "Invite code"
|
msgid "Invite code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/state.ts:339
|
#: src/screens/Signup/state.ts:348
|
||||||
msgid "Invite code not accepted. Check that you input it correctly and try again."
|
msgid "Invite code not accepted. Check that you input it correctly and try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4516,8 +4543,8 @@ msgid "Language Settings"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/LanguageSettings.tsx:78
|
#: src/screens/Settings/LanguageSettings.tsx:78
|
||||||
#: src/screens/Settings/Settings.tsx:224
|
#: src/screens/Settings/Settings.tsx:228
|
||||||
#: src/screens/Settings/Settings.tsx:227
|
#: src/screens/Settings/Settings.tsx:231
|
||||||
msgid "Languages"
|
msgid "Languages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4575,6 +4602,14 @@ msgstr ""
|
|||||||
msgid "Learn more about the moderation applied to this content."
|
msgid "Learn more about the moderation applied to this content."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:149
|
||||||
|
msgid "Learn more about these changes and how to share your thoughts with us by <0>reading our blog post.</0>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:89
|
||||||
|
msgid "Learn more about these changes and how to share your thoughts with us by reading our blog post."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/moderation/PostHider.tsx:110
|
#: src/components/moderation/PostHider.tsx:110
|
||||||
#: src/components/moderation/ScreenHider.tsx:127
|
#: src/components/moderation/ScreenHider.tsx:127
|
||||||
msgid "Learn more about this warning"
|
msgid "Learn more about this warning"
|
||||||
@@ -5023,8 +5058,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/Navigation.tsx:176
|
#: src/Navigation.tsx:176
|
||||||
#: src/screens/Moderation/index.tsx:99
|
#: src/screens/Moderation/index.tsx:99
|
||||||
#: src/screens/Settings/Settings.tsx:184
|
#: src/screens/Settings/Settings.tsx:188
|
||||||
#: src/screens/Settings/Settings.tsx:187
|
#: src/screens/Settings/Settings.tsx:191
|
||||||
msgid "Moderation"
|
msgid "Moderation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5115,7 +5150,7 @@ msgstr ""
|
|||||||
msgid "Music"
|
msgid "Music"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
|
||||||
msgctxt "video"
|
msgctxt "video"
|
||||||
msgid "Mute"
|
msgid "Mute"
|
||||||
@@ -5625,8 +5660,8 @@ msgstr ""
|
|||||||
#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30
|
#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30
|
||||||
#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30
|
#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30
|
||||||
#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30
|
#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30
|
||||||
#: src/screens/Settings/Settings.tsx:192
|
#: src/screens/Settings/Settings.tsx:196
|
||||||
#: src/screens/Settings/Settings.tsx:195
|
#: src/screens/Settings/Settings.tsx:199
|
||||||
#: src/view/screens/Notifications.tsx:130
|
#: src/view/screens/Notifications.tsx:130
|
||||||
#: src/view/shell/bottom-bar/BottomBar.tsx:252
|
#: src/view/shell/bottom-bar/BottomBar.tsx:252
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:655
|
#: src/view/shell/desktop/LeftNav.tsx:655
|
||||||
@@ -5699,7 +5734,7 @@ msgstr ""
|
|||||||
msgid "on<0><1/><2><3/></2></0>"
|
msgid "on<0><1/><2><3/></2></0>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:380
|
#: src/screens/Settings/Settings.tsx:388
|
||||||
msgid "Onboarding reset"
|
msgid "Onboarding reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5797,7 +5832,7 @@ msgstr ""
|
|||||||
msgid "Open message options"
|
msgid "Open message options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:445
|
#: src/screens/Settings/Settings.tsx:453
|
||||||
msgid "Open moderation debug page"
|
msgid "Open moderation debug page"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5826,12 +5861,12 @@ msgstr ""
|
|||||||
msgid "Open starter pack menu"
|
msgid "Open starter pack menu"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:438
|
#: src/screens/Settings/Settings.tsx:446
|
||||||
#: src/screens/Settings/Settings.tsx:452
|
#: src/screens/Settings/Settings.tsx:460
|
||||||
msgid "Open storybook page"
|
msgid "Open storybook page"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:431
|
#: src/screens/Settings/Settings.tsx:439
|
||||||
msgid "Open system log"
|
msgid "Open system log"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5893,7 +5928,7 @@ msgstr ""
|
|||||||
msgid "Opens GIF select dialog"
|
msgid "Opens GIF select dialog"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:233
|
#: src/screens/Settings/Settings.tsx:237
|
||||||
msgid "Opens helpdesk in browser"
|
msgid "Opens helpdesk in browser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5973,6 +6008,10 @@ msgstr ""
|
|||||||
msgid "Other..."
|
msgid "Other..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:50
|
||||||
|
msgid "Our blog post"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/dms/ReportDialog.tsx:348
|
#: src/components/dms/ReportDialog.tsx:348
|
||||||
msgid "Our moderation team has received your report."
|
msgid "Our moderation team has received your report."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6016,7 +6055,7 @@ msgid "Password updated!"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43
|
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369
|
||||||
msgid "Pause"
|
msgid "Pause"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6105,7 +6144,7 @@ msgid "Pinned to your feeds"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43
|
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:370
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:370
|
||||||
msgid "Play"
|
msgid "Play"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6114,7 +6153,7 @@ msgstr ""
|
|||||||
msgid "Play {0}"
|
msgid "Play {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/index.tsx:134
|
#: src/components/Post/Embed/VideoEmbed/index.tsx:132
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321
|
||||||
msgid "Play video"
|
msgid "Play video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6127,7 +6166,7 @@ msgstr ""
|
|||||||
msgid "Plays or pauses the GIF"
|
msgid "Plays or pauses the GIF"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:141
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:138
|
||||||
msgid "Plays or pauses the video"
|
msgid "Plays or pauses the video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6147,11 +6186,11 @@ msgstr ""
|
|||||||
msgid "Please check your email inbox for further instructions. It may take a minute or two to arrive."
|
msgid "Please check your email inbox for further instructions. It may take a minute or two to arrive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/state.ts:286
|
#: src/screens/Signup/state.ts:289
|
||||||
msgid "Please choose your handle."
|
msgid "Please choose your handle."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/state.ts:278
|
#: src/screens/Signup/state.ts:281
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:120
|
#: src/screens/Signup/StepInfo/index.tsx:120
|
||||||
msgid "Please choose your password."
|
msgid "Please choose your password."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6160,7 +6199,7 @@ msgstr ""
|
|||||||
msgid "Please click on the link in the email we just sent you to verify your new email address. This is an important step to allow you to continue enjoying all the features of Bluesky."
|
msgid "Please click on the link in the email we just sent you to verify your new email address. This is an important step to allow you to continue enjoying all the features of Bluesky."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/state.ts:301
|
#: src/screens/Signup/state.ts:304
|
||||||
msgid "Please complete the verification captcha."
|
msgid "Please complete the verification captcha."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6208,7 +6247,7 @@ msgstr ""
|
|||||||
msgid "Please enter the security code we sent to your previous email address."
|
msgid "Please enter the security code we sent to your previous email address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/state.ts:262
|
#: src/screens/Signup/state.ts:265
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:91
|
#: src/screens/Signup/StepInfo/index.tsx:91
|
||||||
msgid "Please enter your email."
|
msgid "Please enter your email."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6241,6 +6280,11 @@ msgstr ""
|
|||||||
msgid "Please explain why you think your chats were incorrectly disabled"
|
msgid "Please explain why you think your chats were incorrectly disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/FocusScope/index.tsx:93
|
||||||
|
#: src/components/FocusScope/index.tsx:117
|
||||||
|
msgid "Please go back, or activate this element to return to the start of the active content."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:98
|
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:98
|
||||||
msgid "Please provide any additional details you feel moderators may need in order to properly assess your Age Assurance status."
|
msgid "Please provide any additional details you feel moderators may need in order to properly assess your Age Assurance status."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6439,8 +6483,8 @@ msgstr ""
|
|||||||
msgid "Privacy"
|
msgid "Privacy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:178
|
#: src/screens/Settings/Settings.tsx:182
|
||||||
#: src/screens/Settings/Settings.tsx:181
|
#: src/screens/Settings/Settings.tsx:185
|
||||||
msgid "Privacy and security"
|
msgid "Privacy and security"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6455,6 +6499,8 @@ msgstr ""
|
|||||||
msgid "Privacy and Security settings"
|
msgid "Privacy and Security settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:35
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:102
|
||||||
#: src/Navigation.tsx:330
|
#: src/Navigation.tsx:330
|
||||||
#: src/screens/Settings/AboutSettings.tsx:92
|
#: src/screens/Settings/AboutSettings.tsx:92
|
||||||
#: src/screens/Settings/AboutSettings.tsx:95
|
#: src/screens/Settings/AboutSettings.tsx:95
|
||||||
@@ -6637,17 +6683,21 @@ msgstr ""
|
|||||||
msgid "Read more replies"
|
msgid "Read more replies"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:112
|
||||||
|
msgid "Read our blog post"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/SplashScreen.web.tsx:173
|
#: src/view/com/auth/SplashScreen.web.tsx:173
|
||||||
msgid "Read the Bluesky blog"
|
msgid "Read the Bluesky blog"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:58
|
#: src/screens/Signup/StepInfo/Policies.tsx:88
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:84
|
#: src/screens/Signup/StepInfo/Policies.tsx:114
|
||||||
msgid "Read the Bluesky Privacy Policy"
|
msgid "Read the Bluesky Privacy Policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:51
|
#: src/screens/Signup/StepInfo/Policies.tsx:81
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:71
|
#: src/screens/Signup/StepInfo/Policies.tsx:101
|
||||||
msgid "Read the Bluesky Terms of Service"
|
msgid "Read the Bluesky Terms of Service"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6699,7 +6749,7 @@ msgstr ""
|
|||||||
#: src/components/FeedCard.tsx:343
|
#: src/components/FeedCard.tsx:343
|
||||||
#: src/components/StarterPack/Wizard/WizardListCard.tsx:102
|
#: src/components/StarterPack/Wizard/WizardListCard.tsx:102
|
||||||
#: src/components/StarterPack/Wizard/WizardListCard.tsx:109
|
#: src/components/StarterPack/Wizard/WizardListCard.tsx:109
|
||||||
#: src/screens/Settings/Settings.tsx:620
|
#: src/screens/Settings/Settings.tsx:662
|
||||||
#: src/view/com/modals/UserAddRemoveLists.tsx:235
|
#: src/view/com/modals/UserAddRemoveLists.tsx:235
|
||||||
#: src/view/com/posts/PostFeedErrorMessage.tsx:217
|
#: src/view/com/posts/PostFeedErrorMessage.tsx:217
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
@@ -6713,8 +6763,8 @@ msgstr ""
|
|||||||
msgid "Remove {historyItem}"
|
msgid "Remove {historyItem}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:599
|
#: src/screens/Settings/Settings.tsx:641
|
||||||
#: src/screens/Settings/Settings.tsx:602
|
#: src/screens/Settings/Settings.tsx:644
|
||||||
msgid "Remove account"
|
msgid "Remove account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6753,7 +6803,7 @@ msgstr ""
|
|||||||
msgid "Remove from my feeds"
|
msgid "Remove from my feeds"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:612
|
#: src/screens/Settings/Settings.tsx:654
|
||||||
msgid "Remove from quick access?"
|
msgid "Remove from quick access?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -7118,8 +7168,8 @@ msgstr ""
|
|||||||
msgid "Resend Verification Email"
|
msgid "Resend Verification Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:474
|
#: src/screens/Settings/Settings.tsx:482
|
||||||
#: src/screens/Settings/Settings.tsx:476
|
#: src/screens/Settings/Settings.tsx:484
|
||||||
msgid "Reset activity subscription nudge"
|
msgid "Reset activity subscription nudge"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -7132,8 +7182,8 @@ msgstr ""
|
|||||||
msgid "Reset Code"
|
msgid "Reset Code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:459
|
#: src/screens/Settings/Settings.tsx:467
|
||||||
#: src/screens/Settings/Settings.tsx:461
|
#: src/screens/Settings/Settings.tsx:469
|
||||||
msgid "Reset onboarding state"
|
msgid "Reset onboarding state"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -7633,7 +7683,7 @@ msgid "Sets email for password reset"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Navigation.tsx:212
|
#: src/Navigation.tsx:212
|
||||||
#: src/screens/Settings/Settings.tsx:95
|
#: src/screens/Settings/Settings.tsx:99
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:728
|
#: src/view/shell/desktop/LeftNav.tsx:728
|
||||||
#: src/view/shell/Drawer.tsx:572
|
#: src/view/shell/Drawer.tsx:572
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
@@ -7893,7 +7943,7 @@ msgstr ""
|
|||||||
msgid "Shows information about when this post was created"
|
msgid "Shows information about when this post was created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:121
|
#: src/screens/Settings/Settings.tsx:125
|
||||||
msgid "Shows other accounts you can switch to"
|
msgid "Shows other accounts you can switch to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -7951,9 +8001,9 @@ msgstr ""
|
|||||||
msgid "Sign in to view post"
|
msgid "Sign in to view post"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:250
|
#: src/screens/Settings/Settings.tsx:254
|
||||||
#: src/screens/Settings/Settings.tsx:252
|
#: src/screens/Settings/Settings.tsx:256
|
||||||
#: src/screens/Settings/Settings.tsx:284
|
#: src/screens/Settings/Settings.tsx:288
|
||||||
#: src/screens/SignupQueued.tsx:93
|
#: src/screens/SignupQueued.tsx:93
|
||||||
#: src/screens/SignupQueued.tsx:96
|
#: src/screens/SignupQueued.tsx:96
|
||||||
#: src/screens/Takendown.tsx:85
|
#: src/screens/Takendown.tsx:85
|
||||||
@@ -7967,7 +8017,7 @@ msgstr ""
|
|||||||
msgid "Sign Out"
|
msgid "Sign Out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:281
|
#: src/screens/Settings/Settings.tsx:285
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:208
|
#: src/view/shell/desktop/LeftNav.tsx:208
|
||||||
msgid "Sign out?"
|
msgid "Sign out?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -8065,8 +8115,8 @@ msgstr ""
|
|||||||
msgid "Something wrong? Let us know."
|
msgid "Something wrong? Let us know."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/App.native.tsx:123
|
#: src/App.native.tsx:124
|
||||||
#: src/App.web.tsx:100
|
#: src/App.web.tsx:101
|
||||||
msgid "Sorry! Your session expired. Please sign in again."
|
msgid "Sorry! Your session expired. Please sign in again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8171,12 +8221,12 @@ msgstr ""
|
|||||||
msgid "Step {0} of {1}"
|
msgid "Step {0} of {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:385
|
#: src/screens/Settings/Settings.tsx:393
|
||||||
msgid "Storage cleared, you need to restart the app now."
|
msgid "Storage cleared, you need to restart the app now."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Navigation.tsx:305
|
#: src/Navigation.tsx:305
|
||||||
#: src/screens/Settings/Settings.tsx:440
|
#: src/screens/Settings/Settings.tsx:448
|
||||||
msgid "Storybook"
|
msgid "Storybook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8269,9 +8319,9 @@ msgstr ""
|
|||||||
msgid "Support"
|
msgid "Support"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:119
|
#: src/screens/Settings/Settings.tsx:123
|
||||||
#: src/screens/Settings/Settings.tsx:133
|
#: src/screens/Settings/Settings.tsx:137
|
||||||
#: src/screens/Settings/Settings.tsx:562
|
#: src/screens/Settings/Settings.tsx:604
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:245
|
#: src/view/shell/desktop/LeftNav.tsx:245
|
||||||
msgid "Switch account"
|
msgid "Switch account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -8298,7 +8348,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/screens/Settings/AboutSettings.tsx:107
|
#: src/screens/Settings/AboutSettings.tsx:107
|
||||||
#: src/screens/Settings/AboutSettings.tsx:110
|
#: src/screens/Settings/AboutSettings.tsx:110
|
||||||
#: src/screens/Settings/Settings.tsx:433
|
#: src/screens/Settings/Settings.tsx:441
|
||||||
msgid "System log"
|
msgid "System log"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8314,6 +8364,10 @@ msgstr ""
|
|||||||
msgid "Tap for more information"
|
msgid "Tap for more information"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:164
|
||||||
|
msgid "Tap to acknowledge that you understand and agree to these updates and continue using Bluesky"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/ContextMenu/Backdrop.ios.tsx:54
|
#: src/components/ContextMenu/Backdrop.ios.tsx:54
|
||||||
#: src/components/ContextMenu/Backdrop.ios.tsx:80
|
#: src/components/ContextMenu/Backdrop.ios.tsx:80
|
||||||
#: src/components/ContextMenu/Backdrop.tsx:46
|
#: src/components/ContextMenu/Backdrop.tsx:46
|
||||||
@@ -8358,6 +8412,8 @@ msgstr ""
|
|||||||
msgid "Terms"
|
msgid "Terms"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:30
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:97
|
||||||
#: src/Navigation.tsx:335
|
#: src/Navigation.tsx:335
|
||||||
#: src/screens/Settings/AboutSettings.tsx:84
|
#: src/screens/Settings/AboutSettings.tsx:84
|
||||||
#: src/screens/Settings/AboutSettings.tsx:87
|
#: src/screens/Settings/AboutSettings.tsx:87
|
||||||
@@ -8813,7 +8869,7 @@ msgstr ""
|
|||||||
msgid "This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others."
|
msgid "This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:37
|
#: src/screens/Signup/StepInfo/Policies.tsx:66
|
||||||
msgid "This service has not provided terms of service or a privacy policy."
|
msgid "This service has not provided terms of service or a privacy policy."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8866,7 +8922,7 @@ msgstr ""
|
|||||||
msgid "This will delete \"{0}\" from your muted words. You can always add it back later."
|
msgid "This will delete \"{0}\" from your muted words. You can always add it back later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:614
|
#: src/screens/Settings/Settings.tsx:656
|
||||||
msgid "This will remove @{0} from the quick access list."
|
msgid "This will remove @{0} from the quick access list."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8939,7 +8995,7 @@ msgstr ""
|
|||||||
msgid "Toggle to enable or disable adult content"
|
msgid "Toggle to enable or disable adult content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:158
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
|
||||||
msgid "Toggles the sound"
|
msgid "Toggles the sound"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9032,11 +9088,11 @@ msgstr ""
|
|||||||
msgid "Unable to delete"
|
msgid "Unable to delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:499
|
#: src/screens/Settings/Settings.tsx:507
|
||||||
msgid "Unapply Pull Request"
|
msgid "Unapply Pull Request"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:501
|
#: src/screens/Settings/Settings.tsx:509
|
||||||
msgid "Unapply Pull Request {currentChannel}"
|
msgid "Unapply Pull Request {currentChannel}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9125,7 +9181,7 @@ msgstr ""
|
|||||||
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:95
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:95
|
||||||
msgctxt "video"
|
msgctxt "video"
|
||||||
msgid "Unmute"
|
msgid "Unmute"
|
||||||
@@ -9199,8 +9255,8 @@ msgstr ""
|
|||||||
msgid "Unpinned from your feeds"
|
msgid "Unpinned from your feeds"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:466
|
#: src/screens/Settings/Settings.tsx:474
|
||||||
#: src/screens/Settings/Settings.tsx:468
|
#: src/screens/Settings/Settings.tsx:476
|
||||||
msgid "Unsnooze email reminder"
|
msgid "Unsnooze email reminder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9518,8 +9574,8 @@ msgstr ""
|
|||||||
msgid "Version {0}"
|
msgid "Version {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:134
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:131
|
||||||
msgid "Video"
|
msgid "Video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9549,7 +9605,7 @@ msgstr ""
|
|||||||
msgid "Video is playing"
|
msgid "Video is playing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:192
|
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:219
|
||||||
msgid "Video not found."
|
msgid "Video not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9561,7 +9617,7 @@ msgstr ""
|
|||||||
msgid "Video uploaded"
|
msgid "Video uploaded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83
|
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81
|
||||||
msgid "Video: {0}"
|
msgid "Video: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9802,6 +9858,14 @@ msgstr ""
|
|||||||
msgid "We'll use this to help customize your experience."
|
msgid "We'll use this to help customize your experience."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:139
|
||||||
|
msgid "We're also updating our <0>Community Guidelines</0>, and we want your input! These new guidelines will take effect on October 13th, 2025."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:82
|
||||||
|
msgid "We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:227
|
#: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:227
|
||||||
msgid "We're confirming your age assurance status with our servers. This should only take a few seconds."
|
msgid "We're confirming your age assurance status with our servers. This should only take a few seconds."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -9848,6 +9912,22 @@ msgstr ""
|
|||||||
msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty."
|
msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:122
|
||||||
|
msgid "We’re updating our <0>Terms of Service</0>, <1>Privacy Policy</1>, and <2>Copyright Policy</2>, effective September 12th, 2025."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:63
|
||||||
|
msgid "We're updating our policies"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:76
|
||||||
|
msgid "We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:61
|
||||||
|
msgid "We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:169
|
#: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:169
|
||||||
msgid "We've confirmed your age assurance status. You can now close this dialog."
|
msgid "We've confirmed your age assurance status. You can now close this dialog."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10008,6 +10088,10 @@ msgstr ""
|
|||||||
msgid "You"
|
msgid "You"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/screens/Signup/StepInfo/Policies.tsx:23
|
||||||
|
msgid "You also agree to <0>Bluesky’s Community Guidelines</0>. An <1>updated version of our Community Guidelines</1> will take effect on October 13th."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/verification/VerifierDialog.tsx:59
|
#: src/components/verification/VerifierDialog.tsx:59
|
||||||
msgid "You are a trusted verifier"
|
msgid "You are a trusted verifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10241,7 +10325,7 @@ msgstr ""
|
|||||||
msgid "You may only select up to 4 images"
|
msgid "You may only select up to 4 images"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:106
|
#: src/screens/Signup/StepInfo/Policies.tsx:136
|
||||||
msgid "You must be 13 years of age or older to create an account."
|
msgid "You must be 13 years of age or older to create an account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -10278,7 +10362,7 @@ msgstr ""
|
|||||||
msgid "You previously deactivated @{0}."
|
msgid "You previously deactivated @{0}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:396
|
#: src/screens/Settings/Settings.tsx:404
|
||||||
msgid "You probably want to restart the app now."
|
msgid "You probably want to restart the app now."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -10290,7 +10374,7 @@ msgstr ""
|
|||||||
msgid "You reacted {0} to {1}"
|
msgid "You reacted {0} to {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:282
|
#: src/screens/Settings/Settings.tsx:286
|
||||||
#: src/view/shell/desktop/LeftNav.tsx:209
|
#: src/view/shell/desktop/LeftNav.tsx:209
|
||||||
msgid "You will be signed out of all your accounts."
|
msgid "You will be signed out of all your accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10369,6 +10453,10 @@ msgstr ""
|
|||||||
msgid "You've found some people to follow"
|
msgid "You've found some people to follow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/FocusScope/index.tsx:114
|
||||||
|
msgid "You've reached the end of the active content."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
||||||
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10377,6 +10465,10 @@ msgstr ""
|
|||||||
msgid "You've reached the maximum number of requests allowed. Please try again later."
|
msgid "You've reached the maximum number of requests allowed. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/FocusScope/index.tsx:90
|
||||||
|
msgid "You've reached the start of the active content."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/composer/state/video.ts:423
|
#: src/view/com/composer/state/video.ts:423
|
||||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10421,7 +10513,7 @@ msgstr ""
|
|||||||
msgid "Your birth date"
|
msgid "Your birth date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:196
|
#: src/components/Post/Embed/VideoEmbed/index.web.tsx:223
|
||||||
msgid "Your browser does not support the video format. Please try a different browser."
|
msgid "Your browser does not support the video format. Please try a different browser."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -10444,7 +10536,7 @@ msgid "Your email"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/screens/Login/ForgotPasswordForm.tsx:51
|
#: src/screens/Login/ForgotPasswordForm.tsx:51
|
||||||
#: src/screens/Signup/state.ts:270
|
#: src/screens/Signup/state.ts:273
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:98
|
#: src/screens/Signup/StepInfo/index.tsx:98
|
||||||
#: src/view/com/modals/ChangePassword.tsx:55
|
#: src/view/com/modals/ChangePassword.tsx:55
|
||||||
msgid "Your email appears to be invalid."
|
msgid "Your email appears to be invalid."
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export enum LogContext {
|
|||||||
FeedFeedback = 'feed-feedback',
|
FeedFeedback = 'feed-feedback',
|
||||||
PostSource = 'post-source',
|
PostSource = 'post-source',
|
||||||
AgeAssurance = 'age-assurance',
|
AgeAssurance = 'age-assurance',
|
||||||
|
PolicyUpdate = 'policy-update',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this
|
* METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {clearStorage} from '#/state/persisted'
|
|||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
||||||
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
|
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
|
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
@@ -35,6 +36,7 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
|||||||
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
|
import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
|
||||||
import {AvatarStackWithFetch} from '#/components/AvatarStack'
|
import {AvatarStackWithFetch} from '#/components/AvatarStack'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
||||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||||
@@ -58,6 +60,7 @@ import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/W
|
|||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useFullVerificationState} from '#/components/verification'
|
import {useFullVerificationState} from '#/components/verification'
|
||||||
@@ -66,6 +69,7 @@ import {
|
|||||||
VerificationCheckButton,
|
VerificationCheckButton,
|
||||||
} from '#/components/verification/VerificationCheckButton'
|
} from '#/components/verification/VerificationCheckButton'
|
||||||
import {IS_INTERNAL} from '#/env'
|
import {IS_INTERNAL} from '#/env'
|
||||||
|
import {device, useStorage} from '#/storage'
|
||||||
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||||
@@ -363,6 +367,10 @@ function ProfilePreview({
|
|||||||
|
|
||||||
function DevOptions() {
|
function DevOptions() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const agent = useAgent()
|
||||||
|
const [override, setOverride] = useStorage(device, [
|
||||||
|
'policyUpdateDebugOverride',
|
||||||
|
])
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
||||||
@@ -502,6 +510,40 @@ function DevOptions() {
|
|||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<View style={[a.p_xl, a.gap_md]}>
|
||||||
|
<Text style={[a.text_lg, a.font_bold]}>PolicyUpdate202508 Debug</Text>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
setOverride(!override)
|
||||||
|
}}
|
||||||
|
label="Toggle"
|
||||||
|
color={override ? 'primary' : 'secondary'}
|
||||||
|
size="small"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<ButtonText>
|
||||||
|
{override ? 'Disable debug mode' : 'Enable debug mode'}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
device.set([PolicyUpdate202508], false)
|
||||||
|
agent.bskyAppRemoveNuxs([PolicyUpdate202508])
|
||||||
|
Toast.show(`Done`, 'info')
|
||||||
|
}}
|
||||||
|
label="Reset policy update nux"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
disabled={!override}>
|
||||||
|
<ButtonText>Reset state</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<SettingsList.Divider />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import React from 'react'
|
import {useEffect, useMemo, useRef} from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
import {WebView, type WebViewNavigation} from 'react-native-webview'
|
||||||
import {WebView, WebViewNavigation} from 'react-native-webview'
|
import {type ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
|
||||||
import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
|
|
||||||
|
|
||||||
import {SignupState} from '#/screens/Signup/state'
|
import {type SignupState} from '#/screens/Signup/state'
|
||||||
|
|
||||||
const ALLOWED_HOSTS = [
|
const ALLOWED_HOSTS = [
|
||||||
'bsky.social',
|
'bsky.social',
|
||||||
'bsky.app',
|
'bsky.app',
|
||||||
'staging.bsky.app',
|
'staging.bsky.app',
|
||||||
'staging.bsky.dev',
|
'staging.bsky.dev',
|
||||||
|
'app.staging.bsky.dev',
|
||||||
'js.hcaptcha.com',
|
'js.hcaptcha.com',
|
||||||
'newassets.hcaptcha.com',
|
'newassets.hcaptcha.com',
|
||||||
'api2.hcaptcha.com',
|
'api2.hcaptcha.com',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const MIN_DELAY = 3_500
|
||||||
|
|
||||||
export function CaptchaWebView({
|
export function CaptchaWebView({
|
||||||
url,
|
url,
|
||||||
stateParam,
|
stateParam,
|
||||||
@@ -28,49 +30,67 @@ export function CaptchaWebView({
|
|||||||
onSuccess: (code: string) => void
|
onSuccess: (code: string) => void
|
||||||
onError: (error: unknown) => void
|
onError: (error: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const redirectHost = React.useMemo(() => {
|
const startedAt = useRef(Date.now())
|
||||||
|
const successTo = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (successTo.current) {
|
||||||
|
clearTimeout(successTo.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const redirectHost = useMemo(() => {
|
||||||
if (!state?.serviceUrl) return 'bsky.app'
|
if (!state?.serviceUrl) return 'bsky.app'
|
||||||
|
|
||||||
return state?.serviceUrl &&
|
return state?.serviceUrl &&
|
||||||
new URL(state?.serviceUrl).host === 'staging.bsky.dev'
|
new URL(state?.serviceUrl).host === 'staging.bsky.dev'
|
||||||
? 'staging.bsky.app'
|
? 'app.staging.bsky.dev'
|
||||||
: 'bsky.app'
|
: 'bsky.app'
|
||||||
}, [state?.serviceUrl])
|
}, [state?.serviceUrl])
|
||||||
|
|
||||||
const wasSuccessful = React.useRef(false)
|
const wasSuccessful = useRef(false)
|
||||||
|
|
||||||
const onShouldStartLoadWithRequest = React.useCallback(
|
const onShouldStartLoadWithRequest = (event: ShouldStartLoadRequest) => {
|
||||||
(event: ShouldStartLoadRequest) => {
|
const urlp = new URL(event.url)
|
||||||
const urlp = new URL(event.url)
|
return ALLOWED_HOSTS.includes(urlp.host)
|
||||||
return ALLOWED_HOSTS.includes(urlp.host)
|
}
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onNavigationStateChange = React.useCallback(
|
const onNavigationStateChange = (e: WebViewNavigation) => {
|
||||||
(e: WebViewNavigation) => {
|
if (wasSuccessful.current) return
|
||||||
if (wasSuccessful.current) return
|
|
||||||
|
|
||||||
const urlp = new URL(e.url)
|
const urlp = new URL(e.url)
|
||||||
if (urlp.host !== redirectHost) return
|
if (urlp.host !== redirectHost || urlp.pathname === '/gate/signup') return
|
||||||
|
|
||||||
const code = urlp.searchParams.get('code')
|
const code = urlp.searchParams.get('code')
|
||||||
if (urlp.searchParams.get('state') !== stateParam || !code) {
|
if (urlp.searchParams.get('state') !== stateParam || !code) {
|
||||||
onError({error: 'Invalid state or code'})
|
onError({error: 'Invalid state or code'})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wasSuccessful.current = true
|
// We want to delay the completion of this screen ever so slightly so that it doesn't appear to be a glitch if it completes too fast
|
||||||
|
wasSuccessful.current = true
|
||||||
|
const now = Date.now()
|
||||||
|
const timeTaken = now - startedAt.current
|
||||||
|
if (timeTaken < MIN_DELAY) {
|
||||||
|
successTo.current = setTimeout(() => {
|
||||||
|
onSuccess(code)
|
||||||
|
}, MIN_DELAY - timeTaken)
|
||||||
|
} else {
|
||||||
onSuccess(code)
|
onSuccess(code)
|
||||||
},
|
}
|
||||||
[redirectHost, stateParam, onSuccess, onError],
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebView
|
<WebView
|
||||||
source={{uri: url}}
|
source={{uri: url}}
|
||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
style={styles.webview}
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||||
onNavigationStateChange={onNavigationStateChange}
|
onNavigationStateChange={onNavigationStateChange}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
@@ -83,11 +103,3 @@ export function CaptchaWebView({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
webview: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
borderRadius: 10,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,21 +1,74 @@
|
|||||||
import React from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {ActivityIndicator, View} from 'react-native'
|
import {ActivityIndicator, Platform, View} from 'react-native'
|
||||||
|
import ReactNativeDeviceAttest from 'react-native-device-attest'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {createFullHandle} from '#/lib/strings/handles'
|
import {createFullHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||||
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
|
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
|
||||||
import {useSignupContext} from '#/screens/Signup/state'
|
import {useSignupContext} from '#/screens/Signup/state'
|
||||||
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
|
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {FormError} from '#/components/forms/FormError'
|
import {FormError} from '#/components/forms/FormError'
|
||||||
|
import {GCP_PROJECT_ID} from '#/env'
|
||||||
import {BackNextButtons} from '../BackNextButtons'
|
import {BackNextButtons} from '../BackNextButtons'
|
||||||
|
|
||||||
const CAPTCHA_PATH = '/gate/signup'
|
const CAPTCHA_PATH =
|
||||||
|
isWeb || GCP_PROJECT_ID === 0 ? '/gate/signup' : '/gate/signup/attempt-attest'
|
||||||
|
|
||||||
export function StepCaptcha() {
|
export function StepCaptcha() {
|
||||||
|
if (isWeb) {
|
||||||
|
return <StepCaptchaInner />
|
||||||
|
} else {
|
||||||
|
return <StepCaptchaNative />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepCaptchaNative() {
|
||||||
|
const [token, setToken] = useState<string>()
|
||||||
|
const [payload, setPayload] = useState<string>()
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
logger.debug('trying to generate attestation token...')
|
||||||
|
try {
|
||||||
|
if (isIOS) {
|
||||||
|
logger.debug('starting to generate devicecheck token...')
|
||||||
|
const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
|
||||||
|
setToken(token)
|
||||||
|
logger.debug(`generated devicecheck token: ${token}`)
|
||||||
|
} else {
|
||||||
|
const {token, payload} =
|
||||||
|
await ReactNativeDeviceAttest.getIntegrityToken('signup')
|
||||||
|
setToken(token)
|
||||||
|
setPayload(base64UrlEncode(payload))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(e)
|
||||||
|
} finally {
|
||||||
|
setReady(true)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return <View />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StepCaptchaInner token={token} payload={payload} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepCaptchaInner({
|
||||||
|
token,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
token?: string
|
||||||
|
payload?: string
|
||||||
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {state, dispatch} = useSignupContext()
|
const {state, dispatch} = useSignupContext()
|
||||||
@@ -33,8 +86,24 @@ export function StepCaptcha() {
|
|||||||
newUrl.searchParams.set('state', stateParam)
|
newUrl.searchParams.set('state', stateParam)
|
||||||
newUrl.searchParams.set('colorScheme', theme.name)
|
newUrl.searchParams.set('colorScheme', theme.name)
|
||||||
|
|
||||||
|
if (isNative && token) {
|
||||||
|
newUrl.searchParams.set('platform', Platform.OS)
|
||||||
|
newUrl.searchParams.set('token', token)
|
||||||
|
if (isAndroid && payload) {
|
||||||
|
newUrl.searchParams.set('payload', payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return newUrl.href
|
return newUrl.href
|
||||||
}, [state.serviceUrl, state.handle, state.userDomain, stateParam, theme.name])
|
}, [
|
||||||
|
state.serviceUrl,
|
||||||
|
state.handle,
|
||||||
|
state.userDomain,
|
||||||
|
stateParam,
|
||||||
|
theme.name,
|
||||||
|
token,
|
||||||
|
payload,
|
||||||
|
])
|
||||||
|
|
||||||
const onSuccess = React.useCallback(
|
const onSuccess = React.useCallback(
|
||||||
(code: string) => {
|
(code: string) => {
|
||||||
@@ -105,3 +174,13 @@ export function StepCaptcha() {
|
|||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function base64UrlEncode(data: string): string {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const bytes = encoder.encode(data)
|
||||||
|
|
||||||
|
const binaryString = String.fromCharCode(...bytes)
|
||||||
|
const base64 = btoa(binaryString)
|
||||||
|
|
||||||
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]/g, '')
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,42 @@ import {type ComAtprotoServerDescribeServer} from '@atproto/api'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {webLinks} from '#/lib/constants'
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
function CommunityGuidelinesNotice({}: {}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const gate = useGate()
|
||||||
|
|
||||||
|
if (gate('disable_onboarding_policy_update_notice')) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.pt_xs]}>
|
||||||
|
<Admonition type="tip">
|
||||||
|
<Trans>
|
||||||
|
You also agree to{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Bluesky's Community Guidelines`)}
|
||||||
|
to={webLinks.communityDeprecated}>
|
||||||
|
Bluesky’s Community Guidelines
|
||||||
|
</InlineLinkText>
|
||||||
|
. An{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Bluesky's Updated Community Guidelines`)}
|
||||||
|
to={webLinks.community}>
|
||||||
|
updated version of our Community Guidelines
|
||||||
|
</InlineLinkText>{' '}
|
||||||
|
will take effect on October 13th.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const Policies = ({
|
export const Policies = ({
|
||||||
serviceDescription,
|
serviceDescription,
|
||||||
needsGuardian,
|
needsGuardian,
|
||||||
@@ -30,14 +61,13 @@ export const Policies = ({
|
|||||||
|
|
||||||
if (!tos && !pp) {
|
if (!tos && !pp) {
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
<View style={[a.gap_sm]}>
|
||||||
<CircleInfo size="md" fill={t.atoms.text_contrast_low.color} />
|
<Admonition type="info">
|
||||||
|
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>
|
<Trans>
|
||||||
This service has not provided terms of service or a privacy policy.
|
This service has not provided terms of service or a privacy policy.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Admonition>
|
||||||
|
<CommunityGuidelinesNotice />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -102,19 +132,21 @@ export const Policies = ({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{under13 ? (
|
{under13 ? (
|
||||||
<Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
|
<Admonition type="error">
|
||||||
<Trans>
|
<Trans>
|
||||||
You must be 13 years of age or older to create an account.
|
You must be 13 years of age or older to create an account.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Admonition>
|
||||||
) : needsGuardian ? (
|
) : needsGuardian ? (
|
||||||
<Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
|
<Admonition type="warning">
|
||||||
<Trans>
|
<Trans>
|
||||||
If you are not yet an adult according to the laws of your country,
|
If you are not yet an adult according to the laws of your country,
|
||||||
your parent or legal guardian must read these Terms on your behalf.
|
your parent or legal guardian must read these Terms on your behalf.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Admonition>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
<CommunityGuidelinesNotice />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import {useEffect, useReducer, useState} from 'react'
|
import {useEffect, useReducer, useState} from 'react'
|
||||||
import {AppState, type AppStateStatus, View} from 'react-native'
|
import {AppState, type AppStateStatus, View} from 'react-native'
|
||||||
|
import ReactNativeDeviceAttest from 'react-native-device-attest'
|
||||||
import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated'
|
import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated'
|
||||||
import {AppBskyGraphStarterpack} from '@atproto/api'
|
import {AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {FEEDBACK_FORM_URL} from '#/lib/constants'
|
import {FEEDBACK_FORM_URL} from '#/lib/constants'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useServiceQuery} from '#/state/queries/service'
|
import {useServiceQuery} from '#/state/queries/service'
|
||||||
import {useStarterPackQuery} from '#/state/queries/starter-packs'
|
import {useStarterPackQuery} from '#/state/queries/starter-packs'
|
||||||
import {useActiveStarterPack} from '#/state/shell/starter-pack'
|
import {useActiveStarterPack} from '#/state/shell/starter-pack'
|
||||||
@@ -26,6 +29,7 @@ import {Divider} from '#/components/Divider'
|
|||||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {GCP_PROJECT_ID} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function Signup({onPressBack}: {onPressBack: () => void}) {
|
export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||||
@@ -101,6 +105,16 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
|||||||
return () => subscription.remove()
|
return () => subscription.remove()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAndroid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err =>
|
||||||
|
logger.error(err),
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignupContext.Provider value={{state, dispatch}}>
|
<SignupContext.Provider value={{state, dispatch}}>
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {getAge} from '#/lib/strings/time'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useSessionApi} from '#/state/session'
|
import {useSessionApi} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate'
|
||||||
|
|
||||||
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
||||||
|
|
||||||
@@ -252,6 +253,8 @@ export function useSubmitSignup() {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {createAccount} = useSessionApi()
|
const {createAccount} = useSessionApi()
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
const preemptivelyCompleteActivePolicyUpdate =
|
||||||
|
usePreemptivelyCompleteActivePolicyUpdate()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (state: SignupState, dispatch: (action: SignupAction) => void) => {
|
async (state: SignupState, dispatch: (action: SignupAction) => void) => {
|
||||||
@@ -325,6 +328,12 @@ export function useSubmitSignup() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks any active policy update as completed, since user just agreed
|
||||||
|
* to TOS/privacy during sign up
|
||||||
|
*/
|
||||||
|
preemptivelyCompleteActivePolicyUpdate()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Must happen last so that if the user has multiple tabs open and
|
* Must happen last so that if the user has multiple tabs open and
|
||||||
* createAccount fails, one tab is not stuck in onboarding — Eric
|
* createAccount fails, one tab is not stuck in onboarding — Eric
|
||||||
@@ -363,6 +372,11 @@ export function useSubmitSignup() {
|
|||||||
dispatch({type: 'setIsLoading', value: false})
|
dispatch({type: 'setIsLoading', value: false})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_, onboardingDispatch, createAccount],
|
[
|
||||||
|
_,
|
||||||
|
onboardingDispatch,
|
||||||
|
createAccount,
|
||||||
|
preemptivelyCompleteActivePolicyUpdate,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
defaults,
|
defaults,
|
||||||
Schema,
|
type Schema,
|
||||||
tryParse,
|
tryParse,
|
||||||
tryStringify,
|
tryStringify,
|
||||||
} from '#/state/persisted/schema'
|
} from '#/state/persisted/schema'
|
||||||
import {PersistedApi} from './types'
|
import {device} from '#/storage'
|
||||||
|
import {type PersistedApi} from './types'
|
||||||
import {normalizeData} from './util'
|
import {normalizeData} from './util'
|
||||||
|
|
||||||
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||||
@@ -53,6 +54,7 @@ onUpdate satisfies PersistedApi['onUpdate']
|
|||||||
export async function clearStorage() {
|
export async function clearStorage() {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.removeItem(BSKY_STORAGE)
|
await AsyncStorage.removeItem(BSKY_STORAGE)
|
||||||
|
device.removeAll()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`persisted store: failed to clear`, {message: e.toString()})
|
logger.error(`persisted store: failed to clear`, {message: e.toString()})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
|
|
||||||
import {FeedTuner} from '#/lib/api/feed-manip'
|
import {FeedTuner} from '#/lib/api/feed-manip'
|
||||||
import {FeedDescriptor} from '../queries/post-feed'
|
import {type FeedDescriptor} from '../queries/post-feed'
|
||||||
import {usePreferencesQuery} from '../queries/preferences'
|
import {usePreferencesQuery} from '../queries/preferences'
|
||||||
import {useSession} from '../session'
|
import {useSession} from '../session'
|
||||||
import {useLanguagePrefs} from './languages'
|
import {useLanguagePrefs} from './languages'
|
||||||
@@ -19,7 +19,10 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (feedDesc.startsWith('feedgen')) {
|
if (feedDesc.startsWith('feedgen')) {
|
||||||
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
return [
|
||||||
|
FeedTuner.preferredLangOnly(langPrefs.contentLanguages),
|
||||||
|
FeedTuner.removeMutedThreads,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
if (feedDesc === 'following' || feedDesc.startsWith('list')) {
|
if (feedDesc === 'following' || feedDesc.startsWith('list')) {
|
||||||
const feedTuners = [FeedTuner.removeOrphans]
|
const feedTuners = [FeedTuner.removeOrphans]
|
||||||
@@ -40,6 +43,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
|||||||
feedTuners.push(FeedTuner.removeQuotePosts)
|
feedTuners.push(FeedTuner.removeQuotePosts)
|
||||||
}
|
}
|
||||||
feedTuners.push(FeedTuner.dedupThreads)
|
feedTuners.push(FeedTuner.dedupThreads)
|
||||||
|
feedTuners.push(FeedTuner.removeMutedThreads)
|
||||||
|
|
||||||
return feedTuners
|
return feedTuners
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {jest} from '@jest/globals'
|
||||||
|
|
||||||
|
export {Nux} from '#/state/queries/nuxs/definitions'
|
||||||
|
|
||||||
|
export const useNuxs = jest.fn(() => {
|
||||||
|
return {
|
||||||
|
nuxs: undefined,
|
||||||
|
status: 'loading' as const,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useNux = jest.fn((id: string) => {
|
||||||
|
return {
|
||||||
|
nux: undefined,
|
||||||
|
status: 'loading' as const,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useSaveNux = jest.fn(() => {
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useResetNuxs = jest.fn(() => {
|
||||||
|
return {}
|
||||||
|
})
|
||||||
@@ -9,6 +9,11 @@ export enum Nux {
|
|||||||
ActivitySubscriptions = 'ActivitySubscriptions',
|
ActivitySubscriptions = 'ActivitySubscriptions',
|
||||||
AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice',
|
AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice',
|
||||||
AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner',
|
AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Blocking announcements. New IDs are required for each new announcement.
|
||||||
|
*/
|
||||||
|
PolicyUpdate202508 = 'PolicyUpdate202508',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nuxNames = new Set(Object.values(Nux))
|
export const nuxNames = new Set(Object.values(Nux))
|
||||||
@@ -38,6 +43,10 @@ export type AppNux = BaseNux<
|
|||||||
id: Nux.AgeAssuranceDismissibleFeedBanner
|
id: Nux.AgeAssuranceDismissibleFeedBanner
|
||||||
data: undefined
|
data: undefined
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
id: Nux.PolicyUpdate202508
|
||||||
|
data: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||||
@@ -47,4 +56,5 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
|||||||
[Nux.ActivitySubscriptions]: undefined,
|
[Nux.ActivitySubscriptions]: undefined,
|
||||||
[Nux.AgeAssuranceDismissibleNotice]: undefined,
|
[Nux.AgeAssuranceDismissibleNotice]: undefined,
|
||||||
[Nux.AgeAssuranceDismissibleFeedBanner]: undefined,
|
[Nux.AgeAssuranceDismissibleFeedBanner]: undefined,
|
||||||
|
[Nux.PolicyUpdate202508]: undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export class Storage<Scopes extends unknown[], Schema> {
|
|||||||
keys.forEach(key => this.remove([...scopes, key]))
|
keys.forEach(key => this.remove([...scopes, key]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For debugging purposes
|
||||||
|
*/
|
||||||
|
removeAll() {
|
||||||
|
this.store.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires a callback when the storage associated with a given key changes
|
* Fires a callback when the storage associated with a given key changes
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device data that's specific to the device and does not vary based account
|
* Device data that's specific to the device and does not vary based account
|
||||||
*/
|
*/
|
||||||
@@ -13,6 +15,12 @@ export type Device = {
|
|||||||
devMode: boolean
|
devMode: boolean
|
||||||
demoMode: boolean
|
demoMode: boolean
|
||||||
activitySubscriptionsNudged?: boolean
|
activitySubscriptionsNudged?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Policy update overlays. New IDs are required for each new announcement.
|
||||||
|
*/
|
||||||
|
policyUpdateDebugOverride?: boolean
|
||||||
|
[PolicyUpdate202508]?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
|
|||||||
@@ -209,33 +209,35 @@ let NotificationFeedItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const firstAuthorLink = (
|
const firstAuthorLink = (
|
||||||
<InlineLinkText
|
<ProfileHoverCard did={firstAuthor.profile.did} inline>
|
||||||
key={firstAuthor.href}
|
<InlineLinkText
|
||||||
style={[t.atoms.text, a.font_bold, a.text_md, a.leading_tight]}
|
key={firstAuthor.href}
|
||||||
to={firstAuthor.href}
|
style={[t.atoms.text, a.font_bold, a.text_md, a.leading_tight]}
|
||||||
disableMismatchWarning
|
to={firstAuthor.href}
|
||||||
emoji
|
disableMismatchWarning
|
||||||
label={_(msg`Go to ${firstAuthorName}'s profile`)}>
|
emoji
|
||||||
{forceLTR(firstAuthorName)}
|
label={_(msg`Go to ${firstAuthorName}'s profile`)}>
|
||||||
{firstAuthorVerification.showBadge && (
|
{forceLTR(firstAuthorName)}
|
||||||
<View
|
{firstAuthorVerification.showBadge && (
|
||||||
style={[
|
<View
|
||||||
a.relative,
|
style={[
|
||||||
{
|
a.relative,
|
||||||
paddingTop: platform({android: 2}),
|
{
|
||||||
marginBottom: platform({ios: -7}),
|
paddingTop: platform({android: 2}),
|
||||||
top: platform({web: 1}),
|
marginBottom: platform({ios: -7}),
|
||||||
paddingLeft: 3,
|
top: platform({web: 1}),
|
||||||
paddingRight: 2,
|
paddingLeft: 3,
|
||||||
},
|
paddingRight: 2,
|
||||||
]}>
|
},
|
||||||
<VerificationCheck
|
]}>
|
||||||
width={14}
|
<VerificationCheck
|
||||||
verifier={firstAuthorVerification.role === 'verifier'}
|
width={14}
|
||||||
/>
|
verifier={firstAuthorVerification.role === 'verifier'}
|
||||||
</View>
|
/>
|
||||||
)}
|
</View>
|
||||||
</InlineLinkText>
|
)}
|
||||||
|
</InlineLinkText>
|
||||||
|
</ProfileHoverCard>
|
||||||
)
|
)
|
||||||
const additionalAuthorsCount = authors.length - 1
|
const additionalAuthorsCount = authors.length - 1
|
||||||
const hasMultipleAuthors = additionalAuthorsCount > 0
|
const hasMultipleAuthors = additionalAuthorsCount > 0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Fragment} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
@@ -5,159 +6,82 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
type ButtonColor,
|
type ButtonColor,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
|
type ButtonSize,
|
||||||
ButtonText,
|
ButtonText,
|
||||||
} from '#/components/Button'
|
} from '#/components/Button'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
import {H1} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function Buttons() {
|
export function Buttons() {
|
||||||
return (
|
return (
|
||||||
<View style={[a.gap_md]}>
|
<View style={[a.gap_md]}>
|
||||||
<H1>Buttons</H1>
|
<Text style={[a.font_heavy, a.text_5xl]}>Buttons</Text>
|
||||||
|
|
||||||
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}>
|
{[
|
||||||
{[
|
'primary',
|
||||||
'primary',
|
'secondary',
|
||||||
'secondary',
|
'secondary_inverted',
|
||||||
'secondary_inverted',
|
'negative',
|
||||||
'negative',
|
'primary_subtle',
|
||||||
'negative_secondary',
|
'negative_subtle',
|
||||||
].map(color => (
|
].map(color => (
|
||||||
<View key={color} style={[a.gap_md, a.align_start]}>
|
<Fragment key={color}>
|
||||||
<Button
|
{['tiny', 'small', 'large'].map(size => (
|
||||||
color={color as ButtonColor}
|
<Fragment key={size}>
|
||||||
size="large"
|
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||||
label="Click here">
|
color={color} size={size}
|
||||||
<ButtonText>Button</ButtonText>
|
</Text>
|
||||||
</Button>
|
<View style={[a.flex_row, a.align_start, a.gap_md]}>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
color={color as ButtonColor}
|
||||||
color={color as ButtonColor}
|
size={size as ButtonSize}
|
||||||
size="large"
|
label="Click here">
|
||||||
label="Click here">
|
<ButtonText>Button</ButtonText>
|
||||||
<ButtonText>Button</ButtonText>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</View>
|
disabled
|
||||||
))}
|
color={color as ButtonColor}
|
||||||
</View>
|
size={size as ButtonSize}
|
||||||
|
label="Click here">
|
||||||
<View style={[a.flex_wrap, a.gap_md, a.align_start]}>
|
<ButtonText>Button</ButtonText>
|
||||||
<Button color="primary" size="large" label="Link out">
|
</Button>
|
||||||
<ButtonText>Button</ButtonText>
|
<Button
|
||||||
</Button>
|
color={color as ButtonColor}
|
||||||
<Button color="primary" size="large" label="Link out">
|
size={size as ButtonSize}
|
||||||
<ButtonText>Button</ButtonText>
|
shape="round"
|
||||||
<ButtonIcon icon={Globe} position="right" />
|
label="Click here">
|
||||||
</Button>
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
<Button color="primary" size="small" label="Link out">
|
<Button
|
||||||
<ButtonText>Button</ButtonText>
|
color={color as ButtonColor}
|
||||||
</Button>
|
size={size as ButtonSize}
|
||||||
<Button color="primary" size="small" label="Link out">
|
shape="square"
|
||||||
<ButtonText>Button</ButtonText>
|
label="Click here">
|
||||||
<ButtonIcon icon={Globe} position="right" />
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</View>
|
||||||
<Button color="primary" size="tiny" label="Link out">
|
<View style={[a.flex_row, a.gap_md]}>
|
||||||
<ButtonIcon icon={Globe} position="left" />
|
<Button
|
||||||
<ButtonText>Button</ButtonText>
|
color={color as ButtonColor}
|
||||||
</Button>
|
size={size as ButtonSize}
|
||||||
</View>
|
label="Click here">
|
||||||
|
<ButtonIcon icon={Globe} position="left" />
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
<ButtonText>Button</ButtonText>
|
||||||
<Button color="primary" size="large" label="Link out">
|
</Button>
|
||||||
<ButtonText>Button</ButtonText>
|
<Button
|
||||||
</Button>
|
disabled
|
||||||
<Button color="primary" size="large" label="Link out">
|
color={color as ButtonColor}
|
||||||
<ButtonText>Button</ButtonText>
|
size={size as ButtonSize}
|
||||||
<ButtonIcon icon={Globe} position="right" />
|
label="Click here">
|
||||||
</Button>
|
<ButtonText>Button</ButtonText>
|
||||||
<Button color="primary" size="large" label="Link out">
|
<ButtonIcon icon={Globe} position="right" />
|
||||||
<ButtonText>Button</ButtonText>
|
</Button>
|
||||||
<ButtonIcon icon={Globe} position="right" size="lg" />
|
</View>
|
||||||
</Button>
|
</Fragment>
|
||||||
<Button color="primary" size="large" shape="round" label="Link out">
|
))}
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
</Fragment>
|
||||||
</Button>
|
))}
|
||||||
<Button color="primary" size="large" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} size="lg" />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
|
||||||
<Button color="primary" size="small" label="Link out">
|
|
||||||
<ButtonText>Button</ButtonText>
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" label="Link out">
|
|
||||||
<ButtonText>Button</ButtonText>
|
|
||||||
<ButtonIcon icon={Globe} position="right" />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} size="lg" />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
|
||||||
<Button color="primary" size="tiny" label="Link out">
|
|
||||||
<ButtonText>Button</ButtonText>
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" label="Link out">
|
|
||||||
<ButtonText>Button</ButtonText>
|
|
||||||
<ButtonIcon icon={Globe} position="right" />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} size="md" />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
|
||||||
<Button color="primary" size="large" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="large" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="round" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
|
||||||
<Button color="primary" size="large" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="large" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="small" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" size="tiny" shape="square" label="Link out">
|
|
||||||
<ButtonIcon icon={ChevronLeft} />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ function StorybookInner() {
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Buttons />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -109,7 +110,6 @@ function StorybookInner() {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
||||||
<Forms />
|
<Forms />
|
||||||
<Buttons />
|
|
||||||
<Typography />
|
<Typography />
|
||||||
<Spacing />
|
<Spacing />
|
||||||
<Shadows />
|
<Shadows />
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {Onboarding} from '#/screens/Onboarding'
|
|||||||
import {SignupQueued} from '#/screens/SignupQueued'
|
import {SignupQueued} from '#/screens/SignupQueued'
|
||||||
import {Takendown} from '#/screens/Takendown'
|
import {Takendown} from '#/screens/Takendown'
|
||||||
import {atoms as a, useLayoutBreakpoints} from '#/alf'
|
import {atoms as a, useLayoutBreakpoints} from '#/alf'
|
||||||
|
import {PolicyUpdateOverlay} from '#/components/PolicyUpdateOverlay'
|
||||||
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
|
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
|
||||||
import {DesktopLeftNav} from './desktop/LeftNav'
|
import {DesktopLeftNav} from './desktop/LeftNav'
|
||||||
import {DesktopRightNav} from './desktop/RightNav'
|
import {DesktopRightNav} from './desktop/RightNav'
|
||||||
@@ -167,6 +168,9 @@ function NativeStackNavigator({
|
|||||||
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Only shown after logged in and onboaring etc are complete */}
|
||||||
|
{hasSession && <PolicyUpdateOverlay />}
|
||||||
</NavigationContent>
|
</NavigationContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsen
|
|||||||
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
|
import {
|
||||||
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
|
usePolicyUpdateContext,
|
||||||
|
} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
import {RoutesContainer, TabsNavigator} from '#/Navigation'
|
import {RoutesContainer, TabsNavigator} from '#/Navigation'
|
||||||
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
||||||
@@ -45,6 +49,7 @@ function ShellInner() {
|
|||||||
const setIsDrawerOpen = useSetDrawerOpen()
|
const setIsDrawerOpen = useSetDrawerOpen()
|
||||||
const winDim = useWindowDimensions()
|
const winDim = useWindowDimensions()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
const {state: policyUpdateState} = usePolicyUpdateContext()
|
||||||
|
|
||||||
const renderDrawerContent = useCallback(() => <DrawerContent />, [])
|
const renderDrawerContent = useCallback(() => <DrawerContent />, [])
|
||||||
const onOpenDrawer = useCallback(
|
const onOpenDrawer = useCallback(
|
||||||
@@ -151,6 +156,7 @@ function ShellInner() {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Composer winHeight={winDim.height} />
|
<Composer winHeight={winDim.height} />
|
||||||
<ModalsContainer />
|
<ModalsContainer />
|
||||||
<MutedWordsDialog />
|
<MutedWordsDialog />
|
||||||
@@ -160,8 +166,16 @@ function ShellInner() {
|
|||||||
<InAppBrowserConsentDialog />
|
<InAppBrowserConsentDialog />
|
||||||
<LinkWarningDialog />
|
<LinkWarningDialog />
|
||||||
<Lightbox />
|
<Lightbox />
|
||||||
<PortalOutlet />
|
|
||||||
<BottomSheetOutlet />
|
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
|
||||||
|
{policyUpdateState.completed && (
|
||||||
|
<>
|
||||||
|
<PortalOutlet />
|
||||||
|
<BottomSheetOutlet />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PolicyUpdateOverlayPortalOutlet />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import {EmailDialog} from '#/components/dialogs/EmailDialog'
|
|||||||
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
|
import {
|
||||||
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
|
usePolicyUpdateContext,
|
||||||
|
} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
import {FlatNavigator, RoutesContainer} from '#/Navigation'
|
import {FlatNavigator, RoutesContainer} from '#/Navigation'
|
||||||
import {Composer} from './Composer.web'
|
import {Composer} from './Composer.web'
|
||||||
@@ -37,6 +41,7 @@ function ShellInner() {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const showDrawer = !isDesktop && isDrawerOpen
|
const showDrawer = !isDesktop && isDrawerOpen
|
||||||
const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
|
const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
|
||||||
|
const {state: policyUpdateState} = usePolicyUpdateContext()
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (showDrawer !== showDrawerDelayedExit) {
|
if (showDrawer !== showDrawerDelayedExit) {
|
||||||
@@ -74,7 +79,13 @@ function ShellInner() {
|
|||||||
<AgeAssuranceRedirectDialog />
|
<AgeAssuranceRedirectDialog />
|
||||||
<LinkWarningDialog />
|
<LinkWarningDialog />
|
||||||
<Lightbox />
|
<Lightbox />
|
||||||
<PortalOutlet />
|
|
||||||
|
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
|
||||||
|
{policyUpdateState.completed && (
|
||||||
|
<>
|
||||||
|
<PortalOutlet />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{showDrawerDelayedExit && (
|
{showDrawerDelayedExit && (
|
||||||
<>
|
<>
|
||||||
@@ -113,6 +124,8 @@ function ShellInner() {
|
|||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PolicyUpdateOverlayPortalOutlet />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11302,10 +11302,10 @@ expo-image-picker@~16.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
expo-image-loader "~5.1.0"
|
expo-image-loader "~5.1.0"
|
||||||
|
|
||||||
expo-image@~2.2.1:
|
expo-image@^2.4.0:
|
||||||
version "2.2.1"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.2.1.tgz#b4aa706a25f7e8902ac854a8da249caf4a90cd67"
|
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
|
||||||
integrity sha512-5ZSggMi0X2G9AN0aM+sdkCyyZ6YcWvGs9KYLYrRBVUN3ph6RBiu6mKGpaNN1TAscySRnH1eHbUE1H+Qeq7qm1g==
|
integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
|
||||||
|
|
||||||
expo-json-utils@~0.15.0:
|
expo-json-utils@~0.15.0:
|
||||||
version "0.15.0"
|
version "0.15.0"
|
||||||
@@ -16801,6 +16801,11 @@ react-native-date-picker@^5.0.12:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-5.0.12.tgz#12540b6a58500811ee7e4fc0244e3accc7cca9c1"
|
resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-5.0.12.tgz#12540b6a58500811ee7e4fc0244e3accc7cca9c1"
|
||||||
integrity sha512-R/mUnCKhcuxbhKPFwYdBQCxQt9HHLqpM4ruRUqlcBjiUZ3N2wdnwOMyc888Ps8qp8e7v29PrDHtUlG8LPuFn9w==
|
integrity sha512-R/mUnCKhcuxbhKPFwYdBQCxQt9HHLqpM4ruRUqlcBjiUZ3N2wdnwOMyc888Ps8qp8e7v29PrDHtUlG8LPuFn9w==
|
||||||
|
|
||||||
|
react-native-device-attest@^0.1.6:
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-device-attest/-/react-native-device-attest-0.1.6.tgz#51796a92d9199b1d231d4aa62d557019753b30c3"
|
||||||
|
integrity sha512-oTgBu6il+czHIMLs2IVWv2+WZ6a/vUtVLQ40q6/Dgns7NuG69mwmR8lS0e2Sl/yOtpD9YZXc8cYzBDKHAyV5MA==
|
||||||
|
|
||||||
react-native-dotenv@^3.4.11:
|
react-native-dotenv@^3.4.11:
|
||||||
version "3.4.11"
|
version "3.4.11"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz#2e6c4eabd55d5f1bf109b3dd9141dadf9c55cdd4"
|
resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz#2e6c4eabd55d5f1bf109b3dd9141dadf9c55cdd4"
|
||||||
|
|||||||
Reference in New Issue
Block a user