From 14e39c4aecb8373b692c706bf27e49b0b72918b2 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:49:35 +0100 Subject: [PATCH 01/20] Tweak accessibility label (#8767) * tweak accessibility label * change `alert` to `message` and add comment instead of context * tweak comment --- src/components/Toast/index.web.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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)} /> From 9aa35e9fbb6136a88a66388ff5e4644ad25c9e4b Mon Sep 17 00:00:00 2001 From: Anastasiya Uraleva Date: Mon, 4 Aug 2025 06:17:50 -0700 Subject: [PATCH 02/20] [APP-1083] bug fix: videos not accurately autoplaying on web (#8692) * update: auto play video on web with intersection position * place back the threshold: 0.5 * update: optimize the intersection observer with a throttled scroll listener --------- Co-authored-by: Anastasiya Co-authored-by: Anastasiya --- .../Post/Embed/VideoEmbed/index.web.tsx | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index 7f601af47b..900bc21889 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -138,22 +138,53 @@ function ViewportObserver({ useEffect(() => { if (!ref.current) return if (isFullscreen && !isFirefox) return + + let scrollTimeout: NodeJS.Timeout | null = null + let lastObserverEntry: IntersectionObserverEntry | null = null + + const updatePositionFromEntry = () => { + if (!lastObserverEntry) return + const rect = lastObserverEntry.boundingClientRect + const position = rect.y + rect.height / 2 + sendPosition(position) + } + + const handleScroll = () => { + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + scrollTimeout = setTimeout(updatePositionFromEntry, 4) // ~240fps + } + const observer = new IntersectionObserver( entries => { const entry = entries[0] if (!entry) return - const position = - entry.boundingClientRect.y + entry.boundingClientRect.height / 2 - sendPosition(position) + lastObserverEntry = entry setNearScreen(entry.isIntersecting) + const rect = entry.boundingClientRect + const position = rect.y + rect.height / 2 + sendPosition(position) }, - {threshold: Array.from({length: 101}, (_, i) => i / 100)}, + {threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0]}, ) - observer.observe(ref.current) - return () => observer.disconnect() - }, [sendPosition, isFullscreen]) - // In case scrolling hasn't started yet, send up the position + observer.observe(ref.current) + + if (nearScreen) { + window.addEventListener('scroll', handleScroll, {passive: true}) + } + + return () => { + observer.disconnect() + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + window.removeEventListener('scroll', handleScroll) + } + }, [sendPosition, isFullscreen, nearScreen]) + + // In case scrolling hasn't started yet, send the original position useEffect(() => { if (ref.current && !isAnyViewActive) { const rect = ref.current.getBoundingClientRect() From b32568260f98ea879468fd1bdedacf85d1e6ae8c Mon Sep 17 00:00:00 2001 From: kindgracekind <155408223+kindgracekind@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:27:27 -0500 Subject: [PATCH 03/20] Add feed tuner to remove muted threads (#8773) --- src/lib/api/feed-manip.ts | 16 ++++++++++++++++ src/state/preferences/feed-tuners.tsx | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) 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/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 } From 7b5d11eb80f247173a11c37a6f1bb08824912a54 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 4 Aug 2025 18:42:55 +0300 Subject: [PATCH 04/20] Fix video thumbnails on native (#8774) --- package.json | 2 +- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 163 +++++++++--------- .../Post/Embed/VideoEmbed/index.tsx | 21 +-- yarn.lock | 8 +- 4 files changed, 94 insertions(+), 100 deletions(-) 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/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/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 ? ( Date: Mon, 4 Aug 2025 19:41:09 +0300 Subject: [PATCH 05/20] Fix android starting at notif screen (#8775) * fix android launching at notif screen * rm heuristic --- src/lib/hooks/useNotificationHandler.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 } } From a5437ebbeb9d2da84a1560d4c68d7c9ce42c5140 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 4 Aug 2025 20:24:53 +0300 Subject: [PATCH 06/20] Fix web video ViewportObserver component (#8776) * Revert "[APP-1083] bug fix: videos not accurately autoplaying on web (#8692)" This reverts commit 9aa35e9fbb6136a88a66388ff5e4644ad25c9e4b. * fix overflow hidden breaking the video viewport observer --- .../Post/Embed/VideoEmbed/index.web.tsx | 106 +++++++++--------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index 900bc21889..5bb54eef88 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -1,4 +1,11 @@ -import {useCallback, useEffect, useRef, useState} from 'react' +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react' import {View} from 'react-native' import {type AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/macro' @@ -83,9 +90,7 @@ export function VideoEmbed({ style={{display: 'flex', flex: 1, cursor: 'default'}} onClick={evt => 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, @@ -138,53 +151,22 @@ function ViewportObserver({ useEffect(() => { if (!ref.current) return if (isFullscreen && !isFirefox) return - - let scrollTimeout: NodeJS.Timeout | null = null - let lastObserverEntry: IntersectionObserverEntry | null = null - - const updatePositionFromEntry = () => { - if (!lastObserverEntry) return - const rect = lastObserverEntry.boundingClientRect - const position = rect.y + rect.height / 2 - sendPosition(position) - } - - const handleScroll = () => { - if (scrollTimeout) { - clearTimeout(scrollTimeout) - } - scrollTimeout = setTimeout(updatePositionFromEntry, 4) // ~240fps - } - const observer = new IntersectionObserver( entries => { const entry = entries[0] if (!entry) return - lastObserverEntry = entry - setNearScreen(entry.isIntersecting) - const rect = entry.boundingClientRect - const position = rect.y + rect.height / 2 + const position = + entry.boundingClientRect.y + entry.boundingClientRect.height / 2 sendPosition(position) + setNearScreen(entry.isIntersecting) }, - {threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0]}, + {threshold: Array.from({length: 101}, (_, i) => i / 100)}, ) - observer.observe(ref.current) + return () => observer.disconnect() + }, [sendPosition, isFullscreen]) - if (nearScreen) { - window.addEventListener('scroll', handleScroll, {passive: true}) - } - - return () => { - observer.disconnect() - if (scrollTimeout) { - clearTimeout(scrollTimeout) - } - window.removeEventListener('scroll', handleScroll) - } - }, [sendPosition, isFullscreen, nearScreen]) - - // In case scrolling hasn't started yet, send the original position + // In case scrolling hasn't started yet, send up the position useEffect(() => { if (ref.current && !isAnyViewActive) { const rect = ref.current.getBoundingClientRect() @@ -195,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() From 53cf0b3b469d7575cdfe4eba29f753b2f3456970 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 5 Aug 2025 02:53:53 +0000 Subject: [PATCH 07/20] Nightly source-language update --- src/locale/locales/en/messages.po | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index f5faf1e5e0..ce8db630fe 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -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 "" @@ -2659,12 +2659,13 @@ msgstr "" msgid "Dismiss interests" msgstr "" -#: src/components/interstitials/TrendingVideos.tsx:89 -msgid "Dismiss this section" +#. Accessibility label for dismissing a toast notification +#: src/components/Toast/index.web.tsx:82 +msgid "Dismiss message" msgstr "" -#: src/components/Toast/index.web.tsx:81 -msgid "Dismiss toast" +#: src/components/interstitials/TrendingVideos.tsx:89 +msgid "Dismiss this section" msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:69 @@ -3101,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 "" @@ -5115,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" @@ -6016,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 "" @@ -6105,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 "" @@ -6114,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 "" @@ -6127,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 "" @@ -8939,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 "" @@ -9125,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" @@ -9518,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 "" @@ -9549,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 "" @@ -9561,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 "" @@ -10421,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 "" From fd37d92f85ddf0f075a67c4e9b2d85bef38f1835 Mon Sep 17 00:00:00 2001 From: Arturo Fonseca <49870466+arturo32@users.noreply.github.com> Date: Tue, 5 Aug 2025 06:04:32 -0300 Subject: [PATCH 08/20] Fix quality of first seconds of videos on web (#8761) (#8778) --- .../Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 } From 328aa2be9482f77cb1cf86c5d227fdcee9981b16 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Aug 2025 15:15:52 -0500 Subject: [PATCH 09/20] [APP-1356] Policy update dialog (#8782) * Add blocking announcement dialog feature * WIP custom dialog * Rework dialog and add native FocusScope * Lock scroll on web, fix backdrop * Add web FocusScope * Create custom Outlet for these announcements * Clean up FocusScope native impl * Comments * Some styling fixes * Handle screen reader specifically * Clean up state, remove Portal edits * Reorg, rename * Add syncing, tests * Revert dialog updates * Revert formatting * Delete unused file * Format * Add FullWindowOverlay * remove mmkv storage in debug btn * Add debug code * fix taps passing through on iOS * Reorg * Reorg, rename everything * Complete policy update after signup * Add logger * Move context around, unmount portals on native * Move a11y prop into FocusScope * Remove useMemo * Update dates * Move debug to dev settings * Unmount web portals until policy update completed * UPdate dates --------- Co-authored-by: Samuel Newman --- src/App.native.tsx | 89 ++++---- src/App.web.tsx | 81 ++++---- src/components/FocusScope/index.tsx | 144 +++++++++++++ src/components/FocusScope/index.web.tsx | 15 ++ src/components/LockScroll/index.tsx | 3 + src/components/LockScroll/index.web.tsx | 3 + src/components/PolicyUpdateOverlay/Badge.tsx | 38 ++++ .../PolicyUpdateOverlay/Overlay.tsx | 139 +++++++++++++ src/components/PolicyUpdateOverlay/Portal.tsx | 7 + .../__tests__/useAnnouncementState.test.ts | 195 ++++++++++++++++++ src/components/PolicyUpdateOverlay/config.ts | 7 + .../PolicyUpdateOverlay/context.tsx | 32 +++ src/components/PolicyUpdateOverlay/index.tsx | 41 ++++ src/components/PolicyUpdateOverlay/logger.ts | 3 + .../updates/202508/config.ts | 7 + .../updates/202508/index.tsx | 190 +++++++++++++++++ .../usePolicyUpdateState.ts | 135 ++++++++++++ ...ePreemptivelyCompleteActivePolicyUpdate.ts | 21 ++ src/logger/types.ts | 1 + src/screens/Settings/Settings.tsx | 42 ++++ src/screens/Signup/state.ts | 16 +- src/state/persisted/index.ts | 6 +- src/state/queries/nuxs/__mocks__/index.ts | 25 +++ src/state/queries/nuxs/definitions.ts | 10 + src/storage/index.ts | 7 + src/storage/schema.ts | 8 + .../createNativeStackNavigatorWithAuth.tsx | 4 + src/view/shell/index.tsx | 18 +- src/view/shell/index.web.tsx | 15 +- 29 files changed, 1214 insertions(+), 88 deletions(-) create mode 100644 src/components/FocusScope/index.tsx create mode 100644 src/components/FocusScope/index.web.tsx create mode 100644 src/components/LockScroll/index.tsx create mode 100644 src/components/LockScroll/index.web.tsx create mode 100644 src/components/PolicyUpdateOverlay/Badge.tsx create mode 100644 src/components/PolicyUpdateOverlay/Overlay.tsx create mode 100644 src/components/PolicyUpdateOverlay/Portal.tsx create mode 100644 src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts create mode 100644 src/components/PolicyUpdateOverlay/config.ts create mode 100644 src/components/PolicyUpdateOverlay/context.tsx create mode 100644 src/components/PolicyUpdateOverlay/index.tsx create mode 100644 src/components/PolicyUpdateOverlay/logger.ts create mode 100644 src/components/PolicyUpdateOverlay/updates/202508/config.ts create mode 100644 src/components/PolicyUpdateOverlay/updates/202508/index.tsx create mode 100644 src/components/PolicyUpdateOverlay/usePolicyUpdateState.ts create mode 100644 src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.ts create mode 100644 src/state/queries/nuxs/__mocks__/index.ts 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/FocusScope/index.tsx b/src/components/FocusScope/index.tsx new file mode 100644 index 0000000000..408381d5b8 --- /dev/null +++ b/src/components/FocusScope/index.tsx @@ -0,0 +1,144 @@ +import { + Children, + cloneElement, + isValidElement, + type ReactElement, + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react' +import { + AccessibilityInfo, + findNodeHandle, + Pressable, + Text, + View, +} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useA11y} from '#/state/a11y' + +/** + * Conditionally wraps children in a `FocusTrap` component based on whether + * screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when + * no better option is available. + */ +export function FocusScope({children}: {children: ReactNode}) { + const {screenReaderEnabled} = useA11y() + + return screenReaderEnabled ? {children} : children +} + +/** + * `FocusTrap` is intended as a last-ditch effort to ensure that users keep + * focus within a certain section of the app, like an overlay. + * + * It works by placing "guards" at the start and end of the active content. + * Then when the user reaches either of those guards, it will announce that + * they have reached the start or end of the content and tell them how to + * remain within the active content section. + */ +function FocusTrap({children}: {children: ReactNode}) { + const {_} = useLingui() + const child = useRef(null) + + /* + * Here we add a ref to the first child of this component. This currently + * overrides any ref already on that first child, so we throw an error here + * to prevent us from ever accidentally doing this. + */ + const decoratedChildren = useMemo(() => { + return Children.toArray(children).map((node, i) => { + if (i === 0 && isValidElement(node)) { + const n = node as ReactElement + if (n.props.ref !== undefined) { + throw new Error( + 'FocusScope needs to override the ref on its first child.', + ) + } + return cloneElement(n, { + ...n.props, + ref: child, + }) + } + return node + }) + }, [children]) + + const focusNode = useCallback((ref: View | null) => { + if (!ref) return + const node = findNodeHandle(ref) + if (node) { + AccessibilityInfo.setAccessibilityFocus(node) + } + }, []) + + useEffect(() => { + setTimeout(() => { + focusNode(child.current) + }, 1e3) + }, [focusNode]) + + return ( + <> + { + switch (event.nativeEvent.actionName) { + case 'activate': { + focusNode(child.current) + } + } + }}> + + + + {decoratedChildren} + + { + switch (event.nativeEvent.actionName) { + case 'activate': { + focusNode(child.current) + } + } + }}> + + + + ) +} + +function Noop() { + return ( + + {' '} + + ) +} diff --git a/src/components/FocusScope/index.web.tsx b/src/components/FocusScope/index.web.tsx new file mode 100644 index 0000000000..43ea06a2df --- /dev/null +++ b/src/components/FocusScope/index.web.tsx @@ -0,0 +1,15 @@ +import {type ReactNode} from 'react' +import {FocusScope as RadixFocusScope} from 'radix-ui/internal' + +/* + * The web version of the FocusScope component is a proper implementation, we + * use this in Dialogs and such already. It's here as a convenient counterpart + * to the hacky native solution. + */ +export function FocusScope({children}: {children: ReactNode}) { + return ( + + {children} + + ) +} diff --git a/src/components/LockScroll/index.tsx b/src/components/LockScroll/index.tsx new file mode 100644 index 0000000000..7ae45f7717 --- /dev/null +++ b/src/components/LockScroll/index.tsx @@ -0,0 +1,3 @@ +export function LockScroll() { + return null +} diff --git a/src/components/LockScroll/index.web.tsx b/src/components/LockScroll/index.web.tsx new file mode 100644 index 0000000000..2110a3cd01 --- /dev/null +++ b/src/components/LockScroll/index.web.tsx @@ -0,0 +1,3 @@ +import {RemoveScrollBar} from 'react-remove-scroll-bar' + +export const LockScroll = RemoveScrollBar diff --git a/src/components/PolicyUpdateOverlay/Badge.tsx b/src/components/PolicyUpdateOverlay/Badge.tsx new file mode 100644 index 0000000000..3829f60a5a --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Badge.tsx @@ -0,0 +1,38 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {Logo} from '#/view/icons/Logo' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' + +export function Badge() { + const t = useTheme() + return ( + + + + + Announcement + + + + ) +} diff --git a/src/components/PolicyUpdateOverlay/Overlay.tsx b/src/components/PolicyUpdateOverlay/Overlay.tsx new file mode 100644 index 0000000000..dd071ef15c --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Overlay.tsx @@ -0,0 +1,139 @@ +import {type ReactNode} from 'react' +import {ScrollView, View} from 'react-native' +import { + useSafeAreaFrame, + useSafeAreaInsets, +} from 'react-native-safe-area-context' +import {LinearGradient} from 'expo-linear-gradient' + +import {isAndroid, isNative} from '#/platform/detection' +import {useA11y} from '#/state/a11y' +import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf' +import {transparentifyColor} from '#/alf/util/colorGeneration' +import {FocusScope} from '#/components/FocusScope' +import {LockScroll} from '#/components/LockScroll' + +const GUTTER = 24 + +export function Overlay({ + children, + label, +}: { + children: ReactNode + label: string +}) { + const t = useTheme() + const {gtPhone} = useBreakpoints() + const {reduceMotionEnabled} = useA11y() + const insets = useSafeAreaInsets() + const frame = useSafeAreaFrame() + + return ( + <> + + + + {gtPhone ? ( + + + + ) : ( + + )} + + + + {/** + * This is needed to prevent centered dialogs from overflowing + * above the screen, and provides a "natural" centering so that + * stacked dialogs appear relatively aligned. + */} + + {!gtPhone && ( + + + + )} + + + + {children} + + + + + + ) +} diff --git a/src/components/PolicyUpdateOverlay/Portal.tsx b/src/components/PolicyUpdateOverlay/Portal.tsx new file mode 100644 index 0000000000..9000079840 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Portal.tsx @@ -0,0 +1,7 @@ +import {createPortalGroup} from '#/components/Portal' + +const portalGroup = createPortalGroup() + +export const Provider = portalGroup.Provider +export const Portal = portalGroup.Portal +export const Outlet = portalGroup.Outlet diff --git a/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts new file mode 100644 index 0000000000..f6055bf34a --- /dev/null +++ b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts @@ -0,0 +1,195 @@ +import {describe, test} from '@jest/globals' + +import { + computeCompletedState, + syncCompletedState, +} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState' + +jest.mock('../../../state/queries/nuxs') + +describe('computeCompletedState', () => { + test(`initial state`, () => { + const completed = computeCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }) + + expect(completed).toBe(true) + }) + + test(`nux loaded state`, () => { + const completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }) + + expect(completed).toBe(false) + }) + + test(`nux saving state`, () => { + const completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: true, + completedForDevice: undefined, + }) + + expect(completed).toBe(true) + }) + + test(`nux is completed`, () => { + const completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }) + + expect(completed).toBe(true) + }) + + test(`initial state, but already completed for device`, () => { + const completed = computeCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + }) + + expect(completed).toBe(true) + }) +}) + +describe('syncCompletedState', () => { + describe('!nuxIsReady', () => { + test(`!completedForDevice, no-op`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + + test(`completedForDevice, no-op`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + }) + + describe('nuxIsReady', () => { + describe(`!nuxIsCompleted`, () => { + describe(`!nuxIsOptimisticallyCompleted`, () => { + test(`!completedForDevice, no-op`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + + test(`completedForDevice, syncs to server`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save, + setCompletedForDevice, + }) + + expect(save).toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + }) + + /** + * Catches the case where we already called `save` to sync device state + * to server, thus `nuxIsOptimisticallyCompleted` is true. + */ + describe(`nuxIsOptimisticallyCompleted`, () => { + test(`completedForDevice, no-op`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: true, + completedForDevice: true, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + }) + }) + + describe(`nuxIsCompleted`, () => { + test(`!completedForDevice, syncs to device`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).toHaveBeenCalled() + }) + + test(`completedForDevice, no-op`, () => { + const save = jest.fn() + const setCompletedForDevice = jest.fn() + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save, + setCompletedForDevice, + }) + + expect(save).not.toHaveBeenCalled() + expect(setCompletedForDevice).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/components/PolicyUpdateOverlay/config.ts b/src/components/PolicyUpdateOverlay/config.ts new file mode 100644 index 0000000000..cd003ed639 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/config.ts @@ -0,0 +1,7 @@ +import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config' + +/** + * The singulary active update ID. This is configured here to ensure that + * the relationship is clear. + */ +export const ACTIVE_UPDATE_ID = ID diff --git a/src/components/PolicyUpdateOverlay/context.tsx b/src/components/PolicyUpdateOverlay/context.tsx new file mode 100644 index 0000000000..68ae7bbd86 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/context.tsx @@ -0,0 +1,32 @@ +import {createContext, type ReactNode, useContext} from 'react' + +import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal' +import { + type PolicyUpdateState, + usePolicyUpdateState, +} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState' + +const Context = createContext({ + completed: true, + complete: () => {}, +}) + +export function usePolicyUpdateStateContext() { + const context = useContext(Context) + if (!context) { + throw new Error( + 'usePolicyUpdateStateContext must be used within a PolicyUpdateProvider', + ) + } + return context +} + +export function Provider({children}: {children?: ReactNode}) { + const state = usePolicyUpdateState() + + return ( + + {children} + + ) +} diff --git a/src/components/PolicyUpdateOverlay/index.tsx b/src/components/PolicyUpdateOverlay/index.tsx new file mode 100644 index 0000000000..1900dc27fd --- /dev/null +++ b/src/components/PolicyUpdateOverlay/index.tsx @@ -0,0 +1,41 @@ +import {View} from 'react-native' + +import {isIOS} from '#/platform/detection' +import {atoms as a} from '#/alf' +import {FullWindowOverlay} from '#/components/FullWindowOverlay' +import {usePolicyUpdateStateContext} from '#/components/PolicyUpdateOverlay/context' +import {Portal} from '#/components/PolicyUpdateOverlay/Portal' +import {Content} from '#/components/PolicyUpdateOverlay/updates/202508' + +export {Provider} from '#/components/PolicyUpdateOverlay/context' +export {usePolicyUpdateStateContext} from '#/components/PolicyUpdateOverlay/context' +export {Outlet} from '#/components/PolicyUpdateOverlay/Portal' + +export function PolicyUpdateOverlay() { + const state = usePolicyUpdateStateContext() + + /* + * See `window.clearNux` example in `/state/queries/nuxs` for a way to clear + * NUX state for local testing and debugging. + */ + + if (state.completed) return null + + return ( + + + + + + + + ) +} diff --git a/src/components/PolicyUpdateOverlay/logger.ts b/src/components/PolicyUpdateOverlay/logger.ts new file mode 100644 index 0000000000..cd66c17098 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/logger.ts @@ -0,0 +1,3 @@ +import {Logger} from '#/logger' + +export const logger = Logger.create(Logger.Context.PolicyUpdate) diff --git a/src/components/PolicyUpdateOverlay/updates/202508/config.ts b/src/components/PolicyUpdateOverlay/updates/202508/config.ts new file mode 100644 index 0000000000..72af31d852 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/updates/202508/config.ts @@ -0,0 +1,7 @@ +/* + * Keep this file separate to avoid import issues. + */ + +import {Nux} from '#/state/queries/nuxs' + +export const ID = Nux.PolicyUpdate202508 diff --git a/src/components/PolicyUpdateOverlay/updates/202508/index.tsx b/src/components/PolicyUpdateOverlay/updates/202508/index.tsx new file mode 100644 index 0000000000..aa667e29a0 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/updates/202508/index.tsx @@ -0,0 +1,190 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isAndroid} from '#/platform/detection' +import {useA11y} from '#/state/a11y' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {InlineLinkText, Link} from '#/components/Link' +import {Badge} from '#/components/PolicyUpdateOverlay/Badge' +import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay' +import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState' +import {Text} from '#/components/Typography' + +export function Content({state}: {state: PolicyUpdateState}) { + const t = useTheme() + const {_} = useLingui() + const {screenReaderEnabled} = useA11y() + + const handleClose = useCallback(() => { + state.complete() + }, [state]) + + const linkStyle = [a.text_md] + const links = { + terms: { + overridePresentation: false, + to: `https://bsky.social/about/support`, + label: _(msg`Terms of Service`), + }, + privacy: { + overridePresentation: false, + to: `https://bsky.social/about/support`, + label: _(msg`Privacy Policy`), + }, + copyright: { + overridePresentation: false, + to: `https://bsky.social/about/support`, + label: _(msg`Copyright Policy`), + }, + guidelines: { + overridePresentation: false, + to: `https://bsky.social/about/support`, + label: _(msg`Community Guidelines`), + }, + blog: { + overridePresentation: false, + to: `https://bsky.social/about/support`, + label: _(msg`Our blog post`), + }, + } + const linkButtonStyles = { + overridePresentation: false, + color: 'secondary', + size: 'small', + } as const + + const label = isAndroid + ? _( + msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`, + ) + : _(msg`We're updating our policies`) + + return ( + + + + + {screenReaderEnabled ? ( + + + Hey there 👋 + + + + We’re updating our Terms of Service, Privacy Policy, and + Copyright Policy, effective September 12th, 2025. + + + + + We're also updating our Community Guidelines, and we want your + input! These new guidelines will take effect on October 13th, + 2025. + + + + + Learn more about these changes and how to share your thoughts + with us by reading our blog post. + + + + + + Terms of Service + + + + + Privacy Policy + + + + + Copyright Policy + + + + + Read our blog post + + + + ) : ( + + + Hey there 👋 + + + + We’re updating our{' '} + + Terms of Service + + ,{' '} + + Privacy Policy + + , and{' '} + + Copyright Policy + + , effective September 12th, 2025. + + + + + We're also updating our{' '} + + Community Guidelines + + , and we want your input! These new guidelines will take effect + on October 13th, 2025. + + + + + Learn more about these changes and how to share your thoughts + with us by{' '} + + reading our blog post. + + + + + )} + + + + + + + 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/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/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/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/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 1c32971d42..bb022a0134 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -40,6 +40,7 @@ import {Onboarding} from '#/screens/Onboarding' import {SignupQueued} from '#/screens/SignupQueued' import {Takendown} from '#/screens/Takendown' import {atoms as a, useLayoutBreakpoints} from '#/alf' +import {PolicyUpdateOverlay} from '#/components/PolicyUpdateOverlay' import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopRightNav} from './desktop/RightNav' @@ -167,6 +168,9 @@ function NativeStackNavigator({ {!isMobile && } )} + + {/* 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() { )} + + ) } From 75094f48603e120605642e20e1048d839be23788 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Aug 2025 15:16:08 -0500 Subject: [PATCH 10/20] [LEG-203] Add onboarding text for policy updates (#8783) * Add onboarding text for policy updates * Add gate, add to other case * Update other UI cases to use Admonitions * Th * Update date --- src/lib/constants.ts | 7 ++++ src/lib/statsig/gates.ts | 1 + src/screens/Signup/StepInfo/Policies.tsx | 52 +++++++++++++++++++----- 3 files changed, 50 insertions(+), 10 deletions(-) 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/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/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} + + ) } From 909ddc86e1b8867880b12fe36b5e816523a9e438 Mon Sep 17 00:00:00 2001 From: Anastasiya Uraleva Date: Wed, 6 Aug 2025 14:27:44 -0700 Subject: [PATCH 11/20] [APP-1365]: Ensure toast text meets a11y guidelines (#8779) * update: toast colors for accessibility * update: toast color codes from figma * condense the colors to remove select calls that are unecessary * remove unused select --- src/components/Toast/Toast.tsx | 120 ++++++--------------------------- 1 file changed, 22 insertions(+), 98 deletions(-) diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 0dc9d4b079..ef15c6d97b 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -2,20 +2,20 @@ import {createContext, useContext, useMemo} from 'react' import {View} from 'react-native' import {atoms as a, select, useTheme} from '#/alf' -import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {type ToastType} from '#/components/Toast/types' import {Text} from '#/components/Typography' +import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '../icons/CircleCheck' type ContextType = { type: ToastType } export const ICONS = { - default: SuccessIcon, - success: SuccessIcon, + default: CircleCheck, + success: CircleCheck, error: ErrorIcon, warning: WarningIcon, info: CircleInfo, @@ -91,114 +91,38 @@ function useToastStyles({type}: {type: ToastType}) { return useMemo(() => { return { default: { - backgroundColor: select(t.name, { - light: t.atoms.bg_contrast_25.backgroundColor, - dim: t.atoms.bg_contrast_100.backgroundColor, - dark: t.atoms.bg_contrast_100.backgroundColor, - }), - borderColor: select(t.name, { - light: t.atoms.border_contrast_low.borderColor, - dim: t.atoms.border_contrast_high.borderColor, - dark: t.atoms.border_contrast_high.borderColor, - }), - iconColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), - textColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_high.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, }, success: { - backgroundColor: select(t.name, { - light: t.palette.primary_100, - dim: t.palette.primary_100, - dark: t.palette.primary_50, - }), - borderColor: select(t.name, { - light: t.palette.primary_500, - dim: t.palette.primary_500, - dark: t.palette.primary_500, - }), - iconColor: select(t.name, { - light: t.palette.primary_500, - dim: t.palette.primary_600, - dark: t.palette.primary_600, - }), - textColor: select(t.name, { - light: t.palette.primary_500, - dim: t.palette.primary_600, - dark: t.palette.primary_600, - }), + backgroundColor: t.palette.primary_25, + borderColor: t.palette.primary_300, + iconColor: t.palette.primary_600, + textColor: t.palette.primary_600, }, error: { - backgroundColor: select(t.name, { - light: t.palette.negative_200, - dim: t.palette.negative_25, - dark: t.palette.negative_25, - }), + backgroundColor: t.palette.negative_25, borderColor: select(t.name, { light: t.palette.negative_300, dim: t.palette.negative_300, dark: t.palette.negative_300, }), - iconColor: select(t.name, { - light: t.palette.negative_600, - dim: t.palette.negative_600, - dark: t.palette.negative_600, - }), - textColor: select(t.name, { - light: t.palette.negative_600, - dim: t.palette.negative_600, - dark: t.palette.negative_600, - }), + iconColor: t.palette.negative_600, + textColor: t.palette.negative_600, }, warning: { - backgroundColor: select(t.name, { - light: t.atoms.bg_contrast_25.backgroundColor, - dim: t.atoms.bg_contrast_100.backgroundColor, - dark: t.atoms.bg_contrast_100.backgroundColor, - }), - borderColor: select(t.name, { - light: t.atoms.border_contrast_low.borderColor, - dim: t.atoms.border_contrast_high.borderColor, - dark: t.atoms.border_contrast_high.borderColor, - }), - iconColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), - textColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_high.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, }, info: { - backgroundColor: select(t.name, { - light: t.atoms.bg_contrast_25.backgroundColor, - dim: t.atoms.bg_contrast_100.backgroundColor, - dark: t.atoms.bg_contrast_100.backgroundColor, - }), - borderColor: select(t.name, { - light: t.atoms.border_contrast_low.borderColor, - dim: t.atoms.border_contrast_high.borderColor, - dark: t.atoms.border_contrast_high.borderColor, - }), - iconColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), - textColor: select(t.name, { - light: t.atoms.text_contrast_medium.color, - dim: t.atoms.text_contrast_medium.color, - dark: t.atoms.text_contrast_medium.color, - }), + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_high.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, }, }[type] }, [t, type]) From dbb9ff69184841fa0001489a2faa9235daf0e70b Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:52:42 +0000 Subject: [PATCH 12/20] Nightly source-language update --- src/locale/locales/en/messages.po | 259 ++++++++++++++++++++---------- 1 file changed, 175 insertions(+), 84 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index ce8db630fe..6e6b8fa755 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -518,8 +518,8 @@ msgstr "" #: src/Navigation.tsx:523 #: src/screens/Settings/AboutSettings.tsx:75 -#: src/screens/Settings/Settings.tsx:240 -#: src/screens/Settings/Settings.tsx:243 +#: src/screens/Settings/Settings.tsx:244 +#: src/screens/Settings/Settings.tsx:247 msgid "About" msgstr "" @@ -538,8 +538,8 @@ msgid "Accept Request" msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:44 -#: src/screens/Settings/Settings.tsx:216 -#: src/screens/Settings/Settings.tsx:219 +#: src/screens/Settings/Settings.tsx:220 +#: src/screens/Settings/Settings.tsx:223 msgid "Accessibility" msgstr "" @@ -550,8 +550,8 @@ msgstr "" #: src/Navigation.tsx:398 #: src/screens/Login/LoginForm.tsx:194 #: src/screens/Settings/AccountSettings.tsx:49 -#: src/screens/Settings/Settings.tsx:170 -#: src/screens/Settings/Settings.tsx:173 +#: src/screens/Settings/Settings.tsx:174 +#: src/screens/Settings/Settings.tsx:177 msgid "Account" msgstr "" @@ -582,11 +582,11 @@ msgstr "" msgid "Account Muted by List" msgstr "" -#: src/screens/Settings/Settings.tsx:582 +#: src/screens/Settings/Settings.tsx:624 msgid "Account options" msgstr "" -#: src/screens/Settings/Settings.tsx:618 +#: src/screens/Settings/Settings.tsx:660 msgid "Account removed from quick access" msgstr "" @@ -668,8 +668,8 @@ msgstr "" msgid "Add alt text (optional)" msgstr "" -#: src/screens/Settings/Settings.tsx:522 -#: src/screens/Settings/Settings.tsx:525 +#: src/screens/Settings/Settings.tsx:564 +#: src/screens/Settings/Settings.tsx:567 #: src/view/shell/desktop/LeftNav.tsx:259 #: src/view/shell/desktop/LeftNav.tsx:263 msgid "Add another account" @@ -999,6 +999,10 @@ msgstr "" msgid "Animated GIF" msgstr "" +#: src/components/PolicyUpdateOverlay/Badge.tsx:33 +msgid "Announcement" +msgstr "" + #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:37 msgid "Announcing verification on Bluesky" msgstr "" @@ -1093,8 +1097,8 @@ msgstr "" #: src/Navigation.tsx:390 #: src/screens/Settings/AppearanceSettings.tsx:88 -#: src/screens/Settings/Settings.tsx:208 -#: src/screens/Settings/Settings.tsx:211 +#: src/screens/Settings/Settings.tsx:212 +#: src/screens/Settings/Settings.tsx:215 msgid "Appearance" msgstr "" @@ -1103,8 +1107,8 @@ msgstr "" msgid "Apply default recommended feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:490 -#: src/screens/Settings/Settings.tsx:492 +#: src/screens/Settings/Settings.tsx:498 +#: src/screens/Settings/Settings.tsx:500 msgid "Apply Pull Request" msgstr "" @@ -1402,6 +1406,14 @@ msgstr "" msgid "Bluesky will proactively verify notable and authentic accounts." msgstr "" +#: src/screens/Signup/StepInfo/Policies.tsx:26 +msgid "Bluesky's Community Guidelines" +msgstr "" + +#: src/screens/Signup/StepInfo/Policies.tsx:32 +msgid "Bluesky's Updated Community Guidelines" +msgstr "" + #: src/screens/Settings/AppIconSettings/index.tsx:99 msgid "Bluesky+" msgstr "" @@ -1480,19 +1492,23 @@ msgstr "" msgid "By <0>{0}" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:181 +msgid "By clicking \"Continue\" you acknowledge that you understand and agree to these updates." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:329 msgid "By continuing, you agree to the <0>KWS Terms of Use and acknowledge that KWS will store your verified status with your hashed email address in accordance with the <1>KWS Privacy Policy. This means you won’t need to verify again the next time you use this email for other apps, games, and services powered by KWS technology." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:81 +#: src/screens/Signup/StepInfo/Policies.tsx:111 msgid "By creating an account you agree to the <0>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:48 +#: src/screens/Signup/StepInfo/Policies.tsx:78 msgid "By creating an account you agree to the <0>Terms of Service and <1>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:68 +#: src/screens/Signup/StepInfo/Policies.tsx:98 msgid "By creating an account you agree to the <0>Terms of Service." msgstr "" @@ -1525,7 +1541,7 @@ msgstr "" #: src/screens/Settings/AppIconSettings/index.tsx:225 #: src/screens/Settings/components/ChangeHandleDialog.tsx:78 #: src/screens/Settings/components/ChangeHandleDialog.tsx:85 -#: src/screens/Settings/Settings.tsx:285 +#: src/screens/Settings/Settings.tsx:289 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 #: src/view/com/composer/Composer.tsx:965 @@ -1748,11 +1764,11 @@ msgstr "" msgid "Choose your username" msgstr "" -#: src/screens/Settings/Settings.tsx:482 +#: src/screens/Settings/Settings.tsx:490 msgid "Clear all storage data" msgstr "" -#: src/screens/Settings/Settings.tsx:484 +#: src/screens/Settings/Settings.tsx:492 msgid "Clear all storage data (restart after this)" msgstr "" @@ -1858,7 +1874,7 @@ msgstr "" msgid "Close dialog" msgstr "" -#: src/view/shell/index.web.tsx:89 +#: src/view/shell/index.web.tsx:100 msgid "Close drawer menu" msgstr "" @@ -1933,6 +1949,7 @@ msgstr "" msgid "Comics" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:45 #: src/Navigation.tsx:340 #: src/view/screens/CommunityGuidelines.tsx:34 msgid "Community Guidelines" @@ -2029,8 +2046,8 @@ msgstr "" msgid "Content & Media" msgstr "" -#: src/screens/Settings/Settings.tsx:200 -#: src/screens/Settings/Settings.tsx:203 +#: src/screens/Settings/Settings.tsx:204 +#: src/screens/Settings/Settings.tsx:207 msgid "Content and media" msgstr "" @@ -2076,6 +2093,8 @@ msgstr "" msgid "Context menu backdrop, click to close the menu." msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:162 +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:170 #: src/screens/Onboarding/StepInterests/index.tsx:244 #: src/screens/Onboarding/StepProfile/index.tsx:276 msgid "Continue" @@ -2230,6 +2249,8 @@ msgstr "" msgid "Copy TXT record value" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:40 +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:107 #: src/Navigation.tsx:345 #: src/view/screens/CopyrightPolicy.tsx:31 msgid "Copyright Policy" @@ -2398,7 +2419,7 @@ msgstr "" msgid "Deactivate account" msgstr "" -#: src/screens/Settings/Settings.tsx:447 +#: src/screens/Settings/Settings.tsx:455 msgid "Debug Moderation" msgstr "" @@ -2447,7 +2468,7 @@ msgstr "" msgid "Delete chat" msgstr "" -#: src/screens/Settings/Settings.tsx:454 +#: src/screens/Settings/Settings.tsx:462 msgid "Delete chat declaration record" msgstr "" @@ -2563,8 +2584,8 @@ msgctxt "toast" msgid "Developer mode enabled" msgstr "" -#: src/screens/Settings/Settings.tsx:267 -#: src/screens/Settings/Settings.tsx:270 +#: src/screens/Settings/Settings.tsx:271 +#: src/screens/Settings/Settings.tsx:274 msgid "Developer options" msgstr "" @@ -4034,8 +4055,8 @@ msgstr "" msgid "Having trouble?" msgstr "" -#: src/screens/Settings/Settings.tsx:232 #: src/screens/Settings/Settings.tsx:236 +#: src/screens/Settings/Settings.tsx:240 #: src/view/shell/desktop/RightNav.tsx:120 #: src/view/shell/desktop/RightNav.tsx:121 #: src/view/shell/Drawer.tsx:370 @@ -4050,6 +4071,11 @@ msgstr "" msgid "Here is your app password!" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:73 +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:119 +msgid "Hey there 👋" +msgstr "" + #: src/components/VideoPostCard.tsx:178 #: src/components/VideoPostCard.tsx:460 msgid "Hidden" @@ -4226,7 +4252,7 @@ msgstr "" msgid "If alt text is long, toggles alt text expanded state" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:112 +#: src/screens/Signup/StepInfo/Policies.tsx:142 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." msgstr "" @@ -4405,7 +4431,7 @@ msgstr "" msgid "Invite code" msgstr "" -#: src/screens/Signup/state.ts:339 +#: src/screens/Signup/state.ts:348 msgid "Invite code not accepted. Check that you input it correctly and try again." msgstr "" @@ -4517,8 +4543,8 @@ msgid "Language Settings" msgstr "" #: src/screens/Settings/LanguageSettings.tsx:78 -#: src/screens/Settings/Settings.tsx:224 -#: src/screens/Settings/Settings.tsx:227 +#: src/screens/Settings/Settings.tsx:228 +#: src/screens/Settings/Settings.tsx:231 msgid "Languages" msgstr "" @@ -4576,6 +4602,14 @@ msgstr "" msgid "Learn more about the moderation applied to this content." msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:149 +msgid "Learn more about these changes and how to share your thoughts with us by <0>reading our blog post." +msgstr "" + +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:89 +msgid "Learn more about these changes and how to share your thoughts with us by reading our blog post." +msgstr "" + #: src/components/moderation/PostHider.tsx:110 #: src/components/moderation/ScreenHider.tsx:127 msgid "Learn more about this warning" @@ -5024,8 +5058,8 @@ msgstr "" #: src/Navigation.tsx:176 #: src/screens/Moderation/index.tsx:99 -#: src/screens/Settings/Settings.tsx:184 -#: src/screens/Settings/Settings.tsx:187 +#: src/screens/Settings/Settings.tsx:188 +#: src/screens/Settings/Settings.tsx:191 msgid "Moderation" msgstr "" @@ -5626,8 +5660,8 @@ msgstr "" #: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30 #: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30 #: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30 -#: src/screens/Settings/Settings.tsx:192 -#: src/screens/Settings/Settings.tsx:195 +#: src/screens/Settings/Settings.tsx:196 +#: 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:655 @@ -5700,7 +5734,7 @@ msgstr "" msgid "on<0><1/><2><3/>" msgstr "" -#: src/screens/Settings/Settings.tsx:380 +#: src/screens/Settings/Settings.tsx:388 msgid "Onboarding reset" msgstr "" @@ -5798,7 +5832,7 @@ msgstr "" msgid "Open message options" msgstr "" -#: src/screens/Settings/Settings.tsx:445 +#: src/screens/Settings/Settings.tsx:453 msgid "Open moderation debug page" msgstr "" @@ -5827,12 +5861,12 @@ msgstr "" msgid "Open starter pack menu" msgstr "" -#: src/screens/Settings/Settings.tsx:438 -#: src/screens/Settings/Settings.tsx:452 +#: src/screens/Settings/Settings.tsx:446 +#: src/screens/Settings/Settings.tsx:460 msgid "Open storybook page" msgstr "" -#: src/screens/Settings/Settings.tsx:431 +#: src/screens/Settings/Settings.tsx:439 msgid "Open system log" msgstr "" @@ -5894,7 +5928,7 @@ msgstr "" msgid "Opens GIF select dialog" msgstr "" -#: src/screens/Settings/Settings.tsx:233 +#: src/screens/Settings/Settings.tsx:237 msgid "Opens helpdesk in browser" msgstr "" @@ -5974,6 +6008,10 @@ msgstr "" msgid "Other..." msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:50 +msgid "Our blog post" +msgstr "" + #: src/components/dms/ReportDialog.tsx:348 msgid "Our moderation team has received your report." msgstr "" @@ -6148,11 +6186,11 @@ msgstr "" msgid "Please check your email inbox for further instructions. It may take a minute or two to arrive." msgstr "" -#: src/screens/Signup/state.ts:286 +#: src/screens/Signup/state.ts:289 msgid "Please choose your handle." msgstr "" -#: src/screens/Signup/state.ts:278 +#: src/screens/Signup/state.ts:281 #: src/screens/Signup/StepInfo/index.tsx:120 msgid "Please choose your password." msgstr "" @@ -6161,7 +6199,7 @@ msgstr "" msgid "Please click on the link in the email we just sent you to verify your new email address. This is an important step to allow you to continue enjoying all the features of Bluesky." msgstr "" -#: src/screens/Signup/state.ts:301 +#: src/screens/Signup/state.ts:304 msgid "Please complete the verification captcha." msgstr "" @@ -6209,7 +6247,7 @@ msgstr "" msgid "Please enter the security code we sent to your previous email address." msgstr "" -#: src/screens/Signup/state.ts:262 +#: src/screens/Signup/state.ts:265 #: src/screens/Signup/StepInfo/index.tsx:91 msgid "Please enter your email." msgstr "" @@ -6242,6 +6280,11 @@ msgstr "" msgid "Please explain why you think your chats were incorrectly disabled" msgstr "" +#: src/components/FocusScope/index.tsx:93 +#: src/components/FocusScope/index.tsx:117 +msgid "Please go back, or activate this element to return to the start of the active content." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:98 msgid "Please provide any additional details you feel moderators may need in order to properly assess your Age Assurance status." msgstr "" @@ -6440,8 +6483,8 @@ msgstr "" msgid "Privacy" msgstr "" -#: src/screens/Settings/Settings.tsx:178 -#: src/screens/Settings/Settings.tsx:181 +#: src/screens/Settings/Settings.tsx:182 +#: src/screens/Settings/Settings.tsx:185 msgid "Privacy and security" msgstr "" @@ -6456,6 +6499,8 @@ msgstr "" msgid "Privacy and Security settings" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:35 +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:102 #: src/Navigation.tsx:330 #: src/screens/Settings/AboutSettings.tsx:92 #: src/screens/Settings/AboutSettings.tsx:95 @@ -6638,17 +6683,21 @@ msgstr "" msgid "Read more replies" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:112 +msgid "Read our blog post" +msgstr "" + #: src/view/com/auth/SplashScreen.web.tsx:173 msgid "Read the Bluesky blog" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:58 -#: src/screens/Signup/StepInfo/Policies.tsx:84 +#: src/screens/Signup/StepInfo/Policies.tsx:88 +#: src/screens/Signup/StepInfo/Policies.tsx:114 msgid "Read the Bluesky Privacy Policy" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:51 -#: src/screens/Signup/StepInfo/Policies.tsx:71 +#: src/screens/Signup/StepInfo/Policies.tsx:81 +#: src/screens/Signup/StepInfo/Policies.tsx:101 msgid "Read the Bluesky Terms of Service" msgstr "" @@ -6700,7 +6749,7 @@ msgstr "" #: src/components/FeedCard.tsx:343 #: src/components/StarterPack/Wizard/WizardListCard.tsx:102 #: src/components/StarterPack/Wizard/WizardListCard.tsx:109 -#: src/screens/Settings/Settings.tsx:620 +#: src/screens/Settings/Settings.tsx:662 #: src/view/com/modals/UserAddRemoveLists.tsx:235 #: src/view/com/posts/PostFeedErrorMessage.tsx:217 msgid "Remove" @@ -6714,8 +6763,8 @@ msgstr "" msgid "Remove {historyItem}" msgstr "" -#: src/screens/Settings/Settings.tsx:599 -#: src/screens/Settings/Settings.tsx:602 +#: src/screens/Settings/Settings.tsx:641 +#: src/screens/Settings/Settings.tsx:644 msgid "Remove account" msgstr "" @@ -6754,7 +6803,7 @@ msgstr "" msgid "Remove from my feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:612 +#: src/screens/Settings/Settings.tsx:654 msgid "Remove from quick access?" msgstr "" @@ -7119,8 +7168,8 @@ msgstr "" msgid "Resend Verification Email" msgstr "" -#: src/screens/Settings/Settings.tsx:474 -#: src/screens/Settings/Settings.tsx:476 +#: src/screens/Settings/Settings.tsx:482 +#: src/screens/Settings/Settings.tsx:484 msgid "Reset activity subscription nudge" msgstr "" @@ -7133,8 +7182,8 @@ msgstr "" msgid "Reset Code" msgstr "" -#: src/screens/Settings/Settings.tsx:459 -#: src/screens/Settings/Settings.tsx:461 +#: src/screens/Settings/Settings.tsx:467 +#: src/screens/Settings/Settings.tsx:469 msgid "Reset onboarding state" msgstr "" @@ -7634,7 +7683,7 @@ msgid "Sets email for password reset" msgstr "" #: src/Navigation.tsx:212 -#: src/screens/Settings/Settings.tsx:95 +#: src/screens/Settings/Settings.tsx:99 #: src/view/shell/desktop/LeftNav.tsx:728 #: src/view/shell/Drawer.tsx:572 msgid "Settings" @@ -7894,7 +7943,7 @@ msgstr "" msgid "Shows information about when this post was created" msgstr "" -#: src/screens/Settings/Settings.tsx:121 +#: src/screens/Settings/Settings.tsx:125 msgid "Shows other accounts you can switch to" msgstr "" @@ -7952,9 +8001,9 @@ msgstr "" msgid "Sign in to view post" msgstr "" -#: src/screens/Settings/Settings.tsx:250 -#: src/screens/Settings/Settings.tsx:252 -#: src/screens/Settings/Settings.tsx:284 +#: src/screens/Settings/Settings.tsx:254 +#: src/screens/Settings/Settings.tsx:256 +#: src/screens/Settings/Settings.tsx:288 #: src/screens/SignupQueued.tsx:93 #: src/screens/SignupQueued.tsx:96 #: src/screens/Takendown.tsx:85 @@ -7968,7 +8017,7 @@ msgstr "" msgid "Sign Out" msgstr "" -#: src/screens/Settings/Settings.tsx:281 +#: src/screens/Settings/Settings.tsx:285 #: src/view/shell/desktop/LeftNav.tsx:208 msgid "Sign out?" msgstr "" @@ -8066,8 +8115,8 @@ msgstr "" msgid "Something wrong? Let us know." msgstr "" -#: src/App.native.tsx:123 -#: src/App.web.tsx:100 +#: src/App.native.tsx:124 +#: src/App.web.tsx:101 msgid "Sorry! Your session expired. Please sign in again." msgstr "" @@ -8172,12 +8221,12 @@ msgstr "" msgid "Step {0} of {1}" msgstr "" -#: src/screens/Settings/Settings.tsx:385 +#: src/screens/Settings/Settings.tsx:393 msgid "Storage cleared, you need to restart the app now." msgstr "" #: src/Navigation.tsx:305 -#: src/screens/Settings/Settings.tsx:440 +#: src/screens/Settings/Settings.tsx:448 msgid "Storybook" msgstr "" @@ -8270,9 +8319,9 @@ msgstr "" msgid "Support" msgstr "" -#: src/screens/Settings/Settings.tsx:119 -#: src/screens/Settings/Settings.tsx:133 -#: src/screens/Settings/Settings.tsx:562 +#: src/screens/Settings/Settings.tsx:123 +#: src/screens/Settings/Settings.tsx:137 +#: src/screens/Settings/Settings.tsx:604 #: src/view/shell/desktop/LeftNav.tsx:245 msgid "Switch account" msgstr "" @@ -8299,7 +8348,7 @@ msgstr "" #: src/screens/Settings/AboutSettings.tsx:107 #: src/screens/Settings/AboutSettings.tsx:110 -#: src/screens/Settings/Settings.tsx:433 +#: src/screens/Settings/Settings.tsx:441 msgid "System log" msgstr "" @@ -8315,6 +8364,10 @@ msgstr "" msgid "Tap for more information" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:164 +msgid "Tap to acknowledge that you understand and agree to these updates and continue using Bluesky" +msgstr "" + #: src/components/ContextMenu/Backdrop.ios.tsx:54 #: src/components/ContextMenu/Backdrop.ios.tsx:80 #: src/components/ContextMenu/Backdrop.tsx:46 @@ -8359,6 +8412,8 @@ msgstr "" msgid "Terms" msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:30 +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:97 #: src/Navigation.tsx:335 #: src/screens/Settings/AboutSettings.tsx:84 #: src/screens/Settings/AboutSettings.tsx:87 @@ -8814,7 +8869,7 @@ msgstr "" msgid "This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:37 +#: src/screens/Signup/StepInfo/Policies.tsx:66 msgid "This service has not provided terms of service or a privacy policy." msgstr "" @@ -8867,7 +8922,7 @@ msgstr "" msgid "This will delete \"{0}\" from your muted words. You can always add it back later." msgstr "" -#: src/screens/Settings/Settings.tsx:614 +#: src/screens/Settings/Settings.tsx:656 msgid "This will remove @{0} from the quick access list." msgstr "" @@ -9033,11 +9088,11 @@ msgstr "" msgid "Unable to delete" msgstr "" -#: src/screens/Settings/Settings.tsx:499 +#: src/screens/Settings/Settings.tsx:507 msgid "Unapply Pull Request" msgstr "" -#: src/screens/Settings/Settings.tsx:501 +#: src/screens/Settings/Settings.tsx:509 msgid "Unapply Pull Request {currentChannel}" msgstr "" @@ -9200,8 +9255,8 @@ msgstr "" msgid "Unpinned from your feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:466 -#: src/screens/Settings/Settings.tsx:468 +#: src/screens/Settings/Settings.tsx:474 +#: src/screens/Settings/Settings.tsx:476 msgid "Unsnooze email reminder" msgstr "" @@ -9803,6 +9858,14 @@ msgstr "" msgid "We'll use this to help customize your experience." msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:139 +msgid "We're also updating our <0>Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025." +msgstr "" + +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:82 +msgid "We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:227 msgid "We're confirming your age assurance status with our servers. This should only take a few seconds." msgstr "" @@ -9849,6 +9912,22 @@ msgstr "" msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty." msgstr "" +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:122 +msgid "We’re updating our <0>Terms of Service, <1>Privacy Policy, and <2>Copyright Policy, effective September 12th, 2025." +msgstr "" + +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:63 +msgid "We're updating our policies" +msgstr "" + +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:76 +msgid "We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025." +msgstr "" + +#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:61 +msgid "We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:169 msgid "We've confirmed your age assurance status. You can now close this dialog." msgstr "" @@ -10009,6 +10088,10 @@ msgstr "" msgid "You" msgstr "" +#: src/screens/Signup/StepInfo/Policies.tsx:23 +msgid "You also agree to <0>Bluesky’s Community Guidelines. An <1>updated version of our Community Guidelines will take effect on October 13th." +msgstr "" + #: src/components/verification/VerifierDialog.tsx:59 msgid "You are a trusted verifier" msgstr "" @@ -10242,7 +10325,7 @@ msgstr "" msgid "You may only select up to 4 images" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:106 +#: src/screens/Signup/StepInfo/Policies.tsx:136 msgid "You must be 13 years of age or older to create an account." msgstr "" @@ -10279,7 +10362,7 @@ msgstr "" msgid "You previously deactivated @{0}." msgstr "" -#: src/screens/Settings/Settings.tsx:396 +#: src/screens/Settings/Settings.tsx:404 msgid "You probably want to restart the app now." msgstr "" @@ -10291,7 +10374,7 @@ msgstr "" msgid "You reacted {0} to {1}" msgstr "" -#: src/screens/Settings/Settings.tsx:282 +#: src/screens/Settings/Settings.tsx:286 #: src/view/shell/desktop/LeftNav.tsx:209 msgid "You will be signed out of all your accounts." msgstr "" @@ -10370,6 +10453,10 @@ msgstr "" msgid "You've found some people to follow" msgstr "" +#: src/components/FocusScope/index.tsx:114 +msgid "You've reached the end of the active content." +msgstr "" + #: src/view/com/posts/FollowingEndOfFeed.tsx:44 msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" @@ -10378,6 +10465,10 @@ msgstr "" msgid "You've reached the maximum number of requests allowed. Please try again later." msgstr "" +#: src/components/FocusScope/index.tsx:90 +msgid "You've reached the start of the active content." +msgstr "" + #: src/view/com/composer/state/video.ts:423 msgid "You've reached your daily limit for video uploads (too many bytes)" msgstr "" @@ -10445,7 +10536,7 @@ msgid "Your email" msgstr "" #: src/screens/Login/ForgotPasswordForm.tsx:51 -#: src/screens/Signup/state.ts:270 +#: src/screens/Signup/state.ts:273 #: src/screens/Signup/StepInfo/index.tsx:98 #: src/view/com/modals/ChangePassword.tsx:55 msgid "Your email appears to be invalid." From c103687d66b1446742b5c7b7ec77a24161f7beca Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 7 Aug 2025 08:45:35 -0500 Subject: [PATCH 13/20] Toast tweaks (#8791) * Tweak colors * Fix e2e * Color tweaks * Fix alignment --- src/components/Toast/Toast.tsx | 62 +++++++++++++++++++++++------- src/components/Toast/index.e2e.tsx | 6 ++- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index ef15c6d97b..2d1ea4261e 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,7 +1,7 @@ import {createContext, useContext, useMemo} from 'react' import {View} from 'react-native' -import {atoms as a, select, useTheme} from '#/alf' +import {atoms as a, select, useAlf, useTheme} from '#/alf' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' @@ -32,9 +32,19 @@ export function Toast({ type: ToastType content: React.ReactNode }) { + const {fonts} = useAlf() const t = useTheme() const styles = useToastStyles({type}) const Icon = ICONS[type] + /** + * Vibes-based number, adjusts `top` of `View` that wraps the text to + * compensate for different type sizes and keep the first line of text + * aligned with the icon. - esb + */ + const fontScaleCompensation = useMemo( + () => parseInt(fonts.scale) * -1 * 0.65, + [fonts.scale], + ) return ( ({type}), [type])}> @@ -56,7 +66,13 @@ export function Toast({ ]}> - + {typeof content === 'string' ? ( {content} ) : ( @@ -92,35 +108,55 @@ function useToastStyles({type}: {type: ToastType}) { return { default: { backgroundColor: t.atoms.bg_contrast_25.backgroundColor, - borderColor: t.atoms.border_contrast_high.borderColor, + borderColor: t.atoms.border_contrast_low.borderColor, iconColor: t.atoms.text.color, textColor: t.atoms.text.color, }, success: { backgroundColor: t.palette.primary_25, - borderColor: t.palette.primary_300, - iconColor: t.palette.primary_600, - textColor: t.palette.primary_600, + borderColor: select(t.name, { + light: t.palette.primary_300, + dim: t.palette.primary_200, + dark: t.palette.primary_100, + }), + iconColor: select(t.name, { + light: t.palette.primary_600, + dim: t.palette.primary_700, + dark: t.palette.primary_700, + }), + textColor: select(t.name, { + light: t.palette.primary_600, + dim: t.palette.primary_700, + dark: t.palette.primary_700, + }), }, error: { backgroundColor: t.palette.negative_25, borderColor: select(t.name, { - light: t.palette.negative_300, - dim: t.palette.negative_300, - dark: t.palette.negative_300, + light: t.palette.negative_200, + dim: t.palette.negative_200, + dark: t.palette.negative_100, + }), + iconColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + textColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, }), - iconColor: t.palette.negative_600, - textColor: t.palette.negative_600, }, warning: { backgroundColor: t.atoms.bg_contrast_25.backgroundColor, - borderColor: t.atoms.border_contrast_high.borderColor, + borderColor: t.atoms.border_contrast_low.borderColor, iconColor: t.atoms.text.color, textColor: t.atoms.text.color, }, info: { backgroundColor: t.atoms.bg_contrast_25.backgroundColor, - borderColor: t.atoms.border_contrast_high.borderColor, + borderColor: t.atoms.border_contrast_low.borderColor, iconColor: t.atoms.text.color, textColor: t.atoms.text.color, }, diff --git a/src/components/Toast/index.e2e.tsx b/src/components/Toast/index.e2e.tsx index 57daf5bf0d..64072d88d0 100644 --- a/src/components/Toast/index.e2e.tsx +++ b/src/components/Toast/index.e2e.tsx @@ -1,5 +1,9 @@ +import {type ToastApi} from '#/components/Toast/types' + export function ToastContainer() { return null } -export function show() {} +export const toast: ToastApi = { + show() {}, +} From 20ac2e2733f8c363b6def0844814bc1928af43bd Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 7 Aug 2025 19:07:43 +0300 Subject: [PATCH 14/20] Only parse notifs with a reason (#8785) * only parse notifs with a reason * move error log to a warn, make it actually fire --- src/lib/hooks/useNotificationHandler.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 3370a72081..7db48f4324 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -301,17 +301,6 @@ export function useNotificationsHandler() { const payload = getNotificationPayload(e.notification) if (payload) { - if (!payload.reason) { - notyLogger.error( - 'useNotificationsHandler: received unknown payload', - { - payload, - identifier: e.notification.request.identifier, - }, - ) - return - } - notyLogger.debug( 'User pressed a notification, opening notifications tab', {}, @@ -393,9 +382,15 @@ export function getNotificationPayload( isIOS ? e.request.trigger.payload : e.request.content.data ) as NotificationPayload - if (payload) { + if (payload && payload.reason) { return payload } else { + if (payload) { + notyLogger.warn('getNotificationPayload: received unknown payload', { + payload, + identifier: e.request.identifier, + }) + } return null } } From cb54082edab73fd8e7f736e5b883b87e052604a8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 7 Aug 2025 11:08:31 -0500 Subject: [PATCH 15/20] [APP-1310] Button refresh (#8758) * Rm gradient buttons from Storybook * TEMP move storybook button section * Remove gradient_sky * Remove actual defs for gradient_sky and gradient_primary * Remove other gradient defs * Remove gradient support entirely * Deprecate 'variant' in favor of 'color' * Fork base styles codepath to make variant deprecation more obvious * Remove text styles for when no color is set, never been used * Fork text styles codepath to make variant deprecation more obvious * Revert temp storybook commit, remove deprecated values * Replace remaining gradient button usage * Update Buttons storybook section * Update tiny styles * Update small styles * Update large sizes * Ensure proper alignment of buttons in storybook * Update button colors * Rename negative_secondary to negative_subtle * Remove unnecessary select() * Update icon size and gap * Update negative_subtle styles * Custom button colors * Add borderCurve --- src/components/Button.tsx | 160 ++++++++++++------- src/components/live/EditLiveDialog.tsx | 2 +- src/view/screens/Storybook/Buttons.tsx | 212 ++++++++----------------- src/view/screens/Storybook/index.tsx | 2 +- 4 files changed, 170 insertions(+), 206 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 57a8530b5f..4759b2a88d 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -37,7 +37,8 @@ export type ButtonColor = | 'secondary' | 'secondary_inverted' | 'negative' - | 'negative_secondary' + | 'primary_subtle' + | 'negative_subtle' export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { @@ -214,7 +215,11 @@ export const Button = React.forwardRef( ) const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles: ViewStyle[] = [] + const baseStyles: ViewStyle[] = [ + { + borderCurve: 'continuous', + }, + ] const hoverStyles: ViewStyle[] = [] /* @@ -233,19 +238,15 @@ export const Button = React.forwardRef( }) } else { baseStyles.push({ - backgroundColor: select(t.name, { - light: t.palette.primary_700, - dim: t.palette.primary_300, - dark: t.palette.primary_300, - }), + backgroundColor: t.palette.primary_200, }) } } else if (color === 'secondary') { if (!disabled) { baseStyles.push(t.atoms.bg_contrast_25) - hoverStyles.push(t.atoms.bg_contrast_50) + hoverStyles.push(t.atoms.bg_contrast_100) } else { - baseStyles.push(t.atoms.bg_contrast_100) + baseStyles.push(t.atoms.bg_contrast_50) } } else if (color === 'secondary_inverted') { if (!disabled) { @@ -253,7 +254,7 @@ export const Button = React.forwardRef( backgroundColor: t.palette.contrast_900, }) hoverStyles.push({ - backgroundColor: t.palette.contrast_950, + backgroundColor: t.palette.contrast_975, }) } else { baseStyles.push({ @@ -269,15 +270,36 @@ export const Button = React.forwardRef( backgroundColor: t.palette.negative_600, }) } else { + baseStyles.push({ + backgroundColor: t.palette.negative_700, + }) + } + } else if (color === 'primary_subtle') { + if (!disabled) { baseStyles.push({ backgroundColor: select(t.name, { - light: t.palette.negative_700, - dim: t.palette.negative_300, - dark: t.palette.negative_300, + light: t.palette.primary_50, + dim: t.palette.primary_100, + dark: t.palette.primary_100, + }), + }) + hoverStyles.push({ + backgroundColor: select(t.name, { + light: t.palette.primary_100, + dim: t.palette.primary_200, + dark: t.palette.primary_200, + }), + }) + } else { + baseStyles.push({ + backgroundColor: select(t.name, { + light: t.palette.primary_25, + dim: t.palette.primary_50, + dark: t.palette.primary_50, }), }) } - } else if (color === 'negative_secondary') { + } else if (color === 'negative_subtle') { if (!disabled) { baseStyles.push({ backgroundColor: select(t.name, { @@ -296,7 +318,7 @@ export const Button = React.forwardRef( } else { baseStyles.push({ backgroundColor: select(t.name, { - light: t.palette.negative_100, + light: t.palette.negative_25, dim: t.palette.negative_50, dark: t.palette.negative_50, }), @@ -407,7 +429,7 @@ export const Button = React.forwardRef( }) } } - } else if (color === 'negative_secondary') { + } else if (color === 'negative_subtle') { if (variant === 'outline') { baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, @@ -442,44 +464,48 @@ export const Button = React.forwardRef( if (shape === 'default') { if (size === 'large') { baseStyles.push({ - paddingVertical: 13, - paddingHorizontal: 20, - borderRadius: 8, - gap: 8, + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 10, + gap: 4, }) } else if (size === 'small') { baseStyles.push({ - paddingVertical: 9, + paddingVertical: 8, paddingHorizontal: 12, - borderRadius: 6, - gap: 6, + borderRadius: 8, + gap: 3, }) } else if (size === 'tiny') { baseStyles.push({ - paddingVertical: 4, + paddingVertical: 6, paddingHorizontal: 8, - borderRadius: 4, - gap: 4, + borderRadius: 6, + gap: 2, }) } } else if (shape === 'round' || shape === 'square') { + /* + * These sizes match the actual rendered size on screen, based on + * Chrome's web inspector + */ if (size === 'large') { if (shape === 'round') { - baseStyles.push({height: 46, width: 46}) + baseStyles.push({height: 45, width: 45}) } else { - baseStyles.push({height: 44, width: 44}) + baseStyles.push({height: 45, width: 45}) } } else if (size === 'small') { if (shape === 'round') { - baseStyles.push({height: 34, width: 34}) + baseStyles.push({height: 33, width: 33}) } else { - baseStyles.push({height: 34, width: 34}) + baseStyles.push({height: 33, width: 33}) } } else if (size === 'tiny') { if (shape === 'round') { - baseStyles.push({height: 22, width: 22}) + baseStyles.push({height: 25, width: 25}) } else { - baseStyles.push({height: 21, width: 21}) + baseStyles.push({height: 25, width: 25}) } } @@ -487,7 +513,9 @@ export const Button = React.forwardRef( baseStyles.push(a.rounded_full) } else if (shape === 'square') { if (size === 'tiny') { - baseStyles.push(a.rounded_xs) + baseStyles.push({ + borderRadius: 6, + }) } else { baseStyles.push(a.rounded_sm) } @@ -567,51 +595,64 @@ export function useSharedButtonTextStyles() { if (!disabled) { baseStyles.push({color: t.palette.white}) } else { - baseStyles.push({color: t.palette.white, opacity: 0.5}) + baseStyles.push({color: t.palette.white}) } } else if (color === 'secondary') { if (!disabled) { - baseStyles.push({ - color: t.palette.contrast_700, - }) + baseStyles.push(t.atoms.text_contrast_medium) } else { baseStyles.push({ - color: t.palette.contrast_400, + color: t.palette.contrast_300, }) } } else if (color === 'secondary_inverted') { if (!disabled) { - baseStyles.push({ - color: t.palette.contrast_50, - }) + baseStyles.push(t.atoms.text_inverted) } else { baseStyles.push({ - color: t.palette.contrast_400, + color: t.palette.contrast_300, }) } } else if (color === 'negative') { if (!disabled) { baseStyles.push({color: t.palette.white}) } else { - baseStyles.push({color: t.palette.white, opacity: 0.5}) + baseStyles.push({color: t.palette.negative_300}) } - } else if (color === 'negative_secondary') { + } else if (color === 'primary_subtle') { if (!disabled) { baseStyles.push({ color: select(t.name, { - light: t.palette.negative_500, - dim: t.palette.negative_950, - dark: t.palette.negative_900, + light: t.palette.primary_600, + dim: t.palette.primary_800, + dark: t.palette.primary_800, }), }) } else { baseStyles.push({ color: select(t.name, { - light: t.palette.negative_500, - dim: t.palette.negative_700, - dark: t.palette.negative_700, + light: t.palette.primary_200, + dim: t.palette.primary_200, + dark: t.palette.primary_200, + }), + }) + } + } else if (color === 'negative_subtle') { + if (!disabled) { + baseStyles.push({ + color: select(t.name, { + light: t.palette.negative_600, + dim: t.palette.negative_800, + dark: t.palette.negative_800, + }), + }) + } else { + baseStyles.push({ + color: select(t.name, { + light: t.palette.negative_200, + dim: t.palette.negative_200, + dark: t.palette.negative_200, }), - opacity: 0.5, }) } } @@ -693,7 +734,7 @@ export function useSharedButtonTextStyles() { baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) } } - } else if (color === 'negative_secondary') { + } else if (color === 'negative_subtle') { if (variant === 'outline') { if (!disabled) { baseStyles.push({color: t.palette.negative_400}) @@ -716,7 +757,7 @@ export function useSharedButtonTextStyles() { if (size === 'large') { baseStyles.push(a.text_md, a.leading_tight) } else if (size === 'small') { - baseStyles.push(a.text_sm, a.leading_tight) + baseStyles.push(a.text_md, a.leading_tight) } else if (size === 'tiny') { baseStyles.push(a.text_xs, a.leading_tight) } @@ -737,14 +778,16 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) { export function ButtonIcon({ icon: Comp, - position, size, }: { icon: React.ComponentType + /** + * @deprecated no longer needed + */ position?: 'left' | 'right' size?: SVGIconProps['size'] }) { - const {size: buttonSize, disabled} = useButtonContext() + const {size: buttonSize} = useButtonContext() const textStyles = useSharedButtonTextStyles() const {iconSize, iconContainerSize} = React.useMemo(() => { /** @@ -779,8 +822,8 @@ export function ButtonIcon({ * don't increase button size */ const iconContainerSize = { - large: 18, - small: 16, + large: 17, + small: 17, tiny: 13, }[buttonSize || 'small'] @@ -797,9 +840,6 @@ export function ButtonIcon({ { width: iconContainerSize, height: iconContainerSize, - opacity: disabled ? 0.7 : 1, - marginLeft: position === 'left' ? -2 : 0, - marginRight: position === 'right' ? -2 : 0, }, ]}> removeLiveStatus()} size={platform({native: 'large', web: 'small'})} - color="negative_secondary" + color="negative_subtle" variant="solid" disabled={isRemovingLiveStatus || isGoingLive}> diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx index eaf8bba7e3..0db0629135 100644 --- a/src/view/screens/Storybook/Buttons.tsx +++ b/src/view/screens/Storybook/Buttons.tsx @@ -1,3 +1,4 @@ +import {Fragment} from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' @@ -5,159 +6,82 @@ import { Button, type ButtonColor, ButtonIcon, + type ButtonSize, ButtonText, } from '#/components/Button' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' -import {H1} from '#/components/Typography' +import {Text} from '#/components/Typography' export function Buttons() { return ( -

Buttons

+ Buttons - - {[ - 'primary', - 'secondary', - 'secondary_inverted', - 'negative', - 'negative_secondary', - ].map(color => ( - - - - - ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {[ + 'primary', + 'secondary', + 'secondary_inverted', + 'negative', + 'primary_subtle', + 'negative_subtle', + ].map(color => ( + + {['tiny', 'small', 'large'].map(size => ( + + + color={color} size={size} + + + + + + + + + + + + + ))} + + ))}
) } diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 1151d5a3ce..8285b0d73b 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -87,6 +87,7 @@ function StorybookInner() {
+