diff --git a/bskylink/src/db/migrations/003-safelink-cursor-constraint.ts b/bskylink/src/db/migrations/003-safelink-cursor-constraint.ts new file mode 100644 index 000000000..1607eabeb --- /dev/null +++ b/bskylink/src/db/migrations/003-safelink-cursor-constraint.ts @@ -0,0 +1,31 @@ +import {type Kysely} from 'kysely' + +export async function up( + db: Kysely<{safelink_rule: {}; safelink_cursor: {}}>, +): Promise { + // Remove existing items from safelink_rule that were duplicated due to broken cursor + await db.deleteFrom(['safelink_rule']).execute() + + // Delete the old cursor + await db.deleteFrom(['safelink_cursor']).execute() + + await db.schema + .alterTable('safelink_cursor') + .addPrimaryKeyConstraint('pk_id', ['id']) + .execute() +} + +export async function down( + db: Kysely<{safelink_rule: {}; safelink_cursor: {}}>, +): Promise { + // Remove any rules that were added + await db.deleteFrom(['safelink_rule']).execute() + + // Delete the cursor + await db.deleteFrom(['safelink_cursor']).execute() + + await db.schema + .alterTable('safelink_cursor') + .dropConstraint('pk_id') + .execute() +} diff --git a/bskylink/src/db/migrations/index.ts b/bskylink/src/db/migrations/index.ts index 1f7385ab1..7a34b1001 100644 --- a/bskylink/src/db/migrations/index.ts +++ b/bskylink/src/db/migrations/index.ts @@ -1,7 +1,9 @@ import * as init from './001-init.js' import * as safelink from './002-safelink.js' +import * as safelinkCursorConstraint from './003-safelink-cursor-constraint.js' export default { '001': init, '002': safelink, + '003': safelinkCursorConstraint, } -- 2.51.2 From 07d4ff4e8cd506b99bd9873900523fda8b687117 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 4 Sep 2025 02:34:11 +0000 Subject: [PATCH 02/12] Nightly source-language update --- src/locale/locales/en/messages.po | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index b16eb563a..9fc487e51 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -944,7 +944,7 @@ msgstr "" msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." msgstr "" -#: src/components/dialogs/GifSelect.tsx:264 +#: src/components/dialogs/GifSelect.tsx:253 #: src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx:362 msgid "An error has occurred" msgstr "" @@ -1890,7 +1890,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:184 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:237 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:243 -#: src/components/dialogs/GifSelect.tsx:280 +#: src/components/dialogs/GifSelect.tsx:269 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:158 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:167 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 @@ -1935,7 +1935,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:224 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:230 -#: src/components/dialogs/GifSelect.tsx:274 +#: src/components/dialogs/GifSelect.tsx:263 #: src/components/verification/VerificationsDialog.tsx:136 #: src/components/verification/VerifierDialog.tsx:141 #: src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx:246 @@ -1953,7 +1953,7 @@ msgstr "" msgid "Close emoji picker" msgstr "" -#: src/components/dialogs/GifSelect.tsx:170 +#: src/components/dialogs/GifSelect.tsx:159 msgid "Close GIF dialog" msgstr "" @@ -3436,7 +3436,7 @@ msgstr "" msgid "Failed to load feeds preferences" msgstr "" -#: src/components/dialogs/GifSelect.tsx:224 +#: src/components/dialogs/GifSelect.tsx:213 msgid "Failed to load GIFs" msgstr "" @@ -5543,7 +5543,7 @@ msgstr "" msgid "No expiry set" msgstr "" -#: src/components/dialogs/GifSelect.tsx:230 +#: src/components/dialogs/GifSelect.tsx:219 msgid "No featured GIFs found. There may be an issue with Tenor." msgstr "" @@ -5637,7 +5637,7 @@ msgstr "" msgid "No results." msgstr "" -#: src/components/dialogs/GifSelect.tsx:228 +#: src/components/dialogs/GifSelect.tsx:217 msgid "No search results found for \"{search}\"." msgstr "" @@ -5764,7 +5764,7 @@ msgstr "" msgid "Off" msgstr "" -#: src/components/dialogs/GifSelect.tsx:267 +#: src/components/dialogs/GifSelect.tsx:256 #: src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx:365 #: src/view/com/util/ErrorBoundary.tsx:57 msgid "Oh no!" @@ -7445,7 +7445,7 @@ msgstr "" msgid "Search for posts, users, or feeds" msgstr "" -#: src/components/dialogs/GifSelect.tsx:178 +#: src/components/dialogs/GifSelect.tsx:167 msgid "Search GIFs" msgstr "" @@ -7472,7 +7472,7 @@ msgstr "" msgid "Search profiles" msgstr "" -#: src/components/dialogs/GifSelect.tsx:179 +#: src/components/dialogs/GifSelect.tsx:168 msgid "Search Tenor" msgstr "" @@ -7580,7 +7580,7 @@ msgstr "" msgid "Select GIF" msgstr "" -#: src/components/dialogs/GifSelect.tsx:305 +#: src/components/dialogs/GifSelect.tsx:294 msgid "Select GIF \"{0}\"" msgstr "" @@ -8663,7 +8663,7 @@ msgstr "" msgid "There is no time limit for account deactivation, come back any time." msgstr "" -#: src/components/dialogs/GifSelect.tsx:225 +#: src/components/dialogs/GifSelect.tsx:214 msgid "There was an issue connecting to Tenor." msgstr "" @@ -8748,7 +8748,7 @@ msgstr "" msgid "There was an issue. Please check your internet connection and try again." msgstr "" -#: src/components/dialogs/GifSelect.tsx:269 +#: src/components/dialogs/GifSelect.tsx:258 #: src/view/com/composer/select-language/SelectPostLanguagesDialog.tsx:367 #: src/view/com/util/ErrorBoundary.tsx:59 msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" -- 2.51.2 From 0b02d9d9a7ca33dab256a35e4fc9b8feabe20d34 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 4 Sep 2025 17:16:52 +0300 Subject: [PATCH 03/12] bump version (#8970) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aa41e942..323fdce78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.107.0", + "version": "1.108.0", "private": true, "engines": { "node": ">=20" -- 2.51.2 From 625b4e61dbf11c1d485bf8e8265df4d5af0c9657 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Thu, 4 Sep 2025 07:20:46 -0700 Subject: [PATCH 04/12] Welcome modal on logged-out homepage (#8944) * Adds welcome modal to logged-out homepage * Adds metrics and feature gate for welcome modal * Slightly smaller text for mobile screens to avoid wrapping * Remove unused SVG * Adds text gradient and "X" close button * Fix color on "Already have an account?" text * tweak hooks, react import * rm stylesheet * use hardcoded colors * add focus guards and scope * no such thing as /home * reduce spacign * use css animations * use session storage * fix animation fill mode * add a11y props * Fix link/button color mismatch, reduce gap between buttons, show modal until user dismisses it * Fix "Already have an account?" line left-aligning in small window sizes * Adds "dismissed" and "presented" metric events --------- Co-authored-by: Samuel Newman --- assets/images/welcome-modal-bg.jpg | Bin 0 -> 116471 bytes src/alf/atoms.ts | 1 + src/components/WelcomeModal.tsx | 247 ++++++++++++++++++ .../hooks/useWelcomeModal.native.ts | 3 + src/components/hooks/useWelcomeModal.ts | 43 +++ src/lib/hooks/useWebMediaQueries.tsx | 3 + src/lib/statsig/gates.ts | 1 + src/logger/metrics.ts | 5 + src/view/shell/index.web.tsx | 10 + 9 files changed, 313 insertions(+) create mode 100644 assets/images/welcome-modal-bg.jpg create mode 100644 src/components/WelcomeModal.tsx create mode 100644 src/components/hooks/useWelcomeModal.native.ts create mode 100644 src/components/hooks/useWelcomeModal.ts diff --git a/assets/images/welcome-modal-bg.jpg b/assets/images/welcome-modal-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..99b6c9d6469bdc899ac134da5d1d6121d5db6140 GIT binary patch literal 116471 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�C@cSS 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 H{{Kw?Ggs7p literal 0 HcmV?d00001 diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index ae88f457d..dc5d9f59c 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -1063,6 +1063,7 @@ export const atoms = { }), fade_out: web({ animation: 'fadeOut ease-out 0.15s', + animationFillMode: 'forwards', }), zoom_in: web({ animation: 'zoomIn ease-out 0.1s', diff --git a/src/components/WelcomeModal.tsx b/src/components/WelcomeModal.tsx new file mode 100644 index 000000000..7c9ecd84d --- /dev/null +++ b/src/components/WelcomeModal.tsx @@ -0,0 +1,247 @@ +import {useEffect, useState} from 'react' +import {Pressable, View} from 'react-native' +import {ImageBackground} from 'expo-image' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {FocusGuards, FocusScope} from 'radix-ui/internal' + +import {logger} from '#/logger' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {Logo} from '#/view/icons/Logo' +import {atoms as a, flatten, useBreakpoints, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' +import {Text} from '#/components/Typography' + +const welcomeModalBg = require('../../assets/images/welcome-modal-bg.jpg') + +interface WelcomeModalProps { + control: { + isOpen: boolean + open: () => void + close: () => void + } +} + +export function WelcomeModal({control}: WelcomeModalProps) { + const {_} = useLingui() + const {requestSwitchToAccount} = useLoggedOutViewControls() + const {gtMobile} = useBreakpoints() + const [isExiting, setIsExiting] = useState(false) + const [signInLinkHovered, setSignInLinkHovered] = useState(false) + + const fadeOutAndClose = (callback?: () => void) => { + setIsExiting(true) + setTimeout(() => { + control.close() + if (callback) callback() + }, 150) + } + + useEffect(() => { + if (control.isOpen) { + logger.metric('welcomeModal:presented', {}) + } + }, [control.isOpen]) + + const onPressCreateAccount = () => { + logger.metric('welcomeModal:signupClicked', {}) + control.close() + requestSwitchToAccount({requestedAccount: 'new'}) + } + + const onPressExplore = () => { + logger.metric('welcomeModal:exploreClicked', {}) + fadeOutAndClose() + } + + const onPressSignIn = () => { + logger.metric('welcomeModal:signinClicked', {}) + control.close() + requestSwitchToAccount({requestedAccount: 'existing'}) + } + + FocusGuards.useFocusGuards() + + return ( + + + + + + + + + + Bluesky + + + + + + Real people. + {'\n'} + Real conversations. + {'\n'} + Social media you control. + + + + + + + + + + Already have an account?{' '} + setSignInLinkHovered(true)} + onPointerLeave={() => setSignInLinkHovered(false)} + accessibilityRole="button" + accessibilityLabel={_(msg`Sign in`)} + accessibilityHint=""> + + Sign in + + + + + + + + + + + + ) +} diff --git a/src/components/hooks/useWelcomeModal.native.ts b/src/components/hooks/useWelcomeModal.native.ts new file mode 100644 index 000000000..f5bc1aa4e --- /dev/null +++ b/src/components/hooks/useWelcomeModal.native.ts @@ -0,0 +1,3 @@ +export function useWelcomeModal() { + throw new Error('useWelcomeModal is web only') +} diff --git a/src/components/hooks/useWelcomeModal.ts b/src/components/hooks/useWelcomeModal.ts new file mode 100644 index 000000000..7183f361e --- /dev/null +++ b/src/components/hooks/useWelcomeModal.ts @@ -0,0 +1,43 @@ +import {useEffect, useState} from 'react' + +import {isWeb} from '#/platform/detection' +import {useSession} from '#/state/session' + +export function useWelcomeModal() { + const {hasSession} = useSession() + const [isOpen, setIsOpen] = useState(false) + + const open = () => setIsOpen(true) + const close = () => { + setIsOpen(false) + // Mark that user has actively closed the modal, don't show again this session + if (typeof window !== 'undefined') { + sessionStorage.setItem('welcomeModalClosed', 'true') + } + } + + useEffect(() => { + // Only show modal if: + // 1. User is not logged in + // 2. We're on the web (this is a web-only feature) + // 3. We're on the homepage (path is '/' or '/home') + // 4. User hasn't actively closed the modal in this session + if (isWeb && !hasSession && typeof window !== 'undefined') { + const currentPath = window.location.pathname + const isHomePage = currentPath === '/' + const hasUserClosedModal = + sessionStorage.getItem('welcomeModalClosed') === 'true' + + if (isHomePage && !hasUserClosedModal) { + // Small delay to ensure the page has loaded + const timer = setTimeout(() => { + open() + }, 1000) + + return () => clearTimeout(timer) + } + } + }, [hasSession]) + + return {isOpen, open, close} +} diff --git a/src/lib/hooks/useWebMediaQueries.tsx b/src/lib/hooks/useWebMediaQueries.tsx index fa9d6ffa6..7778383a4 100644 --- a/src/lib/hooks/useWebMediaQueries.tsx +++ b/src/lib/hooks/useWebMediaQueries.tsx @@ -2,6 +2,9 @@ import {useMediaQuery} from 'react-responsive' import {isNative} from '#/platform/detection' +/** + * @deprecated use `useBreakpoints` from `#/alf` instead + */ export function useWebMediaQueries() { const isDesktop = useMediaQuery({minWidth: 1300}) const isTablet = useMediaQuery({minWidth: 800, maxWidth: 1300 - 1}) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 391314162..b8e2e9a3c 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -15,3 +15,4 @@ export type Gate = | 'remove_show_latest_button' | 'test_gate_1' | 'test_gate_2' + | 'welcome_modal' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 1cb4eb9d3..79d7702b3 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -48,6 +48,11 @@ export type MetricEvents = { // Screen events 'splash:signInPressed': {} 'splash:createAccountPressed': {} + 'welcomeModal:signupClicked': {} + 'welcomeModal:exploreClicked': {} + 'welcomeModal:signinClicked': {} + 'welcomeModal:dismissed': {} + 'welcomeModal:presented': {} 'signup:nextPressed': { activeStep: number phoneVerificationRequired?: boolean diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index f942ab49e..bb09b5f62 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -8,6 +8,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import {useGeolocation} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' @@ -22,11 +23,13 @@ import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' +import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' import { Outlet as PolicyUpdateOverlayPortalOutlet, usePolicyUpdateContext, } from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' +import {WelcomeModal} from '#/components/WelcomeModal' import {FlatNavigator, RoutesContainer} from '#/Navigation' import {Composer} from './Composer.web' import {DrawerContent} from './Drawer' @@ -42,6 +45,8 @@ function ShellInner() { const showDrawer = !isDesktop && isDrawerOpen const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer) const {state: policyUpdateState} = usePolicyUpdateContext() + const welcomeModalControl = useWelcomeModal() + const gate = useGate() useLayoutEffect(() => { if (showDrawer !== showDrawerDelayedExit) { @@ -80,6 +85,11 @@ function ShellInner() { + {/* Show welcome modal if the gate is enabled */} + {welcomeModalControl.isOpen && gate('welcome_modal') && ( + + )} + {/* Until policy update has been completed by the user, don't render anything that is portaled */} {policyUpdateState.completed && ( <> -- 2.51.2 From f8ae0540a062e6346baf9fbf0481f769fb23a120 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Sep 2025 11:07:12 -0500 Subject: [PATCH 05/12] Provide geo-gated users optional GPS fallback for precise location data (#8973) --- .env.example | 6 + app.config.js | 1 + ...LocationFilled_stroke2_corner0_rounded.svg | 1 + .../pinLocation_stroke2_corner0_rounded.svg | 1 + package.json | 1 + src/App.native.tsx | 13 +- src/App.web.tsx | 13 +- src/components/BlockedGeoOverlay.tsx | 175 ++++++++++---- .../ageAssurance/AgeAssuranceAccountCard.tsx | 50 +++- .../dialogs/DeviceLocationRequestDialog.tsx | 171 +++++++++++++ src/components/icons/PinLocation.tsx | 9 + src/env/common.ts | 13 + src/lib/currency.ts | 4 +- src/logger/types.ts | 1 + src/state/ageAssurance/index.tsx | 4 +- src/state/ageAssurance/useInitAgeAssurance.ts | 4 +- .../ageAssurance/useIsAgeAssuranceEnabled.ts | 4 +- src/state/geolocation.tsx | 226 ------------------ src/state/geolocation/config.ts | 143 +++++++++++ src/state/geolocation/const.ts | 30 +++ src/state/geolocation/events.ts | 19 ++ src/state/geolocation/index.tsx | 153 ++++++++++++ src/state/geolocation/logger.ts | 3 + src/state/geolocation/types.ts | 9 + .../geolocation/useRequestDeviceLocation.ts | 43 ++++ .../geolocation/useSyncedDeviceGeolocation.ts | 58 +++++ src/state/geolocation/util.ts | 180 ++++++++++++++ src/storage/schema.ts | 25 +- src/view/shell/index.tsx | 4 +- src/view/shell/index.web.tsx | 4 +- yarn.lock | 5 + 31 files changed, 1075 insertions(+), 298 deletions(-) create mode 100644 assets/icons/pinLocationFilled_stroke2_corner0_rounded.svg create mode 100644 assets/icons/pinLocation_stroke2_corner0_rounded.svg create mode 100644 src/components/dialogs/DeviceLocationRequestDialog.tsx create mode 100644 src/components/icons/PinLocation.tsx delete mode 100644 src/state/geolocation.tsx create mode 100644 src/state/geolocation/config.ts create mode 100644 src/state/geolocation/const.ts create mode 100644 src/state/geolocation/events.ts create mode 100644 src/state/geolocation/index.tsx create mode 100644 src/state/geolocation/logger.ts create mode 100644 src/state/geolocation/types.ts create mode 100644 src/state/geolocation/useRequestDeviceLocation.ts create mode 100644 src/state/geolocation/useSyncedDeviceGeolocation.ts create mode 100644 src/state/geolocation/util.ts diff --git a/.env.example b/.env.example index 4aefa3320..ac8dcab1f 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,9 @@ EXPO_PUBLIC_SENTRY_DSN= # Bitdrift API key. If undefined, Bitdrift will be disabled. EXPO_PUBLIC_BITDRIFT_API_KEY= + +# bapp-config web worker URL +BAPP_CONFIG_DEV_URL= + +# Dev-only passthrough value for bapp-config web worker +BAPP_CONFIG_DEV_BYPASS_SECRET= diff --git a/app.config.js b/app.config.js index 8a0e6e48c..0b469a986 100644 --- a/app.config.js +++ b/app.config.js @@ -360,6 +360,7 @@ module.exports = function (_config) { }, ], ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}], + ['expo-location'], ].filter(Boolean), extra: { eas: { diff --git a/assets/icons/pinLocationFilled_stroke2_corner0_rounded.svg b/assets/icons/pinLocationFilled_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..becc18b12 --- /dev/null +++ b/assets/icons/pinLocationFilled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pinLocation_stroke2_corner0_rounded.svg b/assets/icons/pinLocation_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..11674c912 --- /dev/null +++ b/assets/icons/pinLocation_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index 323fdce78..9e9c6d7c5 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.5", "expo-localization": "~16.1.5", + "expo-location": "~18.1.6", "expo-media-library": "~17.1.7", "expo-notifications": "~0.31.3", "expo-screen-orientation": "~8.1.7", diff --git a/src/App.native.tsx b/src/App.native.tsx index e22ab3f0e..036ecff60 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -32,8 +32,8 @@ import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' import {listenSessionDropped} from '#/state/events' import { - beginResolveGeolocation, - ensureGeolocationResolved, + beginResolveGeolocationConfig, + ensureGeolocationConfigIsResolved, Provider as GeolocationProvider, } from '#/state/geolocation' import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' @@ -91,7 +91,7 @@ if (isAndroid) { /** * Begin geolocation ASAP */ -beginResolveGeolocation() +beginResolveGeolocationConfig() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -203,9 +203,10 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() => - setReady(true), - ) + Promise.all([ + initPersistedState(), + ensureGeolocationConfigIsResolved(), + ]).then(() => setReady(true)) }, []) if (!isReady) { diff --git a/src/App.web.tsx b/src/App.web.tsx index 17434bfcd..c86960172 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -21,8 +21,8 @@ import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' import {listenSessionDropped} from '#/state/events' import { - beginResolveGeolocation, - ensureGeolocationResolved, + beginResolveGeolocationConfig, + ensureGeolocationConfigIsResolved, Provider as GeolocationProvider, } from '#/state/geolocation' import {Provider as HomeBadgeProvider} from '#/state/home-badge' @@ -69,7 +69,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom /** * Begin geolocation ASAP */ -beginResolveGeolocation() +beginResolveGeolocationConfig() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -178,9 +178,10 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() => - setReady(true), - ) + Promise.all([ + initPersistedState(), + ensureGeolocationConfigIsResolved(), + ]).then(() => setReady(true)) }, []) if (!isReady) { diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx index ae5790da9..df8ed63d4 100644 --- a/src/components/BlockedGeoOverlay.tsx +++ b/src/components/BlockedGeoOverlay.tsx @@ -6,16 +6,27 @@ import {useLingui} from '@lingui/react' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' +import {useDeviceGeolocationApi} from '#/state/geolocation' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' +import {Divider} from '#/components/Divider' import {Full as Logo, Mark} from '#/components/icons/Logo' +import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation' import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' +import {Outlet as PortalOutlet} from '#/components/Portal' +import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {BottomSheetOutlet} from '#/../modules/bottom-sheet' export function BlockedGeoOverlay() { const t = useTheme() const {_} = useLingui() const {gtPhone} = useBreakpoints() const insets = useSafeAreaInsets() + const geoDialog = Dialog.useDialogControl() + const {setDeviceGeolocation} = useDeviceGeolocationApi() useEffect(() => { // just counting overall hits here @@ -51,59 +62,133 @@ export function BlockedGeoOverlay() { ] return ( - - + - - - - + + - Announcement - + + + Announcement + + + + + + {blocks.map((block, index) => ( + + {block} + + ))} - - - {blocks.map((block, index) => ( - - {block} - - ))} + {!isWeb && ( + <> + + + + + + + Not in Mississippi? + + + + Confirm your location with GPS. Your location data is not + tracked and does not leave your device. + + + + + + { + if (props.geolocationStatus.isAgeBlockedGeo) { + props.disableDialogAction() + props.setDialogError( + _( + msg`We're sorry, but based on your device's location, you are currently located in a region we cannot provide access at this time.`, + ), + ) + } else { + props.closeDialog(() => { + // set this after close! + setDeviceGeolocation({ + countryCode: props.geolocationStatus.countryCode, + regionCode: props.geolocationStatus.regionCode, + }) + Toast.show(_(msg`Thanks! You're all set.`), { + type: 'success', + }) + }) + } + }} + /> + + )} + + + + + - - - + {/* + * While this blocking overlay is up, other dialogs in the shell + * are not mounted, so it _should_ be safe to use these here + * without fear of other modals showing up. + */} + + + ) } diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx index a00a8c71a..be9935d9f 100644 --- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx @@ -3,8 +3,10 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' +import {isNative} from '#/platform/detection' import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance' import {logger} from '#/state/ageAssurance/util' +import {useDeviceGeolocationApi} from '#/state/geolocation' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' import {Admonition} from '#/components/Admonition' import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' @@ -16,8 +18,10 @@ import { import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' import {Divider} from '#/components/Divider' import {createStaticClick, InlineLinkText} from '#/components/Link' +import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { @@ -35,8 +39,10 @@ function Inner({style}: ViewStyleProp & {}) { const {_, i18n} = useLingui() const control = useDialogControl() const appealControl = Dialog.useDialogControl() + const locationControl = Dialog.useDialogControl() const getTimeAgo = useGetTimeAgo() const {gtPhone} = useBreakpoints() + const {setDeviceGeolocation} = useDeviceGeolocationApi() const copy = useAgeAssuranceCopy() const {status, lastInitiatedAt} = useAgeAssurance() @@ -71,8 +77,50 @@ function Inner({style}: ViewStyleProp & {}) { - + {copy.notice} + + {isNative && ( + <> + + + Is your location not accurate?{' '} + { + locationControl.open() + })}> + Click here to confirm your location. + {' '} + + + + { + if (props.geolocationStatus.isAgeRestrictedGeo) { + props.disableDialogAction() + props.setDialogError( + _( + msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`, + ), + ) + } else { + props.closeDialog(() => { + // set this after close! + setDeviceGeolocation({ + countryCode: props.geolocationStatus.countryCode, + regionCode: props.geolocationStatus.regionCode, + }) + Toast.show(_(msg`Thanks! You're all set.`), { + type: 'success', + }) + }) + } + }} + /> + + )} {isBlocked ? ( diff --git a/src/components/dialogs/DeviceLocationRequestDialog.tsx b/src/components/dialogs/DeviceLocationRequestDialog.tsx new file mode 100644 index 000000000..6c82dc0e9 --- /dev/null +++ b/src/components/dialogs/DeviceLocationRequestDialog.tsx @@ -0,0 +1,171 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {wait} from '#/lib/async/wait' +import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError' +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import { + computeGeolocationStatus, + type GeolocationStatus, + useGeolocationConfig, +} from '#/state/geolocation' +import {useRequestDeviceLocation} from '#/state/geolocation/useRequestDeviceLocation' +import {atoms as a, useTheme, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export type Props = { + onLocationAcquired?: (props: { + geolocationStatus: GeolocationStatus + setDialogError: (error: string) => void + disableDialogAction: () => void + closeDialog: (callback?: () => void) => void + }) => void +} + +export function DeviceLocationRequestDialog({ + control, + onLocationAcquired, +}: Props & { + control: Dialog.DialogOuterProps['control'] +}) { + const {_} = useLingui() + return ( + + + + + + + + + ) +} + +function DeviceLocationRequestDialogInner({onLocationAcquired}: Props) { + const t = useTheme() + const {_} = useLingui() + const {close} = Dialog.useDialogContext() + const requestDeviceLocation = useRequestDeviceLocation() + const {config} = useGeolocationConfig() + const cleanError = useCleanError() + + const [isRequesting, setIsRequesting] = useState(false) + const [error, setError] = useState('') + const [dialogDisabled, setDialogDisabled] = useState(false) + + const onPressConfirm = async () => { + setError('') + setIsRequesting(true) + + try { + const req = await wait(1e3, requestDeviceLocation()) + + if (req.granted) { + const location = req.location + + if (location && location.countryCode) { + const geolocationStatus = computeGeolocationStatus(location, config) + onLocationAcquired?.({ + geolocationStatus, + setDialogError: setError, + disableDialogAction: () => setDialogDisabled(true), + closeDialog: close, + }) + } else { + setError(_(msg`Failed to resolve location. Please try again.`)) + } + } else { + setError( + _( + msg`Unable to access location. You'll need to visit your system settings to enable location services for Bluesky`, + ), + ) + } + } catch (e: any) { + const {clean, raw} = cleanError(e) + setError(clean || raw || e.message) + if (!isNetworkError(e)) { + logger.error(`blockedGeoOverlay: unexpected error`, { + safeMessage: e.message, + }) + } + } finally { + setIsRequesting(false) + } + } + + return ( + + + Confirm your location + + + + + Click below to allow Bluesky to access your GPS location. We will + then use that data to more accurately determine the content and + features available in your region. + + + + + + Your location data is not tracked and does not leave your device. + + + + + {error && ( + + {error} + + )} + + + {!dialogDisabled && ( + + )} + + {!isWeb && ( + + )} + + + ) +} diff --git a/src/components/icons/PinLocation.tsx b/src/components/icons/PinLocation.tsx new file mode 100644 index 000000000..9673995d7 --- /dev/null +++ b/src/components/icons/PinLocation.tsx @@ -0,0 +1,9 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const PinLocation_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a8 8 0 0 1 8 8c0 3.305-1.953 6.29-3.745 8.355a25.964 25.964 0 0 1-3.333 3.197c-.101.08-.181.142-.237.184l-.067.05-.018.014-.005.004-.002.002h-.001c-.003-.004-.042-.055-.592-.806l.592.807a1.001 1.001 0 0 1-1.184 0v-.001l-.003-.002-.005-.004-.018-.014-.067-.05a23.449 23.449 0 0 1-1.066-.877 25.973 25.973 0 0 1-2.504-2.503C5.953 16.29 4 13.305 4 10a8 8 0 0 1 8-8Zm0 2a6 6 0 0 0-6 6c0 2.56 1.547 5.076 3.255 7.044A23.978 23.978 0 0 0 12 19.723a23.976 23.976 0 0 0 2.745-2.679C16.453 15.076 18 12.56 18 10a6 6 0 0 0-6-6Zm-.002 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z', +}) + +export const PinLocationFilled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.591 21.806h.002l.001-.002.006-.004.018-.014a10.028 10.028 0 0 0 .304-.235 25.952 25.952 0 0 0 3.333-3.196C18.048 16.29 20 13.305 20 10a8 8 0 1 0-16 0c0 3.305 1.952 6.29 3.745 8.355a25.955 25.955 0 0 0 3.333 3.196 15.733 15.733 0 0 0 .304.235l.018.014.006.004.002.002a1 1 0 0 0 1.183 0Zm-.593-9.306a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z', +}) diff --git a/src/env/common.ts b/src/env/common.ts index 7b64c35a6..210ad037c 100644 --- a/src/env/common.ts +++ b/src/env/common.ts @@ -93,3 +93,16 @@ export const GCP_PROJECT_ID: number = process.env.EXPO_PUBLIC_GCP_PROJECT_ID === undefined ? 0 : Number(process.env.EXPO_PUBLIC_GCP_PROJECT_ID) + +/** + * URL for the bapp-config web worker _development_ environment. Can be a + * locally running server, see `env.example` for more. + */ +export const BAPP_CONFIG_DEV_URL = process.env.BAPP_CONFIG_DEV_URL + +/** + * Dev environment passthrough value for bapp-config web worker. Allows local + * dev access to the web worker running in `development` mode. + */ +export const BAPP_CONFIG_DEV_BYPASS_SECRET: string = + process.env.BAPP_CONFIG_DEV_BYPASS_SECRET diff --git a/src/lib/currency.ts b/src/lib/currency.ts index c43078a24..cc5a9a7b0 100644 --- a/src/lib/currency.ts +++ b/src/lib/currency.ts @@ -1,7 +1,7 @@ import React from 'react' import {deviceLocales} from '#/locale/deviceLocales' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' import {useLanguagePrefs} from '#/state/preferences' /** @@ -275,7 +275,7 @@ export const countryCodeToCurrency: Record = { export function useFormatCurrency( options?: Parameters[1], ) { - const {geolocation} = useGeolocation() + const {location: geolocation} = useGeolocationStatus() const {appLanguage} = useLanguagePrefs() return React.useMemo(() => { const locale = deviceLocales.at(0) diff --git a/src/logger/types.ts b/src/logger/types.ts index ee3069a08..19e12c504 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -14,6 +14,7 @@ export enum LogContext { PostSource = 'post-source', AgeAssurance = 'age-assurance', PolicyUpdate = 'policy-update', + Geolocation = 'geolocation', /** * METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this diff --git a/src/state/ageAssurance/index.tsx b/src/state/ageAssurance/index.tsx index 23133046a..e85672b7c 100644 --- a/src/state/ageAssurance/index.tsx +++ b/src/state/ageAssurance/index.tsx @@ -11,7 +11,7 @@ import { } from '#/state/ageAssurance/types' import {useIsAgeAssuranceEnabled} from '#/state/ageAssurance/useIsAgeAssuranceEnabled' import {logger} from '#/state/ageAssurance/util' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' import {useAgent} from '#/state/session' export const createAgeAssuranceQueryKey = (did: string) => @@ -43,7 +43,7 @@ AgeAssuranceAPIContext.displayName = 'AgeAssuranceAPIContext' */ export function Provider({children}: {children: React.ReactNode}) { const agent = useAgent() - const {geolocation} = useGeolocation() + const {status: geolocation} = useGeolocationStatus() const isAgeAssuranceEnabled = useIsAgeAssuranceEnabled() const getAndRegisterPushToken = useGetAndRegisterPushToken() const [refetchWhilePending, setRefetchWhilePending] = useState(false) diff --git a/src/state/ageAssurance/useInitAgeAssurance.ts b/src/state/ageAssurance/useInitAgeAssurance.ts index 8776dd29c..c8aaf70a3 100644 --- a/src/state/ageAssurance/useInitAgeAssurance.ts +++ b/src/state/ageAssurance/useInitAgeAssurance.ts @@ -14,7 +14,7 @@ import { import {isNetworkError} from '#/lib/hooks/useCleanError' import {logger} from '#/logger' import {createAgeAssuranceQueryKey} from '#/state/ageAssurance' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' import {useAgent} from '#/state/session' let APPVIEW = PUBLIC_APPVIEW @@ -36,7 +36,7 @@ let APPVIEW_DID = PUBLIC_APPVIEW_DID export function useInitAgeAssurance() { const qc = useQueryClient() const agent = useAgent() - const {geolocation} = useGeolocation() + const {status: geolocation} = useGeolocationStatus() return useMutation({ async mutationFn( props: Omit, diff --git a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts index b020e3c57..6e85edd0b 100644 --- a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts +++ b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts @@ -1,9 +1,9 @@ import {useMemo} from 'react' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' export function useIsAgeAssuranceEnabled() { - const {geolocation} = useGeolocation() + const {status: geolocation} = useGeolocationStatus() return useMemo(() => { return !!geolocation?.isAgeRestrictedGeo diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx deleted file mode 100644 index c4d8cb946..000000000 --- a/src/state/geolocation.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react' -import EventEmitter from 'eventemitter3' - -import {networkRetry} from '#/lib/async/retry' -import {logger} from '#/logger' -import {type Device, device} from '#/storage' - -const IPCC_URL = `https://bsky.app/ipcc` -const BAPP_CONFIG_URL = `https://ip.bsky.app/config` - -const events = new EventEmitter() -const EVENT = 'geolocation-updated' -const emitGeolocationUpdate = (geolocation: Device['geolocation']) => { - events.emit(EVENT, geolocation) -} -const onGeolocationUpdate = ( - listener: (geolocation: Device['geolocation']) => void, -) => { - events.on(EVENT, listener) - return () => { - events.off(EVENT, listener) - } -} - -/** - * Default geolocation value. IF undefined, we fail closed and apply all - * additional mod authorities. - */ -export const DEFAULT_GEOLOCATION: Device['geolocation'] = { - countryCode: undefined, - isAgeBlockedGeo: undefined, - isAgeRestrictedGeo: false, -} - -function sanitizeGeolocation( - geolocation: Device['geolocation'], -): Device['geolocation'] { - return { - countryCode: geolocation?.countryCode ?? undefined, - isAgeBlockedGeo: geolocation?.isAgeBlockedGeo ?? false, - isAgeRestrictedGeo: geolocation?.isAgeRestrictedGeo ?? false, - } -} - -async function getGeolocation(url: string): Promise { - const res = await fetch(url) - - if (!res.ok) { - throw new Error(`geolocation: lookup failed ${res.status}`) - } - - const json = await res.json() - - if (json.countryCode) { - return { - countryCode: json.countryCode, - isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, - isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, - // @ts-ignore - regionCode: json.regionCode ?? undefined, - } - } else { - return undefined - } -} - -async function compareWithIPCC(bapp: Device['geolocation']) { - try { - const ipcc = await getGeolocation(IPCC_URL) - - if (!ipcc || !bapp) return - - logger.metric( - 'geo:debug', - { - bappCountryCode: bapp.countryCode, - // @ts-ignore - bappRegionCode: bapp.regionCode, - bappIsAgeBlockedGeo: bapp.isAgeBlockedGeo, - bappIsAgeRestrictedGeo: bapp.isAgeRestrictedGeo, - ipccCountryCode: ipcc.countryCode, - ipccIsAgeBlockedGeo: ipcc.isAgeBlockedGeo, - ipccIsAgeRestrictedGeo: ipcc.isAgeRestrictedGeo, - }, - { - statsig: false, - }, - ) - } catch {} -} - -/** - * Local promise used within this file only. - */ -let geolocationResolution: Promise<{success: boolean}> | undefined - -/** - * Begin the process of resolving geolocation. This should be called once at - * app start. - * - * THIS METHOD SHOULD NEVER THROW. - * - * This method is otherwise not used for any purpose. To ensure geolocation is - * resolved, use {@link ensureGeolocationResolved} - */ -export function beginResolveGeolocation() { - /** - * In dev, IP server is unavailable, so we just set the default geolocation - * and fail closed. - */ - if (__DEV__) { - geolocationResolution = new Promise(y => y({success: true})) - if (!device.get(['geolocation'])) { - device.set(['geolocation'], DEFAULT_GEOLOCATION) - } - return - } - - geolocationResolution = new Promise(async resolve => { - let success = true - - try { - // Try once, fail fast - const geolocation = await getGeolocation(BAPP_CONFIG_URL) - if (geolocation) { - device.set(['geolocation'], sanitizeGeolocation(geolocation)) - emitGeolocationUpdate(geolocation) - logger.debug(`geolocation: success`, {geolocation}) - compareWithIPCC(geolocation) - } else { - // endpoint should throw on all failures, this is insurance - throw new Error(`geolocation: nothing returned from initial request`) - } - } catch (e: any) { - success = false - - logger.debug(`geolocation: failed initial request`, { - safeMessage: e.message, - }) - - // set to default - device.set(['geolocation'], DEFAULT_GEOLOCATION) - - // retry 3 times, but don't await, proceed with default - networkRetry(3, () => getGeolocation(BAPP_CONFIG_URL)) - .then(geolocation => { - if (geolocation) { - device.set(['geolocation'], sanitizeGeolocation(geolocation)) - emitGeolocationUpdate(geolocation) - logger.debug(`geolocation: success`, {geolocation}) - success = true - compareWithIPCC(geolocation) - } else { - // endpoint should throw on all failures, this is insurance - throw new Error(`geolocation: nothing returned from retries`) - } - }) - .catch((e: any) => { - // complete fail closed - logger.debug(`geolocation: failed retries`, {safeMessage: e.message}) - }) - } finally { - resolve({success}) - } - }) -} - -/** - * Ensure that geolocation has been resolved, or at the very least attempted - * once. Subsequent retries will not be captured by this `await`. Those will be - * reported via {@link events}. - */ -export async function ensureGeolocationResolved() { - if (!geolocationResolution) { - throw new Error(`geolocation: beginResolveGeolocation not called yet`) - } - - const cached = device.get(['geolocation']) - if (cached) { - logger.debug(`geolocation: using cache`, {cached}) - } else { - logger.debug(`geolocation: no cache`) - const {success} = await geolocationResolution - if (success) { - logger.debug(`geolocation: resolved`, { - resolved: device.get(['geolocation']), - }) - } else { - logger.error(`geolocation: failed to resolve`) - } - } -} - -type Context = { - geolocation: Device['geolocation'] -} - -const context = React.createContext({ - geolocation: DEFAULT_GEOLOCATION, -}) -context.displayName = 'GeolocationContext' - -export function Provider({children}: {children: React.ReactNode}) { - const [geolocation, setGeolocation] = React.useState(() => { - const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION - return initial - }) - - React.useEffect(() => { - return onGeolocationUpdate(geolocation => { - setGeolocation(geolocation!) - }) - }, []) - - const ctx = React.useMemo(() => { - return { - geolocation, - } - }, [geolocation]) - - return {children} -} - -export function useGeolocation() { - return React.useContext(context) -} diff --git a/src/state/geolocation/config.ts b/src/state/geolocation/config.ts new file mode 100644 index 000000000..1f7f2daf2 --- /dev/null +++ b/src/state/geolocation/config.ts @@ -0,0 +1,143 @@ +import {networkRetry} from '#/lib/async/retry' +import { + DEFAULT_GEOLOCATION_CONFIG, + GEOLOCATION_CONFIG_URL, +} from '#/state/geolocation/const' +import {emitGeolocationConfigUpdate} from '#/state/geolocation/events' +import {logger} from '#/state/geolocation/logger' +import {BAPP_CONFIG_DEV_BYPASS_SECRET, IS_DEV} from '#/env' +import {type Device, device} from '#/storage' + +async function getGeolocationConfig( + url: string, +): Promise { + const res = await fetch(url, { + headers: IS_DEV + ? { + 'x-dev-bypass-secret': BAPP_CONFIG_DEV_BYPASS_SECRET, + } + : undefined, + }) + + if (!res.ok) { + throw new Error(`geolocation config: fetch failed ${res.status}`) + } + + const json = await res.json() + + if (json.countryCode) { + /** + * Only construct known values here, ignore any extras. + */ + const config: Device['geolocation'] = { + countryCode: json.countryCode, + regionCode: json.regionCode ?? undefined, + ageRestrictedGeos: json.ageRestrictedGeos ?? [], + ageBlockedGeos: json.ageBlockedGeos ?? [], + } + logger.debug(`geolocation config: success`) + return config + } else { + return undefined + } +} + +/** + * Local promise used within this file only. + */ +let geolocationConfigResolution: Promise<{success: boolean}> | undefined + +/** + * Begin the process of resolving geolocation config. This should be called + * once at app start. + * + * THIS METHOD SHOULD NEVER THROW. + * + * This method is otherwise not used for any purpose. To ensure geolocation + * config is resolved, use {@link ensureGeolocationConfigIsResolved} + */ +export function beginResolveGeolocationConfig() { + /** + * Here for debug purposes. Uncomment to prevent hitting the remote geo service, and apply whatever data you require for testing. + */ + // if (__DEV__) { + // geolocationConfigResolution = new Promise(y => y({success: true})) + // device.set(['deviceGeolocation'], undefined) // clears GPS data + // device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG) // clears bapp-config data + // return + // } + + geolocationConfigResolution = new Promise(async resolve => { + let success = true + + try { + // Try once, fail fast + const config = await getGeolocationConfig(GEOLOCATION_CONFIG_URL) + if (config) { + device.set(['geolocation'], config) + emitGeolocationConfigUpdate(config) + } else { + // endpoint should throw on all failures, this is insurance + throw new Error( + `geolocation config: nothing returned from initial request`, + ) + } + } catch (e: any) { + success = false + + logger.debug(`geolocation config: failed initial request`, { + safeMessage: e.message, + }) + + // set to default + device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG) + + // retry 3 times, but don't await, proceed with default + networkRetry(3, () => getGeolocationConfig(GEOLOCATION_CONFIG_URL)) + .then(config => { + if (config) { + device.set(['geolocation'], config) + emitGeolocationConfigUpdate(config) + success = true + } else { + // endpoint should throw on all failures, this is insurance + throw new Error(`geolocation config: nothing returned from retries`) + } + }) + .catch((e: any) => { + // complete fail closed + logger.debug(`geolocation config: failed retries`, { + safeMessage: e.message, + }) + }) + } finally { + resolve({success}) + } + }) +} + +/** + * Ensure that geolocation config has been resolved, or at the very least attempted + * once. Subsequent retries will not be captured by this `await`. Those will be + * reported via {@link emitGeolocationConfigUpdate}. + */ +export async function ensureGeolocationConfigIsResolved() { + if (!geolocationConfigResolution) { + throw new Error( + `geolocation config: beginResolveGeolocationConfig not called yet`, + ) + } + + const cached = device.get(['geolocation']) + if (cached) { + logger.debug(`geolocation config: using cache`) + } else { + logger.debug(`geolocation config: no cache`) + const {success} = await geolocationConfigResolution + if (success) { + logger.debug(`geolocation config: resolved`) + } else { + logger.info(`geolocation config: failed to resolve`) + } + } +} diff --git a/src/state/geolocation/const.ts b/src/state/geolocation/const.ts new file mode 100644 index 000000000..789d001aa --- /dev/null +++ b/src/state/geolocation/const.ts @@ -0,0 +1,30 @@ +import {type GeolocationStatus} from '#/state/geolocation/types' +import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env' +import {type Device} from '#/storage' + +export const IPCC_URL = `https://bsky.app/ipcc` +export const BAPP_CONFIG_URL_PROD = `https://ip.bsky.app/config` +export const BAPP_CONFIG_URL = IS_DEV + ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD) + : BAPP_CONFIG_URL_PROD +export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL + +/** + * Default geolocation config. + */ +export const DEFAULT_GEOLOCATION_CONFIG: Device['geolocation'] = { + countryCode: undefined, + regionCode: undefined, + ageRestrictedGeos: [], + ageBlockedGeos: [], +} + +/** + * Default geolocation status. + */ +export const DEFAULT_GEOLOCATION_STATUS: GeolocationStatus = { + countryCode: undefined, + regionCode: undefined, + isAgeRestrictedGeo: false, + isAgeBlockedGeo: false, +} diff --git a/src/state/geolocation/events.ts b/src/state/geolocation/events.ts new file mode 100644 index 000000000..61433bb2a --- /dev/null +++ b/src/state/geolocation/events.ts @@ -0,0 +1,19 @@ +import EventEmitter from 'eventemitter3' + +import {type Device} from '#/storage' + +const events = new EventEmitter() +const EVENT = 'geolocation-config-updated' + +export const emitGeolocationConfigUpdate = (config: Device['geolocation']) => { + events.emit(EVENT, config) +} + +export const onGeolocationConfigUpdate = ( + listener: (config: Device['geolocation']) => void, +) => { + events.on(EVENT, listener) + return () => { + events.off(EVENT, listener) + } +} diff --git a/src/state/geolocation/index.tsx b/src/state/geolocation/index.tsx new file mode 100644 index 000000000..d01e3f262 --- /dev/null +++ b/src/state/geolocation/index.tsx @@ -0,0 +1,153 @@ +import React from 'react' + +import { + DEFAULT_GEOLOCATION_CONFIG, + DEFAULT_GEOLOCATION_STATUS, +} from '#/state/geolocation/const' +import {onGeolocationConfigUpdate} from '#/state/geolocation/events' +import {logger} from '#/state/geolocation/logger' +import { + type DeviceLocation, + type GeolocationStatus, +} from '#/state/geolocation/types' +import {useSyncedDeviceGeolocation} from '#/state/geolocation/useSyncedDeviceGeolocation' +import { + computeGeolocationStatus, + mergeGeolocation, +} from '#/state/geolocation/util' +import {type Device, device} from '#/storage' + +export * from '#/state/geolocation/config' +export * from '#/state/geolocation/types' +export * from '#/state/geolocation/util' + +type DeviceGeolocationContext = { + deviceGeolocation: DeviceLocation | undefined +} + +type DeviceGeolocationAPIContext = { + setDeviceGeolocation(deviceGeolocation: DeviceLocation): void +} + +type GeolocationConfigContext = { + config: Device['geolocation'] +} + +type GeolocationStatusContext = { + /** + * Merged geolocation from config and device GPS (if available). + */ + location: DeviceLocation + /** + * Computed geolocation status based on the merged location and config. + */ + status: GeolocationStatus +} + +const DeviceGeolocationContext = React.createContext({ + deviceGeolocation: undefined, +}) +DeviceGeolocationContext.displayName = 'DeviceGeolocationContext' + +const DeviceGeolocationAPIContext = + React.createContext({ + setDeviceGeolocation: () => {}, + }) +DeviceGeolocationAPIContext.displayName = 'DeviceGeolocationAPIContext' + +const GeolocationConfigContext = React.createContext({ + config: DEFAULT_GEOLOCATION_CONFIG, +}) +GeolocationConfigContext.displayName = 'GeolocationConfigContext' + +const GeolocationStatusContext = React.createContext({ + location: { + countryCode: undefined, + regionCode: undefined, + }, + status: DEFAULT_GEOLOCATION_STATUS, +}) +GeolocationStatusContext.displayName = 'GeolocationStatusContext' + +/** + * Provider of geolocation config and computed geolocation status. + */ +export function GeolocationStatusProvider({ + children, +}: { + children: React.ReactNode +}) { + const {deviceGeolocation} = React.useContext(DeviceGeolocationContext) + const [config, setConfig] = React.useState(() => { + const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION_CONFIG + return initial + }) + + React.useEffect(() => { + return onGeolocationConfigUpdate(config => { + setConfig(config!) + }) + }, []) + + const configContext = React.useMemo(() => ({config}), [config]) + const statusContext = React.useMemo(() => { + if (deviceGeolocation) { + logger.debug('geolocation: has device geolocation available') + } + const geolocation = mergeGeolocation(deviceGeolocation, config) + const status = computeGeolocationStatus(geolocation, config) + return {location: geolocation, status} + }, [config, deviceGeolocation]) + + return ( + + + {children} + + + ) +} + +/** + * Provider of providers. Provides device geolocation data to lower-level + * `GeolocationStatusProvider`, and device geolocation APIs to children. + */ +export function Provider({children}: {children: React.ReactNode}) { + const [deviceGeolocation, setDeviceGeolocation] = useSyncedDeviceGeolocation() + + const handleSetDeviceGeolocation = React.useCallback( + (location: DeviceLocation) => { + logger.debug('geolocation: setting device geolocation') + setDeviceGeolocation({ + countryCode: location.countryCode ?? undefined, + regionCode: location.regionCode ?? undefined, + }) + }, + [setDeviceGeolocation], + ) + + return ( + ({setDeviceGeolocation: handleSetDeviceGeolocation}), + [handleSetDeviceGeolocation], + )}> + ({deviceGeolocation}), [deviceGeolocation])}> + {children} + + + ) +} + +export function useDeviceGeolocationApi() { + return React.useContext(DeviceGeolocationAPIContext) +} + +export function useGeolocationConfig() { + return React.useContext(GeolocationConfigContext) +} + +export function useGeolocationStatus() { + return React.useContext(GeolocationStatusContext) +} diff --git a/src/state/geolocation/logger.ts b/src/state/geolocation/logger.ts new file mode 100644 index 000000000..afda81136 --- /dev/null +++ b/src/state/geolocation/logger.ts @@ -0,0 +1,3 @@ +import {Logger} from '#/logger' + +export const logger = Logger.create(Logger.Context.Geolocation) diff --git a/src/state/geolocation/types.ts b/src/state/geolocation/types.ts new file mode 100644 index 000000000..174761649 --- /dev/null +++ b/src/state/geolocation/types.ts @@ -0,0 +1,9 @@ +export type DeviceLocation = { + countryCode: string | undefined + regionCode: string | undefined +} + +export type GeolocationStatus = DeviceLocation & { + isAgeRestrictedGeo: boolean + isAgeBlockedGeo: boolean +} diff --git a/src/state/geolocation/useRequestDeviceLocation.ts b/src/state/geolocation/useRequestDeviceLocation.ts new file mode 100644 index 000000000..64e05b056 --- /dev/null +++ b/src/state/geolocation/useRequestDeviceLocation.ts @@ -0,0 +1,43 @@ +import {useCallback} from 'react' +import * as Location from 'expo-location' + +import {type DeviceLocation} from '#/state/geolocation/types' +import {getDeviceGeolocation} from '#/state/geolocation/util' + +export {PermissionStatus} from 'expo-location' + +export function useRequestDeviceLocation(): () => Promise< + | { + granted: true + location: DeviceLocation | undefined + } + | { + granted: false + status: { + canAskAgain: boolean + /** + * Enum, use `PermissionStatus` export for comparisons + */ + permissionStatus: Location.PermissionStatus + } + } +> { + return useCallback(async () => { + const status = await Location.requestForegroundPermissionsAsync() + + if (status.granted) { + return { + granted: true, + location: await getDeviceGeolocation(), + } + } else { + return { + granted: false, + status: { + canAskAgain: status.canAskAgain, + permissionStatus: status.status, + }, + } + } + }, []) +} diff --git a/src/state/geolocation/useSyncedDeviceGeolocation.ts b/src/state/geolocation/useSyncedDeviceGeolocation.ts new file mode 100644 index 000000000..602f29a30 --- /dev/null +++ b/src/state/geolocation/useSyncedDeviceGeolocation.ts @@ -0,0 +1,58 @@ +import {useEffect, useRef} from 'react' +import * as Location from 'expo-location' + +import {logger} from '#/state/geolocation/logger' +import {getDeviceGeolocation} from '#/state/geolocation/util' +import {device, useStorage} from '#/storage' + +/** + * Hook to get and sync the device geolocation from the device GPS and store it + * using device storage. If permissions are not granted, it will clear any cached + * storage value. + */ +export function useSyncedDeviceGeolocation() { + const synced = useRef(false) + const [status] = Location.useForegroundPermissions() + const [deviceGeolocation, setDeviceGeolocation] = useStorage(device, [ + 'deviceGeolocation', + ]) + + useEffect(() => { + async function get() { + // no need to set this more than once per session + if (synced.current) return + + logger.debug('useSyncedDeviceGeolocation: checking perms') + + if (status?.granted) { + const location = await getDeviceGeolocation() + if (location) { + logger.debug('useSyncedDeviceGeolocation: syncing location') + setDeviceGeolocation(location) + synced.current = true + } + } else { + const hasCachedValue = device.get(['deviceGeolocation']) !== undefined + + /** + * If we have a cached value, but user has revoked permissions, + * quietly (will take effect lazily) clear this out. + */ + if (hasCachedValue) { + logger.debug( + 'useSyncedDeviceGeolocation: clearing cached location, perms revoked', + ) + device.set(['deviceGeolocation'], undefined) + } + } + } + + get().catch(e => { + logger.error('useSyncedDeviceGeolocation: failed to sync', { + safeMessage: e, + }) + }) + }, [status, setDeviceGeolocation]) + + return [deviceGeolocation, setDeviceGeolocation] as const +} diff --git a/src/state/geolocation/util.ts b/src/state/geolocation/util.ts new file mode 100644 index 000000000..c92b42b13 --- /dev/null +++ b/src/state/geolocation/util.ts @@ -0,0 +1,180 @@ +import { + getCurrentPositionAsync, + type LocationGeocodedAddress, + reverseGeocodeAsync, +} from 'expo-location' + +import {logger} from '#/state/geolocation/logger' +import {type DeviceLocation} from '#/state/geolocation/types' +import {type Device} from '#/storage' + +/** + * Maps full US region names to their short codes. + * + * Context: in some cases, like on Android, we get the full region name instead + * of the short code. We may need to expand this in the future to other + * countries, hence the prefix. + */ +export const USRegionNameToRegionCode: { + [regionName: string]: string +} = { + Alabama: 'AL', + Alaska: 'AK', + Arizona: 'AZ', + Arkansas: 'AR', + California: 'CA', + Colorado: 'CO', + Connecticut: 'CT', + Delaware: 'DE', + Florida: 'FL', + Georgia: 'GA', + Hawaii: 'HI', + Idaho: 'ID', + Illinois: 'IL', + Indiana: 'IN', + Iowa: 'IA', + Kansas: 'KS', + Kentucky: 'KY', + Louisiana: 'LA', + Maine: 'ME', + Maryland: 'MD', + Massachusetts: 'MA', + Michigan: 'MI', + Minnesota: 'MN', + Mississippi: 'MS', + Missouri: 'MO', + Montana: 'MT', + Nebraska: 'NE', + Nevada: 'NV', + ['New Hampshire']: 'NH', + ['New Jersey']: 'NJ', + ['New Mexico']: 'NM', + ['New York']: 'NY', + ['North Carolina']: 'NC', + ['North Dakota']: 'ND', + Ohio: 'OH', + Oklahoma: 'OK', + Oregon: 'OR', + Pennsylvania: 'PA', + ['Rhode Island']: 'RI', + ['South Carolina']: 'SC', + ['South Dakota']: 'SD', + Tennessee: 'TN', + Texas: 'TX', + Utah: 'UT', + Vermont: 'VT', + Virginia: 'VA', + Washington: 'WA', + ['West Virginia']: 'WV', + Wisconsin: 'WI', + Wyoming: 'WY', +} + +/** + * Normalizes a `LocationGeocodedAddress` into a `DeviceLocation`. + * + * We don't want or care about the full location data, so we trim it down and + * normalize certain fields, like region, into the format we need. + */ +export function normalizeDeviceLocation( + location: LocationGeocodedAddress, +): DeviceLocation { + let {isoCountryCode, region} = location + + if (region) { + if (isoCountryCode === 'US') { + region = USRegionNameToRegionCode[region] ?? region + } + } + + return { + countryCode: isoCountryCode ?? undefined, + regionCode: region ?? undefined, + } +} + +/** + * Combines precise location data with the geolocation config fetched from the + * IP service, with preference to the precise data. + */ +export function mergeGeolocation( + location?: DeviceLocation, + config?: Device['geolocation'], +): DeviceLocation { + if (location?.countryCode) return location + return { + countryCode: config?.countryCode, + regionCode: config?.regionCode, + } +} + +/** + * Computes the geolocation status (age-restricted, age-blocked) based on the + * given location and geolocation config. `location` here should be merged with + * `mergeGeolocation()` ahead of time if needed. + */ +export function computeGeolocationStatus( + location: DeviceLocation, + config: Device['geolocation'], +) { + /** + * We can't do anything if we don't have this data. + */ + if (!location.countryCode) { + return { + ...location, + isAgeRestrictedGeo: false, + isAgeBlockedGeo: false, + } + } + + const isAgeRestrictedGeo = config?.ageRestrictedGeos?.some(rule => { + if (rule.countryCode === location.countryCode) { + if (!rule.regionCode) { + return true // whole country is blocked + } else if (rule.regionCode === location.regionCode) { + return true + } + } + }) + + const isAgeBlockedGeo = config?.ageBlockedGeos?.some(rule => { + if (rule.countryCode === location.countryCode) { + if (!rule.regionCode) { + return true // whole country is blocked + } else if (rule.regionCode === location.regionCode) { + return true + } + } + }) + + return { + ...location, + isAgeRestrictedGeo: !!isAgeRestrictedGeo, + isAgeBlockedGeo: !!isAgeBlockedGeo, + } +} + +export async function getDeviceGeolocation(): Promise { + try { + const geocode = await getCurrentPositionAsync() + const locations = await reverseGeocodeAsync({ + latitude: geocode.coords.latitude, + longitude: geocode.coords.longitude, + }) + const location = locations.at(0) + const normalized = location ? normalizeDeviceLocation(location) : undefined + return { + countryCode: normalized?.countryCode ?? undefined, + regionCode: normalized?.regionCode ?? undefined, + } + } catch (e) { + logger.error('getDeviceGeolocation: failed', { + safeMessage: e, + }) + return { + countryCode: undefined, + regionCode: undefined, + } + } +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index a3f2336cf..d562d9fae 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -7,11 +7,32 @@ export type Device = { fontScale: '-2' | '-1' | '0' | '1' | '2' fontFamily: 'system' | 'theme' lastNuxDialog: string | undefined + + /** + * Geolocation config, fetched from the IP service. This previously did + * double duty as the "status" for geolocation state, but that has since + * moved here to the client. + */ geolocation?: { countryCode: string | undefined - isAgeRestrictedGeo: boolean | undefined - isAgeBlockedGeo: boolean | undefined + regionCode: string | undefined + ageRestrictedGeos: { + countryCode: string + regionCode: string | undefined + }[] + ageBlockedGeos: { + countryCode: string + regionCode: string | undefined + }[] + } + /** + * The GPS-based geolocation, if the user has granted permission. + */ + deviceGeolocation?: { + countryCode: string | undefined + regionCode: string | undefined } + trendingBetaEnabled: boolean devMode: boolean demoMode: boolean diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 8b4c65b8f..277e5c523 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,7 +13,7 @@ import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isAndroid, isIOS} from '#/platform/detection' import {useDialogFullyExpandedCountContext} from '#/state/dialogs' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -184,7 +184,7 @@ function ShellInner() { export function Shell() { const t = useTheme() - const {geolocation} = useGeolocation() + const {status: geolocation} = useGeolocationStatus() const fullyExpandedCount = useDialogFullyExpandedCountContext() useIntentHandler() diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index bb09b5f62..268e77eae 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -9,7 +9,7 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' -import {useGeolocation} from '#/state/geolocation' +import {useGeolocationStatus} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {useCloseAllActiveElements} from '#/state/util' @@ -142,7 +142,7 @@ function ShellInner() { export function Shell() { const t = useTheme() - const {geolocation} = useGeolocation() + const {status: geolocation} = useGeolocationStatus() return ( {geolocation?.isAgeBlockedGeo ? ( diff --git a/yarn.lock b/yarn.lock index 20020f85d..c4b296d5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11386,6 +11386,11 @@ expo-localization@~16.1.5: dependencies: rtl-detect "^1.0.2" +expo-location@~18.1.6: + version "18.1.6" + resolved "https://registry.yarnpkg.com/expo-location/-/expo-location-18.1.6.tgz#b855e14e8b4e29a1bde470fc4dc2a341abecf631" + integrity sha512-l5dQQ2FYkrBgNzaZN1BvSmdhhcztFOUucu2kEfDBMV4wSIuTIt/CKsho+F3RnAiWgvui1wb1WTTf80E8zq48hA== + expo-manifests@~0.16.5: version "0.16.5" resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.16.5.tgz#bb57ceff3db4eb74679d4a155b2ca2050375ce10" -- 2.51.2 From 0dafc2ccd6e221249128ab4d0c570042951f1906 Mon Sep 17 00:00:00 2001 From: estrattonbailey <4732330+estrattonbailey@users.noreply.github.com> Date: Thu, 4 Sep 2025 16:11:27 +0000 Subject: [PATCH 06/12] Nightly source-language update --- src/locale/locales/en/messages.po | 115 ++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 9fc487e51..43bbdd980 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -530,7 +530,7 @@ msgstr "" msgid "A new form of verification" msgstr "" -#: src/components/BlockedGeoOverlay.tsx:39 +#: src/components/BlockedGeoOverlay.tsx:50 msgid "A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies." msgstr "" @@ -845,7 +845,7 @@ msgctxt "toast" msgid "Age assurance inquiry was submitted" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:145 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:193 msgid "Age assurance only takes a few minutes" msgstr "" @@ -879,6 +879,10 @@ msgstr "" msgid "Allow access to your direct messages" msgstr "" +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:152 +msgid "Allow location access" +msgstr "" + #: src/screens/Messages/Settings.tsx:86 #: src/screens/Messages/Settings.tsx:89 msgid "Allow new messages from" @@ -907,6 +911,10 @@ msgstr "" msgid "Already have a code?" msgstr "" +#: src/components/WelcomeModal.tsx:189 +msgid "Already have an account?" +msgstr "" + #: src/screens/Login/ChooseAccountForm.tsx:43 msgid "Already signed in as @{0}" msgstr "" @@ -1047,7 +1055,7 @@ msgstr "" msgid "Animated GIF" msgstr "" -#: src/components/BlockedGeoOverlay.tsx:92 +#: src/components/BlockedGeoOverlay.tsx:104 #: src/components/PolicyUpdateOverlay/Badge.tsx:33 msgid "Announcement" msgstr "" @@ -1223,7 +1231,7 @@ msgstr "" msgid "Artistic or non-erotic nudity." msgstr "" -#: src/components/BlockedGeoOverlay.tsx:42 +#: src/components/BlockedGeoOverlay.tsx:53 msgid "As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending." msgstr "" @@ -1575,6 +1583,7 @@ msgstr "" #: src/components/activity-notifications/SubscribeProfileDialog.tsx:206 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:129 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:135 +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:164 #: src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx:125 #: src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx:131 #: src/components/dialogs/InAppBrowserConsent.tsx:98 @@ -1846,6 +1855,10 @@ msgstr "" msgid "Clear search query" msgstr "" +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:116 +msgid "Click below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." +msgstr "" + #: src/view/com/feeds/MissingFeed.tsx:87 msgid "Click for information" msgstr "" @@ -1944,7 +1957,7 @@ msgstr "" msgid "Close dialog" msgstr "" -#: src/view/shell/index.web.tsx:100 +#: src/view/shell/index.web.tsx:110 msgid "Close drawer menu" msgstr "" @@ -1975,6 +1988,10 @@ msgstr "" msgid "Close this dialog" msgstr "" +#: src/components/WelcomeModal.tsx:214 +msgid "Close welcome modal" +msgstr "" + #: src/screens/Login/PasswordUpdatedForm.tsx:32 msgid "Closes password update alert" msgstr "" @@ -2081,6 +2098,20 @@ msgstr "" msgid "Confirm your birthdate" msgstr "" +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:89 +#: src/components/BlockedGeoOverlay.tsx:141 +#: src/components/BlockedGeoOverlay.tsx:147 +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:45 +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:112 +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:146 +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:159 +msgid "Confirm your location" +msgstr "" + +#: src/components/BlockedGeoOverlay.tsx:135 +msgid "Confirm your location with GPS. Your location data is not tracked and does not leave your device." +msgstr "" + #: src/components/dialogs/EmailDialog/components/TokenField.tsx:36 #: src/screens/Login/LoginForm.tsx:274 #: src/screens/Settings/components/ChangePasswordDialog.tsx:186 @@ -2100,7 +2131,7 @@ msgstr "" msgid "Connection issue" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:84 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:132 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:28 msgid "Contact our moderation team" msgstr "" @@ -2395,6 +2426,8 @@ msgstr "" #: src/components/LoggedOutCTA.tsx:71 #: src/components/LoggedOutCTA.tsx:76 +#: src/components/WelcomeModal.tsx:155 +#: src/components/WelcomeModal.tsx:163 #: src/view/com/auth/SplashScreen.tsx:55 #: src/view/com/auth/SplashScreen.web.tsx:117 #: src/view/shell/bottom-bar/BottomBar.tsx:345 @@ -3338,6 +3371,11 @@ msgstr "" msgid "Explore" msgstr "" +#: src/components/WelcomeModal.tsx:168 +#: src/components/WelcomeModal.tsx:177 +msgid "Explore the app" +msgstr "" + #: src/screens/Settings/AccountSettings.tsx:152 #: src/screens/Settings/AccountSettings.tsx:156 msgid "Export my data" @@ -3495,6 +3533,10 @@ msgstr "" msgid "Failed to remove verification" msgstr "" +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:87 +msgid "Failed to resolve location. Please try again." +msgstr "" + #: src/lib/media/save-image.ts:28 msgid "Failed to save image: {0}" msgstr "" @@ -3853,7 +3895,7 @@ msgstr "" msgid "Food" msgstr "" -#: src/components/BlockedGeoOverlay.tsx:45 +#: src/components/BlockedGeoOverlay.tsx:56 msgid "For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi." msgstr "" @@ -4537,6 +4579,10 @@ msgstr "" msgid "Invites, but personal" msgstr "" +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:86 +msgid "Is your location not accurate? <0>Click here to confirm your location." +msgstr "" + #: src/screens/Signup/StepInfo/index.tsx:293 msgid "It's correct" msgstr "" @@ -4635,11 +4681,11 @@ msgstr "" msgid "Larger" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:139 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:187 msgid "Last initiated {timeAgo} ago" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:137 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185 msgid "Last initiated just now" msgstr "" @@ -5684,6 +5730,10 @@ msgstr "" msgid "Not Found" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:126 +msgid "Not in Mississippi?" +msgstr "" + #: src/view/com/profile/ProfileMenu.tsx:497 msgid "Note about sharing" msgstr "" @@ -6724,7 +6774,7 @@ msgstr "" msgid "Read more replies" msgstr "" -#: src/components/BlockedGeoOverlay.tsx:29 +#: src/components/BlockedGeoOverlay.tsx:40 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:112 msgid "Read our blog post" msgstr "" @@ -6743,6 +6793,14 @@ msgstr "" msgid "Read the Bluesky Terms of Service" msgstr "" +#: src/components/WelcomeModal.tsx:146 +msgid "Real conversations." +msgstr "" + +#: src/components/WelcomeModal.tsx:144 +msgid "Real people." +msgstr "" + #: src/screens/Takendown.tsx:162 #: src/screens/Takendown.tsx:170 msgid "Reason for appeal" @@ -8010,6 +8068,8 @@ msgstr "" #: src/components/dialogs/Signin.tsx:97 #: src/components/dialogs/Signin.tsx:99 +#: src/components/WelcomeModal.tsx:194 +#: src/components/WelcomeModal.tsx:206 #: src/screens/Login/index.tsx:122 #: src/screens/Login/index.tsx:143 #: src/screens/Login/LoginForm.tsx:181 @@ -8118,6 +8178,10 @@ msgstr "" msgid "Snoozes the reminder" msgstr "" +#: src/components/WelcomeModal.tsx:148 +msgid "Social media you control." +msgstr "" + #: src/screens/Onboarding/index.tsx:53 #: src/screens/Onboarding/state.ts:108 msgid "Software Dev" @@ -8525,6 +8589,11 @@ msgstr "" msgid "Thanks, you have successfully verified your email address. You can close this dialog." msgstr "" +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:115 +#: src/components/BlockedGeoOverlay.tsx:169 +msgid "Thanks! You're all set." +msgstr "" + #: src/screens/Settings/components/ChangeHandleDialog.tsx:497 msgid "That contains the following:" msgstr "" @@ -9119,6 +9188,10 @@ msgstr "" msgid "Type:" msgstr "" +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:92 +msgid "Unable to access location. You'll need to visit your system settings to enable location services for Bluesky" +msgstr "" + #: src/lib/hooks/useCleanError.ts:27 #: src/lib/strings/errors.ts:11 msgid "Unable to connect. Please check your internet connection and try again." @@ -9217,7 +9290,7 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/BlockedGeoOverlay.tsx:37 +#: src/components/BlockedGeoOverlay.tsx:48 msgid "Unfortunately, Bluesky is unavailable in Mississippi right now." msgstr "" @@ -9580,7 +9653,7 @@ msgstr "" msgid "Verify account" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:122 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:170 msgid "Verify again" msgstr "" @@ -9606,8 +9679,8 @@ msgstr "" msgid "Verify email dialog" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:110 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:124 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:158 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:172 msgid "Verify now" msgstr "" @@ -9968,6 +10041,14 @@ msgstr "" msgid "We're so excited to have you join us!" msgstr "" +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:105 +msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance." +msgstr "" + +#: src/components/BlockedGeoOverlay.tsx:159 +msgid "We're sorry, but based on your device's location, you are currently located in a region we cannot provide access at this time." +msgstr "" + #: src/view/screens/ProfileList.tsx:117 msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." msgstr "" @@ -10177,7 +10258,7 @@ msgstr "" msgid "You are creating an account on" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:80 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:128 msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team if you believe this is an error." msgstr "" @@ -10676,6 +10757,10 @@ msgstr "" msgid "Your interests help us find what you like!" msgstr "" +#: src/components/dialogs/DeviceLocationRequestDialog.tsx:130 +msgid "Your location data is not tracked and does not leave your device." +msgstr "" + #: src/components/dialogs/MutedWords.tsx:369 msgid "Your muted words" msgstr "" -- 2.51.2 From 9738f4858dea04a5832b9d374e06c4b879abae29 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:08:23 +0100 Subject: [PATCH 07/12] Tweak location-related strings and labels (#8975) * tweak string in BlockedGeoOverlay.tsx * tweak string in AgeAssuranceAccountCard.tsx * tweak string and labels in DeviceLocationRequestDialog.tsx * prettier * add missing `.` in DeviceLocationRequestDialog.tsx --- src/components/BlockedGeoOverlay.tsx | 2 +- .../ageAssurance/AgeAssuranceAccountCard.tsx | 2 +- .../dialogs/DeviceLocationRequestDialog.tsx | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx index df8ed63d4..8dd55c2bf 100644 --- a/src/components/BlockedGeoOverlay.tsx +++ b/src/components/BlockedGeoOverlay.tsx @@ -156,7 +156,7 @@ export function BlockedGeoOverlay() { props.disableDialogAction() props.setDialogError( _( - msg`We're sorry, but based on your device's location, you are currently located in a region we cannot provide access at this time.`, + msg`We're sorry, but based on your device's location, you are currently located in a region where we cannot provide access at this time.`, ), ) } else { diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx index be9935d9f..a7accfa74 100644 --- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx @@ -90,7 +90,7 @@ function Inner({style}: ViewStyleProp & {}) { {...createStaticClick(() => { locationControl.open() })}> - Click here to confirm your location. + Tap here to confirm your location. {' '} diff --git a/src/components/dialogs/DeviceLocationRequestDialog.tsx b/src/components/dialogs/DeviceLocationRequestDialog.tsx index 6c82dc0e9..ac3362406 100644 --- a/src/components/dialogs/DeviceLocationRequestDialog.tsx +++ b/src/components/dialogs/DeviceLocationRequestDialog.tsx @@ -89,7 +89,7 @@ function DeviceLocationRequestDialogInner({onLocationAcquired}: Props) { } else { setError( _( - msg`Unable to access location. You'll need to visit your system settings to enable location services for Bluesky`, + msg`Unable to access location. You'll need to visit your system settings to enable location services for Bluesky.`, ), ) } @@ -114,9 +114,9 @@ function DeviceLocationRequestDialogInner({onLocationAcquired}: Props) { - Click below to allow Bluesky to access your GPS location. We will - then use that data to more accurately determine the content and - features available in your region. + Tap below to allow Bluesky to access your GPS location. We will then + use that data to more accurately determine the content and features + available in your region. @@ -143,7 +143,7 @@ function DeviceLocationRequestDialogInner({onLocationAcquired}: Props) { {!dialogDisabled && ( + )} + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 985d58eec..bb15c5f63 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -12,12 +12,11 @@ import { import {useProfileQuery} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' -import {ActivitySubscriptionsNUX} from '#/components/dialogs/nuxs/ActivitySubscriptions' +import {BookmarksAnnouncement} from '#/components/dialogs/nuxs/BookmarksAnnouncement' /* * NUXs */ import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' -import {isExistingUserAsOf} from '#/components/dialogs/nuxs/utils' type Context = { activeNux: Nux | undefined @@ -34,13 +33,7 @@ const queuedNuxs: { }) => boolean }[] = [ { - id: Nux.ActivitySubscriptions, - enabled: ({currentProfile}) => { - return isExistingUserAsOf( - '2025-07-07T00:00:00.000Z', - currentProfile.createdAt, - ) - }, + id: Nux.BookmarksAnnouncement, }, ] @@ -180,7 +173,7 @@ function Inner({ return ( {/*For example, activeNux === Nux.NeueTypography && */} - {activeNux === Nux.ActivitySubscriptions && } + {activeNux === Nux.BookmarksAnnouncement && } ) } diff --git a/src/components/icons/Bookmark.tsx b/src/components/icons/Bookmark.tsx new file mode 100644 index 000000000..c8fb8242f --- /dev/null +++ b/src/components/icons/Bookmark.tsx @@ -0,0 +1,16 @@ +import {createSinglePathSVG} from './TEMPLATE' + +// custom, not part of icon library +export const Bookmark = createSinglePathSVG({ + path: 'M9.7 16.895a4 4 0 0 1 4.6 0l3.7 2.6V6.5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v12.995l3.7-2.6Zm10.3 2.6c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2.001 2.001 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v12.995Z', +}) + +// custom, not part of icon library +export const BookmarkFilled = createSinglePathSVG({ + path: 'M16 2.5a4 4 0 0 1 4 4v12.995c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2.001 2.001 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8Z', +}) + +// custom, not part of icon library, for LARGE (64px) size +export const BookmarkDeleteLarge = createSinglePathSVG({ + path: 'M14.2 2.625c.834 0 1.482 0 2.001.042.523.043.949.131 1.331.326.635.324 1.151.84 1.475 1.475.195.382.283.807.326 1.33.042.52.042 1.168.042 2.002v11.09c0 .495 0 .893-.027 1.199-.028.301-.087.585-.26.809-.249.323-.63.518-1.037.533-.282.01-.547-.107-.808-.26-.265-.154-.588-.385-.991-.673l-3.54-2.528c-.36-.258-.461-.322-.559-.347a.626.626 0 0 0-.306 0c-.098.025-.199.09-.559.347l-3.54 2.528c-.403.288-.726.519-.991.674-.261.152-.526.269-.808.259a1.376 1.376 0 0 1-1.038-.534c-.172-.223-.231-.507-.259-.808a7.31 7.31 0 0 1-.024-.528l-.003-.67V7.8c0-.834 0-1.482.042-2.001.043-.523.13-.949.325-1.331a3.376 3.376 0 0 1 1.476-1.475c.382-.195.808-.283 1.33-.326.52-.042 1.168-.042 2.002-.042h4.4Zm-4.4.75c-.846 0-1.458 0-1.94.04-.477.039-.792.114-1.051.246A2.626 2.626 0 0 0 5.66 4.81c-.132.259-.208.574-.247 1.051-.04.482-.039 1.094-.039 1.94v11.09l.003.658c.003.186.01.34.021.473.025.267.07.37.106.418a.626.626 0 0 0 .472.243c.059.002.168-.022.4-.158.23-.133.52-.34.935-.636l3.54-2.529c.308-.22.543-.396.81-.464.222-.056.454-.056.676 0 .267.068.5.244.81.464l3.54 2.529c.414.296.704.503.933.636.233.137.343.16.402.158a.626.626 0 0 0 .472-.243c.036-.048.081-.15.106-.419.024-.263.024-.62.024-1.13V7.8c0-.846 0-1.458-.04-1.94-.039-.477-.114-.792-.246-1.051A2.627 2.627 0 0 0 17.19 3.66c-.259-.132-.575-.207-1.051-.246-.482-.04-1.094-.04-1.94-.04H9.8Zm4.056 4.238a.375.375 0 0 1 .53.53L12.53 10l1.857 1.856a.375.375 0 0 1-.53.53L12 10.53l-1.856 1.857a.375.375 0 0 1-.53-.53L11.47 10 9.613 8.144a.375.375 0 0 1 .53-.53L12 9.47l1.856-1.857Z', +}) diff --git a/src/components/icons/Reply.tsx b/src/components/icons/Reply.tsx new file mode 100644 index 000000000..0317dd2f3 --- /dev/null +++ b/src/components/icons/Reply.tsx @@ -0,0 +1,11 @@ +import {createSinglePathSVG} from './TEMPLATE' + +// custom, off spec +export const Reply = createSinglePathSVG({ + path: 'M20.002 7a2 2 0 0 0-2-2h-12a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v1.918l3.375-2.7a1 1 0 0 1 .625-.218h5a2 2 0 0 0 2-2V7Zm2 8a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z', +}) + +// custom, off spec +export const ReplyFilled = createSinglePathSVG({ + path: 'M22.002 15a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z', +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b6b06ee7f..5871821f4 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -124,7 +124,6 @@ export const createHitslop = (size: number): Insets => ({ export const HITSLOP_10 = createHitslop(10) export const HITSLOP_20 = createHitslop(20) export const HITSLOP_30 = createHitslop(30) -export const POST_CTRL_HITSLOP = {top: 5, bottom: 10, left: 10, right: 10} export const LANG_DROPDOWN_HITSLOP = {top: 10, bottom: 10, left: 4, right: 4} export const BACK_HITSLOP = HITSLOP_30 export const MAX_POST_LINES = 25 diff --git a/src/lib/custom-animations/CountWheel.tsx b/src/lib/custom-animations/CountWheel.tsx index 4b131db2d..6db22554e 100644 --- a/src/lib/custom-animations/CountWheel.tsx +++ b/src/lib/custom-animations/CountWheel.tsx @@ -10,9 +10,9 @@ import {i18n} from '@lingui/core' import {decideShouldRoll} from '#/lib/custom-animations/util' import {s} from '#/lib/styles' -import {formatCount} from '#/view/com/util/numeric/format' import {Text} from '#/view/com/util/text/Text' import {atoms as a, useTheme} from '#/alf' +import {formatPostStatCount} from '#/components/PostControls/util' const animationConfig = { duration: 400, @@ -92,11 +92,13 @@ export function CountWheel({ big, isLiked, hasBeenToggled, + compactCount, }: { likeCount: number big?: boolean isLiked: boolean hasBeenToggled: boolean + compactCount?: boolean }) { const t = useTheme() const shouldAnimate = !useReducedMotion() && hasBeenToggled @@ -109,8 +111,12 @@ export function CountWheel({ const [key, setKey] = React.useState(0) const [prevCount, setPrevCount] = React.useState(likeCount) const prevIsLiked = React.useRef(isLiked) - const formattedCount = formatCount(i18n, likeCount) - const formattedPrevCount = formatCount(i18n, prevCount) + const formattedCount = formatPostStatCount(i18n, likeCount, { + compact: compactCount, + }) + const formattedPrevCount = formatPostStatCount(i18n, prevCount, { + compact: compactCount, + }) React.useEffect(() => { if (isLiked === prevIsLiked.current) { diff --git a/src/lib/hooks/useNavigationTabState.ts b/src/lib/hooks/useNavigationTabState.ts index 2d15bce56..7fd76cb1b 100644 --- a/src/lib/hooks/useNavigationTabState.ts +++ b/src/lib/hooks/useNavigationTabState.ts @@ -9,6 +9,7 @@ export function useNavigationTabState() { isAtSearch: getTabState(state, 'Search') !== TabState.Outside, // FeedsTab no longer exists, but this check works for `Feeds` screen as well isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside, + isAtBookmarks: getTabState(state, 'Bookmarks') !== TabState.Outside, isAtNotifications: getTabState(state, 'Notifications') !== TabState.Outside, isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 1725fdfb4..4f7054cb3 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -86,6 +86,7 @@ export type CommonNavigatorParams = { } StarterPackEdit: {rkey?: string} VideoFeed: VideoFeedSourceContext + Bookmarks: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 79d7702b3..1ba1cd3b5 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -238,6 +238,14 @@ export type MetricEvents = { 'post:unmute': {} 'post:pin': {} 'post:unpin': {} + 'post:bookmark': { + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + } + 'post:unbookmark': { + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + } + 'bookmarks:view': {} + 'bookmarks:post-clicked': {} 'profile:follow': { didBecomeMutual: boolean | undefined followeeClout: number | undefined diff --git a/src/routes.ts b/src/routes.ts index 7fc673e2b..1ed913bb2 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -90,4 +90,5 @@ export const router = new Router({ StarterPackShort: '/starter-pack-short/:code', StarterPackWizard: '/starter-pack/create', VideoFeed: '/video-feed', + Bookmarks: '/saved', }) diff --git a/src/screens/Bookmarks/components/EmptyState.tsx b/src/screens/Bookmarks/components/EmptyState.tsx new file mode 100644 index 000000000..bfd80903d --- /dev/null +++ b/src/screens/Bookmarks/components/EmptyState.tsx @@ -0,0 +1,59 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {ButtonText} from '#/components/Button' +import {BookmarkDeleteLarge} from '#/components/icons/Bookmark' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function EmptyState() { + const t = useTheme() + const {_} = useLingui() + + return ( + + + + + Nothing saved yet + + + + + + + Go home + + + + + + ) +} diff --git a/src/screens/Bookmarks/index.tsx b/src/screens/Bookmarks/index.tsx new file mode 100644 index 000000000..72ad1f167 --- /dev/null +++ b/src/screens/Bookmarks/index.tsx @@ -0,0 +1,294 @@ +import {useCallback, useMemo, useState} from 'react' +import {View} from 'react-native' +import { + type $Typed, + type AppBskyBookmarkDefs, + AppBskyFeedDefs, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {logger} from '#/logger' +import {isIOS} from '#/platform/detection' +import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' +import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' +import {useSetMinimalShellMode} from '#/state/shell' +import {Post} from '#/view/com/post/Post' +import {List} from '#/view/com/util/List' +import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {EmptyState} from '#/screens/Bookmarks/components/EmptyState' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {BookmarkFilled} from '#/components/icons/Bookmark' +import {CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon} from '#/components/icons/CircleQuestion' +import * as Layout from '#/components/Layout' +import {ListFooter} from '#/components/Lists' +import * as Skele from '#/components/Skeleton' +import * as toast from '#/components/Toast' +import {Text} from '#/components/Typography' + +type Props = NativeStackScreenProps + +export function BookmarksScreen({}: Props) { + const setMinimalShellMode = useSetMinimalShellMode() + + useFocusEffect( + useCallback(() => { + setMinimalShellMode(false) + logger.metric('bookmarks:view', {}) + }, [setMinimalShellMode]), + ) + + return ( + + + + + + Saved Posts + + + + + + + ) +} + +type ListItem = + | { + type: 'loading' + key: 'loading' + } + | { + type: 'empty' + key: 'empty' + } + | { + type: 'bookmark' + key: string + bookmark: Omit & { + item: $Typed + } + } + | { + type: 'bookmarkNotFound' + key: string + bookmark: Omit & { + item: $Typed + } + } + +function BookmarksInner() { + const initialNumToRender = useInitialNumToRender() + const cleanError = useCleanError() + const [isPTRing, setIsPTRing] = useState(false) + const { + data, + isLoading, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + error, + refetch, + } = useBookmarksQuery() + const cleanedError = useMemo(() => { + const {raw, clean} = cleanError(error) + return clean || raw + }, [error, cleanError]) + + const onRefresh = useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } finally { + setIsPTRing(false) + } + }, [refetch, setIsPTRing]) + + const onEndReached = useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || error) return + try { + await fetchNextPage() + } catch {} + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) + + const items = useMemo(() => { + const i: ListItem[] = [] + + if (isLoading) { + i.push({type: 'loading', key: 'loading'}) + } else if (error || !data) { + // handled in Footer + } else { + const bookmarks = data.pages.flatMap(p => p.bookmarks) + + if (bookmarks.length > 0) { + for (const bookmark of bookmarks) { + if (AppBskyFeedDefs.isNotFoundPost(bookmark.item)) { + i.push({ + type: 'bookmarkNotFound', + key: bookmark.item.uri, + bookmark: { + ...bookmark, + item: bookmark.item as $Typed, + }, + }) + } + if (AppBskyFeedDefs.isPostView(bookmark.item)) { + i.push({ + type: 'bookmark', + key: bookmark.item.uri, + bookmark: { + ...bookmark, + item: bookmark.item as $Typed, + }, + }) + } + } + } else { + i.push({type: 'empty', key: 'empty'}) + } + } + + return i + }, [isLoading, error, data]) + + const isEmpty = items.length === 1 && items[0]?.type === 'empty' + + return ( + + } + initialNumToRender={initialNumToRender} + windowSize={9} + maxToRenderPerBatch={isIOS ? 5 : 1} + updateCellsBatchingPeriod={40} + sideBorders={false} + /> + ) +} + +function BookmarkNotFound({ + hideTopBorder, + post, +}: { + hideTopBorder: boolean + post: $Typed +}) { + const t = useTheme() + const {_} = useLingui() + const {mutateAsync: bookmark} = useBookmarkMutation() + const cleanError = useCleanError() + + const remove = async () => { + try { + await bookmark({action: 'delete', uri: post.uri}) + toast.show(_(msg`Removed from saved posts`), { + type: 'info', + }) + } catch (e: any) { + const {raw, clean} = cleanError(e) + toast.show(clean || raw || e, { + type: 'error', + }) + } + } + + return ( + + + + + + + + + + + + This post was deleted by its author + + + + + ) +} + +function renderItem({item, index}: {item: ListItem; index: number}) { + switch (item.type) { + case 'loading': { + return + } + case 'empty': { + return + } + case 'bookmark': { + return ( + { + logger.metric('bookmarks:post-clicked', {}) + }} + /> + ) + } + case 'bookmarkNotFound': { + return ( + + ) + } + default: + return null + } +} + +const keyExtractor = (item: ListItem) => item.key diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index b59397b0b..08dd272f7 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -32,7 +32,6 @@ import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {type PostSource} from '#/state/unstable-post-source' -import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton' import { @@ -53,6 +52,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' +import {formatPostStatCount} from '#/components/PostControls/util' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -415,13 +415,18 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ /> {post.repostCount !== 0 || post.likeCount !== 0 || - post.quoteCount !== 0 ? ( + post.quoteCount !== 0 || + post.bookmarkCount !== 0 ? ( // Show this section unless we're *sure* it has no engagement. - {formatCount(i18n, post.repostCount)} + {formatPostStatCount(i18n, post.repostCount)} {' '} - {formatCount(i18n, post.quoteCount)} + {formatPostStatCount(i18n, post.quoteCount)} {' '} - {formatCount(i18n, post.likeCount)} + {formatPostStatCount(i18n, post.likeCount)} {' '} ) : null} + {post.bookmarkCount != null && post.bookmarkCount !== 0 ? ( + + + + {formatPostStatCount(i18n, post.bookmarkCount)} + {' '} + + + + ) : null} ) : null} )} [bookmarksQueryKeyRoot] + +export function useBookmarksQuery() { + const agent = useAgent() + + return useInfiniteQuery< + AppBskyBookmarkGetBookmarks.OutputSchema, + Error, + InfiniteData, + QueryKey, + string | undefined + >({ + queryKey: createBookmarksQueryKey(), + async queryFn({pageParam}) { + const res = await agent.app.bsky.bookmark.getBookmarks({ + cursor: pageParam, + }) + return res.data + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + }) +} + +export async function truncateAndInvalidate(qc: QueryClient) { + qc.setQueriesData>( + {queryKey: [bookmarksQueryKeyRoot]}, + data => { + if (data) { + return { + pageParams: data.pageParams.slice(0, 1), + pages: data.pages.slice(0, 1), + } + } + return data + }, + ) + return qc.invalidateQueries({queryKey: [bookmarksQueryKeyRoot]}) +} + +export async function optimisticallySaveBookmark( + qc: QueryClient, + post: AppBskyFeedDefs.PostView, +) { + qc.setQueriesData>( + { + queryKey: [bookmarksQueryKeyRoot], + }, + data => { + if (!data) return data + return { + ...data, + pages: data.pages.map((page, index) => { + if (index === 0) { + post.$type = 'app.bsky.feed.defs#postView' + return { + ...page, + bookmarks: [ + { + createdAt: new Date().toISOString(), + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post as $Typed, + }, + ...page.bookmarks, + ], + } + } + return page + }), + } + }, + ) +} + +export async function optimisticallyDeleteBookmark( + qc: QueryClient, + {uri}: {uri: string}, +) { + qc.setQueriesData>( + { + queryKey: [bookmarksQueryKeyRoot], + }, + data => { + if (!data) return data + return { + ...data, + pages: data.pages.map(page => { + return { + ...page, + bookmarks: page.bookmarks.filter(b => b.subject.uri !== uri), + } + }), + } + }, + ) +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 7577d6b20..165649447 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -9,6 +9,7 @@ export enum Nux { ActivitySubscriptions = 'ActivitySubscriptions', AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice', AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner', + BookmarksAnnouncement = 'BookmarksAnnouncement', /* * Blocking announcements. New IDs are required for each new announcement. @@ -47,6 +48,10 @@ export type AppNux = BaseNux< id: Nux.PolicyUpdate202508 data: undefined } + | { + id: Nux.BookmarksAnnouncement + data: undefined + } > export const NuxSchemas: Record | undefined> = { @@ -57,4 +62,5 @@ export const NuxSchemas: Record | undefined> = { [Nux.AgeAssuranceDismissibleNotice]: undefined, [Nux.AgeAssuranceDismissibleFeedBanner]: undefined, [Nux.PolicyUpdate202508]: undefined, + [Nux.BookmarksAnnouncement]: undefined, } diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index f41f48e40..58cb40f71 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -43,11 +43,13 @@ export function Post({ showReplyLine, hideTopBorder, style, + onBeforePress, }: { post: AppBskyFeedDefs.PostView showReplyLine?: boolean hideTopBorder?: boolean style?: StyleProp + onBeforePress?: () => void }) { const moderationOpts = useModerationOpts() const record = useMemo( @@ -85,6 +87,7 @@ export function Post({ showReplyLine={showReplyLine} hideTopBorder={hideTopBorder} style={style} + onBeforePress={onBeforePress} /> ) } @@ -99,6 +102,7 @@ function PostInner({ showReplyLine, hideTopBorder, style, + onBeforePress: outerOnBeforePress, }: { post: Shadow record: AppBskyFeedPost.Record @@ -107,6 +111,7 @@ function PostInner({ showReplyLine?: boolean hideTopBorder?: boolean style?: StyleProp + onBeforePress?: () => void }) { const queryClient = useQueryClient() const pal = usePalette('default') @@ -142,7 +147,8 @@ function PostInner({ const onBeforePress = useCallback(() => { unstableCacheProfileView(queryClient, post.author) - }, [queryClient, post.author]) + outerOnBeforePress?.() + }, [queryClient, post.author, outerOnBeforePress]) const [hover, setHover] = useState(false) return ( diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 79d8a21ae..10817407f 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -30,6 +30,7 @@ import { Bell_Filled_Corner0_Rounded as BellFilled, Bell_Stroke2_Corner0_Rounded as Bell, } from '#/components/icons/Bell' +import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' import {BulletList_Stroke2_Corner0_Rounded as List} from '#/components/icons/BulletList' import { Hashtag_Filled_Corner0_Rounded as HashtagFilled, @@ -150,6 +151,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { isAtHome, isAtSearch, isAtFeeds, + isAtBookmarks, isAtNotifications, isAtMyProfile, isAtMessages, @@ -231,6 +233,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { setDrawerOpen(false) }, [navigation, setDrawerOpen]) + const onPressBookmarks = React.useCallback(() => { + navigation.navigate('Bookmarks') + setDrawerOpen(false) + }, [navigation, setDrawerOpen]) + const onPressSettings = React.useCallback(() => { navigation.navigate('Settings') setDrawerOpen(false) @@ -292,6 +299,10 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { /> + void}): React.ReactNode => { } ListsMenuItem = React.memo(ListsMenuItem) +let BookmarksMenuItem = ({ + isActive, + onPress, +}: { + isActive: boolean + onPress: () => void +}): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + return ( + + ) : ( + + ) + } + label={_(msg`Saved`)} + onPress={onPress} + /> + ) +} +BookmarksMenuItem = React.memo(BookmarksMenuItem) + let ProfileMenuItem = ({ isActive, onPress, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index cf1ff8425..c1e429c73 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -40,6 +40,7 @@ import { Bell_Filled_Corner0_Rounded as BellFilled, Bell_Stroke2_Corner0_Rounded as Bell, } from '#/components/icons/Bell' +import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' import { BulletList_Filled_Corner0_Rounded as ListFilled, BulletList_Stroke2_Corner0_Rounded as List, @@ -743,6 +744,24 @@ export function DesktopLeftNav() { } label={_(msg`Lists`)} /> + + } + iconFilled={ + + } + label={_(msg`Saved`)} + /> Date: Thu, 4 Sep 2025 22:33:00 +0000 Subject: [PATCH 10/12] Nightly source-language update --- src/locale/locales/en/messages.po | 384 ++++++++++++++++++------------ 1 file changed, 228 insertions(+), 156 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 3578c3e80..339d31a5f 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -76,7 +76,7 @@ msgstr "" #: src/view/shell/bottom-bar/BottomBar.tsx:225 #: src/view/shell/bottom-bar/BottomBar.tsx:257 -#: src/view/shell/Drawer.tsx:487 +#: src/view/shell/Drawer.tsx:498 msgid "{0, plural, one {# unread item} other {# unread items}}" msgstr "" @@ -95,7 +95,7 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:473 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:478 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -103,14 +103,18 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:457 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:462 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:439 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:444 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:490 +msgid "{0, plural, one {save} other {saves}}" +msgstr "" + #: src/screens/Search/modules/ExploreTrendingTopics.tsx:82 msgid "{0, plural, other {{1} posts}}" msgstr "" @@ -212,7 +216,7 @@ msgstr "" msgid "{0}s" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:454 +#: src/view/shell/desktop/LeftNav.tsx:455 msgid "{count, plural, one {# unread item} other {# unread items}}" msgstr "" @@ -464,11 +468,11 @@ msgctxt "feeds" msgid "<0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}} are included in your starter pack" msgstr "" -#: src/view/shell/Drawer.tsx:116 +#: src/view/shell/Drawer.tsx:117 msgid "<0>{0} {1, plural, one {follower} other {followers}}" msgstr "" -#: src/view/shell/Drawer.tsx:127 +#: src/view/shell/Drawer.tsx:128 msgid "<0>{0} {1, plural, one {following} other {following}}" msgstr "" @@ -534,11 +538,15 @@ msgstr "" msgid "A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies." msgstr "" +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:120 +msgid "A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads \"inventing a saturday that immediately follows monday\"." +msgstr "" + #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:113 msgid "A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature." msgstr "" -#: src/Navigation.tsx:523 +#: src/Navigation.tsx:524 #: src/screens/Settings/AboutSettings.tsx:75 #: src/screens/Settings/Settings.tsx:244 #: src/screens/Settings/Settings.tsx:247 @@ -565,11 +573,11 @@ msgstr "" msgid "Accessibility" msgstr "" -#: src/Navigation.tsx:382 +#: src/Navigation.tsx:383 msgid "Accessibility Settings" msgstr "" -#: src/Navigation.tsx:398 +#: src/Navigation.tsx:399 #: src/screens/Login/LoginForm.tsx:194 #: src/screens/Settings/AccountSettings.tsx:51 #: src/screens/Settings/Settings.tsx:174 @@ -639,7 +647,7 @@ msgstr "" msgid "Activity from others" msgstr "" -#: src/Navigation.tsx:491 +#: src/Navigation.tsx:492 msgid "Activity notifications" msgstr "" @@ -694,8 +702,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:564 #: src/screens/Settings/Settings.tsx:567 -#: src/view/shell/desktop/LeftNav.tsx:261 -#: src/view/shell/desktop/LeftNav.tsx:265 +#: src/view/shell/desktop/LeftNav.tsx:262 +#: src/view/shell/desktop/LeftNav.tsx:266 msgid "Add another account" msgstr "" @@ -777,6 +785,10 @@ msgstr "" msgid "Add to lists" msgstr "" +#: src/components/PostControls/BookmarkButton.tsx:126 +msgid "Add to saved posts" +msgstr "" + #: src/components/dialogs/StarterPackDialog.tsx:176 #: src/view/com/profile/ProfileMenu.tsx:308 #: src/view/com/profile/ProfileMenu.tsx:311 @@ -1080,7 +1092,7 @@ msgstr "" msgid "Anyone who follows me" msgstr "" -#: src/Navigation.tsx:531 +#: src/Navigation.tsx:532 #: src/screens/Settings/AppIconSettings/index.tsx:67 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23 @@ -1117,7 +1129,7 @@ msgstr "" msgid "App passwords" msgstr "" -#: src/Navigation.tsx:350 +#: src/Navigation.tsx:351 #: src/screens/Settings/AppPasswords.tsx:51 msgid "App Passwords" msgstr "" @@ -1153,7 +1165,7 @@ msgstr "" msgid "Appeal this decision" msgstr "" -#: src/Navigation.tsx:390 +#: src/Navigation.tsx:391 #: src/screens/Settings/AppearanceSettings.tsx:86 #: src/screens/Settings/Settings.tsx:212 #: src/screens/Settings/Settings.tsx:215 @@ -1170,12 +1182,12 @@ msgstr "" msgid "Apply Pull Request" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:643 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:664 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:612 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:651 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:633 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:672 msgid "Archived post" msgstr "" @@ -1394,7 +1406,7 @@ msgstr "" msgid "Blocked accounts" msgstr "" -#: src/Navigation.tsx:191 +#: src/Navigation.tsx:192 #: src/view/screens/ModerationBlockedAccounts.tsx:104 msgid "Blocked Accounts" msgstr "" @@ -1429,7 +1441,7 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:668 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:689 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1593,7 +1605,7 @@ msgstr "" #: src/components/live/GoLiveDialog.tsx:247 #: src/components/live/GoLiveDialog.tsx:253 #: src/components/Menu/index.tsx:350 -#: src/components/PostControls/RepostButton.tsx:209 +#: src/components/PostControls/RepostButton.tsx:211 #: src/components/Prompt.tsx:144 #: src/components/Prompt.tsx:146 #: src/screens/Deactivated.tsx:158 @@ -1615,7 +1627,7 @@ msgstr "" #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/modals/CreateOrEditList.tsx:333 #: src/view/com/modals/CropImage.web.tsx:97 -#: src/view/shell/desktop/LeftNav.tsx:212 +#: src/view/shell/desktop/LeftNav.tsx:213 msgid "Cancel" msgstr "" @@ -1635,7 +1647,7 @@ msgstr "" msgid "Cancel image crop" msgstr "" -#: src/components/PostControls/RepostButton.tsx:203 +#: src/components/PostControls/RepostButton.tsx:205 msgid "Cancel quote post" msgstr "" @@ -1647,9 +1659,9 @@ msgstr "" msgid "Cancel search" msgstr "" -#: src/components/PostControls/index.tsx:101 -#: src/components/PostControls/index.tsx:132 -#: src/components/PostControls/index.tsx:160 +#: src/components/PostControls/index.tsx:104 +#: src/components/PostControls/index.tsx:135 +#: src/components/PostControls/index.tsx:163 #: src/state/shell/composer/index.tsx:94 msgid "Cannot interact with a blocked user" msgstr "" @@ -1724,10 +1736,10 @@ msgid "Changes saved" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:99 -#: src/Navigation.tsx:548 +#: src/Navigation.tsx:549 #: src/view/shell/bottom-bar/BottomBar.tsx:221 -#: src/view/shell/desktop/LeftNav.tsx:606 -#: src/view/shell/Drawer.tsx:455 +#: src/view/shell/desktop/LeftNav.tsx:607 +#: src/view/shell/Drawer.tsx:466 msgid "Chat" msgstr "" @@ -1750,7 +1762,7 @@ msgctxt "toast" msgid "Chat muted" msgstr "" -#: src/Navigation.tsx:558 +#: src/Navigation.tsx:559 #: src/screens/Messages/components/InboxPreview.tsx:22 msgid "Chat request inbox" msgstr "" @@ -1762,7 +1774,7 @@ msgid "Chat requests" msgstr "" #: src/components/dms/ConvoMenu.tsx:76 -#: src/Navigation.tsx:553 +#: src/Navigation.tsx:554 #: src/screens/Messages/ChatList.tsx:367 msgid "Chat settings" msgstr "" @@ -1904,6 +1916,8 @@ msgstr "" #: src/components/dialogs/GifSelect.tsx:269 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:158 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:167 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:159 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:167 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 #: src/components/dialogs/SearchablePeopleList.tsx:295 @@ -2035,7 +2049,7 @@ msgid "Comics" msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:45 -#: src/Navigation.tsx:340 +#: src/Navigation.tsx:341 #: src/view/screens/CommunityGuidelines.tsx:34 msgid "Community Guidelines" msgstr "" @@ -2049,7 +2063,7 @@ msgstr "" msgid "Complete the challenge" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:571 +#: src/view/shell/desktop/LeftNav.tsx:572 msgid "Compose new post" msgstr "" @@ -2151,7 +2165,7 @@ msgstr "" msgid "Content and media" msgstr "" -#: src/Navigation.tsx:507 +#: src/Navigation.tsx:508 msgid "Content and Media" msgstr "" @@ -2352,7 +2366,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:40 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:107 -#: src/Navigation.tsx:345 +#: src/Navigation.tsx:346 #: src/view/screens/CopyrightPolicy.tsx:31 msgid "Copyright Policy" msgstr "" @@ -2412,7 +2426,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:178 #: src/components/StarterPack/ProfileStarterPacks.tsx:287 -#: src/Navigation.tsx:588 +#: src/Navigation.tsx:589 msgid "Create a starter pack" msgstr "" @@ -2983,7 +2997,7 @@ msgstr "" msgid "Edit Moderation List" msgstr "" -#: src/Navigation.tsx:355 +#: src/Navigation.tsx:356 #: src/view/screens/Feeds.tsx:518 msgid "Edit My Feeds" msgstr "" @@ -3025,7 +3039,7 @@ msgstr "" msgid "Edit who can reply" msgstr "" -#: src/Navigation.tsx:593 +#: src/Navigation.tsx:594 msgid "Edit your starter pack" msgstr "" @@ -3360,10 +3374,10 @@ msgstr "" msgid "Explicit sexual images." msgstr "" -#: src/Navigation.tsx:750 +#: src/Navigation.tsx:759 #: src/screens/Search/Shell.tsx:307 -#: src/view/shell/desktop/LeftNav.tsx:688 -#: src/view/shell/Drawer.tsx:403 +#: src/view/shell/desktop/LeftNav.tsx:689 +#: src/view/shell/Drawer.tsx:414 msgid "Explore" msgstr "" @@ -3396,7 +3410,7 @@ msgstr "" msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." msgstr "" -#: src/Navigation.tsx:374 +#: src/Navigation.tsx:375 #: src/screens/Settings/ExternalMediaPreferences.tsx:34 msgid "External Media Preferences" msgstr "" @@ -3600,7 +3614,7 @@ msgstr "" msgid "Failed to verify handle. Please try again." msgstr "" -#: src/Navigation.tsx:290 +#: src/Navigation.tsx:291 msgid "Feed" msgstr "" @@ -3631,7 +3645,7 @@ msgstr "" #: src/view/shell/desktop/RightNav.tsx:106 #: src/view/shell/desktop/RightNav.tsx:107 -#: src/view/shell/Drawer.tsx:357 +#: src/view/shell/Drawer.tsx:368 msgid "Feedback" msgstr "" @@ -3641,14 +3655,14 @@ msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" -#: src/Navigation.tsx:573 +#: src/Navigation.tsx:574 #: src/screens/Search/SearchResults.tsx:73 #: src/screens/StarterPack/StarterPackScreen.tsx:190 #: src/view/screens/Feeds.tsx:511 #: src/view/screens/Profile.tsx:230 #: src/view/screens/SavedFeeds.tsx:104 -#: src/view/shell/desktop/LeftNav.tsx:726 -#: src/view/shell/Drawer.tsx:519 +#: src/view/shell/desktop/LeftNav.tsx:727 +#: src/view/shell/Drawer.tsx:530 msgid "Feeds" msgstr "" @@ -3699,6 +3713,10 @@ msgstr "" msgid "Finalizing" msgstr "" +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:150 +msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime." +msgstr "" + #: src/view/com/posts/CustomFeedEmptyState.tsx:48 #: src/view/com/posts/FollowingEmptyState.tsx:53 #: src/view/com/posts/FollowingEndOfFeed.tsx:54 @@ -3826,7 +3844,7 @@ msgstr "" msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" msgstr "" -#: src/Navigation.tsx:244 +#: src/Navigation.tsx:245 msgid "Followers of @{0} that you know" msgstr "" @@ -3865,7 +3883,7 @@ msgstr "" msgid "Following feed preferences" msgstr "" -#: src/Navigation.tsx:361 +#: src/Navigation.tsx:362 #: src/screens/Settings/FollowingFeedPreferences.tsx:56 msgid "Following Feed Preferences" msgstr "" @@ -3959,7 +3977,7 @@ msgstr "" msgid "Generate a starter pack" msgstr "" -#: src/view/shell/Drawer.tsx:361 +#: src/view/shell/Drawer.tsx:372 msgid "Get help" msgstr "" @@ -4077,6 +4095,12 @@ msgstr "" msgid "Go back to previous step" msgstr "" +#: src/screens/Bookmarks/components/EmptyState.tsx:43 +#: src/screens/Bookmarks/components/EmptyState.tsx:51 +msgctxt "Button to go back to the home timeline" +msgid "Go home" +msgstr "" + #: src/view/screens/NotFound.tsx:57 msgid "Go home" msgstr "" @@ -4121,8 +4145,8 @@ msgid "Go to next" msgstr "" #: src/components/dms/ConvoMenu.tsx:227 -#: src/view/shell/desktop/LeftNav.tsx:316 -#: src/view/shell/desktop/LeftNav.tsx:322 +#: src/view/shell/desktop/LeftNav.tsx:317 +#: src/view/shell/desktop/LeftNav.tsx:323 msgid "Go to profile" msgstr "" @@ -4169,7 +4193,7 @@ msgstr "" msgid "Harassment, trolling, or intolerance" msgstr "" -#: src/Navigation.tsx:538 +#: src/Navigation.tsx:539 msgid "Hashtag" msgstr "" @@ -4190,7 +4214,7 @@ msgstr "" #: src/screens/Settings/Settings.tsx:240 #: src/view/shell/desktop/RightNav.tsx:124 #: src/view/shell/desktop/RightNav.tsx:125 -#: src/view/shell/Drawer.tsx:370 +#: src/view/shell/Drawer.tsx:381 msgid "Help" msgstr "" @@ -4331,11 +4355,11 @@ msgstr "" msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" -#: src/Navigation.tsx:745 -#: src/Navigation.tsx:765 +#: src/Navigation.tsx:754 +#: src/Navigation.tsx:774 #: src/view/shell/bottom-bar/BottomBar.tsx:178 -#: src/view/shell/desktop/LeftNav.tsx:670 -#: src/view/shell/Drawer.tsx:429 +#: src/view/shell/desktop/LeftNav.tsx:671 +#: src/view/shell/Drawer.tsx:440 msgid "Home" msgstr "" @@ -4526,6 +4550,10 @@ msgstr "" msgid "Introducing activity notifications" msgstr "" +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:37 +msgid "Introducing saved posts AKA bookmarks" +msgstr "" + #: src/screens/Login/LoginForm.tsx:156 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:70 msgid "Invalid 2FA confirmation code." @@ -4663,7 +4691,7 @@ msgstr "" msgid "Labels on your content" msgstr "" -#: src/Navigation.tsx:217 +#: src/Navigation.tsx:218 msgid "Language Settings" msgstr "" @@ -4815,7 +4843,7 @@ msgid "Like" msgstr "" #. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form -#: src/components/PostControls/index.tsx:253 +#: src/components/PostControls/index.tsx:271 msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -4828,7 +4856,7 @@ msgstr "" msgid "Like 10 posts to train the Discover feed" msgstr "" -#: src/Navigation.tsx:451 +#: src/Navigation.tsx:452 msgid "Like notifications" msgstr "" @@ -4840,8 +4868,8 @@ msgstr "" msgid "Like this labeler" msgstr "" -#: src/Navigation.tsx:295 -#: src/Navigation.tsx:300 +#: src/Navigation.tsx:296 +#: src/Navigation.tsx:301 msgid "Liked by" msgstr "" @@ -4876,11 +4904,11 @@ msgstr "" msgid "Likes of your reposts" msgstr "" -#: src/Navigation.tsx:475 +#: src/Navigation.tsx:476 msgid "Likes of your reposts notifications" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:471 msgid "Likes on this post" msgstr "" @@ -4889,7 +4917,7 @@ msgstr "" msgid "Linear" msgstr "" -#: src/Navigation.tsx:250 +#: src/Navigation.tsx:251 msgid "List" msgstr "" @@ -4951,12 +4979,12 @@ msgctxt "toast" msgid "List unmuted" msgstr "" -#: src/Navigation.tsx:171 +#: src/Navigation.tsx:172 #: src/view/screens/Lists.tsx:65 #: src/view/screens/Profile.tsx:224 #: src/view/screens/Profile.tsx:232 -#: src/view/shell/desktop/LeftNav.tsx:744 -#: src/view/shell/Drawer.tsx:534 +#: src/view/shell/desktop/LeftNav.tsx:745 +#: src/view/shell/Drawer.tsx:545 msgid "Lists" msgstr "" @@ -5004,7 +5032,7 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/Navigation.tsx:320 +#: src/Navigation.tsx:321 msgid "Log" msgstr "" @@ -5017,7 +5045,7 @@ msgid "Logo by @sawaratsuki.bsky.social" msgstr "" #: src/view/shell/desktop/RightNav.tsx:131 -#: src/view/shell/Drawer.tsx:672 +#: src/view/shell/Drawer.tsx:709 msgid "Logo by <0>@sawaratsuki.bsky.social" msgstr "" @@ -5095,7 +5123,7 @@ msgstr "" msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" -#: src/Navigation.tsx:435 +#: src/Navigation.tsx:436 msgid "Mention notifications" msgstr "" @@ -5152,7 +5180,7 @@ msgstr "" msgid "Message options" msgstr "" -#: src/Navigation.tsx:760 +#: src/Navigation.tsx:769 msgid "Messages" msgstr "" @@ -5161,7 +5189,7 @@ msgctxt "Name of app icon variant" msgid "Midnight" msgstr "" -#: src/Navigation.tsx:499 +#: src/Navigation.tsx:500 msgid "Miscellaneous notifications" msgstr "" @@ -5175,7 +5203,7 @@ msgstr "" msgid "Misleading Post" msgstr "" -#: src/Navigation.tsx:176 +#: src/Navigation.tsx:177 #: src/screens/Moderation/index.tsx:100 #: src/screens/Settings/Settings.tsx:188 #: src/screens/Settings/Settings.tsx:191 @@ -5214,7 +5242,7 @@ msgstr "" msgid "Moderation lists" msgstr "" -#: src/Navigation.tsx:181 +#: src/Navigation.tsx:182 #: src/view/screens/ModerationModlists.tsx:65 msgid "Moderation Lists" msgstr "" @@ -5223,7 +5251,7 @@ msgstr "" msgid "moderation settings" msgstr "" -#: src/Navigation.tsx:310 +#: src/Navigation.tsx:311 msgid "Moderation states" msgstr "" @@ -5332,7 +5360,7 @@ msgstr "" msgid "Muted accounts" msgstr "" -#: src/Navigation.tsx:186 +#: src/Navigation.tsx:187 #: src/view/screens/ModerationMutedAccounts.tsx:118 msgid "Muted Accounts" msgstr "" @@ -5400,7 +5428,7 @@ msgstr "" msgid "Navigates to the next screen" msgstr "" -#: src/view/shell/Drawer.tsx:77 +#: src/view/shell/Drawer.tsx:78 msgid "Navigates to your profile" msgstr "" @@ -5450,11 +5478,12 @@ msgid "New email address" msgstr "" #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:74 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:73 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:63 msgid "New Feature" msgstr "" -#: src/Navigation.tsx:467 +#: src/Navigation.tsx:468 msgid "New follower notifications" msgstr "" @@ -5503,7 +5532,7 @@ msgctxt "action" msgid "New post" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:579 +#: src/view/shell/desktop/LeftNav.tsx:580 msgctxt "action" msgid "New Post" msgstr "" @@ -5721,7 +5750,7 @@ msgstr "" msgid "Not followed by anyone you're following" msgstr "" -#: src/Navigation.tsx:166 +#: src/Navigation.tsx:167 #: src/view/screens/Profile.tsx:125 msgid "Not Found" msgstr "" @@ -5746,8 +5775,12 @@ msgstr "" msgid "Nothing here" msgstr "" -#: src/Navigation.tsx:421 -#: src/Navigation.tsx:568 +#: src/screens/Bookmarks/components/EmptyState.tsx:35 +msgid "Nothing saved yet" +msgstr "" + +#: src/Navigation.tsx:422 +#: src/Navigation.tsx:569 #: src/view/screens/Notifications.tsx:136 msgid "Notification settings" msgstr "" @@ -5760,8 +5793,8 @@ msgstr "" msgid "Notification Sounds" msgstr "" -#: src/Navigation.tsx:563 -#: src/Navigation.tsx:755 +#: src/Navigation.tsx:564 +#: src/Navigation.tsx:764 #: src/screens/Notifications/ActivityList.tsx:29 #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:90 #: src/screens/Settings/NotificationSettings/index.tsx:92 @@ -5778,8 +5811,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:199 #: src/view/screens/Notifications.tsx:130 #: src/view/shell/bottom-bar/BottomBar.tsx:252 -#: src/view/shell/desktop/LeftNav.tsx:707 -#: src/view/shell/Drawer.tsx:482 +#: src/view/shell/desktop/LeftNav.tsx:708 +#: src/view/shell/Drawer.tsx:493 msgid "Notifications" msgstr "" @@ -5829,7 +5862,7 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:673 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:694 msgid "Okay" msgstr "" @@ -5958,7 +5991,7 @@ msgstr "" msgid "Open pack" msgstr "" -#: src/components/PostControls/PostMenu/index.tsx:62 +#: src/components/PostControls/PostMenu/index.tsx:65 msgid "Open post options menu" msgstr "" @@ -5967,7 +6000,7 @@ msgstr "" msgid "Open profile" msgstr "" -#: src/components/PostControls/ShareMenu/index.tsx:87 +#: src/components/PostControls/ShareMenu/index.tsx:90 msgid "Open share menu" msgstr "" @@ -6177,11 +6210,11 @@ msgstr "" msgid "People" msgstr "" -#: src/Navigation.tsx:237 +#: src/Navigation.tsx:238 msgid "People followed by @{0}" msgstr "" -#: src/Navigation.tsx:230 +#: src/Navigation.tsx:231 msgid "People following @{0}" msgstr "" @@ -6454,10 +6487,10 @@ msgstr "" msgid "Post blocked" msgstr "" -#: src/Navigation.tsx:263 -#: src/Navigation.tsx:270 -#: src/Navigation.tsx:277 -#: src/Navigation.tsx:284 +#: src/Navigation.tsx:264 +#: src/Navigation.tsx:271 +#: src/Navigation.tsx:278 +#: src/Navigation.tsx:285 msgid "Post by @{0}" msgstr "" @@ -6491,7 +6524,7 @@ msgstr "" msgid "Post interaction settings" msgstr "" -#: src/Navigation.tsx:197 +#: src/Navigation.tsx:198 #: src/screens/ModerationInteractionSettings/index.tsx:34 msgid "Post Interaction Settings" msgstr "" @@ -6511,6 +6544,10 @@ msgctxt "toast" msgid "Post pinned" msgstr "" +#: src/components/PostControls/BookmarkButton.tsx:57 +msgid "Post saved" +msgstr "" + #: src/state/queries/pinned-post.ts:61 msgctxt "toast" msgid "Post unpinned" @@ -6583,8 +6620,8 @@ msgstr "" msgid "Privacy and security" msgstr "" -#: src/Navigation.tsx:406 -#: src/Navigation.tsx:414 +#: src/Navigation.tsx:407 +#: src/Navigation.tsx:415 #: src/screens/Settings/ActivityPrivacySettings.tsx:40 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:45 msgid "Privacy and Security" @@ -6596,12 +6633,12 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:35 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:102 -#: src/Navigation.tsx:330 +#: src/Navigation.tsx:331 #: src/screens/Settings/AboutSettings.tsx:92 #: src/screens/Settings/AboutSettings.tsx:95 #: src/view/screens/PrivacyPolicy.tsx:31 -#: src/view/shell/Drawer.tsx:667 -#: src/view/shell/Drawer.tsx:668 +#: src/view/shell/Drawer.tsx:704 +#: src/view/shell/Drawer.tsx:705 msgid "Privacy Policy" msgstr "" @@ -6620,9 +6657,9 @@ msgid "profile" msgstr "" #: src/view/shell/bottom-bar/BottomBar.tsx:316 -#: src/view/shell/desktop/LeftNav.tsx:762 -#: src/view/shell/Drawer.tsx:76 -#: src/view/shell/Drawer.tsx:559 +#: src/view/shell/desktop/LeftNav.tsx:781 +#: src/view/shell/Drawer.tsx:77 +#: src/view/shell/Drawer.tsx:596 msgid "Profile" msgstr "" @@ -6691,12 +6728,12 @@ msgstr "" msgid "QR code saved to your camera roll!" msgstr "" -#: src/Navigation.tsx:443 +#: src/Navigation.tsx:444 msgid "Quote notifications" msgstr "" -#: src/components/PostControls/RepostButton.tsx:174 -#: src/components/PostControls/RepostButton.tsx:197 +#: src/components/PostControls/RepostButton.tsx:176 +#: src/components/PostControls/RepostButton.tsx:199 #: src/components/PostControls/RepostButton.web.tsx:84 #: src/components/PostControls/RepostButton.web.tsx:91 msgid "Quote post" @@ -6710,8 +6747,8 @@ msgstr "" msgid "Quote post was successfully detached" msgstr "" -#: src/components/PostControls/RepostButton.tsx:173 -#: src/components/PostControls/RepostButton.tsx:195 +#: src/components/PostControls/RepostButton.tsx:175 +#: src/components/PostControls/RepostButton.tsx:197 #: src/components/PostControls/RepostButton.web.tsx:83 #: src/components/PostControls/RepostButton.web.tsx:90 msgid "Quote posts disabled" @@ -6728,7 +6765,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:450 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:455 msgid "Quotes of this post" msgstr "" @@ -6851,6 +6888,7 @@ msgstr "" #: src/components/FeedCard.tsx:343 #: src/components/StarterPack/Wizard/WizardListCard.tsx:104 #: src/components/StarterPack/Wizard/WizardListCard.tsx:111 +#: src/screens/Bookmarks/index.tsx:255 #: src/screens/Settings/Settings.tsx:662 #: src/view/com/modals/UserAddRemoveLists.tsx:235 #: src/view/com/posts/PostFeedErrorMessage.tsx:217 @@ -6914,6 +6952,11 @@ msgstr "" msgid "Remove from saved feeds" msgstr "" +#: src/components/PostControls/BookmarkButton.tsx:125 +#: src/screens/Bookmarks/index.tsx:249 +msgid "Remove from saved posts" +msgstr "" + #: src/components/FeedCard.tsx:338 msgid "Remove from your feeds?" msgstr "" @@ -6935,8 +6978,8 @@ msgstr "" msgid "Remove profile" msgstr "" -#: src/components/PostControls/RepostButton.tsx:151 -#: src/components/PostControls/RepostButton.tsx:161 +#: src/components/PostControls/RepostButton.tsx:153 +#: src/components/PostControls/RepostButton.tsx:163 msgid "Remove repost" msgstr "" @@ -6980,6 +7023,11 @@ msgstr "" msgid "Removed from saved feeds" msgstr "" +#: src/components/PostControls/BookmarkButton.tsx:92 +#: src/screens/Bookmarks/index.tsx:207 +msgid "Removed from saved posts" +msgstr "" + #: src/components/dialogs/StarterPackDialog.tsx:277 msgid "Removed from starter pack" msgstr "" @@ -7043,7 +7091,7 @@ msgid "Reply" msgstr "" #. Accessibility label for the reply button, verb form followed by number of replies and noun form -#: src/components/PostControls/index.tsx:207 +#: src/components/PostControls/index.tsx:222 msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" @@ -7057,7 +7105,7 @@ msgstr "" msgid "Reply Hidden by You" msgstr "" -#: src/Navigation.tsx:427 +#: src/Navigation.tsx:428 msgid "Reply notifications" msgstr "" @@ -7170,8 +7218,8 @@ msgstr "" msgid "Report this user" msgstr "" -#: src/components/PostControls/RepostButton.tsx:152 -#: src/components/PostControls/RepostButton.tsx:163 +#: src/components/PostControls/RepostButton.tsx:154 +#: src/components/PostControls/RepostButton.tsx:165 #: src/components/PostControls/RepostButton.web.tsx:68 #: src/components/PostControls/RepostButton.web.tsx:75 msgctxt "action" @@ -7179,15 +7227,15 @@ msgid "Repost" msgstr "" #. Accessibility label for the repost button when the post has not been reposted, verb form followed by number of reposts and noun form -#: src/components/PostControls/RepostButton.tsx:76 +#: src/components/PostControls/RepostButton.tsx:78 msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" -#: src/Navigation.tsx:459 +#: src/Navigation.tsx:460 msgid "Repost notifications" msgstr "" -#: src/components/PostControls/RepostButton.tsx:144 +#: src/components/PostControls/RepostButton.tsx:146 #: src/components/PostControls/RepostButton.web.tsx:43 #: src/components/PostControls/RepostButton.web.tsx:103 #: src/screens/StarterPack/StarterPackScreen.tsx:561 @@ -7217,7 +7265,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:432 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:437 msgid "Reposts of this post" msgstr "" @@ -7227,7 +7275,7 @@ msgstr "" msgid "Reposts of your reposts" msgstr "" -#: src/Navigation.tsx:483 +#: src/Navigation.tsx:484 msgid "Reposts of your reposts notifications" msgstr "" @@ -7406,10 +7454,21 @@ msgstr "" msgid "Save to my feeds" msgstr "" +#: src/view/shell/desktop/LeftNav.tsx:763 +#: src/view/shell/Drawer.tsx:571 +msgid "Saved" +msgstr "" + #: src/view/screens/SavedFeeds.tsx:172 msgid "Saved Feeds" msgstr "" +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:139 +#: src/Navigation.tsx:608 +#: src/screens/Bookmarks/index.tsx:55 +msgid "Saved Posts" +msgstr "" + #: src/screens/Profile/components/ProfileFeedHeader.tsx:132 #: src/view/screens/ProfileList.tsx:372 msgid "Saved to your feeds" @@ -7419,6 +7478,10 @@ msgstr "" msgid "Saves image crop settings" msgstr "" +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:483 +msgid "Saves of this post" +msgstr "" + #: src/components/dms/ChatEmptyPill.tsx:33 #: src/components/NewskieDialog.tsx:105 #: src/view/com/notifications/NotificationFeedItem.tsx:751 @@ -7452,7 +7515,7 @@ msgstr "" msgid "Search" msgstr "" -#: src/Navigation.tsx:256 +#: src/Navigation.tsx:257 #: src/screens/Profile/ProfileSearch.tsx:37 msgid "Search @{0}'s posts" msgstr "" @@ -7734,7 +7797,7 @@ msgctxt "action" msgid "Send Email" msgstr "" -#: src/view/shell/Drawer.tsx:350 +#: src/view/shell/Drawer.tsx:361 msgid "Send feedback" msgstr "" @@ -7807,10 +7870,10 @@ msgstr "" msgid "Sets email for password reset" msgstr "" -#: src/Navigation.tsx:212 +#: src/Navigation.tsx:213 #: src/screens/Settings/Settings.tsx:99 -#: src/view/shell/desktop/LeftNav.tsx:780 -#: src/view/shell/Drawer.tsx:572 +#: src/view/shell/desktop/LeftNav.tsx:799 +#: src/view/shell/Drawer.tsx:609 msgid "Settings" msgstr "" @@ -7947,7 +8010,7 @@ msgstr "" msgid "Share your favorite feed!" msgstr "" -#: src/Navigation.tsx:315 +#: src/Navigation.tsx:316 msgid "Shared Preferences Tester" msgstr "" @@ -8049,7 +8112,7 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:614 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 msgid "Shows information about when this post was created" msgstr "" @@ -8119,9 +8182,9 @@ msgstr "" #: src/screens/SignupQueued.tsx:93 #: src/screens/SignupQueued.tsx:96 #: src/screens/Takendown.tsx:85 -#: src/view/shell/desktop/LeftNav.tsx:211 -#: src/view/shell/desktop/LeftNav.tsx:268 -#: src/view/shell/desktop/LeftNav.tsx:271 +#: src/view/shell/desktop/LeftNav.tsx:212 +#: src/view/shell/desktop/LeftNav.tsx:269 +#: src/view/shell/desktop/LeftNav.tsx:272 msgid "Sign out" msgstr "" @@ -8130,7 +8193,7 @@ msgid "Sign Out" msgstr "" #: src/screens/Settings/Settings.tsx:285 -#: src/view/shell/desktop/LeftNav.tsx:208 +#: src/view/shell/desktop/LeftNav.tsx:209 msgid "Sign out?" msgstr "" @@ -8302,8 +8365,8 @@ msgstr "" msgid "Start chat with {displayName}" msgstr "" -#: src/Navigation.tsx:578 -#: src/Navigation.tsx:583 +#: src/Navigation.tsx:579 +#: src/Navigation.tsx:584 #: src/screens/StarterPack/Wizard/index.tsx:209 msgid "Starter Pack" msgstr "" @@ -8347,7 +8410,7 @@ msgstr "" msgid "Storage cleared, you need to restart the app now." msgstr "" -#: src/Navigation.tsx:305 +#: src/Navigation.tsx:306 #: src/screens/Settings/Settings.tsx:448 msgid "Storybook" msgstr "" @@ -8437,7 +8500,7 @@ msgctxt "Name of app icon variant" msgid "Sunset" msgstr "" -#: src/Navigation.tsx:325 +#: src/Navigation.tsx:326 #: src/view/screens/Support.tsx:31 #: src/view/screens/Support.tsx:34 msgid "Support" @@ -8446,7 +8509,7 @@ msgstr "" #: src/screens/Settings/Settings.tsx:123 #: src/screens/Settings/Settings.tsx:137 #: src/screens/Settings/Settings.tsx:604 -#: src/view/shell/desktop/LeftNav.tsx:246 +#: src/view/shell/desktop/LeftNav.tsx:247 msgid "Switch account" msgstr "" @@ -8455,11 +8518,11 @@ msgstr "" msgid "Switch Account" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:109 +#: src/view/shell/desktop/LeftNav.tsx:110 msgid "Switch accounts" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:345 +#: src/view/shell/desktop/LeftNav.tsx:346 msgid "Switch to {0}" msgstr "" @@ -8544,12 +8607,12 @@ msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:135 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:30 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:97 -#: src/Navigation.tsx:335 +#: src/Navigation.tsx:336 #: src/screens/Settings/AboutSettings.tsx:84 #: src/screens/Settings/AboutSettings.tsx:87 #: src/view/screens/TermsOfService.tsx:31 -#: src/view/shell/Drawer.tsx:660 -#: src/view/shell/Drawer.tsx:662 +#: src/view/shell/Drawer.tsx:697 +#: src/view/shell/Drawer.tsx:699 msgid "Terms of Service" msgstr "" @@ -8979,7 +9042,7 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:654 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:675 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -8991,6 +9054,10 @@ msgstr "" msgid "This post is only visible to logged-in users." msgstr "" +#: src/screens/Bookmarks/index.tsx:245 +msgid "This post was deleted by its author" +msgstr "" + #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" @@ -9087,7 +9154,7 @@ msgstr "" msgid "Threaded" msgstr "" -#: src/Navigation.tsx:368 +#: src/Navigation.tsx:369 msgid "Threads Preferences" msgstr "" @@ -9137,7 +9204,7 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:543 +#: src/Navigation.tsx:544 msgid "Topic" msgstr "" @@ -9145,8 +9212,8 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:139 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:576 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:579 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:597 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:600 msgid "Translate" msgstr "" @@ -9262,13 +9329,18 @@ msgstr "" msgid "Unblock list" msgstr "" +#: src/components/PostControls/BookmarkButton.tsx:38 +msgctxt "Button label to undo saving/removing a post from saved posts." +msgid "Undo" +msgstr "" + #: src/components/PostControls/RepostButton.web.tsx:67 #: src/components/PostControls/RepostButton.web.tsx:74 msgid "Undo repost" msgstr "" #. Accessibility label for the repost button when the post has been reposted, verb followed by number of reposts and noun -#: src/components/PostControls/RepostButton.tsx:66 +#: src/components/PostControls/RepostButton.tsx:68 msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" @@ -9307,7 +9379,7 @@ msgid "Unlike" msgstr "" #. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun -#: src/components/PostControls/index.tsx:243 +#: src/components/PostControls/index.tsx:261 msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -9633,7 +9705,7 @@ msgstr "" msgid "Verification settings" msgstr "" -#: src/Navigation.tsx:205 +#: src/Navigation.tsx:206 #: src/screens/Moderation/VerificationSettings.tsx:32 msgid "Verification Settings" msgstr "" @@ -9725,7 +9797,7 @@ msgstr "" msgid "Video failed to process" msgstr "" -#: src/Navigation.tsx:599 +#: src/Navigation.tsx:600 msgid "Video Feed" msgstr "" @@ -10549,7 +10621,7 @@ msgid "You reacted {0} to {1}" msgstr "" #: src/screens/Settings/Settings.tsx:286 -#: src/view/shell/desktop/LeftNav.tsx:209 +#: src/view/shell/desktop/LeftNav.tsx:210 msgid "You will be signed out of all your accounts." msgstr "" @@ -10740,7 +10812,7 @@ msgstr "" msgid "Your full handle will be <0>@{0}" msgstr "" -#: src/Navigation.tsx:515 +#: src/Navigation.tsx:516 #: src/screens/Search/modules/ExploreInterestsCard.tsx:67 #: src/screens/Settings/ContentAndMediaSettings.tsx:92 #: src/screens/Settings/ContentAndMediaSettings.tsx:95 -- 2.51.2 From c129108b786a3389181c401b0bdfe1a3de528ebb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Sep 2025 19:36:23 -0500 Subject: [PATCH 11/12] 108 fixes (#8977) * Translation comment * Fix error handling in starter pack generation * Allow access to DM settings for age restricted users * Leave post stat unit formatting up to translators --- src/components/PostControls/RepostButton.tsx | 9 ++- src/components/PostControls/index.tsx | 11 ++-- src/components/PostControls/util.ts | 58 +++++++++++++------ .../StarterPack/ProfileStarterPacks.tsx | 2 +- .../ageAssurance/AgeRestrictedScreen.tsx | 6 +- .../dialogs/nuxs/BookmarksAnnouncement.tsx | 6 +- src/lib/custom-animations/CountWheel.tsx | 14 ++--- src/lib/custom-animations/CountWheel.web.tsx | 8 +-- src/lib/notifications/notifications.ts | 3 +- src/screens/Messages/ChatList.tsx | 13 ++++- src/screens/Messages/Settings.tsx | 13 +---- .../components/ThreadItemAnchor.tsx | 13 +++-- 12 files changed, 89 insertions(+), 67 deletions(-) diff --git a/src/components/PostControls/RepostButton.tsx b/src/components/PostControls/RepostButton.tsx index 522e80dd3..d4a3960a7 100644 --- a/src/components/PostControls/RepostButton.tsx +++ b/src/components/PostControls/RepostButton.tsx @@ -10,7 +10,7 @@ import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote' import {Repost_Stroke2_Corner3_Rounded as Repost} from '#/components/icons/Repost' -import {formatPostStatCount} from '#/components/PostControls/util' +import {useFormatPostStatCount} from '#/components/PostControls/util' import {Text} from '#/components/Typography' import { PostControlButton, @@ -25,7 +25,6 @@ interface Props { onQuote: () => void big?: boolean embeddingDisabled: boolean - compactCount?: boolean } let RepostButton = ({ @@ -35,12 +34,12 @@ let RepostButton = ({ onQuote, big, embeddingDisabled, - compactCount, }: Props): React.ReactNode => { const t = useTheme() - const {_, i18n} = useLingui() + const {_} = useLingui() const requireAuth = useRequireAuth() const dialogControl = Dialog.useDialogControl() + const formatPostStatCount = useFormatPostStatCount() const onPress = () => requireAuth(() => dialogControl.open()) @@ -88,7 +87,7 @@ let RepostButton = ({ {typeof repostCount !== 'undefined' && repostCount > 0 && ( - {formatPostStatCount(i18n, repostCount, {compact: compactCount})} + {formatPostStatCount(repostCount)} )} diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 834ad8e7d..f91bcd8a5 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -27,7 +27,7 @@ import { import * as Toast from '#/view/com/util/Toast' import {atoms as a, flatten, useBreakpoints} from '#/alf' import {Reply as Bubble} from '#/components/icons/Reply' -import {formatPostStatCount} from '#/components/PostControls/util' +import {useFormatPostStatCount} from '#/components/PostControls/util' import {BookmarkButton} from './BookmarkButton' import { PostControlButton, @@ -69,7 +69,7 @@ let PostControls = ({ viaRepost?: {uri: string; cid: string} variant?: 'compact' | 'normal' | 'large' }): React.ReactNode => { - const {_, i18n} = useLingui() + const {_} = useLingui() const {openComposer} = useOpenComposer() const {feedDescriptor} = useFeedFeedbackContext() const [queueLike, queueUnlike] = usePostLikeMutationQueue( @@ -95,6 +95,7 @@ let PostControls = ({ ) const replyDisabled = post.viewer?.replyDisabled const {gtPhone} = useBreakpoints() + const formatPostStatCount = useFormatPostStatCount() const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = useState(false) @@ -232,9 +233,7 @@ let PostControls = ({ {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && ( - {formatPostStatCount(i18n, post.replyCount, { - compact: variant === 'compact', - })} + {formatPostStatCount(post.replyCount)} )} @@ -247,7 +246,6 @@ let PostControls = ({ onQuote={onQuote} big={big} embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} - compactCount={variant === 'compact'} /> @@ -288,7 +286,6 @@ let PostControls = ({ big={big} isLiked={Boolean(post.viewer?.like)} hasBeenToggled={hasLikeIconBeenToggled} - compactCount={variant === 'compact'} /> diff --git a/src/components/PostControls/util.ts b/src/components/PostControls/util.ts index 5d3ea74e4..b8050a85a 100644 --- a/src/components/PostControls/util.ts +++ b/src/components/PostControls/util.ts @@ -1,24 +1,48 @@ -import {type I18n} from '@lingui/core' +import {useCallback} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' /** * This matches `formatCount` from `view/com/util/numeric/format.ts`, but has * additional truncation logic for large numbers. `roundingMode` should always * match the original impl, regardless of if we add more formatting here. */ -export function formatPostStatCount( - i18n: I18n, - count: number, - { - compact = false, - }: { - compact?: boolean - } = {}, -): string { - const isOver10k = count >= 10_000 - return i18n.number(count, { - notation: 'compact', - maximumFractionDigits: isOver10k || compact ? 0 : 1, - // @ts-expect-error - roundingMode not in the types - roundingMode: 'trunc', - }) +export function useFormatPostStatCount() { + const {i18n} = useLingui() + + return useCallback( + (postStatCount: number) => { + const isOver1k = postStatCount >= 1_000 + const isOver10k = postStatCount >= 10_000 + const isOver1M = postStatCount >= 1_000_000 + const formatted = i18n.number(postStatCount, { + notation: 'compact', + maximumFractionDigits: isOver10k ? 0 : 1, + // @ts-expect-error - roundingMode not in the types + roundingMode: 'trunc', + }) + const count = formatted.replace(/\D+$/g, '') + + if (isOver1M) { + return i18n._( + msg({ + message: `${count}M`, + comment: + 'For post statistics. Indicates a number in the millions. Please use the shortest format appropriate for your language.', + }), + ) + } else if (isOver1k) { + return i18n._( + msg({ + message: `${count}K`, + comment: + 'For post statistics. Indicates a number in the thousands. Please use the shortest format appropriate for your language.', + }), + ) + } else { + return count + } + }, + [i18n], + ) } diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index 73aee28f4..bbe0bc52b 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -214,7 +214,7 @@ function Empty() { onError: e => { logger.error('Failed to generate starter pack', {safeMessage: e}) setIsGenerating(false) - if (e.name === 'NOT_ENOUGH_FOLLOWERS') { + if (e.message.includes('NOT_ENOUGH_FOLLOWERS')) { followersDialogControl.open() } else { errorDialogControl.open() diff --git a/src/components/ageAssurance/AgeRestrictedScreen.tsx b/src/components/ageAssurance/AgeRestrictedScreen.tsx index b47cc5b0c..1430aaaff 100644 --- a/src/components/ageAssurance/AgeRestrictedScreen.tsx +++ b/src/components/ageAssurance/AgeRestrictedScreen.tsx @@ -18,10 +18,12 @@ export function AgeRestrictedScreen({ children, screenTitle, infoText, + rightHeaderSlot, }: { children: React.ReactNode screenTitle?: string infoText?: string + rightHeaderSlot?: React.ReactNode }) { const {_} = useLingui() const copy = useAgeAssuranceCopy() @@ -46,12 +48,12 @@ export function AgeRestrictedScreen({ - + {screenTitle ?? Unavailable} - + {rightHeaderSlot ?? } diff --git a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx index c63558334..266e226a6 100644 --- a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx @@ -117,7 +117,11 @@ export function BookmarksAnnouncement() { }, ]} alt={_( - msg`A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads "inventing a saturday that immediately follows monday".`, + msg({ + message: `A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads "inventing a saturday that immediately follows monday".`, + comment: + 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', + }), )} /> diff --git a/src/lib/custom-animations/CountWheel.tsx b/src/lib/custom-animations/CountWheel.tsx index 6db22554e..b8763827a 100644 --- a/src/lib/custom-animations/CountWheel.tsx +++ b/src/lib/custom-animations/CountWheel.tsx @@ -6,13 +6,12 @@ import Animated, { useReducedMotion, withTiming, } from 'react-native-reanimated' -import {i18n} from '@lingui/core' import {decideShouldRoll} from '#/lib/custom-animations/util' import {s} from '#/lib/styles' import {Text} from '#/view/com/util/text/Text' import {atoms as a, useTheme} from '#/alf' -import {formatPostStatCount} from '#/components/PostControls/util' +import {useFormatPostStatCount} from '#/components/PostControls/util' const animationConfig = { duration: 400, @@ -92,13 +91,11 @@ export function CountWheel({ big, isLiked, hasBeenToggled, - compactCount, }: { likeCount: number big?: boolean isLiked: boolean hasBeenToggled: boolean - compactCount?: boolean }) { const t = useTheme() const shouldAnimate = !useReducedMotion() && hasBeenToggled @@ -111,12 +108,9 @@ export function CountWheel({ const [key, setKey] = React.useState(0) const [prevCount, setPrevCount] = React.useState(likeCount) const prevIsLiked = React.useRef(isLiked) - const formattedCount = formatPostStatCount(i18n, likeCount, { - compact: compactCount, - }) - const formattedPrevCount = formatPostStatCount(i18n, prevCount, { - compact: compactCount, - }) + const formatPostStatCount = useFormatPostStatCount() + const formattedCount = formatPostStatCount(likeCount) + const formattedPrevCount = formatPostStatCount(prevCount) React.useEffect(() => { if (isLiked === prevIsLiked.current) { diff --git a/src/lib/custom-animations/CountWheel.web.tsx b/src/lib/custom-animations/CountWheel.web.tsx index fc2505ead..be48e19d7 100644 --- a/src/lib/custom-animations/CountWheel.web.tsx +++ b/src/lib/custom-animations/CountWheel.web.tsx @@ -1,13 +1,12 @@ import React from 'react' import {View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' -import {i18n} from '@lingui/core' import {decideShouldRoll} from '#/lib/custom-animations/util' import {s} from '#/lib/styles' -import {formatCount} from '#/view/com/util/numeric/format' import {Text} from '#/view/com/util/text/Text' import {atoms as a, useTheme} from '#/alf' +import {useFormatPostStatCount} from '#/components/PostControls/util' const animationConfig = { duration: 400, @@ -55,8 +54,9 @@ export function CountWheel({ const [prevCount, setPrevCount] = React.useState(likeCount) const prevIsLiked = React.useRef(isLiked) - const formattedCount = formatCount(i18n, likeCount) - const formattedPrevCount = formatCount(i18n, prevCount) + const formatPostStatCount = useFormatPostStatCount() + const formattedCount = formatPostStatCount(likeCount) + const formattedPrevCount = formatPostStatCount(prevCount) React.useEffect(() => { if (isLiked === prevIsLiked.current) { diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 67a38a52c..03dc4726f 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -11,6 +11,7 @@ import {isNative} from '#/platform/detection' import {useAgeAssuranceContext} from '#/state/ageAssurance' import {type SessionAccount, useAgent, useSession} from '#/state/session' import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' +import {IS_DEV} from '#/env' /** * @private @@ -129,7 +130,7 @@ export function useGetAndRegisterPushToken() { }: { isAgeRestricted?: boolean } = {}) => { - if (!isNative) return + if (!isNative || IS_DEV) return /** * This will also fire the listener added via `addPushTokenListener`. That diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 345446464..cb8598e79 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -74,7 +74,18 @@ export function MessagesScreen(props: Props) { return ( + infoText={aaCopy.chatsInfoText} + rightHeaderSlot={ + + + Chat settings + + + }> ) diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index 6015c07cd..b4b84aafe 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -12,8 +12,6 @@ import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' -import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' -import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {Divider} from '#/components/Divider' import * as Toggle from '#/components/forms/Toggle' import * as Layout from '#/components/Layout' @@ -25,16 +23,7 @@ type AllowIncoming = 'all' | 'none' | 'following' type Props = NativeStackScreenProps export function MessagesSettingsScreen(props: Props) { - const {_} = useLingui() - const aaCopy = useAgeAssuranceCopy() - - return ( - - - - ) + return } export function MessagesSettingsScreenInner({}: Props) { diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 08dd272f7..66ef96200 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -52,7 +52,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' -import {formatPostStatCount} from '#/components/PostControls/util' +import {useFormatPostStatCount} from '#/components/PostControls/util' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -176,11 +176,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ postSource?: PostSource }) { const t = useTheme() - const {_, i18n} = useLingui() + const {_} = useLingui() const {openComposer} = useOpenComposer() const {currentAccount, hasSession} = useSession() const {gtTablet} = useBreakpoints() const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession) + const formatPostStatCount = useFormatPostStatCount() const post = postShadow const record = item.value.post.record @@ -439,7 +440,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ testID="repostCount-expanded" style={[a.text_md, t.atoms.text_contrast_medium]}> - {formatPostStatCount(i18n, post.repostCount)} + {formatPostStatCount(post.repostCount)} {' '} - {formatPostStatCount(i18n, post.quoteCount)} + {formatPostStatCount(post.quoteCount)} {' '} - {formatPostStatCount(i18n, post.likeCount)} + {formatPostStatCount(post.likeCount)} {' '} @@ -485,7 +486,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ testID="bookmarkCount-expanded" style={[a.text_md, t.atoms.text_contrast_medium]}> - {formatPostStatCount(i18n, post.bookmarkCount)} + {formatPostStatCount(post.bookmarkCount)} {' '} Date: Fri, 5 Sep 2025 00:40:16 +0000 Subject: [PATCH 12/12] Nightly source-language update --- src/locale/locales/en/messages.po | 160 ++++++++++++++++-------------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 339d31a5f..6f06c1fe2 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -95,7 +95,7 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:478 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:479 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -103,15 +103,15 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:462 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:463 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:444 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:445 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:490 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:491 msgid "{0, plural, one {save} other {saves}}" msgstr "" @@ -220,6 +220,16 @@ msgstr "" msgid "{count, plural, one {# unread item} other {# unread items}}" msgstr "" +#. For post statistics. Indicates a number in the thousands. Please use the shortest format appropriate for your language. +#: src/components/PostControls/util.ts:36 +msgid "{count}K" +msgstr "" + +#. For post statistics. Indicates a number in the millions. Please use the shortest format appropriate for your language. +#: src/components/PostControls/util.ts:28 +msgid "{count}M" +msgstr "" + #: src/screens/Profile/Header/EditProfileDialog.tsx:385 msgid "{DESCRIPTION_MAX_GRAPHEMES, plural, other {Description is too long. The maximum number of characters is #.}}" msgstr "" @@ -538,6 +548,7 @@ msgstr "" msgid "A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies." msgstr "" +#. Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English. #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:120 msgid "A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads \"inventing a saturday that immediately follows monday\"." msgstr "" @@ -896,8 +907,8 @@ msgstr "" msgid "Allow location access" msgstr "" -#: src/screens/Messages/Settings.tsx:86 -#: src/screens/Messages/Settings.tsx:89 +#: src/screens/Messages/Settings.tsx:75 +#: src/screens/Messages/Settings.tsx:78 msgid "Allow new messages from" msgstr "" @@ -1182,12 +1193,12 @@ msgstr "" msgid "Apply Pull Request" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:664 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:665 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:633 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:672 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:634 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:673 msgid "Archived post" msgstr "" @@ -1321,7 +1332,7 @@ msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:54 #: src/components/dms/MessageProfileButton.tsx:58 -#: src/screens/Messages/ChatList.tsx:358 +#: src/screens/Messages/ChatList.tsx:369 #: src/screens/Messages/Conversation.tsx:228 msgid "Before you can message another user, you must first verify your email." msgstr "" @@ -1441,7 +1452,7 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:689 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:690 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1605,7 +1616,7 @@ msgstr "" #: src/components/live/GoLiveDialog.tsx:247 #: src/components/live/GoLiveDialog.tsx:253 #: src/components/Menu/index.tsx:350 -#: src/components/PostControls/RepostButton.tsx:211 +#: src/components/PostControls/RepostButton.tsx:210 #: src/components/Prompt.tsx:144 #: src/components/Prompt.tsx:146 #: src/screens/Deactivated.tsx:158 @@ -1647,7 +1658,7 @@ msgstr "" msgid "Cancel image crop" msgstr "" -#: src/components/PostControls/RepostButton.tsx:205 +#: src/components/PostControls/RepostButton.tsx:204 msgid "Cancel quote post" msgstr "" @@ -1659,9 +1670,9 @@ msgstr "" msgid "Cancel search" msgstr "" -#: src/components/PostControls/index.tsx:104 -#: src/components/PostControls/index.tsx:135 -#: src/components/PostControls/index.tsx:163 +#: src/components/PostControls/index.tsx:105 +#: src/components/PostControls/index.tsx:136 +#: src/components/PostControls/index.tsx:164 #: src/state/shell/composer/index.tsx:94 msgid "Cannot interact with a blocked user" msgstr "" @@ -1775,12 +1786,13 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:76 #: src/Navigation.tsx:554 -#: src/screens/Messages/ChatList.tsx:367 +#: src/screens/Messages/ChatList.tsx:81 +#: src/screens/Messages/ChatList.tsx:85 +#: src/screens/Messages/ChatList.tsx:378 msgid "Chat settings" msgstr "" -#: src/screens/Messages/Settings.tsx:33 -#: src/screens/Messages/Settings.tsx:78 +#: src/screens/Messages/Settings.tsx:67 msgid "Chat Settings" msgstr "" @@ -1790,8 +1802,8 @@ msgid "Chat unmuted" msgstr "" #: src/screens/Messages/ChatList.tsx:76 -#: src/screens/Messages/ChatList.tsx:383 -#: src/screens/Messages/ChatList.tsx:407 +#: src/screens/Messages/ChatList.tsx:394 +#: src/screens/Messages/ChatList.tsx:418 msgid "Chats" msgstr "" @@ -1916,8 +1928,8 @@ msgstr "" #: src/components/dialogs/GifSelect.tsx:269 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:158 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:167 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:159 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:167 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:163 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:171 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 #: src/components/dialogs/SearchablePeopleList.tsx:295 @@ -2738,8 +2750,8 @@ msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:32 #: src/lib/moderation/useLabelBehaviorDescription.ts:42 #: src/lib/moderation/useLabelBehaviorDescription.ts:68 -#: src/screens/Messages/Settings.tsx:155 -#: src/screens/Messages/Settings.tsx:158 +#: src/screens/Messages/Settings.tsx:144 +#: src/screens/Messages/Settings.tsx:147 #: src/screens/Moderation/index.tsx:413 msgid "Disabled" msgstr "" @@ -3159,8 +3171,8 @@ msgstr "" msgid "Enable trending videos in your Discover feed" msgstr "" -#: src/screens/Messages/Settings.tsx:146 -#: src/screens/Messages/Settings.tsx:149 +#: src/screens/Messages/Settings.tsx:135 +#: src/screens/Messages/Settings.tsx:138 #: src/screens/Moderation/index.tsx:411 msgid "Enabled" msgstr "" @@ -3274,8 +3286,8 @@ msgstr "" msgid "Everybody can reply to this post." msgstr "" -#: src/screens/Messages/Settings.tsx:99 -#: src/screens/Messages/Settings.tsx:102 +#: src/screens/Messages/Settings.tsx:88 +#: src/screens/Messages/Settings.tsx:91 #: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:164 #: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:174 msgid "Everyone" @@ -3472,7 +3484,7 @@ msgstr "" msgid "Failed to follow all suggested accounts, please try again" msgstr "" -#: src/screens/Messages/ChatList.tsx:270 +#: src/screens/Messages/ChatList.tsx:281 #: src/screens/Messages/Inbox.tsx:208 msgid "Failed to load conversations" msgstr "" @@ -3595,7 +3607,7 @@ msgstr "" msgid "Failed to update notification declaration" msgstr "" -#: src/screens/Messages/Settings.tsx:50 +#: src/screens/Messages/Settings.tsx:39 msgid "Failed to update settings" msgstr "" @@ -3713,7 +3725,7 @@ msgstr "" msgid "Finalizing" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:150 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:154 msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime." msgstr "" @@ -4130,8 +4142,8 @@ msgid "Go to {firstAuthorName}'s profile" msgstr "" #: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:89 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:75 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:84 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:77 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:86 #: src/screens/Moderation/index.tsx:214 msgid "Go to account settings" msgstr "" @@ -4843,7 +4855,7 @@ msgid "Like" msgstr "" #. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form -#: src/components/PostControls/index.tsx:271 +#: src/components/PostControls/index.tsx:269 msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -4908,7 +4920,7 @@ msgstr "" msgid "Likes of your reposts notifications" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:471 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:472 msgid "Likes on this post" msgstr "" @@ -5468,8 +5480,8 @@ msgid "New {postsCount, plural, one {post} other {posts}} from {firstAuthorName} msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:67 -#: src/screens/Messages/ChatList.tsx:390 -#: src/screens/Messages/ChatList.tsx:397 +#: src/screens/Messages/ChatList.tsx:401 +#: src/screens/Messages/ChatList.tsx:408 msgid "New chat" msgstr "" @@ -5648,8 +5660,8 @@ msgstr "" msgid "No notifications yet!" msgstr "" -#: src/screens/Messages/Settings.tsx:117 -#: src/screens/Messages/Settings.tsx:120 +#: src/screens/Messages/Settings.tsx:106 +#: src/screens/Messages/Settings.tsx:109 #: src/screens/Settings/ActivityPrivacySettings.tsx:129 #: src/screens/Settings/ActivityPrivacySettings.tsx:134 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:160 @@ -5771,7 +5783,7 @@ msgstr "" msgid "Note: This post is only visible to logged-in users." msgstr "" -#: src/screens/Messages/ChatList.tsx:291 +#: src/screens/Messages/ChatList.tsx:302 msgid "Nothing here" msgstr "" @@ -5785,11 +5797,11 @@ msgstr "" msgid "Notification settings" msgstr "" -#: src/screens/Messages/Settings.tsx:139 +#: src/screens/Messages/Settings.tsx:128 msgid "Notification sounds" msgstr "" -#: src/screens/Messages/Settings.tsx:136 +#: src/screens/Messages/Settings.tsx:125 msgid "Notification Sounds" msgstr "" @@ -5862,7 +5874,7 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:694 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:695 msgid "Okay" msgstr "" @@ -6732,8 +6744,8 @@ msgstr "" msgid "Quote notifications" msgstr "" -#: src/components/PostControls/RepostButton.tsx:176 -#: src/components/PostControls/RepostButton.tsx:199 +#: src/components/PostControls/RepostButton.tsx:175 +#: src/components/PostControls/RepostButton.tsx:198 #: src/components/PostControls/RepostButton.web.tsx:84 #: src/components/PostControls/RepostButton.web.tsx:91 msgid "Quote post" @@ -6747,8 +6759,8 @@ msgstr "" msgid "Quote post was successfully detached" msgstr "" -#: src/components/PostControls/RepostButton.tsx:175 -#: src/components/PostControls/RepostButton.tsx:197 +#: src/components/PostControls/RepostButton.tsx:174 +#: src/components/PostControls/RepostButton.tsx:196 #: src/components/PostControls/RepostButton.web.tsx:83 #: src/components/PostControls/RepostButton.web.tsx:90 msgid "Quote posts disabled" @@ -6765,7 +6777,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:455 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:456 msgid "Quotes of this post" msgstr "" @@ -6876,7 +6888,7 @@ msgstr "" msgid "Reject chat request" msgstr "" -#: src/screens/Messages/ChatList.tsx:274 +#: src/screens/Messages/ChatList.tsx:285 #: src/screens/Messages/Inbox.tsx:212 msgid "Reload conversations" msgstr "" @@ -6978,8 +6990,8 @@ msgstr "" msgid "Remove profile" msgstr "" -#: src/components/PostControls/RepostButton.tsx:153 -#: src/components/PostControls/RepostButton.tsx:163 +#: src/components/PostControls/RepostButton.tsx:152 +#: src/components/PostControls/RepostButton.tsx:162 msgid "Remove repost" msgstr "" @@ -7091,7 +7103,7 @@ msgid "Reply" msgstr "" #. Accessibility label for the reply button, verb form followed by number of replies and noun form -#: src/components/PostControls/index.tsx:222 +#: src/components/PostControls/index.tsx:223 msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" @@ -7218,8 +7230,8 @@ msgstr "" msgid "Report this user" msgstr "" -#: src/components/PostControls/RepostButton.tsx:154 -#: src/components/PostControls/RepostButton.tsx:165 +#: src/components/PostControls/RepostButton.tsx:153 +#: src/components/PostControls/RepostButton.tsx:164 #: src/components/PostControls/RepostButton.web.tsx:68 #: src/components/PostControls/RepostButton.web.tsx:75 msgctxt "action" @@ -7227,7 +7239,7 @@ msgid "Repost" msgstr "" #. Accessibility label for the repost button when the post has not been reposted, verb form followed by number of reposts and noun form -#: src/components/PostControls/RepostButton.tsx:78 +#: src/components/PostControls/RepostButton.tsx:77 msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" @@ -7235,7 +7247,7 @@ msgstr "" msgid "Repost notifications" msgstr "" -#: src/components/PostControls/RepostButton.tsx:146 +#: src/components/PostControls/RepostButton.tsx:145 #: src/components/PostControls/RepostButton.web.tsx:43 #: src/components/PostControls/RepostButton.web.tsx:103 #: src/screens/StarterPack/StarterPackScreen.tsx:561 @@ -7265,7 +7277,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:437 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:438 msgid "Reposts of this post" msgstr "" @@ -7354,7 +7366,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:346 #: src/screens/Login/LoginForm.tsx:323 #: src/screens/Login/LoginForm.tsx:330 -#: src/screens/Messages/ChatList.tsx:280 +#: src/screens/Messages/ChatList.tsx:291 #: src/screens/Messages/components/MessageListError.tsx:25 #: src/screens/Messages/Inbox.tsx:218 #: src/screens/Onboarding/StepInterests/index.tsx:226 @@ -7463,7 +7475,7 @@ msgstr "" msgid "Saved Feeds" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:139 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:143 #: src/Navigation.tsx:608 #: src/screens/Bookmarks/index.tsx:55 msgid "Saved Posts" @@ -7478,7 +7490,7 @@ msgstr "" msgid "Saves image crop settings" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:483 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:484 msgid "Saves of this post" msgstr "" @@ -8112,7 +8124,7 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:636 msgid "Shows information about when this post was created" msgstr "" @@ -9042,7 +9054,7 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:675 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:676 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -9212,8 +9224,8 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:139 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:597 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:600 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:598 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:601 msgid "Translate" msgstr "" @@ -9289,7 +9301,7 @@ msgstr "" msgid "Unapply Pull Request {currentChannel}" msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:51 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:53 msgid "Unavailable" msgstr "" @@ -9340,7 +9352,7 @@ msgid "Undo repost" msgstr "" #. Accessibility label for the repost button when the post has been reposted, verb followed by number of reposts and noun -#: src/components/PostControls/RepostButton.tsx:68 +#: src/components/PostControls/RepostButton.tsx:67 msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" @@ -9379,7 +9391,7 @@ msgid "Unlike" msgstr "" #. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun -#: src/components/PostControls/index.tsx:261 +#: src/components/PostControls/index.tsx:259 msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -9680,8 +9692,8 @@ msgstr "" msgid "users following <0>@{0}" msgstr "" -#: src/screens/Messages/Settings.tsx:108 -#: src/screens/Messages/Settings.tsx:111 +#: src/screens/Messages/Settings.tsx:97 +#: src/screens/Messages/Settings.tsx:100 msgid "Users I follow" msgstr "" @@ -10209,7 +10221,7 @@ msgid "Who can verify?" msgstr "" #: src/screens/Home/NoFeedsPinned.tsx:79 -#: src/screens/Messages/ChatList.tsx:258 +#: src/screens/Messages/ChatList.tsx:269 #: src/screens/Messages/Inbox.tsx:197 msgid "Whoops!" msgstr "" @@ -10392,7 +10404,7 @@ msgstr "" msgid "You can choose whether chat notifications have sound in the chat settings within the app" msgstr "" -#: src/screens/Messages/Settings.tsx:127 +#: src/screens/Messages/Settings.tsx:116 msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" @@ -10493,7 +10505,7 @@ msgstr "" msgid "You have muted this user" msgstr "" -#: src/screens/Messages/ChatList.tsx:301 +#: src/screens/Messages/ChatList.tsx:312 msgid "You have no conversations yet. Start one!" msgstr "" @@ -10579,7 +10591,7 @@ msgstr "" msgid "You must complete age assurance in order to access the settings below." msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:64 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:66 msgid "You must complete age assurance in order to access this screen." msgstr ""