diff --git a/README.md b/README.md index f86d52b530..eed6ac4fec 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Get the app itself: ## Development Resources -This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). +This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), which are also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. @@ -19,7 +19,7 @@ The [Build Instructions](./docs/build.md) are a good place to get started with t The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at: - [Overview and Guides](https://atproto.com/guides/overview) -- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions +- [GitHub Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions - [Protocol Specifications](https://atproto.com/specs/atp) - [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol) @@ -27,6 +27,7 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th ## Contributions +> [!NOTE] > While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. **Rules:** @@ -59,7 +60,7 @@ Please be sure to: ## Security disclosures -If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly. +If you discover any security issues, please send an email to security@bsky.app. The email is automatically CC'd to the entire team and we'll respond promptly. ## Are you a developer interested in building on atproto? diff --git a/package.json b/package.json index 77908a1615..f89c7089c5 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "expo-file-system": "~18.1.10", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", - "expo-image": "~2.2.1", + "expo-image": "^2.4.0", "expo-image-crop-tool": "^0.1.8", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", diff --git a/src/App.native.tsx b/src/App.native.tsx index 87429d8458..4037eedd27 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -70,6 +70,7 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu' import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' +import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' import {Provider as PortalProvider} from '#/components/Portal' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Splash} from '#/Splash' @@ -137,49 +138,51 @@ function InnerApp() { // Resets the entire tree below when it changes: key={currentAccount?.did}> - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 1f795cb3e0..2897aa3f2f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -57,6 +57,7 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu' import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' +import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' import {Provider as PortalProvider} from '#/components/Portal' import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' @@ -117,45 +118,47 @@ function InnerApp() { // Resets the entire tree below when it changes: key={currentAccount?.did}> - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 2a3a00ba7d..18da12b22c 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -378,8 +378,8 @@ export function ProfileGrid({ style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> {gtMobile ? ( - + {content} @@ -411,9 +411,8 @@ export function ProfileGrid({ horizontal showsHorizontalScrollIndicator={false} snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} - decelerationRate="fast" - style={[a.overflow_visible]}> - + decelerationRate="fast"> + {content} @@ -434,6 +433,7 @@ function SeeMoreSuggestedProfilesCard() { return ( + + + + By clicking "Continue" you acknowledge that you understand and + agree to these updates. + + + + + + ) +} diff --git a/src/components/PolicyUpdateOverlay/usePolicyUpdateState.ts b/src/components/PolicyUpdateOverlay/usePolicyUpdateState.ts new file mode 100644 index 0000000000..29d8afe067 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/usePolicyUpdateState.ts @@ -0,0 +1,135 @@ +import {useMemo} from 'react' + +import {useNux, useSaveNux} from '#/state/queries/nuxs' +import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config' +import {logger} from '#/components/PolicyUpdateOverlay/logger' +import {IS_DEV} from '#/env' +import {device, useStorage} from '#/storage' + +export type PolicyUpdateState = { + completed: boolean + complete: () => void +} + +export function usePolicyUpdateState() { + const nux = useNux(ACTIVE_UPDATE_ID) + const {mutate: save, variables} = useSaveNux() + const deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID]) + const debugOverride = + !!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV + return useMemo(() => { + const nuxIsReady = nux.status === 'ready' + const nuxIsCompleted = nux.nux?.completed === true + const nuxIsOptimisticallyCompleted = !!variables?.completed + const [completedForDevice, setCompletedForDevice] = deviceStorage + + const completed = computeCompletedState({ + nuxIsReady, + nuxIsCompleted, + nuxIsOptimisticallyCompleted, + completedForDevice, + }) + + logger.debug(`state`, { + completed, + nux, + completedForDevice, + }) + + if (!debugOverride) { + syncCompletedState({ + nuxIsReady, + nuxIsCompleted, + nuxIsOptimisticallyCompleted, + completedForDevice, + save, + setCompletedForDevice, + }) + } + + return { + completed, + complete() { + logger.debug(`user completed`) + save({ + id: ACTIVE_UPDATE_ID, + completed: true, + data: undefined, + }) + setCompletedForDevice(true) + }, + } + }, [nux, save, variables, deviceStorage, debugOverride]) +} + +export function computeCompletedState({ + nuxIsReady, + nuxIsCompleted, + nuxIsOptimisticallyCompleted, + completedForDevice, +}: { + nuxIsReady: boolean + nuxIsCompleted: boolean + nuxIsOptimisticallyCompleted: boolean + completedForDevice: boolean | undefined +}): boolean { + /** + * Assume completed to prevent flash + */ + let completed = true + + /** + * Prefer server state, if available + */ + if (nuxIsReady) { + completed = nuxIsCompleted + } + + /** + * Override with optimistic state or device state + */ + if (nuxIsOptimisticallyCompleted || !!completedForDevice) { + completed = true + } + + return completed +} + +export function syncCompletedState({ + nuxIsReady, + nuxIsCompleted, + nuxIsOptimisticallyCompleted, + completedForDevice, + save, + setCompletedForDevice, +}: { + nuxIsReady: boolean + nuxIsCompleted: boolean + nuxIsOptimisticallyCompleted: boolean + completedForDevice: boolean | undefined + save: ReturnType['mutate'] + setCompletedForDevice: (value: boolean) => void +}) { + /* + * Sync device state to server state for this account + */ + if ( + nuxIsReady && + !nuxIsCompleted && + !nuxIsOptimisticallyCompleted && + !!completedForDevice + ) { + logger.debug(`syncing device state to server state`) + save({ + id: ACTIVE_UPDATE_ID, + completed: true, + data: undefined, + }) + } else if (nuxIsReady && nuxIsCompleted && !completedForDevice) { + logger.debug(`syncing server state to device state`) + /* + * Sync server state to device state + */ + setCompletedForDevice(true) + } +} diff --git a/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.ts b/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.ts new file mode 100644 index 0000000000..f41b3e6d7d --- /dev/null +++ b/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.ts @@ -0,0 +1,21 @@ +import {useCallback} from 'react' + +import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config' +import {logger} from '#/components/PolicyUpdateOverlay/logger' +import {device, useStorage} from '#/storage' + +/* + * Marks the active policy update as completed in device storage. + * `usePolicyUpdateState` will react to this and replicate this status in the + * server NUX state for this account. + */ +export function usePreemptivelyCompleteActivePolicyUpdate() { + const [_completedForDevice, setCompletedForDevice] = useStorage(device, [ + ACTIVE_UPDATE_ID, + ]) + + return useCallback(() => { + logger.debug(`preemptively completing active policy update`) + setCompletedForDevice(true) + }, [setCompletedForDevice]) +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 351e9f3056..ecc36dc33b 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import {useImperativeHandle, useRef, useState} from 'react' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' import {type AppBskyEmbedVideo} from '@atproto/api' import {BlueskyVideoView} from '@haileyok/bluesky-video' @@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {TimeIndicator} from './TimeIndicator' -export const VideoEmbedInnerNative = React.forwardRef( - function VideoEmbedInnerNative( - { - embed, - setStatus, - setIsLoading, - setIsActive, - }: { - embed: AppBskyEmbedVideo.View - setStatus: (status: 'playing' | 'paused') => void - setIsLoading: (isLoading: boolean) => void - setIsActive: (isActive: boolean) => void +export function VideoEmbedInnerNative({ + ref, + embed, + setStatus, + setIsLoading, + setIsActive, +}: { + ref: React.Ref<{togglePlayback: () => void}> + embed: AppBskyEmbedVideo.View + setStatus: (status: 'playing' | 'paused') => void + setIsLoading: (isLoading: boolean) => void + setIsActive: (isActive: boolean) => void +}) { + const {_} = useLingui() + const videoRef = useRef(null) + const autoplayDisabled = useAutoplayDisabled() + const isWithinMessage = useIsWithinMessage() + const [muted, setMuted] = useVideoMuteState() + + const [isPlaying, setIsPlaying] = useState(false) + const [timeRemaining, setTimeRemaining] = useState(0) + const [error, setError] = useState() + + useImperativeHandle(ref, () => ({ + togglePlayback: () => { + videoRef.current?.togglePlayback() }, - ref: React.Ref<{togglePlayback: () => void}>, - ) { - const {_} = useLingui() - const videoRef = useRef(null) - const autoplayDisabled = useAutoplayDisabled() - const isWithinMessage = useIsWithinMessage() - const [muted, setMuted] = useVideoMuteState() + })) - const [isPlaying, setIsPlaying] = React.useState(false) - const [timeRemaining, setTimeRemaining] = React.useState(0) - const [error, setError] = React.useState() + if (error) { + throw new Error(error) + } - React.useImperativeHandle(ref, () => ({ - togglePlayback: () => { - videoRef.current?.togglePlayback() - }, - })) - - if (error) { - throw new Error(error) - } - - return ( - - { - setIsActive(e.nativeEvent.isActive) - }} - onLoadingChange={e => { - setIsLoading(e.nativeEvent.isLoading) - }} - onMutedChange={e => { - setMuted(e.nativeEvent.isMuted) - }} - onStatusChange={e => { - setStatus(e.nativeEvent.status) - setIsPlaying(e.nativeEvent.status === 'playing') - }} - onTimeRemainingChange={e => { - setTimeRemaining(e.nativeEvent.timeRemaining) - }} - onError={e => { - setError(e.nativeEvent.error) - }} - ref={videoRef} - accessibilityLabel={ - embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) - } - accessibilityHint="" - /> - { - videoRef.current?.enterFullscreen(true) - }} - toggleMuted={() => { - videoRef.current?.toggleMuted() - }} - togglePlayback={() => { - videoRef.current?.togglePlayback() - }} - isPlaying={isPlaying} - timeRemaining={timeRemaining} - /> - - - ) - }, -) + return ( + + { + setIsActive(e.nativeEvent.isActive) + }} + onLoadingChange={e => { + setIsLoading(e.nativeEvent.isLoading) + }} + onMutedChange={e => { + setMuted(e.nativeEvent.isMuted) + }} + onStatusChange={e => { + setStatus(e.nativeEvent.status) + setIsPlaying(e.nativeEvent.status === 'playing') + }} + onTimeRemainingChange={e => { + setTimeRemaining(e.nativeEvent.timeRemaining) + }} + onError={e => { + setError(e.nativeEvent.error) + }} + ref={videoRef} + accessibilityLabel={ + embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) + } + accessibilityHint="" + /> + { + videoRef.current?.enterFullscreen(true) + }} + toggleMuted={() => { + videoRef.current?.toggleMuted() + }} + togglePlayback={() => { + videoRef.current?.togglePlayback() + }} + isPlaying={isPlaying} + timeRemaining={timeRemaining} + /> + + + ) +} function VideoControls({ enterFullscreen, diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index ce3a7b2c90..266438c044 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -224,15 +224,19 @@ function useHLS({ throw new HLSUnsupportedError() } + const latestEstimate = BandwidthEstimate.get() const hls = new Hls({ maxMaxBufferLength: 10, // only load 10s ahead // note: the amount buffered is affected by both maxBufferLength and maxBufferSize // it will buffer until it is greater than *both* of those values // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead + startLevel: + latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel, + // the '-1' value makes a test request to estimate bandwidth and quality level + // before showing the first fragment }) hlsRef.current = hls - const latestEstimate = BandwidthEstimate.get() if (latestEstimate !== undefined) { hls.bandwidthEstimate = latestEstimate } diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index 8cb78ff70b..c66d1a2185 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react' +import {useCallback, useRef, useState} from 'react' import {ActivityIndicator, View} from 'react-native' import {ImageBackground} from 'expo-image' import {type AppBskyEmbedVideo} from '@atproto/api' @@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) { function InnerWrapper({embed}: Props) { const {_} = useLingui() - const ref = React.useRef<{togglePlayback: () => void}>(null) + const ref = useRef<{togglePlayback: () => void}>(null) - const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>( + const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>( 'pending', ) - const [isLoading, setIsLoading] = React.useState(false) - const [isActive, setIsActive] = React.useState(false) + const [isLoading, setIsLoading] = useState(false) + const [isActive, setIsActive] = useState(false) const showSpinner = useThrottledValue(isActive && isLoading, 100) const showOverlay = @@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) { (status === 'paused' && !isActive) || status === 'pending' - React.useEffect(() => { - if (!isActive && status !== 'pending') { - setStatus('pending') - } - }, [isActive, status]) + if (!isActive && status !== 'pending') { + setStatus('pending') + } return ( <> @@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) { onPress={() => { ref.current?.togglePlayback() }} - label={_(msg`Play video`)} - color="secondary"> + label={_(msg`Play video`)}> {showSpinner ? ( evt.stopPropagation()}> - + - + ) return ( - {cropDisabled ? ( - - {contents} - - ) : ( - - {contents} - - )} + + {cropDisabled ? ( + + {contents} + + ) : ( + + {contents} + + )} + ) } +const NearScreenContext = createContext(false) + /** * Renders a 100vh tall div and watches it with an IntersectionObserver to * send the position of the div when it's near the screen. + * + * IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container. */ function ViewportObserver({ children, @@ -164,7 +177,9 @@ function ViewportObserver({ return ( - {nearScreen && children} + + {children} +
{ + const nearScreen = useContext(NearScreenContext) + + return nearScreen ? children : null +} + function VideoError({error, retry}: {error: unknown; retry: () => void}) { const {_} = useLingui() diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx index f6ceda568e..f2517e28da 100644 --- a/src/components/Toast/index.web.tsx +++ b/src/components/Toast/index.web.tsx @@ -78,7 +78,12 @@ export const ToastContainer: React.FC = ({}) => { setActiveToast(undefined)} /> diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 3309191f34..335bf28c84 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -41,6 +41,7 @@ export class FeedViewPostsSlice { isIncompleteThread: boolean isFallbackMarker: boolean isOrphan: boolean + isThreadMuted: boolean rootUri: string feedPostUri: string @@ -50,6 +51,7 @@ export class FeedViewPostsSlice { this.isIncompleteThread = false this.isFallbackMarker = false this.isOrphan = false + this.isThreadMuted = post.viewer?.threadMuted ?? false this.feedPostUri = post.uri if (AppBskyFeedDefs.isPostView(reply?.root)) { this.rootUri = reply.root.uri @@ -361,6 +363,20 @@ export class FeedTuner { return slices } + static removeMutedThreads( + tuner: FeedTuner, + slices: FeedViewPostsSlice[], + _dryRun: boolean, + ) { + for (let i = 0; i < slices.length; i++) { + if (slices[i].isThreadMuted) { + slices.splice(i, 1) + i-- + } + } + return slices + } + static dedupThreads( tuner: FeedTuner, slices: FeedViewPostsSlice[], diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3f0d499894..ab52b87107 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -207,3 +207,10 @@ export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app' export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev' export const DEV_ENV_APPVIEW = `http://localhost:2584` // always the same + +export const webLinks = { + tos: `https://bsky.social/about/support/tos`, + privacy: `https://bsky.social/about/support/privacy-policy`, + community: `https://bsky.social/about/support/community-guidelines`, + communityDeprecated: `https://bsky.social/about/support/community-guidelines-deprecated`, +} diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index ddee11fb5d..3370a72081 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -400,9 +400,7 @@ export function getNotificationPayload( } } -export function notificationToURL( - payload: NotificationPayload, -): string | undefined { +export function notificationToURL(payload: NotificationPayload): string | null { switch (payload?.reason) { case 'like': case 'repost': @@ -433,10 +431,12 @@ export function notificationToURL( } case 'chat-message': // should be handled separately - return undefined + return null case 'verified': case 'unverified': - default: return '/notifications' + default: + // do nothing if we don't know what to do with it + return null } } diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 3b1106480d..c3bd1a7cbf 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -3,6 +3,7 @@ export type Gate = | 'alt_share_icon' | 'debug_show_feedcontext' | 'debug_subscriptions' + | 'disable_onboarding_policy_update_notice' | 'explore_show_suggested_feeds' | 'old_postonboarding' | 'onboarding_add_video_feed' diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 91699294d3..ce8db630fe 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -657,8 +657,8 @@ msgstr "" #: src/view/com/composer/GifAltText.tsx:76 #: src/view/com/composer/GifAltText.tsx:144 #: src/view/com/composer/GifAltText.tsx:210 -#: src/view/com/composer/photos/Gallery.tsx:169 -#: src/view/com/composer/photos/Gallery.tsx:216 +#: src/view/com/composer/photos/Gallery.tsx:170 +#: src/view/com/composer/photos/Gallery.tsx:217 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:88 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:93 msgid "Add alt text" @@ -870,7 +870,7 @@ msgstr "" #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:186 #: src/view/com/composer/GifAltText.tsx:100 -#: src/view/com/composer/photos/Gallery.tsx:187 +#: src/view/com/composer/photos/Gallery.tsx:188 msgid "ALT" msgstr "" @@ -888,7 +888,7 @@ msgstr "" msgid "Alt Text" msgstr "" -#: src/view/com/composer/photos/Gallery.tsx:255 +#: src/view/com/composer/photos/Gallery.tsx:260 msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." msgstr "" @@ -921,11 +921,11 @@ msgstr "" msgid "An error occurred while generating your starter pack. Want to try again?" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:160 +#: src/components/Post/Embed/VideoEmbed/index.tsx:157 msgid "An error occurred while loading the video. Please try again later." msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:199 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:226 msgid "An error occurred while loading the video. Please try again." msgstr "" @@ -1423,7 +1423,7 @@ msgstr "" msgid "Books" msgstr "" -#: src/components/FeedInterstitials.tsx:436 +#: src/components/FeedInterstitials.tsx:435 msgid "Browse more accounts on the Explore page" msgstr "" @@ -2309,12 +2309,9 @@ msgstr "" msgid "Create Account" msgstr "" -#: src/screens/Search/SearchResults.tsx:266 -msgid "create an account" -msgstr "" - #: src/components/dialogs/Signin.tsx:86 #: src/components/dialogs/Signin.tsx:88 +#: src/screens/Search/SearchResults.tsx:266 msgid "Create an account" msgstr "" @@ -2662,6 +2659,11 @@ msgstr "" msgid "Dismiss interests" msgstr "" +#. Accessibility label for dismissing a toast notification +#: src/components/Toast/index.web.tsx:82 +msgid "Dismiss message" +msgstr "" + #: src/components/interstitials/TrendingVideos.tsx:89 msgid "Dismiss this section" msgstr "" @@ -2846,7 +2848,7 @@ msgstr "" #: src/view/com/composer/photos/EditImageDialog.web.tsx:85 #: src/view/com/composer/photos/EditImageDialog.web.tsx:89 -#: src/view/com/composer/photos/Gallery.tsx:194 +#: src/view/com/composer/photos/Gallery.tsx:195 msgid "Edit image" msgstr "" @@ -3100,7 +3102,7 @@ msgstr "" msgid "Enter your username and password" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:135 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:132 msgid "Enters full screen" msgstr "" @@ -5114,7 +5116,7 @@ msgstr "" msgid "Music" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96 msgctxt "video" msgid "Mute" @@ -5515,7 +5517,7 @@ msgstr "" msgid "No results for \"{0}\"." msgstr "" -#: src/components/Lists.tsx:189 +#: src/components/Lists.tsx:190 msgid "No results found" msgstr "" @@ -5740,11 +5742,11 @@ msgstr "" msgid "Only WebVTT (.vtt) files are supported" msgstr "" -#: src/components/Lists.tsx:94 +#: src/components/Lists.tsx:95 msgid "Oops, something went wrong!" msgstr "" -#: src/components/Lists.tsx:173 +#: src/components/Lists.tsx:174 #: src/components/StarterPack/ProfileStarterPacks.tsx:332 #: src/components/StarterPack/ProfileStarterPacks.tsx:341 #: src/screens/Settings/AppPasswords.tsx:59 @@ -5980,7 +5982,7 @@ msgstr "" msgid "Our moderators have reviewed reports and decided to disable your access to chats on Bluesky." msgstr "" -#: src/components/Lists.tsx:190 +#: src/components/Lists.tsx:191 #: src/view/screens/NotFound.tsx:47 msgid "Page not found" msgstr "" @@ -6015,7 +6017,7 @@ msgid "Password updated!" msgstr "" #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43 -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369 msgid "Pause" msgstr "" @@ -6104,7 +6106,7 @@ msgid "Pinned to your feeds" msgstr "" #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43 -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:370 msgid "Play" msgstr "" @@ -6113,7 +6115,7 @@ msgstr "" msgid "Play {0}" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:134 +#: src/components/Post/Embed/VideoEmbed/index.tsx:132 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321 msgid "Play video" msgstr "" @@ -6126,7 +6128,7 @@ msgstr "" msgid "Plays or pauses the GIF" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:141 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:138 msgid "Plays or pauses the video" msgstr "" @@ -6408,7 +6410,7 @@ msgid "Press to attempt reconnection" msgstr "" #: src/components/Error.tsx:60 -#: src/components/Lists.tsx:99 +#: src/components/Lists.tsx:100 #: src/screens/Messages/components/MessageListError.tsx:24 #: src/screens/Signup/BackNextButtons.tsx:47 msgid "Press to retry" @@ -6765,7 +6767,7 @@ msgstr "" msgid "Remove from your feeds?" msgstr "" -#: src/view/com/composer/photos/Gallery.tsx:203 +#: src/view/com/composer/photos/Gallery.tsx:204 msgid "Remove image" msgstr "" @@ -7151,7 +7153,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:321 #: src/components/Error.tsx:65 -#: src/components/Lists.tsx:110 +#: src/components/Lists.tsx:111 #: src/components/moderation/ReportDialog/index.tsx:229 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:57 @@ -7901,15 +7903,12 @@ msgstr "" msgid "Shows the content" msgstr "" -#: src/screens/Search/SearchResults.tsx:258 -msgid "sign in" -msgstr "" - #: src/components/dialogs/Signin.tsx:97 #: src/components/dialogs/Signin.tsx:99 #: src/screens/Login/index.tsx:122 #: src/screens/Login/index.tsx:143 #: src/screens/Login/LoginForm.tsx:181 +#: src/screens/Search/SearchResults.tsx:258 #: src/view/com/auth/SplashScreen.tsx:61 #: src/view/com/auth/SplashScreen.tsx:69 #: src/view/com/auth/SplashScreen.web.tsx:123 @@ -8051,7 +8050,7 @@ msgstr "" msgid "Something went wrong, please try again." msgstr "" -#: src/components/Lists.tsx:174 +#: src/components/Lists.tsx:175 msgid "Something went wrong!" msgstr "" @@ -8941,7 +8940,7 @@ msgstr "" msgid "Toggle to enable or disable adult content" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:158 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155 msgid "Toggles the sound" msgstr "" @@ -9127,7 +9126,7 @@ msgstr "" msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:95 msgctxt "video" msgid "Unmute" @@ -9520,8 +9519,8 @@ msgstr "" msgid "Version {0}" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:134 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:131 msgid "Video" msgstr "" @@ -9551,7 +9550,7 @@ msgstr "" msgid "Video is playing" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:192 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:219 msgid "Video not found." msgstr "" @@ -9563,7 +9562,7 @@ msgstr "" msgid "Video uploaded" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:81 msgid "Video: {0}" msgstr "" @@ -9841,7 +9840,7 @@ msgstr "" msgid "We're sorry! The post you are replying to has been deleted." msgstr "" -#: src/components/Lists.tsx:194 +#: src/components/Lists.tsx:195 #: src/view/screens/NotFound.tsx:50 msgid "We're sorry! We can't find the page you were looking for." msgstr "" @@ -10194,7 +10193,7 @@ msgstr "" msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." msgstr "" -#: src/components/Lists.tsx:57 +#: src/components/Lists.tsx:58 msgid "You have reached the end" msgstr "" @@ -10423,7 +10422,7 @@ msgstr "" msgid "Your birth date" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:196 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:223 msgid "Your browser does not support the video format. Please try a different browser." msgstr "" diff --git a/src/logger/types.ts b/src/logger/types.ts index 4743e866c4..ee3069a083 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -13,6 +13,7 @@ export enum LogContext { FeedFeedback = 'feed-feedback', PostSource = 'post-source', AgeAssurance = 'age-assurance', + PolicyUpdate = 'policy-update', /** * METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 719bbf9a29..5023982eb1 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -25,6 +25,7 @@ import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' +import {useAgent} from '#/state/session' import {type SessionAccount, useSession, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' @@ -35,6 +36,7 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice' import {AvatarStackWithFetch} from '#/components/AvatarStack' +import {Button, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' @@ -58,6 +60,7 @@ import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/W import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Menu from '#/components/Menu' +import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {useFullVerificationState} from '#/components/verification' @@ -66,6 +69,7 @@ import { VerificationCheckButton, } from '#/components/verification/VerificationCheckButton' import {IS_INTERNAL} from '#/env' +import {device, useStorage} from '#/storage' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' type Props = NativeStackScreenProps @@ -363,6 +367,10 @@ function ProfilePreview({ function DevOptions() { const {_} = useLingui() + const agent = useAgent() + const [override, setOverride] = useStorage(device, [ + 'policyUpdateDebugOverride', + ]) const onboardingDispatch = useOnboardingDispatch() const navigation = useNavigation() const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() @@ -502,6 +510,40 @@ function DevOptions() { ) : null} + + + + PolicyUpdate202508 Debug + + + + + + + + ) } diff --git a/src/screens/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx index 17980172de..0bc14fa6ab 100644 --- a/src/screens/Signup/StepInfo/Policies.tsx +++ b/src/screens/Signup/StepInfo/Policies.tsx @@ -4,11 +4,42 @@ import {type ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {webLinks} from '#/lib/constants' +import {useGate} from '#/lib/statsig/statsig' import {atoms as a, useTheme} from '#/alf' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {Admonition} from '#/components/Admonition' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' +function CommunityGuidelinesNotice({}: {}) { + const {_} = useLingui() + const gate = useGate() + + if (gate('disable_onboarding_policy_update_notice')) return null + + return ( + + + + You also agree to{' '} + + Bluesky’s Community Guidelines + + . An{' '} + + updated version of our Community Guidelines + {' '} + will take effect on October 13th. + + + + ) +} + export const Policies = ({ serviceDescription, needsGuardian, @@ -30,14 +61,13 @@ export const Policies = ({ if (!tos && !pp) { return ( - - - - + + This service has not provided terms of service or a privacy policy. - + + ) } @@ -102,19 +132,21 @@ export const Policies = ({ ) : null} {under13 ? ( - + You must be 13 years of age or older to create an account. - + ) : needsGuardian ? ( - + If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf. - + ) : undefined} + + ) } diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 48ea4ccd93..ae0b20f1c5 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -15,6 +15,7 @@ import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' import {useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' +import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate' export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -252,6 +253,8 @@ export function useSubmitSignup() { const {_} = useLingui() const {createAccount} = useSessionApi() const onboardingDispatch = useOnboardingDispatch() + const preemptivelyCompleteActivePolicyUpdate = + usePreemptivelyCompleteActivePolicyUpdate() return useCallback( async (state: SignupState, dispatch: (action: SignupAction) => void) => { @@ -325,6 +328,12 @@ export function useSubmitSignup() { }, ) + /** + * Marks any active policy update as completed, since user just agreed + * to TOS/privacy during sign up + */ + preemptivelyCompleteActivePolicyUpdate() + /* * Must happen last so that if the user has multiple tabs open and * createAccount fails, one tab is not stuck in onboarding — Eric @@ -363,6 +372,11 @@ export function useSubmitSignup() { dispatch({type: 'setIsLoading', value: false}) } }, - [_, onboardingDispatch, createAccount], + [ + _, + onboardingDispatch, + createAccount, + preemptivelyCompleteActivePolicyUpdate, + ], ) } diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts index 51d757ad8b..8c043a3420 100644 --- a/src/state/persisted/index.ts +++ b/src/state/persisted/index.ts @@ -3,11 +3,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import {logger} from '#/logger' import { defaults, - Schema, + type Schema, tryParse, tryStringify, } from '#/state/persisted/schema' -import {PersistedApi} from './types' +import {device} from '#/storage' +import {type PersistedApi} from './types' import {normalizeData} from './util' export type {PersistedAccount, Schema} from '#/state/persisted/schema' @@ -53,6 +54,7 @@ onUpdate satisfies PersistedApi['onUpdate'] export async function clearStorage() { try { await AsyncStorage.removeItem(BSKY_STORAGE) + device.removeAll() } catch (e: any) { logger.error(`persisted store: failed to clear`, {message: e.toString()}) } diff --git a/src/state/preferences/feed-tuners.tsx b/src/state/preferences/feed-tuners.tsx index 3ed60e5988..feeab6f9af 100644 --- a/src/state/preferences/feed-tuners.tsx +++ b/src/state/preferences/feed-tuners.tsx @@ -1,7 +1,7 @@ import {useMemo} from 'react' import {FeedTuner} from '#/lib/api/feed-manip' -import {FeedDescriptor} from '../queries/post-feed' +import {type FeedDescriptor} from '../queries/post-feed' import {usePreferencesQuery} from '../queries/preferences' import {useSession} from '../session' import {useLanguagePrefs} from './languages' @@ -19,7 +19,10 @@ export function useFeedTuners(feedDesc: FeedDescriptor) { } } if (feedDesc.startsWith('feedgen')) { - return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)] + return [ + FeedTuner.preferredLangOnly(langPrefs.contentLanguages), + FeedTuner.removeMutedThreads, + ] } if (feedDesc === 'following' || feedDesc.startsWith('list')) { const feedTuners = [FeedTuner.removeOrphans] @@ -40,6 +43,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) { feedTuners.push(FeedTuner.removeQuotePosts) } feedTuners.push(FeedTuner.dedupThreads) + feedTuners.push(FeedTuner.removeMutedThreads) return feedTuners } diff --git a/src/state/queries/nuxs/__mocks__/index.ts b/src/state/queries/nuxs/__mocks__/index.ts new file mode 100644 index 0000000000..c718b15940 --- /dev/null +++ b/src/state/queries/nuxs/__mocks__/index.ts @@ -0,0 +1,25 @@ +import {jest} from '@jest/globals' + +export {Nux} from '#/state/queries/nuxs/definitions' + +export const useNuxs = jest.fn(() => { + return { + nuxs: undefined, + status: 'loading' as const, + } +}) + +export const useNux = jest.fn((id: string) => { + return { + nux: undefined, + status: 'loading' as const, + } +}) + +export const useSaveNux = jest.fn(() => { + return {} +}) + +export const useResetNuxs = jest.fn(() => { + return {} +}) diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 3d5c132f26..7577d6b205 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -9,6 +9,11 @@ export enum Nux { ActivitySubscriptions = 'ActivitySubscriptions', AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice', AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner', + + /* + * Blocking announcements. New IDs are required for each new announcement. + */ + PolicyUpdate202508 = 'PolicyUpdate202508', } export const nuxNames = new Set(Object.values(Nux)) @@ -38,6 +43,10 @@ export type AppNux = BaseNux< id: Nux.AgeAssuranceDismissibleFeedBanner data: undefined } + | { + id: Nux.PolicyUpdate202508 + data: undefined + } > export const NuxSchemas: Record | undefined> = { @@ -47,4 +56,5 @@ export const NuxSchemas: Record | undefined> = { [Nux.ActivitySubscriptions]: undefined, [Nux.AgeAssuranceDismissibleNotice]: undefined, [Nux.AgeAssuranceDismissibleFeedBanner]: undefined, + [Nux.PolicyUpdate202508]: undefined, } diff --git a/src/storage/index.ts b/src/storage/index.ts index 4d45134e1b..4c42005104 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -66,6 +66,13 @@ export class Storage { keys.forEach(key => this.remove([...scopes, key])) } + /** + * For debugging purposes + */ + removeAll() { + this.store.clearAll() + } + /** * Fires a callback when the storage associated with a given key changes * diff --git a/src/storage/schema.ts b/src/storage/schema.ts index c05a7531df..421264ac1e 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,3 +1,5 @@ +import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' + /** * Device data that's specific to the device and does not vary based account */ @@ -13,6 +15,12 @@ export type Device = { devMode: boolean demoMode: boolean activitySubscriptionsNudged?: boolean + + /** + * Policy update overlays. New IDs are required for each new announcement. + */ + policyUpdateDebugOverride?: boolean + [PolicyUpdate202508]?: boolean } export type Account = { diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index c44461b60d..a66d65d97a 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,6 +21,7 @@ import {type ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {type PostAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' @@ -227,8 +228,12 @@ const GalleryItem = ({ }} accessible={true} accessibilityIgnoresInvertColors + cachePolicy="none" + autoplay={false} /> + + } )} + + {/* Only shown after logged in and onboaring etc are complete */} + {hasSession && } ) } diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 4d1a8c51b6..0d8c245665 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -31,6 +31,10 @@ import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsen import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' +import { + Outlet as PolicyUpdateOverlayPortalOutlet, + usePolicyUpdateStateContext, +} from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' import {RoutesContainer, TabsNavigator} from '#/Navigation' import {BottomSheetOutlet} from '../../../modules/bottom-sheet' @@ -45,6 +49,7 @@ function ShellInner() { const setIsDrawerOpen = useSetDrawerOpen() const winDim = useWindowDimensions() const insets = useSafeAreaInsets() + const policyUpdateState = usePolicyUpdateStateContext() const renderDrawerContent = useCallback(() => , []) const onOpenDrawer = useCallback( @@ -151,6 +156,7 @@ function ShellInner() { + @@ -160,8 +166,16 @@ function ShellInner() { - - + + {/* Until policy update has been completed by the user, don't render anything that is portaled */} + {policyUpdateState.completed && ( + <> + + + + )} + + ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 77c3f45f62..c1565e8ee7 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -22,6 +22,10 @@ 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 { + Outlet as PolicyUpdateOverlayPortalOutlet, + usePolicyUpdateStateContext, +} from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' import {FlatNavigator, RoutesContainer} from '#/Navigation' import {Composer} from './Composer.web' @@ -37,6 +41,7 @@ function ShellInner() { const {_} = useLingui() const showDrawer = !isDesktop && isDrawerOpen const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer) + const policyUpdateState = usePolicyUpdateStateContext() useLayoutEffect(() => { if (showDrawer !== showDrawerDelayedExit) { @@ -74,7 +79,13 @@ function ShellInner() { - + + {/* Until policy update has been completed by the user, don't render anything that is portaled */} + {policyUpdateState.completed && ( + <> + + + )} {showDrawerDelayedExit && ( <> @@ -113,6 +124,8 @@ function ShellInner() { )} + + ) } diff --git a/yarn.lock b/yarn.lock index 8079c90bfc..bc7e35f67d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11301,10 +11301,10 @@ expo-image-picker@~16.1.4: dependencies: expo-image-loader "~5.1.0" -expo-image@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.2.1.tgz#b4aa706a25f7e8902ac854a8da249caf4a90cd67" - integrity sha512-5ZSggMi0X2G9AN0aM+sdkCyyZ6YcWvGs9KYLYrRBVUN3ph6RBiu6mKGpaNN1TAscySRnH1eHbUE1H+Qeq7qm1g== +expo-image@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e" + integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw== expo-json-utils@~0.15.0: version "0.15.0"