From 2d8de1ddc3d9916a598b14411c879e651b66adf3 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Tue, 13 Jan 2026 02:42:26 -0800 Subject: [PATCH 01/11] Standardize metadata for client events in feeds (#9653) --- .../PostControls/BookmarkButton.tsx | 16 +++- .../PostControls/PostMenu/PostMenuItems.tsx | 26 ++++++ .../PostControls/PostMenu/index.tsx | 3 + .../PostControls/ShareMenu/index.tsx | 24 +++++- src/components/PostControls/index.tsx | 20 ++++- src/logger/metrics.ts | 86 +++++++++++++++++-- .../components/ThreadItemAnchor.tsx | 12 +++ src/state/feed-feedback.tsx | 21 +---- src/state/queries/post.ts | 2 - src/view/com/posts/PostFeedItem.tsx | 22 ++++- 10 files changed, 194 insertions(+), 38 deletions(-) diff --git a/src/components/PostControls/BookmarkButton.tsx b/src/components/PostControls/BookmarkButton.tsx index f729515202..ac391a1ae9 100644 --- a/src/components/PostControls/BookmarkButton.tsx +++ b/src/components/PostControls/BookmarkButton.tsx @@ -8,6 +8,7 @@ import type React from 'react' import {useCleanError} from '#/lib/hooks/useCleanError' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' import {useRequireAuth} from '#/state/session' import {useTheme} from '#/alf' @@ -32,6 +33,7 @@ export const BookmarkButton = memo(function BookmarkButton({ const {mutateAsync: bookmark} = useBookmarkMutation() const cleanError = useCleanError() const requireAuth = useRequireAuth() + const {feedDescriptor} = useFeedFeedbackContext() const {viewer} = post const isBookmarked = !!viewer?.bookmarked @@ -50,7 +52,12 @@ export const BookmarkButton = memo(function BookmarkButton({ post, }) - logger.metric('post:bookmark', {logContext}) + logger.metric('post:bookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( @@ -85,7 +92,12 @@ export const BookmarkButton = memo(function BookmarkButton({ uri: post.uri, }) - logger.metric('post:unbookmark', {logContext}) + logger.metric('post:unbookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index f8b410be97..ce93e81ab1 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -98,6 +98,7 @@ let PostMenuItems = ({ richText, threadgateRecord, onShowLess, + logContext, }: { testID: string post: Shadow @@ -111,6 +112,7 @@ let PostMenuItems = ({ timestamp: string threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const {_} = useLingui() @@ -210,9 +212,21 @@ let PostMenuItems = ({ try { if (isThreadMuted) { unmuteThread() + logger.metric('post:unmute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show(_(msg`You will now receive notifications for this thread`)) } else { muteThread() + logger.metric('post:mute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg`You will no longer receive notifications for this thread`), ) @@ -272,6 +286,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showMore', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), ) @@ -284,6 +304,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showLess', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) if (onShowLess) { onShowLess({ item: postUri, diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index 950bc4f6d9..f418587a5c 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -29,6 +29,7 @@ let PostMenuButton = ({ threadgateRecord, onShowLess, hitSlop, + logContext, }: { testID: string post: Shadow @@ -41,6 +42,7 @@ let PostMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() @@ -87,6 +89,7 @@ let PostMenuButton = ({ timestamp={timestamp} threadgateRecord={threadgateRecord} onShowLess={onShowLess} + logContext={logContext} /> )} diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index 6127ca41db..3fd9583113 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {EventStopper} from '#/view/com/util/EventStopper' import {native} from '#/alf' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' @@ -35,6 +36,7 @@ let ShareMenuButton = ({ threadgateRecord, onShare, hitSlop, + logContext, }: { testID: string post: Shadow @@ -45,9 +47,11 @@ let ShareMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShare: () => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() const gate = useGate() + const {feedDescriptor} = useFeedFeedbackContext() const ShareIcon = gate('alt_share_icon') ? ArrowShareRightIcon @@ -65,13 +69,27 @@ let ShareMenuButton = ({ setTimeout(menuControl.open) logger.metric( - 'share:open', - {context: big ? 'thread' : 'feed'}, + 'post:share', + { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + postContext: big ? 'thread' : 'feed', + }, {statsig: true}, ) }, }), - [menuControl, setHasBeenOpen, big], + [ + menuControl, + setHasBeenOpen, + big, + logContext, + feedDescriptor, + post.uri, + post.author.did, + ], ) const onNativeLongPress = () => { diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index ec0016815f..45688c2617 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -13,6 +13,7 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {logger} from '#/logger' import {type Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' import { @@ -174,6 +175,12 @@ let PostControls = ({ feedContext, reqId, }) + logger.metric('post:clickQuotePost', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) openComposer({ quote: post, onPost: onPostReply, @@ -217,7 +224,16 @@ let PostControls = ({ testID="replyBtn" onPress={ !replyDisabled - ? () => requireAuth(() => onPressReply()) + ? () => + requireAuth(() => { + logger.metric('post:clickReply', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) + onPressReply() + }) : undefined } label={_( @@ -315,6 +331,7 @@ let PostControls = ({ left: secondaryControlSpacingStyles.gap / 2, right: secondaryControlSpacingStyles.gap / 2, }} + logContext={logContext} /> diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 949c883b7c..dddca72a17 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -176,13 +176,19 @@ export type MetricEvents = { 'feed:suggestion:press': { feedUrl: string } - 'feed:showMore': { - feed: string - feedContext: string + 'post:showMore': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } - 'feed:showLess': { - feed: string - feedContext: string + 'post:showLess': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'feed:clickthrough': { feed: string @@ -257,15 +263,70 @@ export type MetricEvents = { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } - 'post:mute': {} - 'post:unmute': {} + 'post:mute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:unmute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } 'post:pin': {} 'post:unpin': {} 'post:bookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:unbookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickReply': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickQuotePost': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughAuthor': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughItem': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughEmbed': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:view': { uri: string @@ -565,7 +626,14 @@ export type MetricEvents = { 'live:view:profile': {subject: string} 'live:view:post': {subject: string; feed?: string} - 'share:open': {context: 'feed' | 'thread'} + 'post:share': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + postContext: 'feed' | 'thread' + position?: number + } 'share:press:copyLink': {} 'share:press:nativeShare': {} 'share:press:openDmSearch': {} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 7785afe2ca..fbed7965b1 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -281,6 +281,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ ]) const onOpenAuthor = () => { + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, @@ -292,6 +298,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ } const onOpenEmbed = () => { + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 1d719978a5..28f13fb5f2 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -137,7 +137,6 @@ export function useFeedFeedback( sendOrAggregateInteractionsForStats( aggregatedStats.current, interactionsToSend, - feed?.feedDescriptor ?? 'unknown', ) throttledFlushAggregatedStats() logger.debug('flushed') @@ -274,28 +273,10 @@ function createAggregatedStats(): AggregatedStats { function sendOrAggregateInteractionsForStats( stats: AggregatedStats, interactions: AppBskyFeedDefs.Interaction[], - feed: string, ) { for (let interaction of interactions) { switch (interaction.event) { - // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. - // This lets us send the feed context together with them. - case 'app.bsky.feed.defs#requestLess': { - logger.metric('feed:showLess', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - case 'app.bsky.feed.defs#requestMore': { - logger.metric('feed:showMore', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - - // The rest of the events are aggregated and sent later in batches. + // The events are aggregated and sent later in batches. case 'app.bsky.feed.defs#clickthroughAuthor': case 'app.bsky.feed.defs#clickthroughEmbed': case 'app.bsky.feed.defs#clickthroughItem': diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 09ced9874e..8fdfc584b6 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -373,7 +373,6 @@ function useThreadMuteMutation() { {uri: string} // the root post's uri >({ mutationFn: ({uri}) => { - logger.metric('post:mute', {}) return agent.api.app.bsky.graph.muteThread({root: uri}) }, }) @@ -383,7 +382,6 @@ function useThreadUnmuteMutation() { const agent = useAgent() return useMutation<{}, Error, {uri: string}>({ mutationFn: ({uri}) => { - logger.metric('post:unmute', {}) return agent.api.app.bsky.graph.unmuteThread({root: uri}) }, }) diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 244ebde05f..5b6ce7841a 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -21,6 +21,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {countLines} from '#/lib/strings/helpers' +import {logger} from '#/logger' import { POST_TOMBSTONE, type Shadow, @@ -173,7 +174,8 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey] }, [post.uri, post.author]) - const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() + const {sendInteraction, feedSourceInfo, feedDescriptor} = + useFeedFeedbackContext() const onPressReply = () => { sendInteraction({ @@ -209,6 +211,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onOpenReposter = () => { @@ -227,6 +235,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onBeforePress = () => { @@ -236,6 +250,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughItem', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) unstableCacheProfileView(queryClient, post.author) setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { feedSourceInfo, From 9aa360d32853e55c05cceaa3a2151625954b567e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 13 Jan 2026 09:37:00 -0600 Subject: [PATCH 02/11] Do not render links if uri is invalid (#9663) * Do not render links if uri is invalid * Update comment --- src/components/RichText.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 9908679c57..eca0d7c635 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -11,6 +11,9 @@ import {RichTextTag} from '#/components/RichTextTag' import {Text, type TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} +// lifted from facet detection in `RichText` impl, _without_ `gm` flags +const URL_REGEX = + /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i export type RichTextProps = TextStyleProp & Pick & { @@ -115,7 +118,8 @@ export function RichText({ , ) } else if (link && AppBskyRichtextFacet.validateLink(link).success) { - if (disableLinks) { + const isValidLink = URL_REGEX.test(link.uri) + if (!isValidLink || disableLinks) { els.push(toShortUrl(segment.text)) } else { els.push( From 61c919110e8446d9c8f90d082854f16b8524f22e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 13 Jan 2026 19:56:06 +0200 Subject: [PATCH 03/11] Expose QueryClient for TanStack Query DevTools browser extension (#9678) Co-authored-by: Claude Opus 4.5 --- src/lib/react-query.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index fe3ec6f4c7..e788bf410c 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useEffect, useRef, useState} from 'react' import {AppState, type AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' @@ -9,9 +9,15 @@ import { } from '@tanstack/react-query-persist-client' import type React from 'react' -import {isNative} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' +declare global { + interface Window { + __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient + } +} + // any query keys in this array will be persisted to AsyncStorage export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] @@ -180,6 +186,11 @@ function QueryProviderInner({ dehydrateOptions, } }) + useEffect(() => { + if (isWeb) { + window.__TANSTACK_QUERY_CLIENT__ = queryClient + } + }, [queryClient]) return ( Date: Wed, 14 Jan 2026 02:52:08 +0000 Subject: [PATCH 04/11] Nightly source-language update --- src/locale/locales/en/messages.po | 226 +++++++++++++++--------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index d920c4dbe4..213c0a9038 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -99,7 +99,7 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:477 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:489 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -107,15 +107,15 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:461 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:473 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:443 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:455 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:488 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:500 msgid "{0, plural, one {save} other {saves}}" msgstr "" @@ -621,7 +621,7 @@ msgstr "" msgid "Account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:393 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:419 #: src/screens/Messages/components/RequestButtons.tsx:91 #: src/view/com/profile/ProfileMenu.tsx:169 msgctxt "toast" @@ -633,7 +633,7 @@ msgctxt "toast" msgid "Account followed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:416 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:442 #: src/view/com/profile/ProfileMenu.tsx:145 msgctxt "toast" msgid "Account muted" @@ -672,7 +672,7 @@ msgctxt "toast" msgid "Account unfollowed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:406 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:432 #: src/view/com/profile/ProfileMenu.tsx:135 msgctxt "toast" msgid "Account unmuted" @@ -830,7 +830,7 @@ msgstr "" msgid "Add to lists" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:129 +#: src/components/PostControls/BookmarkButton.tsx:141 msgid "Add to saved posts" msgstr "" @@ -1296,12 +1296,12 @@ msgstr "" msgid "Apply Pull Request" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:658 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:670 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:627 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:666 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:639 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:678 msgid "Archived post" msgstr "" @@ -1358,8 +1358,8 @@ msgstr "" msgid "Artistic or non-erotic nudity." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:529 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:531 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:555 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:557 msgid "Assign topic for algo" msgstr "" @@ -1465,7 +1465,7 @@ msgstr "" msgid "Birthday" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:792 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:818 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 #: src/view/com/profile/ProfileMenu.tsx:504 msgid "Block" @@ -1473,8 +1473,8 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:261 #: src/components/dms/ConvoMenu.tsx:264 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:677 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:679 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:703 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:705 #: src/screens/Messages/components/RequestButtons.tsx:144 #: src/screens/Messages/components/RequestButtons.tsx:146 #: src/view/com/profile/ProfileMenu.tsx:410 @@ -1482,7 +1482,7 @@ msgstr "" msgid "Block account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:813 #: src/view/com/profile/ProfileMenu.tsx:487 msgid "Block Account?" msgstr "" @@ -1534,7 +1534,7 @@ msgstr "" msgid "Blocked Accounts" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:815 #: src/view/com/profile/ProfileMenu.tsx:499 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -1564,7 +1564,7 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:688 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:700 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1803,9 +1803,9 @@ msgstr "" msgid "Cancel search" msgstr "" -#: src/components/PostControls/index.tsx:106 -#: src/components/PostControls/index.tsx:137 -#: src/components/PostControls/index.tsx:165 +#: src/components/PostControls/index.tsx:107 +#: src/components/PostControls/index.tsx:138 +#: src/components/PostControls/index.tsx:166 #: src/state/shell/composer/index.tsx:95 msgid "Cannot interact with a blocked user" msgstr "" @@ -2419,7 +2419,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:57 #: src/components/PostControls/DiscoverDebug.tsx:36 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:235 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:249 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:72 #: src/lib/sharing.ts:25 #: src/lib/sharing.ts:41 @@ -2508,8 +2508,8 @@ msgstr "" msgid "Copy post at:// URI" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:486 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:510 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:512 msgid "Copy post text" msgstr "" @@ -2770,7 +2770,7 @@ msgid "Default icons" msgstr "" #: src/components/dms/MessageContextMenu.tsx:202 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:736 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:762 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:280 #: src/screens/Settings/AppPasswords.tsx:212 @@ -2842,8 +2842,8 @@ msgstr "" msgid "Delete my account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:717 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:719 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:743 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:745 #: src/view/com/composer/Composer.tsx:969 msgid "Delete post" msgstr "" @@ -2861,7 +2861,7 @@ msgstr "" msgid "Delete this list?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:731 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 msgid "Delete this post?" msgstr "" @@ -2896,12 +2896,12 @@ msgstr "" msgid "Descriptive alt text" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:647 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:657 msgid "Detach quote" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:767 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:793 msgid "Detach quote post?" msgstr "" @@ -3214,8 +3214,8 @@ msgstr "" msgid "Edit image" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:698 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:711 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:724 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:737 msgid "Edit interaction settings" msgstr "" @@ -3695,7 +3695,7 @@ msgstr "" msgid "Failed to delete message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:204 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:206 msgid "Failed to delete post, please try again" msgstr "" @@ -3831,7 +3831,7 @@ msgstr "" msgid "Failed to submit appeal, please try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:224 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:238 msgid "Failed to toggle thread mute, please try again" msgstr "" @@ -3918,8 +3918,8 @@ msgstr "" msgid "Feedback" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:276 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:294 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:296 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:320 msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" @@ -4603,7 +4603,7 @@ msgstr "" #: src/components/moderation/ContentHider.tsx:208 #: src/components/moderation/LabelPreference.tsx:140 #: src/components/moderation/PostHider.tsx:137 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:747 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:773 #: src/lib/moderation/useLabelBehaviorDescription.ts:18 #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 @@ -4625,18 +4625,18 @@ msgstr "" msgid "Hide lists" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:578 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:610 msgid "Hide post for me" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:595 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:605 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 msgid "Hide reply for everyone" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:577 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:583 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:603 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:609 msgid "Hide reply for me" msgstr "" @@ -4644,12 +4644,12 @@ msgstr "" msgid "Hide this card" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 msgid "Hide this post?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:777 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 msgid "Hide this reply?" msgstr "" @@ -4805,7 +4805,7 @@ msgstr "" msgid "If you need to update your email, <0>click here." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:733 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:759 msgid "If you remove this post, you won't be able to recover it." msgstr "" @@ -4967,7 +4967,7 @@ msgstr "" msgid "Invalid handle. Please try a different one." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:365 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:391 msgctxt "toast" msgid "Invalid interaction settings." msgstr "" @@ -5269,7 +5269,7 @@ msgid "Like" msgstr "" #. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form -#: src/components/PostControls/index.tsx:270 +#: src/components/PostControls/index.tsx:286 msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -5334,7 +5334,7 @@ msgstr "" msgid "Likes of your reposts notifications" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:470 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:482 msgid "Likes on this post" msgstr "" @@ -5761,8 +5761,8 @@ msgstr "" msgid "Mute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:660 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:666 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692 #: src/view/com/profile/ProfileMenu.tsx:389 #: src/view/com/profile/ProfileMenu.tsx:396 msgid "Mute account" @@ -5814,13 +5814,13 @@ msgstr "" msgid "Mute this word until you unmute it" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Mute thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:558 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:560 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:586 msgid "Mute words & tags" msgstr "" @@ -6338,7 +6338,7 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:693 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:705 msgid "Okay" msgstr "" @@ -6467,7 +6467,7 @@ msgstr "" msgid "Open pack" msgstr "" -#: src/components/PostControls/PostMenu/index.tsx:64 +#: src/components/PostControls/PostMenu/index.tsx:66 msgid "Open post options menu" msgstr "" @@ -6476,7 +6476,7 @@ msgstr "" msgid "Open profile" msgstr "" -#: src/components/PostControls/ShareMenu/index.tsx:89 +#: src/components/PostControls/ShareMenu/index.tsx:107 msgid "Open share menu" msgstr "" @@ -6792,8 +6792,8 @@ msgstr "" msgid "Pin to Home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:452 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:459 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:485 msgid "Pin to your profile" msgstr "" @@ -7046,7 +7046,7 @@ msgstr "" msgid "Post by @{0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:184 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:186 msgctxt "toast" msgid "Post deleted" msgstr "" @@ -7096,7 +7096,7 @@ msgctxt "toast" msgid "Post pinned" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:59 +#: src/components/PostControls/BookmarkButton.tsx:66 msgid "Post saved" msgstr "" @@ -7299,11 +7299,11 @@ msgstr "" msgid "Quote post" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:314 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:340 msgid "Quote post was re-attached" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:313 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:339 msgid "Quote post was successfully detached" msgstr "" @@ -7321,7 +7321,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:454 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 msgid "Quotes of this post" msgstr "" @@ -7333,8 +7333,8 @@ msgstr "" msgid "Rate limit exceeded. Please try again later." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:646 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:656 msgid "Re-attach quote" msgstr "" @@ -7518,7 +7518,7 @@ msgstr "" msgid "Remove from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:128 +#: src/components/PostControls/BookmarkButton.tsx:140 #: src/screens/Bookmarks/index.tsx:260 msgid "Remove from saved posts" msgstr "" @@ -7594,7 +7594,7 @@ msgstr "" msgid "Removed from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:94 +#: src/components/PostControls/BookmarkButton.tsx:106 #: src/screens/Bookmarks/index.tsx:218 msgid "Removed from saved posts" msgstr "" @@ -7662,7 +7662,7 @@ msgid "Reply" msgstr "" #. Accessibility label for the reply button, verb form followed by number of replies and noun form -#: src/components/PostControls/index.tsx:224 +#: src/components/PostControls/index.tsx:240 msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" @@ -7688,12 +7688,12 @@ msgstr "" msgid "Reply sorting" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:351 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:377 msgctxt "toast" msgid "Reply visibility updated" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:350 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:376 msgid "Reply was successfully hidden" msgstr "" @@ -7736,8 +7736,8 @@ msgstr "" msgid "Report message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:688 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:714 msgid "Report post" msgstr "" @@ -7828,7 +7828,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:436 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 msgid "Reposts of this post" msgstr "" @@ -8671,8 +8671,8 @@ msgstr "" msgid "Show customization options" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:515 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:517 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:541 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:543 msgid "Show less like this" msgstr "" @@ -8688,8 +8688,8 @@ msgstr "" msgid "Show More" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:507 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:509 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:533 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:535 msgid "Show more like this" msgstr "" @@ -8715,8 +8715,8 @@ msgstr "" msgid "Show replies as" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:594 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 msgid "Show reply for everyone" msgstr "" @@ -8738,7 +8738,7 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:629 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Shows information about when this post was created" msgstr "" @@ -8798,8 +8798,8 @@ msgstr "" msgid "Sign in to Bluesky or create a new account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:493 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:495 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:519 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:521 msgid "Sign in to view post" msgstr "" @@ -9497,9 +9497,9 @@ msgstr "" msgid "There was an issue updating your feeds, please check your internet connection and try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:397 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:410 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:420 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:423 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:436 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:90 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:101 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88 @@ -9708,7 +9708,7 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:669 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:681 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -9724,7 +9724,7 @@ msgstr "" msgid "This post was deleted by its author" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:744 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:770 msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" @@ -9736,7 +9736,7 @@ msgstr "" msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:779 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:805 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 "" @@ -9797,7 +9797,7 @@ msgstr "" msgid "This will remove @{0} from the quick access list." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:769 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:795 msgid "This will remove your post from this quote post for all users, and replace it with a placeholder." msgstr "" @@ -9892,10 +9892,10 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:139 #: src/components/dms/MessageContextMenu.tsx:141 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:476 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:591 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:594 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:502 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:504 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:606 msgid "Translate" msgstr "" @@ -10028,7 +10028,7 @@ msgstr "" msgid "Unblock list" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:40 +#: src/components/PostControls/BookmarkButton.tsx:42 msgctxt "Button label to undo saving/removing a post from saved posts." msgid "Undo" msgstr "" @@ -10085,7 +10085,7 @@ msgid "Unlike" msgstr "" #. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun -#: src/components/PostControls/index.tsx:260 +#: src/components/PostControls/index.tsx:276 msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -10105,8 +10105,8 @@ msgstr "" msgid "Unmute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:659 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:665 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691 #: src/view/com/profile/ProfileMenu.tsx:388 #: src/view/com/profile/ProfileMenu.tsx:394 msgid "Unmute account" @@ -10121,8 +10121,8 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Unmute thread" msgstr "" @@ -10150,8 +10150,8 @@ msgstr "" msgid "Unpin from home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:451 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:458 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:477 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 msgid "Unpin from profile" msgstr "" @@ -10223,12 +10223,12 @@ msgstr "" msgid "Update your email" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:318 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:344 msgctxt "toast" msgid "Updating quote attachment failed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:370 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:396 msgctxt "toast" msgid "Updating reply visibility failed" msgstr "" @@ -11043,11 +11043,11 @@ msgstr "" msgid "Yes, delete this starter pack" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:772 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:798 msgid "Yes, detach" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:782 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:808 msgid "Yes, hide" msgstr "" @@ -11146,7 +11146,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:357 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:383 msgctxt "toast" msgid "You can hide a maximum of {MAX_HIDDEN_REPLIES} replies." msgstr "" @@ -11392,11 +11392,11 @@ msgstr "" msgid "You will no longer receive notifications for {0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:217 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:231 msgid "You will no longer receive notifications for this thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:213 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:221 msgid "You will now receive notifications for this thread" msgstr "" From af92610864af4fbd8fdfc4a79e69be2466361bf6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 21:16:34 +0200 Subject: [PATCH 05/11] Add comprehensive CLAUDE.md development guide (#9666) * Add comprehensive CLAUDE.md development guide Document the codebase architecture, styling system (ALF), component patterns (Dialog, Menu, Button), i18n with Lingui, state management with TanStack Query, and navigation patterns to help Claude work effectively in this codebase. * Add footguns section to CLAUDE.md Document critical pitfalls including: - Dialog close callback pattern (control.close(() => ...)) for avoiding race conditions with navigation, state updates, and opening other dialogs - Controlled vs uncontrolled input guidance - Platform-specific component behavior differences * Add React Compiler note to footguns section Document that useMemo/useCallback are unnecessary since React Compiler handles memoization automatically. Only use them for specific cases like effect dependencies or non-React library interop. --------- Co-authored-by: Claude --- CLAUDE.md | 591 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..e1a7d174ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,591 @@ +# CLAUDE.md - Bluesky Social App Development Guide + +This document provides guidance for working effectively in the Bluesky Social app codebase. + +## Project Overview + +Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network. + +**Tech Stack:** +- React Native 0.81 with Expo 54 +- TypeScript +- React Navigation for routing +- TanStack Query (React Query) for data fetching +- Lingui for internationalization +- Custom design system called ALF (Application Layout Framework) + +## Essential Commands + +```bash +# Development +yarn start # Start Expo dev server +yarn web # Start web version +yarn android # Run on Android +yarn ios # Run on iOS + +# Testing & Quality +yarn test # Run Jest tests +yarn lint # Run ESLint +yarn typecheck # Run TypeScript type checking + +# Internationalization +yarn intl:extract # Extract translation strings +yarn intl:compile # Compile translations for runtime + +# Build +yarn build-web # Build web version +yarn prebuild # Generate native projects +``` + +## Project Structure + +``` +src/ +├── alf/ # Design system (ALF) - themes, atoms, tokens +├── components/ # Shared UI components (Button, Dialog, Menu, etc.) +├── screens/ # Full-page screen components (newer pattern) +├── view/ +│ ├── screens/ # Full-page screens (legacy location) +│ ├── com/ # Reusable view components +│ └── shell/ # App shell (navigation bars, tabs) +├── state/ +│ ├── queries/ # TanStack Query hooks +│ ├── preferences/ # User preferences (React Context) +│ ├── session/ # Authentication state +│ └── persisted/ # Persistent storage layer +├── lib/ # Utilities, constants, helpers +├── locale/ # i18n configuration and language files +└── Navigation.tsx # Main navigation configuration +``` + +## Styling System (ALF) + +ALF is the custom design system. It uses Tailwind-inspired naming with underscores instead of hyphens. + +### Basic Usage + +```tsx +import {atoms as a, useTheme} from '#/alf' + +function MyComponent() { + const t = useTheme() + + return ( + + + Hello + + + ) +} +``` + +### Key Concepts + +**Static Atoms** - Theme-independent styles imported from `atoms`: +```tsx +import {atoms as a} from '#/alf' +// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. +``` + +**Theme Atoms** - Theme-dependent colors from `useTheme()`: +```tsx +const t = useTheme() +// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. +// t.palette.primary_500, t.palette.negative_400, etc. +``` + +**Platform Utilities** - For platform-specific styles: +```tsx +import {web, native, ios, android, platform} from '#/alf' + +const styles = [ + a.p_md, + web({cursor: 'pointer'}), + native({paddingBottom: 20}), + platform({ios: {...}, android: {...}, web: {...}}), +] +``` + +**Breakpoints** - Responsive design: +```tsx +import {useBreakpoints} from '#/alf' + +const {gtPhone, gtMobile, gtTablet} = useBreakpoints() +if (gtMobile) { + // Tablet or desktop layout +} +``` + +### Naming Conventions + +- Spacing: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl` (t-shirt sizes) +- Text: `text_xs`, `text_sm`, `text_md`, `text_lg`, `text_xl` +- Gaps/Padding: `gap_sm`, `p_md`, `px_lg`, `py_xl` +- Flex: `flex_row`, `flex_1`, `align_center`, `justify_between` +- Borders: `border`, `border_t`, `rounded_md`, `rounded_full` + +## Component Patterns + +### Dialog Component + +Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. + +```tsx +import * as Dialog from '#/components/Dialog' + +function MyFeature() { + const control = Dialog.useDialogControl() + + return ( + <> + + + + {/* Native drag handle */} + + + Title + + + Dialog content here + + + + + + ) +} +``` + +### Menu Component + +Menus render as a dropdown on web and a bottom sheet dialog on native. + +```tsx +import * as Menu from '#/components/Menu' + +function MyMenu() { + return ( + + + {({props}) => ( + + )} + + + + + + + Edit + + + + Delete + + + + + ) +} +``` + +### Button Component + +```tsx +import {Button, ButtonText, ButtonIcon} from '#/components/Button' + +// Solid primary button (most common) + + +// With icon + + +// Icon-only button + + +// Ghost variant (deprecated - use color prop) + +``` + +**Button Props:** +- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` +- `size`: `'tiny'` | `'small'` | `'large'` +- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` +- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, use `color`) + +### Typography + +```tsx +import {Text, H1, H2, P} from '#/components/Typography' + +

Heading

+

Paragraph text with default styling.

+Custom text + +// For text with emoji, add the emoji prop +Hello! 👋 +``` + +### TextField + +```tsx +import * as TextField from '#/components/forms/TextField' + +Email + + + + +``` + +## Internationalization (i18n) + +All user-facing strings must be wrapped for translation using Lingui. + +```tsx +import {msg, Trans, plural} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +function MyComponent() { + const {_} = useLingui() + + // Simple strings - use msg() with _() function + const title = _(msg`Settings`) + const errorMessage = _(msg`Something went wrong`) + + // Strings with variables + const greeting = _(msg`Hello, ${name}!`) + + // Pluralization + const countLabel = _(plural(count, { + one: '# item', + other: '# items', + })) + + // JSX content - use Trans component + return ( + + Welcome to Bluesky + + ) +} +``` + +**Commands:** +```bash +yarn intl:extract # Extract new strings to locale files +yarn intl:compile # Compile for runtime (required after changes) +``` + +## State Management + +### TanStack Query (Data Fetching) + +```tsx +// src/state/queries/profile.ts +import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' + +// Query key pattern +const RQKEY_ROOT = 'profile' +export const RQKEY = (did: string) => [RQKEY_ROOT, did] + +// Query hook +export function useProfileQuery({did}: {did: string}) { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY(did), + queryFn: async () => { + const res = await agent.getProfile({actor: did}) + return res.data + }, + staleTime: STALE.MINUTES.FIVE, + enabled: !!did, + }) +} + +// Mutation hook +export function useUpdateProfile() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data) => { + // Update logic + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({queryKey: RQKEY(variables.did)}) + }, + }) +} +``` + +**Stale Time Constants** (from `src/state/queries/index.ts`): +```tsx +STALE.SECONDS.FIFTEEN // 15 seconds +STALE.MINUTES.ONE // 1 minute +STALE.MINUTES.FIVE // 5 minutes +STALE.HOURS.ONE // 1 hour +STALE.INFINITY // Never stale +``` + +### Preferences (React Context) + +```tsx +// Simple boolean preference pattern +import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' + +function SettingsScreen() { + const autoplayDisabled = useAutoplayDisabled() + const setAutoplayDisabled = useSetAutoplayDisabled() + + return ( + + ) +} +``` + +### Session State + +```tsx +import {useSession, useAgent} from '#/state/session' + +function MyComponent() { + const {hasSession, currentAccount} = useSession() + const agent = useAgent() + + if (!hasSession) { + return + } + + // Use agent for API calls + const response = await agent.getProfile({actor: currentAccount.did}) +} +``` + +## Navigation + +Navigation uses React Navigation with type-safe route parameters. + +```tsx +// Screen component +import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {type CommonNavigatorParams} from '#/lib/routes/types' + +type Props = NativeStackScreenProps + +export function ProfileScreen({route, navigation}: Props) { + const {name} = route.params // Type-safe params + + return ( + + {/* Screen content */} + + ) +} + +// Programmatic navigation +import {useNavigation} from '@react-navigation/native' + +const navigation = useNavigation() +navigation.navigate('Profile', {name: 'alice.bsky.social'}) + +// Or use the navigate helper +import {navigate} from '#/Navigation' +navigate('Profile', {name: 'alice.bsky.social'}) +``` + +## Platform-Specific Code + +Use file extensions for platform-specific implementations: + +``` +Component.tsx # Shared/default +Component.web.tsx # Web-only +Component.native.tsx # iOS + Android +Component.ios.tsx # iOS-only +Component.android.tsx # Android-only +``` + +Example from Dialog: +- `src/components/Dialog/index.tsx` - Native (uses BottomSheet) +- `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives) + +Platform detection: +```tsx +import {isWeb, isNative, isIOS, isAndroid} from '#/platform/detection' + +if (isNative) { + // Native-specific logic +} +``` + +## Import Aliases + +Always use the `#/` alias for absolute imports: + +```tsx +// Good +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' + +// Avoid +import {useSession} from '../../../state/session' +``` + +## Footguns + +Common pitfalls to avoid in this codebase: + +### Dialog Close Callback (Critical) + +**Always use `control.close(() => ...)` when performing actions after closing a dialog.** The callback ensures the action runs after the dialog's close animation completes. Failing to do this causes race conditions with React state updates. + +```tsx +// WRONG - causes bugs with state updates, navigation, opening other dialogs +const onConfirm = () => { + control.close() + navigation.navigate('Home') // May race with dialog animation +} + +// WRONG - same problem +const onConfirm = () => { + control.close() + otherDialogControl.open() // Will likely fail or cause visual glitches +} + +// CORRECT - action runs after dialog fully closes +const onConfirm = () => { + control.close(() => { + navigation.navigate('Home') + }) +} + +// CORRECT - opening another dialog after close +const onConfirm = () => { + control.close(() => { + otherDialogControl.open() + }) +} + +// CORRECT - state updates after close +const onConfirm = () => { + control.close(() => { + setSomeState(newValue) + onCallback?.() + }) +} +``` + +This applies to: +- Navigation (`navigation.navigate()`, `navigation.push()`) +- Opening other dialogs or menus +- State updates that affect UI (`setState`, `queryClient.invalidateQueries`) +- Callbacks passed from parent components + +The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`. + +### Controlled vs Uncontrolled Inputs + +Prefer `defaultValue` over `value` for TextInput on the old architecture: + +```tsx +// Preferred - uncontrolled + + +// Avoid when possible - controlled (can cause performance issues) + +``` + +### Platform-Specific Behavior + +Some components behave differently across platforms: +- `Dialog.Handle` - Only renders on native (drag handle for bottom sheet) +- `Dialog.Close` - Only renders on web (X button) +- `Menu.Divider` - Only renders on web +- `Menu.ContainerItem` - Only works on native + +Always test on multiple platforms when using these components. + +### React Compiler is Enabled + +This codebase uses React Compiler, so **don't proactively add `useMemo` or `useCallback`**. The compiler handles memoization automatically. + +```tsx +// UNNECESSARY - React Compiler handles this +const handlePress = useCallback(() => { + doSomething() +}, [doSomething]) + +// JUST WRITE THIS +const handlePress = () => { + doSomething() +} +``` + +Only use `useMemo`/`useCallback` when you have a specific reason, such as: +- The value is immediately used in an effect's dependency array +- You're passing a callback to a non-React library that needs referential stability + +## Best Practices + +1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful + +2. **Translations**: Wrap ALL user-facing strings with `msg()` or `` + +3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles + +4. **State**: Use TanStack Query for server state, React Context for UI preferences + +5. **Components**: Check if a component exists in `#/components/` before creating new ones + +6. **Types**: Define explicit types for props, use `NativeStackScreenProps` for screens + +7. **Testing**: Components should have `testID` props for E2E testing + +## Key Files Reference + +| Purpose | Location | +|---------|----------| +| Theme definitions | `src/alf/themes.ts` | +| Design tokens | `src/alf/tokens.ts` | +| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | +| Navigation config | `src/Navigation.tsx` | +| Route definitions | `src/routes.ts` | +| Route types | `src/lib/routes/types.ts` | +| Query hooks | `src/state/queries/*.ts` | +| Session state | `src/state/session/index.tsx` | +| i18n setup | `src/locale/i18n.ts` | From cb6e65e3135d9c92b953839de79fe9d9ad90b5b8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 21:16:49 +0200 Subject: [PATCH 06/11] pin AsyncStorage to v2.2.0 (#9656) --- package.json | 1 + yarn.lock | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 185e530bd3..61e042e05b 100644 --- a/package.json +++ b/package.json @@ -284,6 +284,7 @@ "@react-native/babel-preset": "0.81.5", "@react-native/normalize-colors": "0.81.5", "**/@expo/image-utils": "0.8.7", + "**/@react-native-async-storage/async-storage": "2.2.0", "**/expo-constants": "18.0.8", "**/expo-device": "7.1.4", "**/zod": "3.23.8", diff --git a/yarn.lock b/yarn.lock index a010c615be..d0741715da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6144,20 +6144,13 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@react-native-async-storage/async-storage@2.2.0": +"@react-native-async-storage/async-storage@2.2.0", "@react-native-async-storage/async-storage@^1.15.2": version "2.2.0" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz#a3aa565253e46286655560172f4e366e8969f5ad" integrity sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw== dependencies: merge-options "^3.0.4" -"@react-native-async-storage/async-storage@^1.15.2": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc" - integrity sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA== - dependencies: - merge-options "^3.0.4" - "@react-native/assets-registry@0.81.5": version "0.81.5" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.5.tgz#d22c924fa6f6d4a463c5af34ce91f38756c0fa7d" From 226b19ae4327bbd4621e2eb25470b02df1e5e933 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 22:22:26 +0200 Subject: [PATCH 07/11] Reduce startup hang by not querying fonts (#9670) * query fonts lazily * use `lazy var` --- patches/expo-font+14.0.10.patch | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 patches/expo-font+14.0.10.patch diff --git a/patches/expo-font+14.0.10.patch b/patches/expo-font+14.0.10.patch new file mode 100644 index 0000000000..ac7526e23d --- /dev/null +++ b/patches/expo-font+14.0.10.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/expo-font/ios/FontLoaderModule.swift b/node_modules/expo-font/ios/FontLoaderModule.swift +index 183480f..7b64f6e 100644 +--- a/node_modules/expo-font/ios/FontLoaderModule.swift ++++ b/node_modules/expo-font/ios/FontLoaderModule.swift +@@ -2,10 +2,9 @@ import ExpoModulesCore + + public final class FontLoaderModule: Module { + // could be a Set, but to be able to pass to JS we keep it as an array +- private var registeredFonts: [String] ++ private lazy var registeredFonts: [String] = queryCustomNativeFonts() + + public required init(appContext: AppContext) { +- self.registeredFonts = queryCustomNativeFonts() + super.init(appContext: appContext) + } + From e684c21572074d34c2ada7875c5e790f6dbe2f63 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Wed, 14 Jan 2026 12:53:24 -0800 Subject: [PATCH 08/11] Improved client events for feed interactions (#9695) --- src/logger/metrics.ts | 8 ++++++++ src/state/queries/post.ts | 40 +++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index dddca72a17..0af92a6e7c 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -244,6 +244,8 @@ export type MetricEvents = { isReply: boolean } 'post:like': { + uri: string + authorDid: string doesLikerFollowPoster: boolean | undefined doesPosterFollowLiker: boolean | undefined likerClout: number | undefined @@ -252,14 +254,20 @@ export type MetricEvents = { feedDescriptor?: string } 'post:repost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unlike': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unrepost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 8fdfc584b6..3043485b3e 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -111,7 +111,7 @@ export function usePostLikeMutationQueue( const postCid = post.cid const initialLikeUri = post.viewer?.like const likeMutation = usePostLikeMutation(feedDescriptor, logContext, post) - const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext) + const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext, post) const queueToggle = useToggleMutationQueue({ initialState: initialLikeUri, @@ -182,6 +182,8 @@ function usePostLikeMutation( ownProfile = findProfileQueryData(queryClient, currentAccount.did) } logger.metric('post:like', { + uri, + authorDid: postAuthor.did, logContext, doesPosterFollowLiker: postAuthor.viewer ? Boolean(postAuthor.viewer.followedBy) @@ -206,11 +208,17 @@ function usePostLikeMutation( function usePostUnlikeMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unlike']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({likeUri}) => { - logger.metric('post:unlike', {logContext, feedDescriptor}) + mutationFn: ({postUri, likeUri}) => { + logger.metric('post:unlike', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteLike(likeUri) }, }) @@ -227,8 +235,12 @@ export function usePostRepostMutationQueue( const postUri = post.uri const postCid = post.cid const initialRepostUri = post.viewer?.repost - const repostMutation = usePostRepostMutation(feedDescriptor, logContext) - const unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext) + const repostMutation = usePostRepostMutation(feedDescriptor, logContext, post) + const unrepostMutation = usePostUnrepostMutation( + feedDescriptor, + logContext, + post, + ) const queueToggle = useToggleMutationQueue({ initialState: initialRepostUri, @@ -280,6 +292,7 @@ export function usePostRepostMutationQueue( function usePostRepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:repost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation< @@ -288,7 +301,12 @@ function usePostRepostMutation( {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present >({ mutationFn: ({uri, cid, via}) => { - logger.metric('post:repost', {logContext, feedDescriptor}) + logger.metric('post:repost', { + uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.repost(uri, cid, via) }, }) @@ -297,11 +315,17 @@ function usePostRepostMutation( function usePostUnrepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unrepost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({repostUri}) => { - logger.metric('post:unrepost', {logContext, feedDescriptor}) + mutationFn: ({postUri, repostUri}) => { + logger.metric('post:unrepost', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteRepost(repostUri) }, }) From 5922c6622fcc61f008296c09406be1c54bf8667b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 14 Jan 2026 15:09:01 -0600 Subject: [PATCH 09/11] [APP-1767] Live event feeds (#9696) * Update env config for new worker setup * Add liveEvents context * Add live event feed card to Explore * Add Discover banner/trending handling, device storage * Add prefs methods, hook up to Discover banner * Add settings toggle * Fix up metrics and mutations * Simplify undo logic * Handle previews * Update overlay color and text color handling * Update metrics * Handle live event in feed card * Show multiple feeds on Explore for bsky team * Fix dev/preview handling bug * Add sidebar cards, update types * Fix type error * Fix type error * Feedback * Blurhash fix * Copy update * Dev only debug hook --- .env.example | 7 +- .eslintrc.js | 1 + package.json | 2 +- src/App.native.tsx | 93 +++++----- src/App.web.tsx | 85 +++++---- src/alf/index.tsx | 7 +- src/components/FeedCard.tsx | 38 +++- src/components/FeedInterstitials.tsx | 1 + src/components/Post/Embed/FeedEmbed.tsx | 6 +- src/components/interstitials/Trending.tsx | 2 +- src/env/common.ts | 20 +- ...verFeedLiveEventFeedsAndTrendingBanner.tsx | 89 +++++++++ .../ExploreScreenLiveEventFeedsBanner.tsx | 17 ++ .../components/LiveEventFeedCardCompact.tsx | 132 ++++++++++++++ .../components/LiveEventFeedCardWide.tsx | 139 ++++++++++++++ .../components/LiveEventFeedOptionsMenu.tsx | 171 ++++++++++++++++++ .../LiveEventFeedsSettingsToggle.tsx | 43 +++++ .../SidebarLiveEventFeedsBanner.tsx | 13 ++ src/features/liveEvents/context.tsx | 110 +++++++++++ src/features/liveEvents/preferences.ts | 161 +++++++++++++++++ src/features/liveEvents/types.ts | 27 +++ src/geolocation/const.ts | 4 +- src/lib/hooks/useIsBskyTeam.ts | 8 + src/lib/statsig/gates.ts | 1 + src/logger/index.ts | 2 + src/logger/metrics.ts | 24 +++ src/screens/Search/Explore.tsx | 10 + .../Settings/ContentAndMediaSettings.tsx | 9 +- src/state/queries/preferences/const.ts | 4 + src/view/com/feeds/ComposerPrompt.tsx | 2 - src/view/com/posts/PostFeed.tsx | 22 ++- src/view/shell/desktop/RightNav.tsx | 7 +- yarn.lock | 40 ++-- 33 files changed, 1162 insertions(+), 135 deletions(-) create mode 100644 src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx create mode 100644 src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedCardCompact.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedCardWide.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx create mode 100644 src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx create mode 100644 src/features/liveEvents/context.tsx create mode 100644 src/features/liveEvents/preferences.ts create mode 100644 src/features/liveEvents/types.ts create mode 100644 src/lib/hooks/useIsBskyTeam.ts diff --git a/.env.example b/.env.example index 96a1548d66..e90541cabb 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,8 @@ EXPO_PUBLIC_SENTRY_DSN= # Bitdrift API key. If undefined, Bitdrift will be disabled. EXPO_PUBLIC_BITDRIFT_API_KEY= -# bapp-config web worker URL -BAPP_CONFIG_DEV_URL= +# geolocation web worker URL +GEOLOCATION_DEV_URL= + +# live-events web worker URL +LIVE_EVENTS_DEV_URL= diff --git a/.eslintrc.js b/.eslintrc.js index 37ed895aa4..16e844a050 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { 'Admonition', 'Admonition.Admonition', 'Toast.Action', + 'toast.Action', 'AgeAssuranceAdmonition', 'Span', 'StackedButton', diff --git a/package.json b/package.json index 61e042e05b..32b78e18fb 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.18.13", + "@atproto/api": "^0.18.15", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.6", diff --git a/src/App.native.tsx b/src/App.native.tsx index 34c3cc204c..2c4d6fa413 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -69,6 +69,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' @@ -92,6 +96,7 @@ if (isAndroid) { */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -141,49 +146,51 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 956c52005c..460d9ff174 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -57,6 +57,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -67,6 +71,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -117,45 +122,47 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alf/index.tsx b/src/alf/index.tsx index eed3fcbeb2..3aff9ddb57 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -11,7 +11,12 @@ import { import {themes} from '#/alf/themes' import {type Device} from '#/storage' -export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' +export { + type TextStyleProp, + type Theme, + utils, + type ViewStyleProp, +} from '@bsky.app/alf' export {atoms} from '#/alf/atoms' export * from '#/alf/breakpoints' export * from '#/alf/fonts' diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index c5e6200527..1fcedc6916 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' import { type AppBskyFeedDefs, @@ -21,19 +21,21 @@ import { import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, select, useTheme} from '#/alf' import { Button, ButtonIcon, type ButtonProps, ButtonText, } from '#/components/Button' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' import {Link as InternalLink, type LinkProps} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {RichText, type RichTextProps} from '#/components/RichText' import {Text} from '#/components/Typography' +import {useActiveLiveEventFeedUris} from '#/features/liveEvents/context' import type * as bsky from '#/types/bsky' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash' @@ -49,7 +51,11 @@ export function Default(props: Props) {
- +
@@ -118,14 +124,40 @@ export function AvatarPlaceholder({size = 40}: Omit) { export function TitleAndByline({ title, creator, + uri, }: { title: string creator?: bsky.profile.AnyProfileView + uri?: string }) { const t = useTheme() + const activeLiveEvents = useActiveLiveEventFeedUris() + const liveColor = useMemo( + () => + select(t.name, { + dark: t.palette.negative_600, + dim: t.palette.negative_600, + light: t.palette.negative_500, + }), + [t], + ) return ( + {uri && activeLiveEvents.has(uri) && ( + + + + Happening now + + + )} + style={[a.border, t.atoms.border_contrast_low, a.p_sm, a.rounded_md]}> - + - ) diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 3d47385ee8..830dfb3b66 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -41,7 +41,7 @@ export function Inner() { }, [setTrendingDisabled]) return error || noTopics ? null : ( - + + } else { + // no feed, no trending + return null + } + } + + // On desktop, we show in the sidebar + if (rightNavVisible) return null + + return events.feeds.map(feed => ) +} + +function Inner({feed}: {feed: LiveEventFeed}) { + const {_} = useLingui() + const optionsMenuControl = useDialogControl() + const layout = feed.layouts.wide + + return ( + <> + + + + + + + + + + + ) +} diff --git a/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..bf80ed6a2e --- /dev/null +++ b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx @@ -0,0 +1,17 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function ExploreScreenLiveEventFeedsBanner() { + const t = useTheme() + const events = useLiveEvents() + return events.feeds.map(feed => ( + + + + )) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx new file mode 100644 index 0000000000..69c24b2dc5 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -0,0 +1,132 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, utils} from '#/alf' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_md, a.curve_continuous] + +export function LiveEventFeedCardCompact({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + + const layout = feed.layouts.compact + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardWide.tsx b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx new file mode 100644 index 0000000000..1ed3a49df2 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx @@ -0,0 +1,139 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, useBreakpoints, utils} from '#/alf' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_lg, a.curve_continuous] + +export function LiveEventFeedCardWide({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + + const layout = feed.layouts.wide + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + {feed.preview ? ( + Preview + ) : ( + Happening now + )} + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx new file mode 100644 index 0000000000..b2f50840a9 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx @@ -0,0 +1,171 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {isNative} from '#/platform/detection' +import {atoms as a, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import * as toast from '#/components/Toast' +import {Span, Text} from '#/components/Typography' +import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export {useDialogControl} from '#/components/Dialog' + +export function LiveEventFeedOptionsMenu({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + return ( + + + + + + + + ) +} + +function Inner({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const { + isPending, + mutate: update, + error: rawError, + variables, + } = useUpdateLiveEventPreferences({ + feed, + metricContext, + onUpdateSuccess({undoAction}) { + toast.show( + + + + Your live event preferences have been updated. + + {undoAction && ( + { + if (undoAction) { + update(undoAction) + } + }}> + Undo + + )} + , + { + type: 'success', + }, + ) + + /* + * If there is no `undoAction`, it means that the action was already + * undone, and therefore the menu would have been closed prior to the + * undo happening. + */ + if (undoAction) { + control.close() + } + }, + }) + const cleanError = useCleanError() + const error = rawError ? cleanError(rawError) : undefined + + const isHidingFeed = variables?.type === 'hideFeed' && isPending + const isHidingAllFeeds = variables?.type === 'toggleHideAllFeeds' && isPending + + return ( + + + + Live event options + + + + + Live events appear occasionally when something exciting is + happening. If you'd like, you can hide this particular event, or all + events for this placement in your app interface. + + + + + + If you choose to hide all events, you can always re-enable them from{' '} + Settings → Content & Media. + + + + + + + + {isNative && ( + + )} + + + {error && ( + + {error.clean || error.raw || _(msg`An unknown error occurred.`)} + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx new file mode 100644 index 0000000000..35d4039ce1 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx @@ -0,0 +1,43 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Toggle from '#/components/forms/Toggle' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import { + useLiveEventPreferences, + useUpdateLiveEventPreferences, +} from '#/features/liveEvents/preferences' + +export function LiveEventFeedsSettingsToggle() { + const {_} = useLingui() + const {data: prefs} = useLiveEventPreferences() + const { + isPending, + data: updatedPrefs, + mutate: update, + } = useUpdateLiveEventPreferences({ + metricContext: 'settings', + }) + const hideAllFeeds = !!(updatedPrefs || prefs)?.hideAllFeeds + + return ( + { + if (!isPending) { + update({type: 'toggleHideAllFeeds'}) + } + }}> + + + + Show live events in your Discover Feed + + + + + ) +} diff --git a/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..25a5db8c8c --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx @@ -0,0 +1,13 @@ +import {LiveEventFeedCardCompact} from '#/features/liveEvents/components/LiveEventFeedCardCompact' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function SidebarLiveEventFeedsBanner() { + const events = useLiveEvents() + return events.feeds.map(feed => ( + + )) +} diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx new file mode 100644 index 0000000000..fa2aba2260 --- /dev/null +++ b/src/features/liveEvents/context.tsx @@ -0,0 +1,110 @@ +import {createContext, useContext, useMemo} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {useIsBskyTeam} from '#/lib/hooks/useIsBskyTeam' +import { + convertBskyAppUrlIfNeeded, + isBskyCustomFeedUrl, + makeRecordUri, +} from '#/lib/strings/url-helpers' +import {IS_DEV, LIVE_EVENTS_URL} from '#/env' +import {useLiveEventPreferences} from '#/features/liveEvents/preferences' +import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' +import {useDevMode} from '#/storage/hooks/dev-mode' + +const qc = new QueryClient() +const liveEventsQueryKey = ['live-events'] + +export const DEFAULT_LIVE_EVENTS = { + feeds: [], +} + +async function fetchLiveEvents(): Promise { + try { + const res = await fetch(`${LIVE_EVENTS_URL}/config`) + if (!res.ok) return null + const data = await res.json() + return data + } catch { + return null + } +} + +const Context = createContext(DEFAULT_LIVE_EVENTS) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [isDevMode] = useDevMode() + const isBskyTeam = useIsBskyTeam() + const {data} = useQuery( + { + staleTime: IS_DEV ? 5e3 : 1000 * 60, + queryKey: liveEventsQueryKey, + async queryFn() { + return fetchLiveEvents() + }, + }, + qc, + ) + + const ctx = useMemo(() => { + if (!data) return DEFAULT_LIVE_EVENTS + const feeds = data.feeds.filter(f => { + if (f.preview && !isBskyTeam) return false + return true + }) + return { + ...data, + // only one at a time for now, unless bsky team and dev mode + feeds: isBskyTeam && isDevMode ? feeds : feeds.slice(0, 1), + } + }, [data, isBskyTeam, isDevMode]) + + return {children} +} + +export async function prefetchLiveEvents() { + const data = await fetchLiveEvents() + if (data) { + qc.setQueryData(liveEventsQueryKey, data) + } +} + +export function useLiveEvents() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider') + } + return ctx +} + +export function useUserPreferencedLiveEvents() { + const events = useLiveEvents() + const {data, isLoading} = useLiveEventPreferences() + if (isLoading) return DEFAULT_LIVE_EVENTS + const {hideAllFeeds, hiddenFeedIds} = data + return { + ...events, + feeds: hideAllFeeds + ? [] + : events.feeds.filter(f => { + const hidden = f?.id ? hiddenFeedIds.includes(f?.id || '') : false + return !hidden + }), + } +} + +export function useActiveLiveEventFeedUris() { + const {feeds} = useLiveEvents() + + return new Set( + feeds + // insurance + .filter(f => isBskyCustomFeedUrl(f.url)) + .map(f => { + const uri = convertBskyAppUrlIfNeeded(f.url) + const [_0, did, _1, rkey] = uri.split('/').filter(Boolean) + const urip = makeRecordUri(did, 'app.bsky.feed.generator', rkey) + return urip.toString() + }), + ) +} diff --git a/src/features/liveEvents/preferences.ts b/src/features/liveEvents/preferences.ts new file mode 100644 index 0000000000..2f7af094a8 --- /dev/null +++ b/src/features/liveEvents/preferences.ts @@ -0,0 +1,161 @@ +import {useEffect} from 'react' +import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import { + preferencesQueryKey, + usePreferencesQuery, +} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' +import * as env from '#/env' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export type LiveEventPreferencesAction = Parameters< + Agent['updateLiveEventPreferences'] +>[0] & { + /** + * Flag that is internal to this hook, do not set when updating prefs + */ + __canUndo?: boolean +} + +export function useLiveEventPreferences() { + const query = usePreferencesQuery() + useWebOnlyDebugLiveEventPreferences() + return { + ...query, + data: query.data?.liveEventPreferences || { + hideAllFeeds: false, + hiddenFeedIds: [], + }, + } +} + +function useWebOnlyDebugLiveEventPreferences() { + const queryClient = useQueryClient() + const agent = useAgent() + + useEffect(() => { + if (env.IS_DEV && isWeb && typeof window !== 'undefined') { + // @ts-ignore + window.__updateLiveEventPreferences = async ( + action: LiveEventPreferencesAction, + ) => { + await agent.updateLiveEventPreferences(action) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + } + } + }, [agent, queryClient]) +} + +export function useUpdateLiveEventPreferences(props: { + feed?: LiveEventFeed + metricContext: LiveEventFeedMetricContext + onUpdateSuccess?: (props: { + undoAction: LiveEventPreferencesAction | null + }) => void +}) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation< + AppBskyActorDefs.LiveEventPreferences, + Error, + LiveEventPreferencesAction, + {undoAction: LiveEventPreferencesAction | null} + >({ + onSettled(data, error, variables) { + /* + * `onSettled` runs after the mutation completes, success or no. The idea + * here is that we want to invert the action that was just passed in, and + * provide it as an `undoAction` to the `onUpdateSuccess` callback. + * + * If the operation was not a success, we don't provide the `undoAction`. + * + * Upon the first call of the mutation, the `__canUndo` flag is undefined, + * so we allow the undo. However, when we create the `undoAction`, we + * set its `__canUndo` flag to false, so that if the user were to call + * the undo action, we would not provide another undo for that. + */ + const canUndo = variables.__canUndo === undefined ? true : false + let undoAction: LiveEventPreferencesAction | null = null + + switch (variables.type) { + case 'hideFeed': + undoAction = {type: 'unhideFeed', id: variables.id, __canUndo: false} + break + case 'unhideFeed': + undoAction = {type: 'hideFeed', id: variables.id, __canUndo: false} + break + case 'toggleHideAllFeeds': + undoAction = {type: 'toggleHideAllFeeds', __canUndo: false} + break + } + + if (data && !error) { + props?.onUpdateSuccess?.({ + undoAction: canUndo ? undoAction : null, + }) + } + }, + mutationFn: async action => { + const updated = await agent.updateLiveEventPreferences(action) + const prefs = updated.find(p => + asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p), + ) + + switch (action.type) { + case 'hideFeed': + case 'unhideFeed': { + if (!props.feed) { + logger.error( + `useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding`, + { + action, + }, + ) + break + } + + logger.metric( + action.type === 'hideFeed' + ? 'liveEvents:feedBanner:hide' + : 'liveEvents:feedBanner:unhide', + { + feed: props.feed.url, + context: props.metricContext, + }, + ) + break + } + case 'toggleHideAllFeeds': { + if (prefs!.hideAllFeeds) { + logger.metric('liveEvents:hideAllFeedBanners', { + context: props.metricContext, + }) + } else { + logger.metric('liveEvents:unhideAllFeedBanners', { + context: props.metricContext, + }) + } + break + } + } + + // triggers a refetch + queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + + return prefs! + }, + }) +} diff --git a/src/features/liveEvents/types.ts b/src/features/liveEvents/types.ts new file mode 100644 index 0000000000..8bc88c448c --- /dev/null +++ b/src/features/liveEvents/types.ts @@ -0,0 +1,27 @@ +export type LiveEventFeedImageLayout = 'wide' | 'compact' // maybe more in the future + +export type LiveEventFeedLayout = { + title: string + overlayColor: string + textColor: string + image: string + blurhash: string +} + +export type LiveEventFeed = { + id: string + preview: boolean + title: string + url: string + layouts: Record +} + +export type LiveEventsWorkerResponse = { + feeds: LiveEventFeed[] +} + +export type LiveEventFeedMetricContext = + | 'explore' + | 'discover' + | 'sidebar' + | 'settings' diff --git a/src/geolocation/const.ts b/src/geolocation/const.ts index b12f37140f..653e829bad 100644 --- a/src/geolocation/const.ts +++ b/src/geolocation/const.ts @@ -1,7 +1,7 @@ -import {BAPP_CONFIG_URL} from '#/env' +import {GEOLOCATION_URL} from '#/env' import {type Geolocation} from '#/geolocation/types' -export const GEOLOCATION_SERVICE_URL = `${BAPP_CONFIG_URL}/geolocation` +export const GEOLOCATION_SERVICE_URL = `${GEOLOCATION_URL}/geolocation` /** * Default geolocation config. diff --git a/src/lib/hooks/useIsBskyTeam.ts b/src/lib/hooks/useIsBskyTeam.ts new file mode 100644 index 0000000000..d653e7fd5e --- /dev/null +++ b/src/lib/hooks/useIsBskyTeam.ts @@ -0,0 +1,8 @@ +import {useMemo} from 'react' + +import {useGate} from '#/lib/statsig/statsig' + +export function useIsBskyTeam() { + const gate = useGate() + return useMemo(() => gate('is_bsky_team_member'), [gate]) +} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 357548d3b1..dcb330764c 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -7,6 +7,7 @@ export type Gate = | 'disable_settings_find_contacts' | 'explore_show_suggested_feeds' | 'feed_reply_button_open_thread' + | 'is_bsky_team_member' // special, do not remove | 'old_postonboarding' | 'onboarding_add_video_feed' | 'onboarding_suggested_starterpacks' diff --git a/src/logger/index.ts b/src/logger/index.ts index 998d025818..57d430b5dd 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -16,6 +16,8 @@ import {enabledLogLevels} from '#/logger/util' import {isNative} from '#/platform/detection' import {ENV} from '#/env' +export {type MetricEvents as Metrics} from '#/logger/metrics' + const TRANSPORTS: Transport[] = (function configureTransports() { switch (ENV) { case 'production': { diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 0af92a6e7c..7afe7f3bbe 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -1,5 +1,6 @@ import {type NotificationReason} from '#/lib/hooks/useNotificationHandler' import {type FeedDescriptor} from '#/state/queries/post-feed' +import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types' export type MetricEvents = { // App events @@ -796,4 +797,27 @@ export type MetricEvents = { } // user pressed the remove all data button 'contacts:settings:removeData': {} + + 'liveEvents:feedBanner:seen': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:click': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:hide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:unhide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:hideAllFeedBanners': { + context: LiveEventFeedMetricContext + } + 'liveEvents:unhideAllFeedBanners': { + context: LiveEventFeedMetricContext + } } diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index b3e26dcfd3..6b49588db9 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -68,6 +68,7 @@ import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' +import {ExploreScreenLiveEventFeedsBanner} from '#/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner' import * as ModuleHeader from './components/ModuleHeader' import { SuggestedAccountsTabBar, @@ -201,6 +202,10 @@ type ExploreScreenItems = type: 'interests-card' key: 'interests-card' } + | { + type: 'liveEventFeedsBanner' + key: string + } export function Explore({ focusSearchInput, @@ -684,6 +689,8 @@ export function Explore({ i.push(topBorder) i.push(...interestsNuxModule) + i.push({type: 'liveEventFeedsBanner', key: 'liveEventFeedsBanner'}) + if (useFullExperience) { i.push(trendingTopicsModule) i.push(...suggestedFeedsModule) @@ -998,6 +1005,9 @@ export function Explore({ case 'interests-card': { return } + case 'liveEventFeedsBanner': { + return + } } }, [ diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index e1144d0645..ebcf95a7af 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -26,6 +26,7 @@ import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' +import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -124,7 +125,7 @@ export function ContentAndMediaSettingsScreen({}: Props) { - {trendingEnabled && ( + {trendingEnabled ? ( <> + + ) : ( + <> + + + )} diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 54d063a502..7508fa4f6d 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -45,4 +45,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { verificationPrefs: { hideBadges: false, }, + liveEventPreferences: { + hideAllFeeds: false, + hiddenFeedIds: [], + }, } diff --git a/src/view/com/feeds/ComposerPrompt.tsx b/src/view/com/feeds/ComposerPrompt.tsx index be88ee2dc1..5d4685a4e6 100644 --- a/src/view/com/feeds/ComposerPrompt.tsx +++ b/src/view/com/feeds/ComposerPrompt.tsx @@ -148,8 +148,6 @@ export function ComposerPrompt() { a.relative, a.flex_row, a.align_start, - a.border_t, - t.atoms.border_contrast_low, { paddingLeft: 18, paddingRight: 15, diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 4f25468c95..9e6fcdcccb 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -70,6 +70,7 @@ import { } from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' +import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' import {ComposerPrompt} from '../feeds/ComposerPrompt' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' @@ -155,6 +156,10 @@ type FeedRow = type: 'composerPrompt' key: string } + | { + type: 'liveEventFeedsAndTrendingBanner' + key: string + } export function getItemsForFeedback(feedRow: FeedRow): { item: FeedPostSliceItem @@ -360,7 +365,7 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible - const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() + const {trendingVideoDisabled} = useTrendingSettings() const ageAssuranceBannerState = useAgeAssuranceBannerState() const selectedFeed = useSelectedFeed() @@ -510,13 +515,10 @@ let PostFeed = ({ }) } } - if (!rightNavVisible && !trendingDisabled) { - arr.push({ - type: 'interstitialTrending', - key: - 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, - }) - } + arr.push({ + type: 'liveEventFeedsAndTrendingBanner', + key: 'liveEventFeedsAndTrendingBanner-' + sliceIndex, + }) // Show composer prompt for Discover and Following feeds if ( hasSession && @@ -672,9 +674,7 @@ let PostFeed = ({ feedTab, hasSession, showProgressIntersitial, - trendingDisabled, trendingVideoDisabled, - rightNavVisible, gtMobile, isVideoFeed, areVideoFeedsEnabled, @@ -773,6 +773,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'liveEventFeedsAndTrendingBanner') { + return } else if (row.type === 'composerPrompt') { return } else if (row.type === 'interstitialTrendingVideos') { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 788df6e64c..04dddaf739 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -22,6 +22,7 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' import {Text} from '#/components/Typography' +import {SidebarLiveEventFeedsBanner} from '#/features/liveEvents/components/SidebarLiveEventFeedsBanner' function useWebQueryParams() { const navigation = useNavigation() @@ -49,7 +50,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const isSearchScreen = routeName === 'Search' const webqueryParams = useWebQueryParams() const searchQuery = webqueryParams?.q - const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) + const showExploreScreenDuplicatedContent = + !isSearchScreen || (isSearchScreen && !!searchQuery) const {rightNavVisible, centerColumnOffset, leftNavMinimal} = useLayoutBreakpoints() @@ -90,7 +92,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { )} - {showTrending && } + {showExploreScreenDuplicatedContent && } + {showExploreScreenDuplicatedContent && } {hasSession && ( diff --git a/yarn.lock b/yarn.lock index d0741715da..ff0610ac4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,12 +82,12 @@ "@atproto/xrpc" "^0.7.6" "@atproto/xrpc-server" "^0.10.0" -"@atproto/api@^0.18.13": - version "0.18.13" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b" - integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q== +"@atproto/api@^0.18.15": + version "0.18.15" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.15.tgz#25ce82081216bdefbf5397220de76ac3ba9e3f5d" + integrity sha512-GeaTP7HMRZa8jD6trMuTACa8t2jkFtRmcwWgrB0FT7l9jVCXrKpYupWeIeauEgWHNwWUUiaq3LmCox+HBy8ZMQ== dependencies: - "@atproto/common-web" "^0.4.11" + "@atproto/common-web" "^0.4.12" "@atproto/lexicon" "^0.6.0" "@atproto/syntax" "^0.4.2" "@atproto/xrpc" "^0.7.7" @@ -208,13 +208,13 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/common-web@^0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0" - integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ== +"@atproto/common-web@^0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.12.tgz#04135bef480d9e12cfef124ee45d8236764e7509" + integrity sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw== dependencies: - "@atproto/lex-data" "0.0.7" - "@atproto/lex-json" "0.0.7" + "@atproto/lex-data" "0.0.8" + "@atproto/lex-json" "0.0.8" zod "^3.23.8" "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": @@ -415,10 +415,10 @@ uint8arrays "3.0.0" unicode-segmenter "^0.14.0" -"@atproto/lex-data@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8" - integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw== +"@atproto/lex-data@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.8.tgz#46cc261efbfa6cc05bf04439d2d73cd8386b467d" + integrity sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA== dependencies: "@atproto/syntax" "0.4.2" multiformats "^9.9.0" @@ -451,12 +451,12 @@ "@atproto/lex-data" "0.0.3" tslib "^2.8.1" -"@atproto/lex-json@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866" - integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g== +"@atproto/lex-json@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.8.tgz#03290762d9368b029488ee0a0766d1a34063255c" + integrity sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg== dependencies: - "@atproto/lex-data" "0.0.7" + "@atproto/lex-data" "0.0.8" tslib "^2.8.1" "@atproto/lex-resolver@0.0.5": From 2223babc2b2b6207a4c5404231dbdefe7a1ca1b0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 14 Jan 2026 16:57:21 -0600 Subject: [PATCH 10/11] [APP-1759] Live Now Open Beta (#9699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add report dialog to LiveStatus dialog * Mr Worldwide™ * Bug fix bug fix * Copy update * Simplify parsing * Bump api sdk * update dev-env * Update yarn.lock * Bump api sdk * Refactor useActorStatus, add disablement and appeal dialog * Fix types * Guard against chat profile views * Go live (disabled) * Fix types * Update config and gates * Add allowed services * Merge conflicts * Fix gradient handling for nudge * Only show nudge on your own profile * Fix live avi overflow breakage * Add TODO * Feedback * Update nux image --------- Co-authored-by: Samuel Newman --- assets/images/live_now_beta.webp | Bin 0 -> 14882 bytes src/components/Dialog/index.tsx | 10 +- src/components/Menu/index.tsx | 12 +- src/components/Menu/index.web.tsx | 11 +- src/components/Menu/types.ts | 1 + .../dialogs/nuxs/LiveNowBetaDialog.tsx | 209 ++++++++++++++++++ src/components/dialogs/nuxs/index.tsx | 14 +- src/components/live/EditLiveDialog.tsx | 28 +-- src/components/live/GoLiveDialog.tsx | 43 ++-- src/components/live/queries.ts | 15 +- src/components/live/utils.ts | 25 +++ src/features/nuxs/components/Dot.tsx | 31 +++ src/features/nuxs/components/Gradient.tsx | 24 ++ src/lib/actor-status.ts | 13 +- src/lib/statsig/gates.ts | 1 + src/state/queries/nuxs/definitions.ts | 12 + src/state/service-config.tsx | 32 ++- src/view/com/posts/PostFeed.tsx | 2 +- src/view/com/profile/ProfileMenu.tsx | 80 +++++-- 19 files changed, 456 insertions(+), 107 deletions(-) create mode 100644 assets/images/live_now_beta.webp create mode 100644 src/components/dialogs/nuxs/LiveNowBetaDialog.tsx create mode 100644 src/features/nuxs/components/Dot.tsx create mode 100644 src/features/nuxs/components/Gradient.tsx diff --git a/assets/images/live_now_beta.webp b/assets/images/live_now_beta.webp new file mode 100644 index 0000000000000000000000000000000000000000..7b5b074835217dd4276b5638a88745604e5d8645 GIT binary patch literal 14882 zcmV+-I^D%mNk&E*IsgDyMM6+kP&il$0000G0001s0sztg06|PpNTUG&00EGdZExL3 zKLkS%ghB{}Krn-vLBwE&fy1D~KoAT;5DdW=Ikvl3{~i<(6M#3$oV8Xn-wyJ0cW-8| zs%3iM(^IZKh_%%$ACrm=g0(E4kd{73))#r(vk#*6{2sAEwk~gY_CdG3-1apnce$w! z(yh1Hpk2G64&tqsmO;H=7oLNB%PHyX)8&j0`n3~z5O6sVXP}<;mO;U~BhEoRZSo-D zvN0Ai05WJe0hxy~P;s5Y|_&@v~{ty3$|HJ>`|L}kKKl~s55C4b%!~fy`-=i~8ry~C+S~DgxE?SxB zhl;8)krpN@GZW3iM32Nofb&oTRv!8R3J+yKct`*k3pD^x7RsB{Nk}$O4yxEqoq}W! z%0P<4ftNv$(WH=Xd|kEv)ibMGwD-R)Ji%z|HD$P=Fm09H^q zAW%2}01(aqodGJ00@DCKO(c#*qM{)Y>5W_{1jMqpeh}=L#WG6zzX|={;&0>Er}oRd z*WT`B9vjCWoxi?+Wc|1P>+~P~7pt%BU$7qF|E1sHzpQ@c|5fOA8_HX^K_MdkT z`=7Ty@;|EDY!2>)sPgZ|h5kJjJEzHR%r-YAL+z)e~?>l*|xBOpFUtYd-{y+Q|`EU6j z;yo|Fxcqbd|M^q%&+)(S|Gxf3eSrTW{(b&8{0Ht&_<#TZPk+tu^!wlXzuBLFKjpvL zKi_|S|1JAf^v(S5{U7u{?Z5MXrS|Fh|M|Nr~_ z=709~{ri>w&)qlnAOHXSq7b!vmB)cnm>fW}U=r{2JGu`qd8Cj2-(VZB$m z?0h)Ld6nL&3Lhp25;SQWVo`{Sw*@{lLri+jsbw3dbddAA`m-XD%}`u zFg!J1f>mQ_^-MZmKqF%&v(X1s>fVFgAxIn<)vAJdys%0l;Jjh)PfAo%`o#GybF{!I_vYcx+~&Eqh6S0)t|+vJV{hc_pq66dpn@WD)JCC-#i!$?Jctv7TX*{mi% z>!8|nlDt?0b%VFq(7@d3!(2V_ZY5I>UF>5w2bFSH`6=Bst@r{krb)xXMU8D2mt%!u1={ieXB z2RU8X#m0_r*b|ySAL&qKlS6$7cgvig*DO9{NDX@4=f+imz!S1sCP7?kk85kEkVyBi zs+FqT`H$Qv8}1OU^^7GZC{RHL9%X66S`1fQA?vUF>poFEFPo6em0!$?O6w)P$m^iE)!ld%RcRy za=ae2&vEa*i~=N-NurWfUz)n8=w>sMcYM!=>V5Qml0fqbK$4NQFfoZ|EP!mr)HHVS zl+`Th+{dPUXGF7k5hkLnktGipUGpd?f*;N{2&L)M9ExGX8etZT+G8b&FS%)!?20NlZ0A zSFdR(u_%~0Xx0UE8R{<{bFBvQqZU9?K@^Lm@6ko1>Lw4Z0Oz~?4ugQeaf&}3ft0Xu zGKhPcw&y}d4{3E)onekc$`M7N*)GQR9r0ONyPje7UcN=$l^~#i^N#GerJ`T&9#$b`T8WLYNd*@s})$tSXD&_tRRdB9c z=Z*b-oZ9>~v_@sxee4H1$C_WX`}2qLx@E7vkT6sWK*b3`^y>;y$9$1hc+PqF0R)v$ znwz86>Xsd}=Tm!%^q^)(P2()}nR9r``xC)2#}ZBM+{H;kI|>kPi>Y;fb^Z9*x&Ywh zzE1`tMk+|FA>kbIv2Kr5=7Fmgv9HanGFs-L&p(&uv_k&~Ly&?;dK*t>BXIqCN4rF4 zXYx>khkHH_fABg%+5%3Q&I3qEE?bBhV#y<{1ntJL+Wp_fTG?}VEoc;>69cK@M-`W( z+7(`x_yuSPWC$kcU&tHE*r`Ev=r-8$MZ@X}=P0+JWu^or#0Wxi{@U94kL) z$2M(l6WAO5k{5tXe)w>WIr8GZZoMheVLfWK8F61XUXYX_z>s?=MW?SO|Gf@A zxX2OrM^Awu_E3vYUQPdc9DQ++Bkqo$05;W0Qd;e>3N?gTaq2j_6`ajnArRM&yr&v;{csn$I1q0$PPBJEXuhvTM9yyC*|Lv6 zNB_wUl32mt0PF{L&$69alNTZj(ZrF^dGqR;&t@4!Di|Pmb7E z#W5kRp{yiK%%^0*1Kt^4tO%$jxtP23Ku{}kpak!nUmO9Ei1j=Zw)C@Q3X(KXTIjdV zef+5wyM0|6;T`Lg``uZEX)7x_#^dh|-~a#s000000)hK~ySc69-$dSfaxu0UT-6o6 z#W&@tie~AjxN-BGC+~t=i-mQwQ=*EOzA^iCeiEsk>N=(Q(l(69tvR#vB_yj@T)u%~ zsKING76P?a3OmPkUY13QIfs~)Z=M7~uItS5>L$@&|M4rSip@D*1mHjaiEV=Y3%n&9 zj&Q$mxD18tG#-7XL+#&LUNZW_{eYpZxU#nLUM1^VR!@u)z!&J-7?Js7IV@&F*z^{~ zCQDV^%(GFUR$y04C;K|{y&rw_eBis3zv&eXSyOBdgWatN$I1b`B6RFT`L!RU_x-c0$i|8R46u2KvQ#IhSfs`1r|+C;-qLCk9=!nfLH5m9KUD&AlOlIz%9 zn=Kl5KLN3VwITB!aS{AAA2-3kr?GXNM$?o!iVXp;i!pr5gMWPDY?gA5%nIZ|cv~$X z-+Md0LL_@AX z5(?VFEaY=IHmjm#BSQRO-P_;K0Y_oyj{j^^e14f0imNJ0{hWthKnuU=Th`!YBMgpI z5yT#piM(0XjPJ6~=&!z20PVV`F~_QP7gkNOL&`7!{d;i+;zL|-O+sXg9W@@o7zzUj zK!K>NYGXKDkc7>2O*EDO!Vlh|uNyTcA1?B%EwNR5b(jkc3~=gn^pq&*Yl(ae;pBo6 z8PqGT$>2YBqh;dEUoVTSQWT8aeg{ncTd^T^CilK&v{)P)Eo6XI5T3E=JOI69O@bxz z^|)-#b^OYb4S385FEa6L=Zf#)IaPDP$Wc&Ill2g8PzO|Wv)g2LseX2p;0!jm3|+X0 zK-mwREnyMR!-Dob2o|tFi%Jqn@ryI;h_`$T*_k){qFkk$Gr67*< zC=8{Wk~ZtF{jQYhElb*|GD38hAa9rZd$?sYN3E1v3bk?#Lx2-%^h@F8?E{Jw8L0@# z8}wytDY(||L6;;0xlUvWx;csZC3+I7s3zFu1+{4Jl*o&99UqIs5t%8~;z>I_g2tN& z3%j?cRFqnA=SYOLCfAcGZ){HW6XCE$ii>Kr_$ z0S6q8Jkx%-6jszc#}ouJ41 z|7A^CvLeNM>T2Do!vM;^+oTPD{WN-)v8G)Z)S(I?mF^QL{&1HWtMt{4<>uKzJB%oo zs`_Q-$%o)bRZqYiI`e@AmwM`Q)*%F4dgm8-wO|J-K4DJI15;h?cJ`ZIv~}_RDK`xs z^n^ePyc64SZw->884bEnF~BL&PPN5_YHSD?gy)E&9YxZ|KXkZ;N8TK^e_6#-2HXL5 zSLUla8Eu2O`T-~p{Q+kjAm6opN0;CW#2%21qzI>_S&5XxRc^>IGw>WID&ja5OK@MS zpYbi-l7b6YnWm*y2j!!7Nnh9&TZvM+s_@g%1Q;Wzt{CWIvy?NZfpy);<=h*+j116K z89yb&WZBQ*GbmIJv(8X!`m~o)KF)bqm8rZvWS@mALKrX$dmx+^Ip-+X$?_qfzq)V* zNQF=Xc+|#yV{6F)ks9WI-C&1fDi|5>7W>25nA@s0F!7LjB?=;WDanTr)7^Iy)`%7W z;qcKB;agIzKVfI8MU+Cps6w1&_}{15i3a$C2ah8f0FIfbj7{vh<2s%p{F6Ssc@|U( z6skSek?9?sjX7(W{O?OF!WQ+Sz}EhvBovhO!|2fY)n_6-eIuF~w*KQ;pT_DcAN3Rg zaJrh8&+N)1IcN_Mz8>nz5C|}{vmD*zMhVhDdv>w=PKlaDXv2XR+2CgnjH9@g{l`Tk3+fh~kHa6T3GYAcjlCl-?V zbpEtBsW2OiP(u0rG~Jxy#dYq2&(_T09r)5pD?gu%{=Mr+;e0MQ0D@fJWl_3ehfKC3*1}CK}l%P)6f@?{sH51GX z4noHlF2EN!OJd_EVT)Db9Q_>?HA z8IS+~hB>jvZ+U7Jt~scAq5RU|`wtae_B{&K2fu70LxxJ+b1p$A4@cgcFCWSdIhP&7 z1yjlzhn94UVpC&$qx!{wpJnsuHW-1LC2ei$f`xdECjwHHTE&GD#z>oj#I_AFvh9Hl znsq^oO18?GHU9yR=Gv5@u#lu+(#+-WZ(-l(zot!S?6xuZ9T`KV@ta3JMvqUtTywmt z1Cl^3HqvENX&{EbDlt33ujIC}`LR;@d6sc)`C#5w$^3v|cxCr!dpsue9qk&y^Ee5rGozIFK#uwj20tDY4j;WpiLZ%e4?tHy3Z7khfQaogXLTQ%9bL{YNI z*#O(SMmwxngAD;yw~_R;dGOK|s4H+**k&3NU!skCttTW4_+uDe3%o$scnc(~O3?HH zP6)6g{(I-7^(upD0J9^tSqAC`XVZ9 zs|-r#ldw*4JKVCuG6mz1-n)%J5W;~)kM#M+7pWzRw2S}=LlKkR)Mmt5gdw(nYOqV} zI1yh4Ky{R}9*&VDi2=RbB(BF@PI&#s(kX?-%F)wF?|gB6tLJHBhidYBH8ho#W}_MZ z`s^mcBmHn~7M~V748#A6&ArwI14l(BaAKFwn2Z*H$+^n=cQYU(^QoU|ZM2`qS!>DM zC=gQX{Y~XyW*7>1wdzGXx$c|HY2_-5mJ6S5TgBIpe4vLw)+!{y2%Q@43_8-vFvHoM z+r(@62*U9?y}r-heL2qK8xbe)JBB2W4!=RhjU{<{+zPetgZmhqAG2lWcY73F*|Ckc zHs>M3XRd@#l;7`@ldf(QM0FIUZ_~D_2)L7LUf7Kx?^r=DxYJLB<%{SAnQ=oYtgAFF z8rsx0H1CFOl-Qv$Xv#w_(gNP?x0-_`$~L>pGvtf<)gYuYkT9+@H)&3F&W`VLhca8V z=c3ZRq&{gpG~+ss`MhZc)+($Y!`)mykj z^;hf1yT&kCyIW@seXNG?XXb}IKmDd(Y9q><>O{9`emTUk>hQqAFUss+p3&Czf7XDl zLjKfg(s&ag8!g+0LGaCne3tN+^lAp)ND&Hd1Wb@nHtV)`K_7umi7U2(Sl{!)M^tZf z5h_m&4d&Rb+B=(R=r@!2QdGL1+Dezo#q;(t@6_3oPm@bNr|ws8??H+q^y2bqEP-6j zc}d1T$ke;JJZb6W56d-rmA}q)h<>Wc0fRy1^3>831blFF97=k*z9$s;Rn&0oRBiFp zC-v*~-rMDFt40j_Cn27cK`I`YaxaUuVnTj!}-zqk*dJE9&ccEa-^LLxJ(j z6n|#v3)7ak*10J6h*-8^XO2JRTWHQQ?%p-u*HAN7-wZ_!(X0tusOIds-t$InO+?XN z0%gz(FT$b|?E`S0F%38BK|)JGp})LboKIS%)j10vz0{Vu^l>#w_lRO4kP}k7Cz5)+ zp&3Mx16-Md_qq)s$mW_WKdD~|%KdWq!L!2-@(>yaEEURHuvJkCAPgAGzguL%WdxWw z7pr81RUIBFKC!#4M|dBie((x*Dh@)DXicX4A&D7)AfJH6Cmn|ai|Z}U`~p3swOyJ6 zWi21{N+ueGrZJinlR~0QaNjN|x=zxB@Fpe8gsiQ~9Qfw_fhY|UV6Q~4fM}2IA!eKw z34Qa)q9VjisNqgqP_F6@4&_S9$ug`2nldds#A8Hjm;HGB+HLX5Rf~-~Kp1VmO3Fr0 zjdx35;zFI7KP3GH`C%m@w0)I{*1%grHlNmD+)sv#7Iu*6P<4RPPzvek)wnF~YSm23 zg#){{Y?AE4)kLi#A59yAHx;7mQo%x&`CYi8APfh6(Dsfc0EXd)BK}Z`hND_yE3_0pPOVDjz|v^T0TufX7}K=%GvhNO$o6&`L*3zNHG?47 z6+Z&HwT?5&)4Cpgx;Y^Q6x%+c{z1Slqy^BROSZ|mC_rAi`l->)f77O^X~d6~Cv>|> zm{~^iE`maN@De_sfLUpI2}0~IG7y}HgnOl-rq)-1hxoDaSoa1QIccUx=@`;aMEB#D z{ji7X-mwdv=b!8y#yBKd46Ga<;4roA(gsTRv#$jkDfmp~ejg33z%q7gMsuM{_>7Cg zT5jPmtd#YP;s`5w`c9DHv@N0O1xn(3d%EIx@00 z>LePM=d;8ZD8rw@Gm6)AD4e0>z^b6oaF+36kDz@eO;G@Km{40sbQf?}18C|wQy2S# zbBlEWD|bDs0m6Z6wVP(dBQ`~ARL^%BuwB}1<6GWwP6}x;pb7u*(d>;%>=QFxYlAW6cp27@CB#l3s-P6nAD~!)|(#_6D zXd!`2{`8!u&9?4o^}p1vy&>iMpXpGf=^Egq6D5?s*+ya(*SCrvXRjECfwk}GT+0&& zo6yBYPM2Aeu9J8(*9*C#EYK~dy#eZM1sqR*WhBZ^{A$5xj=l&V0jM@VP-L;Sj_Y@` z_LIqI=l0m&yZF8VEOh0eX>Pz7m)1T=)kprWd%}w@Pz(iQ7pLamSkDOq-9j~-mvu9NMR!G$Kf6k2 z^Mv%ID}B;Ltrx=_G2YTgNz@~XVL9ea>W1TGB|27uXtcO!R#08KgB7&QpqJ#txQ=Ow zl1k3Fa&izU9O0A$@gvg+QeX=wB@$uNqF@9T7;;*c?}kJ#j)9vg#4nB=L}|LCyrZP3 zfv!@PP*SW_gX4^&>wlAsGjv`zc(>s+6+aIg%cPjbzNZezjpm(jo?LFJ$-rt&zxv3a z(RJjn+Qv9jMw?0aV1^A1T_ zC*hCyJ1l72dp(Ef2n!dzMeh=9i5YozMx)QQlVhf;zGGjNvf=KkC7K<1EK4E&4C}OI zsjZxw866%ryp#B?cW<`yXP(P#`w3|OE&6M8706)n@JSiR5;Cz@c=n4%#1b6{b!-&r z-|EF=>bZ-*@j>Ax7{bcU5!t)q8jP`)Z><2)DF4M@5lMKENQDl+@7(oXRyCNQRjkZ! zu2|8rTjR_{Rw8Uo-k_9lljJHOG?6J4P$da4&M;xb)tjsOc0vLP-3S!*!BQNh_hEj4K_;K zuSdV@87EOyT1!b~DKdWmx2Mfs^UC=e7x*~2+#dLQyN?IfsaOSzLrC+CqI>S`+p|_A zDpdeF8p9VXH!u&dR`2ir_O6z8w*I|3m$KZc0|%wf>>SzEGcur(GC{j+#jO6Jxp_QM z5hG$kX6!Mgm$>j%@pyxD!6aJDNmcq%R1?&Thfj|&()jM)Z5Tq~c#u?haL!0#>Y8m* zEkao=WO`t%Z72&q&EDH+L=IJV5^_tCM2OrzB0zD};`r z1gA8^M33aiV#r=;tzy%Vw#nbnR5R*uv^t@sS3Pg9P&iZ4i>+gOF8&cS+pdG|M0eSN z!>8#&b8jUKd^ZPn?C5-bWD7rw5n56}agplR-mU1+h7{FlBc3VnRO0|`+gdiJ?u$zQ>bG_jT;_DHcvq)hEf~vErMNrdX!qoIRC0~VhaV(uXu-WCGqd0) zM5W|M@BvPl?lDI)y{KM6M`G8lxb>C2GK!{d)Ati!^AQ>8<-CVUu;cz=9MdCdqALxf zJWgLJUz2;{##WMbo0`k06<$7{CIYT5vUua|-2D ztH!2ygC_dP=vppm<^#FlH9d6fPTsV_&kG|D)-OQ#@*) znY_oOoOGZjTW6ZiukNSg!&Wfc8^q@1eyOG$5$1`bCU<$Z@Dwur>u7?dd}{z9T$D6C z5&}uYGO&ce9HlnVf*S}mucBkNNPp!)elTkzWNjS<(mWY$Oww8&yWmR}Ym}I3p0T6o zcY#d@p=}TAoF2mJ8<)0Zak0#2LzZ%uBrCc%mHJkMoDl?h+qgH|6tekmj%@=L$AOvO z_FT3OXD)O_LJnMEQswxO`0cu)yblt_ep`>ydW;|lZMU=IuItmaB|($Tjn2mXY=A#a zzAUpyii@n-gnF=yI!-GBC9F+-c`Ud4ZCSN2Cx=WQc9x`g8Q;WcfK|`}#78c{Ae0~M z=s<9C)mZZFav&}zKH}>{g2Fn({}1Tgiu}#i{M-sx-G9_+VMJ*}qFh>&BzIs;^(y}i z7&>%i)CZ?s{4!4B-d~Q|;U&CBO0PEmhRlUDR7OC&7%&RqA$i!35zHoFYs1V34%3+n ztS~Zu7Rp<7{1@P+iDmzW?C&(=gvAF1omgQ6OC%j1^ekT z^~j;i0ruh&mYI{`gtEmMRUhs3w}D1qiP`=3qzPNeA2%Z*+b`3B)uBxIqA+c?BK55x zCedtv=1pA1p^VqHxIx=ic`_^rtAsFyeQ~4dB-7RzJt8A`O^TBZ?@D}i*-d1wZIMx7 zZRjvyVI7q29wH$vP}ep$xYjKN)r=O;y_lc|IM$HxQHoxnQhT`k&!nh?~zz$wR9gBO&U`$(JIQ@FmPVx!Z)!=nHFhZbqbLONs#6+FdOu@8i5;R@7^4 zFe05jCPEFzOKJ~Ri72j?0sA0p4TZf{ByUQ#GLS-x3ryTXK84w!OW@xchiPH|Ji_Yo zq~OakK3pG4jhXXlWSQHO4DE?F_R42UQmp-J!%sgy5}S_hjJJm}Yt5r6JeHF1vB*Ipdfhp>x~)f8m2p z#Jf&nW@=|%@9PKRSg6330jn;u<89#B5r|2432L}Z1^~m*77DsHlL@(D;-R=c+23@s=x$U-}P9FzJ zbUIs@2uP*12WALO=J2`81Su84^cQG&crq`^(QJ;gH|j|o03bEiDD8*Ii!B`mxAjGn z4?eV)hK1aL4Ac{@st0NjxbGugg@9u=Qzs1mGZTCZCeSF76b9Nt7rMFII1=;c&`R%6 z-ti|PDI=F`xh&~=byxv;L@Er%R{2<7bMeiLSNJY%I_~0HdFMe@6cm+0!y2DFmK^Mo zlN3*Ee3yFgl(B+7*42qDGoc~W9l-M4^>S3R4>*Dw%xI|ZGRrynzMWu*188tyx9#AE zhD*ch4#n0gjqpG3Ki_~zES;U_Sn@JRW)p0eh(pACR>!{lqGe|#iE9pD2qk20fS2JA zzDjBH7`_H_N11%;x4hOk-}sL}v|%owYQI(|>1md%=;?QEty`FRMPu*HQiNjxA z<6vhlt@>8$#yIJHart58DdX{*nY@S?4k;BTi$}iErHU!D@*QN3cOnPb@3yM4`e~VA zI<$Nb@IlK2^6NP?#Y`GVR}|OKU5Ik{9kHF($QFrzmWNDL+qpgd#$(X~9T2V5^C=bI zk_j>z<6EmkfZ$UyKb;wNpvT*tAm4Z%6G=aQPJ?rOX{=T%`;iZYfA9;WYAE0965f6% z=$5?N!Jvtlar>N1H>@M}VVHtl|3{5{>W9*wE_4rmwxj31Q6l4IBUg3wJfp{!1m=A6 zUd7QbQz%?lKg;;+Y$Rj5_k#(7Iy zKNY8Iq)gK-1iA+?C;h^V|4yuMsJb5^c%UWPl9SAPBfR3o=GQ}`be~UcQ~XaND7#|l zI=HgXGZgc>%}dUi>lr3iuz}}{4Ia%=MX};tt)At=;lA&OcYgNg%xB;=`8`fQ5ovAb zl})j{SarEy24Z^eB3d>o(s$86(?Gp3lQ5yfl!d#B-m>QIC@d#5dYFgSpZQgj>O@tY z-Y_ji^7)z83hGs+0v8DET@|5shA)3Xf}o< zz5x!r+G(Y$N&i>dibnpzj}tN99NFre=fp_M{lM8_#nwhkhh$TAcT2RpY1|Lx$akWq zoV_=HP8pPK;cZH}G zks_83l-lo^fekHJ=cZzB0~3EQmU{Ov>8zqML$z{qqc3%cFbjNnwI;C#vb&{~N+m7a zP0TK_Z6TlQJhjF7;37_>91gKo6kP9#cYBhN{qauzq-tFyQzY~N@}KI{5o#Ol#Pdyv zz=&J`xZ{;+UBLs-hHrslDyTXB9>48uA|7+Kk#rAw@3^=zup<&__cA%{uLrL#I3F& zJ?fCX-Tx&`>8tczq8Wd8Kd53#kqWwxh|f?g-n5akr=Q7l0qBg>Y8&>|xBj>-KLqv< zHSf3_w5%(Cbpu{KDO#4Wy40S5P~2HJRVp>DYPc_?{5zHMC>z1bTV+{Hq_d*z7|io5 zhu*yfYJlw}L;hC_Gypt|DYrc+p0)nlkBA^{ESmVm)4Z)N=zC|+$^+?2)y&B~@?Ar) zb(R>JZ+ab#Xv&mEV0DqHv{r+?IN3E5D{#97!<7(LwTzP~a?!E}i$;5k3~WZyCKbqX zguu0`pjyEcbAvr&6^!QgRU1__HhC5Bh%1|jzkzgW;FT~s{yg_&gWTlM9gw}vc}?^1 za2!j~FOvBF{F#5WY!P0Y`t<+H5r}KbCZxn=8O~SH=Al1uGJqwK*Qb(I->>T10|#bi zcTi?9k;=yj>EG1ZRl5ao=*2wLoJd|+*bG8u;g~VG2K~qbJAZXo9I0*G5@w`hOV3%Z z!O{-ad~mTgkM%6HMEo>MDShl1SM?O+X{P?tC|RG~dx-EiotCuN*PAP8pP{Y1+W=x# zorX(H{Y(RL%coBW+n5X|SM?TCMnpw0j;YP7Lpd?Bxk`ytf62{22$V^Q(eM&rjJ;v^ zoSMq26u)~cqMp#hWPI3;ClIlVxh{&u!?yc<&{Z%mlO!WgE|)UA3YDr5bx7KAKMV0c zcGYx$=Fga2tWGn{1{m-Aqyd<;Sqb;_?d$NQtv196y0=Y7GbHP%h!ph>b(z`y!=$I1 zu+jYPn7ObKr&)99xt6U^WX-ZQ3BWFQ+|^dQZG7#kM@H;xgFKEm7-uZ?teASn(g*W+ ze>Wb=SUrpsuCy=on;SBs!475maD!vw9mN&_DO^ z!zd&KkiZwLQj^FrUY?9*D0Q?C+wOEH=M+6LiBBI}Xg5KCh?yWRfuRpcbrL!VQ* z>~TqQ~*TvsT;P{Y}DRc%Qpgfj@3aLkh)qrx3Q!X4hmc6RbeYB_|l z)~E7_I~EDdp<6>W9yVdbib>pRGu3%{86HfUkKOSqPr1w3+?}i`w0MmksQ|FNDGKxd zooiBj^(KSJPuh?BR(4XD>2cQN_sL=RsRpT_&CyetFhzg-i#j-q2cQ zG%ZY^60aAlrtfqlY+k;#Z^rT~VCpnv2oE-XhK7yr)=dGqh>F_UtnFgc#`4KYw^xyV z^ZGLFrBl03;1kgZC*ZPe*mFYNuL<`%i5VwA00#e zO*9%ErEj5?rU?vkDZ@j`ZJiOEfPc}j!)dnG`v~!|PKFuHJR!qOa|+Pwxir+Yd>Eyx zvFKxp%=q0p^_YUj%-BztHP#fWAM2?YV6=0JOul;=T z0jx)R-1}54A}{vk0a*-LxS)PM36cjmP6*FaVvNa}V%6Jb*l@avHtfwd==nItaNbE7 z-mJ{nHD|6X$|5{_t!l^Pw$c#*{Q4ebbJv8m9c0MsRYGd?wUzbbZVi)Ggl9Dc6)+{X zkpl?a3V=Q`Tf>LpyR@HH^yd(wEVpWico%EX?%XI{1{)3IU03ACi zuG>2D9ECp%%8=u3#pR}rfsuxkLigPwI4OyXu6qlG4)+;i0nWroPv0Zu;XYIVZV5vp!=@34yLPmTfm61ocBL-X@Bkmzc2 zs~k|3A^>`)u%qi}IF#3Oe9Si(fVR#Q-B$ReLfi$|s~4uPAk67#NZkwKlqo24bar}ze5M8N+A?L zzbnA3LwZX|#A-DOj!`XimsHFI|<79aK?wnmTzM#(sboPlp0X=}@C?<$Dv5VE!dHSA1y z|1-l{H_qSWL&n*os2wo$Z9n>u!KsMi*F;DD0^eAkSx&1>22ddIjIH0xosr}&2^8LH17``b{bVIp9};T-{~eT@dlle;)*opH4K z$?3JUT86jLaL}}D?|3JalmP=wChk}t#Exb%Q-d8K{Ic6hy^?Din)zq)vM5m73N(I2 zV18>271ZRn9|jn<8-Bf0_B77N4hLSrS~JREWG06uj8d=W#!ciRONL=v6(!ZXrs_xN z%RehKtht9wvtGihf~ebwp7>-<2=o0@nZNV$`%&-Ed1y^L-_+;m4-J-5hd=!#e|Sz7 zAyR<*fl`^)NMY{y&8|;o-*gq~dHn<@FB80AzGvGr(=~p9!+RPQ2WNf1yL-e-nNN|@ zUt2*3Wy~7kiyNs(?(5*ajdbffGGPfbel$vuEA?XDlS&)VI#`p6DJVcmi%PEoFIksL z4d}JQAGS(QW96x}TAkfC!Hxm2(U-^~n}!h9um>a~*CwZKgc?R)I1>U_N!)WAAs5ow zc-`D$7^Jv2$5tup+7`v#wc}3e{?JiH#EG~bbyv<Y>IG_lIVIiOpv=gMMzjXtG%efd=Q72=v4uE(oVUE_69ZR#@MIw^y^MFA&T6Ah z)6C$!TcS7dd8~;lNVM! zBOETl=k8%(@6qDa2^^dr9-VOptc3>K$$w;OV%w{8`mFxEojtrja3(*T3mJ@$e=+BK zQ%$|8GC^9f7;b&RzhYHj#s7)G{G4l)d6s`^B)6g?rmq0LxFj(bcMo|(3+pGZ!akiG zn)q~#9Sswe~~`|5Q05-?5trS?hvJI@<& z!3gBJhGj(VtCO~gXn1PVUv8HZ>~bFlk-z(0al=YVRgIt$FWEI(?cWJ=c$W`UnX<&q zxkr*?+ScUQ1z=2JifV;UTZ@DL3}4$YFU=>wAloURqme*IOE=UadSnk#X3?1aIfK3p{obst$GAP_=>z%*DS)FZQoC;D{ zy$O|aRufw;;&E z2X^O|MeaOm4e5@^vnK4|nRSUnCW4B-j)I;}&@0NX)*n*utYUc!EN6vSh$*UJ=2AC( z!HqTS;n!#)s*5>xd`}vPs z;&avSRg7*p+ErnE%o+nwqEZy!Bo#gDacstl#TY?_4&G1FBs1)>`vTw$cg;9d1dO!&b$SvZmPjp(WRRkb8=(3?vf@#AWePH9O&a)UcsC-j zTmTP4rN*u?nqoGX+FGgVzI?g#pT!i>Sz*ThAgX&1yQ}ZF=o4g}HeT50MdEoep7KGZfG6Xq^4IC@LPfu39Jz*>^(tjb=i97Ka z7zkN6Z&r0SC^1?AeY`A51`7ZMr5*M{C2A>=Qd43&2~UUsgBjMD?%TBGYxpjvnScUu zKmY&$0000X7A{!RJlGT_qq;XCUG}^^#fQcaeCJ_mb z{m{nSj3$TwKXS^^5WFFkDPPW;(7|R)P?bL*>VtQ-NsRgZIz>2VGG(*LjTUEwOk)FL z+odf3Zu5D*HLItjY+H1tpY7gnH^#Me^o@&dl(YT2&F1)4CxyVg&KxYvQnK}=Uo6Q8 UlXSte|Afc?{~jGU@C4uh0AJQb @@ -202,16 +202,18 @@ export function ItemText({children, style}: ItemTextProps) { ) } -export function ItemIcon({icon: Comp}: ItemIconProps) { +export function ItemIcon({icon: Comp, fill}: ItemIconProps) { const t = useTheme() const {disabled} = useMenuItemContext() return ( ) diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index 242449bf49..339b26350c 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -262,6 +262,7 @@ export function Item({children, label, onPress, style, ...rest}: ItemProps) { a.gap_lg, a.py_sm, a.rounded_xs, + a.overflow_hidden, {minHeight: 32, paddingHorizontal: 10}, web({outline: 0}), (hovered || focused) && @@ -302,7 +303,7 @@ export function ItemText({children, style}: ItemTextProps) { ) } -export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { +export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) { const t = useTheme() const {disabled} = useMenuItemContext() return ( @@ -319,9 +320,11 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts index e9ee082f4c..a5fce102ba 100644 --- a/src/components/Menu/types.ts +++ b/src/components/Menu/types.ts @@ -107,6 +107,7 @@ export type ItemTextProps = React.PropsWithChildren export type ItemIconProps = React.PropsWithChildren<{ icon: React.ComponentType position?: 'left' | 'right' + fill?: (props: {disabled: boolean}) => string }> export type GroupProps = React.PropsWithChildren diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx new file mode 100644 index 0000000000..b081322c44 --- /dev/null +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx @@ -0,0 +1,209 @@ +import {useCallback, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from '#/platform/detection' +import {atoms as a, select, useTheme, utils, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' +import { + createIsEnabledCheck, + isExistingUserAsOf, +} from '#/components/dialogs/nuxs/utils' +import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' +import {Text} from '#/components/Typography' +import {IS_E2E} from '#/env' + +export const enabled = createIsEnabledCheck(props => { + return ( + !IS_E2E && + isExistingUserAsOf( + '2026-01-16T00:00:00.000Z', + props.currentProfile.createdAt, + ) && + props.gate('live_now_beta') + ) +}) + +export function LiveNowBetaDialog() { + const t = useTheme() + const {_} = useLingui() + const nuxDialogs = useNuxDialogContext() + const control = Dialog.useDialogControl() + + Dialog.useAutoOpen(control) + + const onClose = useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + const shadowColor = useMemo(() => { + return select(t.name, { + light: utils.alpha(t.palette.primary_900, 0.4), + dark: utils.alpha(t.palette.primary_25, 0.4), + dim: utils.alpha(t.palette.primary_25, 0.4), + }) + }, [t]) + + return ( + + + + + + + + + + Beta Feature + + + + + + {_( + + + + + + + Show when you’re live + + + + Streaming on Twitch? Set your live status on Bluesky to add a + badge to your avatar. Tapping it takes people straight to your + stream. + + + + + {!isWeb && ( + + )} + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 2ab319fafd..eadadeb8eb 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -20,9 +20,9 @@ import {useProfileQuery} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' import { - enabled as isFindContactsAnnouncementEnabled, - FindContactsAnnouncement, -} from '#/components/dialogs/nuxs/FindContactsAnnouncement' + enabled as isLiveNowBetaDialogEnabled, + LiveNowBetaDialog, +} from '#/components/dialogs/nuxs/LiveNowBetaDialog' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils' import {useGeolocation} from '#/geolocation' @@ -37,8 +37,8 @@ const queuedNuxs: { enabled?: (props: EnabledCheckProps) => boolean }[] = [ { - id: Nux.FindContactsAnnouncement, - enabled: isFindContactsAnnouncementEnabled, + id: Nux.LiveNowBetaDialog, + enabled: isLiveNowBetaDialogEnabled, }, ] @@ -186,9 +186,7 @@ function Inner({ return ( {/*For example, activeNux === Nux.NeueTypography && */} - {activeNux === Nux.FindContactsAnnouncement && ( - - )} + {activeNux === Nux.LiveNowBetaDialog && } ) } diff --git a/src/components/live/EditLiveDialog.tsx b/src/components/live/EditLiveDialog.tsx index 3304bceb61..ba5e2b5b7a 100644 --- a/src/components/live/EditLiveDialog.tsx +++ b/src/components/live/EditLiveDialog.tsx @@ -18,7 +18,6 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock' -import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {LinkPreview} from './LinkPreview' @@ -172,26 +171,13 @@ function DialogInner({ {(liveLinkError || linkMetaError) && ( - - - - {liveLinkError ? ( - This is not a valid link - ) : ( - cleanError(linkMetaError) - )} - - + + {liveLinkError ? ( + This is not a valid link + ) : ( + cleanError(linkMetaError) + )} + )} diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx index 44c604cde7..9d866fd6a5 100644 --- a/src/components/live/GoLiveDialog.tsx +++ b/src/components/live/GoLiveDialog.tsx @@ -6,13 +6,14 @@ import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {definitelyUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useLiveNowConfig} from '#/state/service-config' import {useTickEveryMinute} from '#/state/shell' import {atoms as a, ios, native, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' -import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {getLiveServiceNames} from '#/components/live/utils' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Select from '#/components/Select' @@ -49,6 +50,10 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { const [duration, setDuration] = useState(60) const moderationOpts = useModerationOpts() const tick = useTickEveryMinute() + const liveNowConfig = useLiveNowConfig() + const {formatted: allowedServices} = getLiveServiceNames( + liveNowConfig.allowedDomains, + ) const time = useCallback( (offset: number) => { @@ -139,27 +144,21 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { /> - {(liveLinkError || linkMetaError) && ( - - - - {liveLinkError ? ( - This is not a valid link - ) : ( - cleanError(linkMetaError) - )} - - + {liveLinkError || linkMetaError ? ( + + {liveLinkError ? ( + This is not a valid link + ) : ( + cleanError(linkMetaError) + )} + + ) : ( + + + The following services are enabled for your account:{' '} + {allowedServices} + + )} diff --git a/src/components/live/queries.ts b/src/components/live/queries.ts index 19cb54ebf2..7db944d06d 100644 --- a/src/components/live/queries.ts +++ b/src/components/live/queries.ts @@ -18,10 +18,10 @@ import {useLiveNowConfig} from '#/state/service-config' import {useAgent, useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {useDialogContext} from '#/components/Dialog' +import {getLiveServiceNames} from '#/components/live/utils' export function useLiveLinkMetaQuery(url: string | null) { const liveNowConfig = useLiveNowConfig() - const {currentAccount} = useSession() const {_} = useLingui() const agent = useAgent() @@ -30,13 +30,14 @@ export function useLiveLinkMetaQuery(url: string | null) { queryKey: ['link-meta', url], queryFn: async () => { if (!url) return undefined - const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did) - - if (!config) throw new Error(_(msg`You are not allowed to go live`)) - const urlp = new URL(url) - if (!config.domains.includes(urlp.hostname)) { - throw new Error(_(msg`${urlp.hostname} is not a valid URL`)) + if (!liveNowConfig.allowedDomains.has(urlp.hostname)) { + const {formatted} = getLiveServiceNames(liveNowConfig.allowedDomains) + throw new Error( + _( + msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`, + ), + ) } return await getLinkMeta(agent, url) diff --git a/src/components/live/utils.ts b/src/components/live/utils.ts index 6b4267cb01..10b0ba8f90 100644 --- a/src/components/live/utils.ts +++ b/src/components/live/utils.ts @@ -35,3 +35,28 @@ export function useDebouncedValue(val: T, delayMs: number): T { return prev } + +const serviceUrlToNameMap: Record = { + 'twitch.tv': 'Twitch', + 'www.twitch.tv': 'Twitch', + 'youtube.com': 'YouTube', + 'www.youtube.com': 'YouTube', + 'youtu.be': 'YouTube', + 'nba.com': 'NBA', + 'www.nba.com': 'NBA', + 'nba.smart.link': 'nba.smart.link', + 'espn.com': 'ESPN', + 'www.espn.com': 'ESPN', + 'stream.place': 'Streamplace', + 'skylight.social': 'Skylight', +} + +export function getLiveServiceNames(domains: Set) { + const names = Array.from( + new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)), + ) + return { + names, + formatted: names.join(', '), + } +} diff --git a/src/features/nuxs/components/Dot.tsx b/src/features/nuxs/components/Dot.tsx new file mode 100644 index 0000000000..7eb82bf60f --- /dev/null +++ b/src/features/nuxs/components/Dot.tsx @@ -0,0 +1,31 @@ +import {View, type ViewStyle} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' + +/** + * The little blue dot used to nudge a user towards a certain feature. The dot + * is absolutely positioned, and is intended to be configured by passing in + * positional styles via `top`, `bottom`, `left`, and `right` props. + */ +export function Dot({ + top, + bottom, + left, + right, +}: Pick) { + const t = useTheme() + return ( + + + + ) +} diff --git a/src/features/nuxs/components/Gradient.tsx b/src/features/nuxs/components/Gradient.tsx new file mode 100644 index 0000000000..5da8b4aca3 --- /dev/null +++ b/src/features/nuxs/components/Gradient.tsx @@ -0,0 +1,24 @@ +import {LinearGradient} from 'expo-linear-gradient' + +import {atoms as a, useTheme, utils, type ViewStyleProp} from '#/alf' + +/** + * A gradient overlay using the primary color at low opacity. This component is + * absolutely positioned and intended to be composed within other components, + * with optional styling allowed, such as adjusting border radius. + */ +export function Gradient({style}: ViewStyleProp) { + const t = useTheme() + return ( + + ) +} diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 31e532eaa0..7c18b5dc66 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -7,7 +7,7 @@ import { import {isAfter, parseISO} from 'date-fns' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' -import {useLiveNowConfig} from '#/state/service-config' +import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config' import {useTickEveryMinute} from '#/state/shell' import type * as bsky from '#/types/bsky' @@ -20,7 +20,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { tick! // revalidate every minute if (shadowed && 'status' in shadowed && shadowed.status) { - const isValid = validateStatus(shadowed.did, shadowed.status, config) + const isValid = validateStatus(shadowed.status, config) const isDisabled = shadowed.status.isDisabled || false const isActive = isStatusStillActive(shadowed.status.expiresAt) if (isValid && !isDisabled && isActive) { @@ -65,19 +65,14 @@ export function isStatusStillActive(timeStr: string | undefined) { } export function validateStatus( - did: string, status: AppBskyActorDefs.StatusView, - config: {did: string; domains: string[]}[], + config: LiveNowConfig, ) { if (status.status !== 'app.bsky.actor.status#live') return false - const sources = config.find(cfg => cfg.did === did) - if (!sources) { - return false - } try { if (AppBskyEmbedExternal.isView(status.embed)) { const url = new URL(status.embed.external.uri) - return sources.domains.includes(url.hostname) + return config.allowedDomains.has(url.hostname) } else { return false } diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index dcb330764c..700ca12f83 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -8,6 +8,7 @@ export type Gate = | 'explore_show_suggested_feeds' | 'feed_reply_button_open_thread' | 'is_bsky_team_member' // special, do not remove + | 'live_now_beta' | 'old_postonboarding' | 'onboarding_add_video_feed' | 'onboarding_suggested_starterpacks' diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 6c03c936a6..e23050c465 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -12,6 +12,8 @@ export enum Nux { BookmarksAnnouncement = 'BookmarksAnnouncement', FindContactsAnnouncement = 'FindContactsAnnouncement', FindContactsDismissibleBanner = 'FindContactsDismissibleBanner', + LiveNowBetaDialog = 'LiveNowBetaDialog', + LiveNowBetaNudge = 'LiveNowBetaNudge', /* * Blocking announcements. New IDs are required for each new announcement. @@ -62,6 +64,14 @@ export type AppNux = BaseNux< id: Nux.FindContactsDismissibleBanner data: undefined } + | { + id: Nux.LiveNowBetaDialog + data: undefined + } + | { + id: Nux.LiveNowBetaNudge + data: undefined + } > export const NuxSchemas: Record | undefined> = { @@ -75,4 +85,6 @@ export const NuxSchemas: Record | undefined> = { [Nux.BookmarksAnnouncement]: undefined, [Nux.FindContactsAnnouncement]: undefined, [Nux.FindContactsDismissibleBanner]: undefined, + [Nux.LiveNowBetaDialog]: undefined, + [Nux.LiveNowBetaNudge]: undefined, } diff --git a/src/state/service-config.tsx b/src/state/service-config.tsx index 242022b60d..a04cbb51ee 100644 --- a/src/state/service-config.tsx +++ b/src/state/service-config.tsx @@ -1,7 +1,10 @@ import {createContext, useContext, useMemo} from 'react' +import {useGate} from '#/lib/statsig/statsig' import {useLanguagePrefs} from '#/state/preferences/languages' import {useServiceConfigQuery} from '#/state/queries/service-config' +import {useSession} from '#/state/session' +import {IS_DEV} from '#/env' import {device} from '#/storage' type TrendingContext = { @@ -18,7 +21,7 @@ const TrendingContext = createContext({ }) TrendingContext.displayName = 'TrendingContext' -const LiveNowContext = createContext(null) +const LiveNowContext = createContext([]) LiveNowContext.displayName = 'LiveNowContext' const CheckEmailConfirmedContext = createContext(null) @@ -82,19 +85,28 @@ export function useTrendingConfig() { return useContext(TrendingContext) } -export function useLiveNowConfig() { +const DEFAULT_LIVE_ALLOWED_DOMAINS = ['twitch.tv', 'www.twitch.tv'] +export type LiveNowConfig = { + allowedDomains: Set +} +export function useLiveNowConfig(): LiveNowConfig { const ctx = useContext(LiveNowContext) - if (!ctx) { - throw new Error( - 'useLiveNowConfig must be used within a ServiceConfigManager', - ) + const canGoLive = useCanGoLive() + const {currentAccount} = useSession() + if (!currentAccount?.did || !canGoLive) return {allowedDomains: new Set()} + const vip = ctx.find(live => live.did === currentAccount.did) + return { + allowedDomains: new Set( + DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []), + ), } - return ctx } -export function useCanGoLive(did?: string) { - const config = useLiveNowConfig() - return !!config.find(cfg => cfg.did === did) +export function useCanGoLive() { + const gate = useGate() + const {hasSession} = useSession() + if (!hasSession) return false + return IS_DEV ? true : gate('live_now_beta') } export function useCheckEmailConfirmed() { diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 9e6fcdcccb..97e5e9731e 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -954,7 +954,7 @@ let PostFeed = ({ const actor = post.author if ( actor.status && - validateStatus(actor.did, actor.status, liveNowConfig) && + validateStatus(actor.status, liveNowConfig) && isStatusStillActive(actor.status.expiresAt) ) { if (!seenActorWithStatusRef.current.has(actor.did)) { diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 8cf6d2c968..f9c1bac007 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -15,6 +15,7 @@ import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {type Shadow} from '#/state/cache/types' import {useModalControls} from '#/state/modals' +import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import { RQKEY as profileQueryKey, useProfileBlockMutationQueue, @@ -25,6 +26,7 @@ import {useCanGoLive} from '#/state/service-config' import {useSession} from '#/state/session' import {EventStopper} from '#/view/com/util/EventStopper' import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' @@ -59,6 +61,8 @@ import * as Prompt from '#/components/Prompt' import {useFullVerificationState} from '#/components/verification' import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' +import {Dot} from '#/features/nuxs/components/Dot' +import {Gradient} from '#/features/nuxs/components/Gradient' import {useDevMode} from '#/storage/hooks/dev-mode' let ProfileMenu = ({ @@ -66,6 +70,7 @@ let ProfileMenu = ({ }: { profile: Shadow }): React.ReactNode => { + const t = useTheme() const {_} = useLingui() const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() @@ -79,8 +84,15 @@ let ProfileMenu = ({ const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) - const canGoLive = useCanGoLive(currentAccount?.did) + const canGoLive = useCanGoLive() const status = useActorStatus(profile) + const statusNudge = useNux(Nux.LiveNowBetaNudge) + const statusNudgeActive = + isSelf && + canGoLive && + statusNudge.status === 'ready' && + !statusNudge.nux?.completed + const {mutate: saveNux} = useSaveNux() const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) @@ -229,17 +241,22 @@ let ProfileMenu = ({ {({props}) => { return ( - + <> + + + {statusNudgeActive && } + ) }} @@ -337,11 +354,19 @@ let ProfileMenu = ({ ? _(msg`Edit live status`) : _(msg`Go live`) } - onPress={ - status.isDisabled - ? goLiveDisabledDialogControl.open - : goLiveDialogControl.open - }> + onPress={() => { + if (status.isDisabled) { + goLiveDisabledDialogControl.open() + } else { + goLiveDialogControl.open() + } + saveNux({ + id: Nux.LiveNowBetaNudge, + data: undefined, + completed: true, + }) + }}> + {statusNudgeActive && } {status.isDisabled ? ( Go live (disabled) @@ -351,7 +376,26 @@ let ProfileMenu = ({ Go live )} - + {statusNudgeActive && ( + + New + + )} + t.palette.primary_500 + : undefined + } + /> )} {verification.viewer.role === 'verifier' && From 498a09e79711770ea8072b6ab09ba87e0075f76f Mon Sep 17 00:00:00 2001 From: estrattonbailey <4732330+estrattonbailey@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:59:53 +0000 Subject: [PATCH 11/11] Nightly source-language update --- src/locale/locales/en/messages.po | 426 ++++++++++++++++++------------ 1 file changed, 254 insertions(+), 172 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 213c0a9038..95b6706cbd 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -159,10 +159,6 @@ msgstr "" msgid "{0} is live" msgstr "" -#: src/components/live/queries.ts:39 -msgid "{0} is not a valid URL" -msgstr "" - #: src/screens/Signup/StepHandle/index.tsx:188 msgid "{0} is not available" msgstr "" @@ -565,6 +561,11 @@ msgstr "" msgid "A new form of verification" msgstr "" +#. Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English. +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:147 +msgid "A screenshot of a post from @esb.lol, showing the user is currently livestreaming content on Twitch. The post reads: \"Hello! I'm live on Twitch, and I'm testing Bluesky's latest feature too!\"" +msgstr "" + #. Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English. #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:120 msgid "A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads \"inventing a saturday that immediately follows monday\"." @@ -623,18 +624,18 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:419 #: src/screens/Messages/components/RequestButtons.tsx:91 -#: src/view/com/profile/ProfileMenu.tsx:169 +#: src/view/com/profile/ProfileMenu.tsx:181 msgctxt "toast" msgid "Account blocked" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:182 +#: src/view/com/profile/ProfileMenu.tsx:194 msgctxt "toast" msgid "Account followed" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:442 -#: src/view/com/profile/ProfileMenu.tsx:145 +#: src/view/com/profile/ProfileMenu.tsx:157 msgctxt "toast" msgid "Account muted" msgstr "" @@ -662,18 +663,18 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:84 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:290 -#: src/view/com/profile/ProfileMenu.tsx:159 +#: src/view/com/profile/ProfileMenu.tsx:171 msgctxt "toast" msgid "Account unblocked" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:194 +#: src/view/com/profile/ProfileMenu.tsx:206 msgctxt "toast" msgid "Account unfollowed" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:432 -#: src/view/com/profile/ProfileMenu.tsx:135 +#: src/view/com/profile/ProfileMenu.tsx:147 msgctxt "toast" msgid "Account unmuted" msgstr "" @@ -713,7 +714,7 @@ msgstr "" msgid "Add a content warning" msgstr "" -#: src/components/live/GoLiveDialog.tsx:98 +#: src/components/live/GoLiveDialog.tsx:103 msgid "Add a temporary live status to your profile. When someone clicks on your avatar, they’ll see information about your live event." msgstr "" @@ -770,7 +771,7 @@ msgstr "" msgid "Add emoji reaction" msgstr "" -#: src/view/com/feeds/ComposerPrompt.tsx:222 +#: src/view/com/feeds/ComposerPrompt.tsx:220 msgid "Add image" msgstr "" @@ -821,12 +822,12 @@ msgstr "" msgid "Add the following DNS record to your domain:" msgstr "" -#: src/components/FeedCard.tsx:305 +#: src/components/FeedCard.tsx:337 msgid "Add this feed to your feeds" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:323 -#: src/view/com/profile/ProfileMenu.tsx:326 +#: src/view/com/profile/ProfileMenu.tsx:340 +#: src/view/com/profile/ProfileMenu.tsx:343 msgid "Add to lists" msgstr "" @@ -835,8 +836,8 @@ msgid "Add to saved posts" msgstr "" #: src/components/dialogs/StarterPackDialog.tsx:176 -#: src/view/com/profile/ProfileMenu.tsx:314 -#: src/view/com/profile/ProfileMenu.tsx:317 +#: src/view/com/profile/ProfileMenu.tsx:331 +#: src/view/com/profile/ProfileMenu.tsx:334 msgid "Add to starter packs" msgstr "" @@ -1150,6 +1151,10 @@ msgstr "" msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name." msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:166 +msgid "An unknown error occurred." +msgstr "" + #: src/components/moderation/ModerationDetailsDialog.tsx:136 #: src/lib/moderation/useModerationCauseDescription.ts:145 msgid "an unknown labeler" @@ -1330,7 +1335,7 @@ msgstr "" msgid "Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant." msgstr "" -#: src/components/FeedCard.tsx:340 +#: src/components/FeedCard.tsx:372 msgid "Are you sure you want to remove this from your feeds?" msgstr "" @@ -1372,8 +1377,8 @@ msgctxt "Name of app icon variant" msgid "Aurora" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:116 -#: src/screens/Settings/ContentAndMediaSettings.tsx:122 +#: src/screens/Settings/ContentAndMediaSettings.tsx:117 +#: src/screens/Settings/ContentAndMediaSettings.tsx:123 msgid "Autoplay videos and GIFs" msgstr "" @@ -1457,6 +1462,10 @@ msgstr "" msgid "Begin the age assurance process by completing the fields below." msgstr "" +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:99 +msgid "Beta Feature" +msgstr "" + #: src/components/dialogs/BirthDateSettings.tsx:162 msgid "Birthdate" msgstr "" @@ -1467,7 +1476,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:818 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 -#: src/view/com/profile/ProfileMenu.tsx:504 +#: src/view/com/profile/ProfileMenu.tsx:548 msgid "Block" msgstr "" @@ -1477,13 +1486,13 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:705 #: src/screens/Messages/components/RequestButtons.tsx:144 #: src/screens/Messages/components/RequestButtons.tsx:146 -#: src/view/com/profile/ProfileMenu.tsx:410 -#: src/view/com/profile/ProfileMenu.tsx:417 +#: src/view/com/profile/ProfileMenu.tsx:454 +#: src/view/com/profile/ProfileMenu.tsx:461 msgid "Block account" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:813 -#: src/view/com/profile/ProfileMenu.tsx:487 +#: src/view/com/profile/ProfileMenu.tsx:531 msgid "Block Account?" msgstr "" @@ -1535,7 +1544,7 @@ msgid "Blocked Accounts" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:815 -#: src/view/com/profile/ProfileMenu.tsx:499 +#: src/view/com/profile/ProfileMenu.tsx:543 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -1551,7 +1560,7 @@ msgstr "" msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:496 +#: src/view/com/profile/ProfileMenu.tsx:540 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." msgstr "" @@ -1654,16 +1663,16 @@ msgstr "" msgid "Browse more accounts" msgstr "" -#: src/components/FeedInterstitials.tsx:907 +#: src/components/FeedInterstitials.tsx:908 msgid "Browse more feeds on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:888 -#: src/components/FeedInterstitials.tsx:891 +#: src/components/FeedInterstitials.tsx:889 +#: src/components/FeedInterstitials.tsx:892 msgid "Browse more suggestions" msgstr "" -#: src/components/FeedInterstitials.tsx:916 +#: src/components/FeedInterstitials.tsx:917 msgid "Browse more suggestions on the Explore page" msgstr "" @@ -1703,7 +1712,7 @@ msgstr "" #: src/components/LabelingServiceCard/index.tsx:62 #: src/components/moderation/ReportDialog/index.tsx:842 #: src/screens/Search/components/StarterPackCard.tsx:106 -#: src/screens/Search/Explore.tsx:936 +#: src/screens/Search/Explore.tsx:943 msgid "By {0}" msgstr "" @@ -1752,12 +1761,14 @@ msgstr "" #: src/components/dialogs/InAppBrowserConsent.tsx:104 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:274 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:282 -#: src/components/live/GoLiveDialog.tsx:244 -#: src/components/live/GoLiveDialog.tsx:250 -#: src/components/Menu/index.tsx:350 +#: src/components/live/GoLiveDialog.tsx:243 +#: src/components/live/GoLiveDialog.tsx:249 +#: src/components/Menu/index.tsx:352 #: src/components/PostControls/RepostButton.tsx:209 #: src/components/Prompt.tsx:144 #: src/components/Prompt.tsx:146 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:153 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:158 #: src/lib/media/picker.tsx:38 #: src/screens/Deactivated.tsx:149 #: src/screens/Profile/Header/EditProfileDialog.tsx:218 @@ -2078,6 +2089,8 @@ msgstr "" #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:171 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:191 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:199 #: src/components/dialogs/SearchablePeopleList.tsx:295 #: src/components/dialogs/StarterPackDialog.tsx:179 #: src/components/dms/AfterReportDialog.tsx:93 @@ -2085,8 +2098,8 @@ msgstr "" #: src/components/dms/AfterReportDialog.tsx:208 #: src/components/dms/AfterReportDialog.tsx:213 #: src/components/dms/EmojiPopup.android.tsx:58 -#: src/components/live/EditLiveDialog.tsx:229 -#: src/components/live/EditLiveDialog.tsx:235 +#: src/components/live/EditLiveDialog.tsx:215 +#: src/components/live/EditLiveDialog.tsx:221 #: src/components/NewskieDialog.tsx:167 #: src/components/NewskieDialog.tsx:173 #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:208 @@ -2156,7 +2169,7 @@ msgstr "" msgid "Close menu" msgstr "" -#: src/components/Menu/index.tsx:344 +#: src/components/Menu/index.tsx:346 msgid "Close this dialog" msgstr "" @@ -2241,6 +2254,11 @@ msgstr "" msgid "Configure content filtering setting for category: {name}" msgstr "" +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:51 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:36 +msgid "Configure live event banner" +msgstr "" + #: src/components/moderation/LabelPreference.tsx:253 msgid "Configured in <0>moderation settings." msgstr "" @@ -2316,7 +2334,7 @@ msgstr "" msgid "Contacts removed" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:51 +#: src/screens/Settings/ContentAndMediaSettings.tsx:52 msgid "Content & Media" msgstr "" @@ -2443,8 +2461,8 @@ msgstr "" msgid "Copy App Password" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:447 -#: src/view/com/profile/ProfileMenu.tsx:450 +#: src/view/com/profile/ProfileMenu.tsx:491 +#: src/view/com/profile/ProfileMenu.tsx:494 msgid "Copy at:// URI" msgstr "" @@ -2459,8 +2477,8 @@ msgid "Copy code" msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:502 -#: src/view/com/profile/ProfileMenu.tsx:456 -#: src/view/com/profile/ProfileMenu.tsx:459 +#: src/view/com/profile/ProfileMenu.tsx:500 +#: src/view/com/profile/ProfileMenu.tsx:503 msgid "Copy DID" msgstr "" @@ -2489,8 +2507,8 @@ msgstr "" msgid "Copy link to post" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:252 -#: src/view/com/profile/ProfileMenu.tsx:263 +#: src/view/com/profile/ProfileMenu.tsx:269 +#: src/view/com/profile/ProfileMenu.tsx:280 msgid "Copy link to profile" msgstr "" @@ -2997,12 +3015,12 @@ msgstr "" msgid "Discover new custom feeds" msgstr "" -#: src/screens/Search/Explore.tsx:431 +#: src/screens/Search/Explore.tsx:436 #: src/view/screens/Feeds.tsx:730 msgid "Discover New Feeds" msgstr "" -#: src/components/Dialog/index.tsx:373 +#: src/components/Dialog/index.tsx:379 msgid "Dismiss" msgstr "" @@ -3116,7 +3134,7 @@ msgstr "" msgid "Double tap or long press the message to add a reaction" msgstr "" -#: src/components/Dialog/index.tsx:374 +#: src/components/Dialog/index.tsx:380 msgid "Double tap to close the dialog" msgstr "" @@ -3229,8 +3247,8 @@ msgstr "" msgid "Edit list details" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:337 -#: src/view/com/profile/ProfileMenu.tsx:349 +#: src/view/com/profile/ProfileMenu.tsx:354 +#: src/view/com/profile/ProfileMenu.tsx:374 msgid "Edit live status" msgstr "" @@ -3394,13 +3412,13 @@ msgstr "" msgid "Enable this source only" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:132 -#: src/screens/Settings/ContentAndMediaSettings.tsx:146 +#: src/screens/Settings/ContentAndMediaSettings.tsx:133 +#: src/screens/Settings/ContentAndMediaSettings.tsx:147 msgid "Enable trending topics" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:153 -#: src/screens/Settings/ContentAndMediaSettings.tsx:167 +#: src/screens/Settings/ContentAndMediaSettings.tsx:155 +#: src/screens/Settings/ContentAndMediaSettings.tsx:169 msgid "Enable trending videos in your Discover feed" msgstr "" @@ -3596,7 +3614,7 @@ msgstr "" msgid "Expires in {0}" msgstr "" -#: src/components/live/EditLiveDialog.tsx:131 +#: src/components/live/EditLiveDialog.tsx:130 msgid "Expires in {0} at {1}" msgstr "" @@ -3630,8 +3648,8 @@ msgstr "" msgid "Export My Data" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:84 -#: src/screens/Settings/ContentAndMediaSettings.tsx:87 +#: src/screens/Settings/ContentAndMediaSettings.tsx:85 +#: src/screens/Settings/ContentAndMediaSettings.tsx:88 msgid "External media" msgstr "" @@ -3724,10 +3742,10 @@ msgstr "" msgid "Failed to load conversations" msgstr "" -#: src/screens/Search/Explore.tsx:467 -#: src/screens/Search/Explore.tsx:519 -#: src/screens/Search/Explore.tsx:557 -#: src/screens/Search/Explore.tsx:596 +#: src/screens/Search/Explore.tsx:472 +#: src/screens/Search/Explore.tsx:524 +#: src/screens/Search/Explore.tsx:562 +#: src/screens/Search/Explore.tsx:601 msgid "Failed to load feeds preferences" msgstr "" @@ -3757,14 +3775,14 @@ msgstr "" msgid "Failed to load preference." msgstr "" -#: src/screens/Search/Explore.tsx:460 -#: src/screens/Search/Explore.tsx:512 -#: src/screens/Search/Explore.tsx:550 -#: src/screens/Search/Explore.tsx:589 +#: src/screens/Search/Explore.tsx:465 +#: src/screens/Search/Explore.tsx:517 +#: src/screens/Search/Explore.tsx:555 +#: src/screens/Search/Explore.tsx:594 msgid "Failed to load suggested feeds" msgstr "" -#: src/screens/Search/Explore.tsx:370 +#: src/screens/Search/Explore.tsx:375 msgid "Failed to load suggested follows" msgstr "" @@ -3848,7 +3866,7 @@ msgstr "" msgid "Failed to update email, please try again." msgstr "" -#: src/components/FeedCard.tsx:285 +#: src/components/FeedCard.tsx:317 msgid "Failed to update feeds" msgstr "" @@ -3887,7 +3905,7 @@ msgstr "" msgid "Feed" msgstr "" -#: src/components/FeedCard.tsx:139 +#: src/components/FeedCard.tsx:171 #: src/view/com/feeds/FeedSourceCard.tsx:150 msgid "Feed by {0}" msgstr "" @@ -3912,8 +3930,8 @@ msgstr "" msgid "Feed unavailable" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:104 -#: src/view/shell/desktop/RightNav.tsx:105 +#: src/view/shell/desktop/RightNav.tsx:107 +#: src/view/shell/desktop/RightNav.tsx:108 #: src/view/shell/Drawer.tsx:368 msgid "Feedback" msgstr "" @@ -3939,7 +3957,7 @@ msgstr "" msgid "Feeds are custom algorithms that users build with a little coding expertise. <0>See this guide for more information." msgstr "" -#: src/components/FeedCard.tsx:282 +#: src/components/FeedCard.tsx:314 #: src/screens/SavedFeeds.tsx:90 msgctxt "toast" msgid "Feeds updated!" @@ -4099,8 +4117,8 @@ msgstr "" msgid "Follow 7 accounts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:293 -#: src/view/com/profile/ProfileMenu.tsx:304 +#: src/view/com/profile/ProfileMenu.tsx:310 +#: src/view/com/profile/ProfileMenu.tsx:321 msgid "Follow account" msgstr "" @@ -4191,8 +4209,8 @@ msgstr "" msgid "Following {handle}" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:76 -#: src/screens/Settings/ContentAndMediaSettings.tsx:79 +#: src/screens/Settings/ContentAndMediaSettings.tsx:77 +#: src/screens/Settings/ContentAndMediaSettings.tsx:80 msgid "Following feed preferences" msgstr "" @@ -4411,24 +4429,24 @@ msgstr "" msgid "Go Home" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:338 -#: src/view/com/profile/ProfileMenu.tsx:351 +#: src/view/com/profile/ProfileMenu.tsx:355 +#: src/view/com/profile/ProfileMenu.tsx:376 msgid "Go live" msgstr "" -#: src/components/live/GoLiveDialog.tsx:90 #: src/components/live/GoLiveDialog.tsx:95 -#: src/components/live/GoLiveDialog.tsx:229 -#: src/components/live/GoLiveDialog.tsx:238 +#: src/components/live/GoLiveDialog.tsx:100 +#: src/components/live/GoLiveDialog.tsx:228 +#: src/components/live/GoLiveDialog.tsx:237 msgid "Go Live" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:335 -#: src/view/com/profile/ProfileMenu.tsx:347 +#: src/view/com/profile/ProfileMenu.tsx:352 +#: src/view/com/profile/ProfileMenu.tsx:372 msgid "Go live (disabled)" msgstr "" -#: src/components/live/GoLiveDialog.tsx:171 +#: src/components/live/GoLiveDialog.tsx:170 msgid "Go live for" msgstr "" @@ -4511,6 +4529,11 @@ msgstr "" msgid "Handle too long. Please try a shorter one." msgstr "" +#: src/components/FeedCard.tsx:157 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:120 +msgid "Happening now" +msgstr "" + #: src/screens/Settings/AccessibilitySettings.tsx:85 msgid "Haptics" msgstr "" @@ -4558,8 +4581,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:254 #: src/screens/Settings/Settings.tsx:258 -#: src/view/shell/desktop/RightNav.tsx:125 #: src/view/shell/desktop/RightNav.tsx:128 +#: src/view/shell/desktop/RightNav.tsx:131 #: src/view/shell/Drawer.tsx:381 msgid "Help" msgstr "" @@ -4617,6 +4640,11 @@ msgctxt "action" msgid "Hide" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:140 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:147 +msgid "Hide all events" +msgstr "" + #: src/components/dialogs/Embed.tsx:128 msgid "Hide customization options" msgstr "" @@ -4644,6 +4672,11 @@ msgstr "" msgid "Hide this card" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:128 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:135 +msgid "Hide this event" +msgstr "" + #: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 msgid "Hide this post?" msgstr "" @@ -4793,6 +4826,10 @@ msgstr "" msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here." msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:119 +msgid "If you choose to hide all events, you can always re-enable them from <0>Settings → Content & Media." +msgstr "" + #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:277 msgid "If you delete this list, you won't be able to recover it." msgstr "" @@ -5305,7 +5342,7 @@ msgstr "" msgid "Liked By" msgstr "" -#: src/components/FeedCard.tsx:218 +#: src/components/FeedCard.tsx:250 #: src/view/com/feeds/FeedSourceCard.tsx:172 msgid "Liked by {0, plural, one {# user} other {# users}}" msgstr "" @@ -5445,23 +5482,36 @@ msgstr "" msgid "LIVE" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedCardCompact.tsx:52 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:52 +msgid "Live event happening now: {0}" +msgstr "" + +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:107 +msgid "Live event options" +msgstr "" + +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:111 +msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." +msgstr "" + #: src/components/live/LiveStatusDialog.tsx:235 msgid "Live feature is in beta" msgstr "" -#: src/components/live/EditLiveDialog.tsx:148 -#: src/components/live/EditLiveDialog.tsx:152 -#: src/components/live/GoLiveDialog.tsx:121 -#: src/components/live/GoLiveDialog.tsx:125 +#: src/components/live/EditLiveDialog.tsx:147 +#: src/components/live/EditLiveDialog.tsx:151 +#: src/components/live/GoLiveDialog.tsx:126 +#: src/components/live/GoLiveDialog.tsx:130 msgid "Live link" msgstr "" -#: src/screens/Search/Explore.tsx:83 +#: src/screens/Search/Explore.tsx:84 msgid "Load more" msgstr "" -#: src/screens/Search/Explore.tsx:501 -#: src/screens/Search/Explore.tsx:578 +#: src/screens/Search/Explore.tsx:506 +#: src/screens/Search/Explore.tsx:583 msgid "Load more suggested feeds" msgstr "" @@ -5500,11 +5550,11 @@ msgstr "" msgid "Logged-out visibility" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:137 +#: src/view/shell/desktop/RightNav.tsx:140 msgid "Logo by @sawaratsuki.bsky.social" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:134 +#: src/view/shell/desktop/RightNav.tsx:137 #: src/view/shell/Drawer.tsx:709 msgid "Logo by <0>@sawaratsuki.bsky.social" msgstr "" @@ -5541,8 +5591,8 @@ msgstr "" msgid "Make sure this is where you intend to go!" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:60 -#: src/screens/Settings/ContentAndMediaSettings.tsx:63 +#: src/screens/Settings/ContentAndMediaSettings.tsx:61 +#: src/screens/Settings/ContentAndMediaSettings.tsx:64 msgid "Manage saved feeds" msgstr "" @@ -5729,8 +5779,8 @@ msgid "More languages..." msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:149 -#: src/view/com/profile/ProfileMenu.tsx:229 -#: src/view/com/profile/ProfileMenu.tsx:235 +#: src/view/com/profile/ProfileMenu.tsx:241 +#: src/view/com/profile/ProfileMenu.tsx:248 msgid "More options" msgstr "" @@ -5763,8 +5813,8 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:692 -#: src/view/com/profile/ProfileMenu.tsx:389 -#: src/view/com/profile/ProfileMenu.tsx:396 +#: src/view/com/profile/ProfileMenu.tsx:433 +#: src/view/com/profile/ProfileMenu.tsx:440 msgid "Mute account" msgstr "" @@ -5894,6 +5944,7 @@ msgid "Nevermind, create a handle for me" msgstr "" #: src/screens/Search/modules/ExploreTrendingTopics.tsx:196 +#: src/view/com/profile/ProfileMenu.tsx:388 msgid "New" msgstr "" @@ -6054,7 +6105,7 @@ msgstr "" msgid "No DNS Panel" msgstr "" -#: src/components/live/EditLiveDialog.tsx:140 +#: src/components/live/EditLiveDialog.tsx:139 msgid "No expiry set" msgstr "" @@ -6153,7 +6204,7 @@ msgstr "" msgid "No results" msgstr "" -#: src/screens/Search/Explore.tsx:793 +#: src/screens/Search/Explore.tsx:800 msgid "No results for \"{0}\"." msgstr "" @@ -6170,7 +6221,7 @@ msgstr "" msgid "No results found for \"<0>{query}\"." msgstr "" -#: src/screens/Search/Explore.tsx:797 +#: src/screens/Search/Explore.tsx:804 msgid "No results." msgstr "" @@ -6238,7 +6289,7 @@ msgstr "" msgid "Not Found" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:511 +#: src/view/com/profile/ProfileMenu.tsx:555 msgid "Note about sharing" msgstr "" @@ -6416,7 +6467,7 @@ msgstr "" msgid "Open avatar creator" msgstr "" -#: src/view/com/feeds/ComposerPrompt.tsx:200 +#: src/view/com/feeds/ComposerPrompt.tsx:198 msgid "Open camera" msgstr "" @@ -6529,7 +6580,7 @@ msgstr "" msgid "Opens composer" msgstr "" -#: src/view/com/feeds/ComposerPrompt.tsx:201 +#: src/view/com/feeds/ComposerPrompt.tsx:199 msgid "Opens device camera" msgstr "" @@ -6560,7 +6611,7 @@ msgstr "" msgid "Opens helpdesk in browser" msgstr "" -#: src/view/com/feeds/ComposerPrompt.tsx:223 +#: src/view/com/feeds/ComposerPrompt.tsx:221 msgid "Opens image picker" msgstr "" @@ -6779,7 +6830,7 @@ msgstr "" msgid "Pin feed" msgstr "" -#: src/components/FeedCard.tsx:329 +#: src/components/FeedCard.tsx:361 msgid "Pin Feed" msgstr "" @@ -7153,6 +7204,10 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:118 +msgid "Preview" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:151 msgid "Previous image" msgstr "" @@ -7165,8 +7220,8 @@ msgstr "" msgid "Primary Language" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:113 -#: src/view/shell/desktop/RightNav.tsx:114 +#: src/view/shell/desktop/RightNav.tsx:116 +#: src/view/shell/desktop/RightNav.tsx:117 msgid "Privacy" msgstr "" @@ -7445,7 +7500,7 @@ msgstr "" #: src/components/dialogs/MutedWords.tsx:443 #: src/components/dialogs/StarterPackDialog.tsx:374 #: src/components/dialogs/StarterPackDialog.tsx:380 -#: src/components/FeedCard.tsx:343 +#: src/components/FeedCard.tsx:375 #: src/components/StarterPack/Wizard/WizardListCard.tsx:105 #: src/components/StarterPack/Wizard/WizardListCard.tsx:112 #: src/screens/Bookmarks/index.tsx:266 @@ -7523,7 +7578,7 @@ msgstr "" msgid "Remove from saved posts" msgstr "" -#: src/components/FeedCard.tsx:338 +#: src/components/FeedCard.tsx:370 msgid "Remove from your feeds?" msgstr "" @@ -7531,8 +7586,8 @@ msgstr "" msgid "Remove image" msgstr "" -#: src/components/live/EditLiveDialog.tsx:240 -#: src/components/live/EditLiveDialog.tsx:247 +#: src/components/live/EditLiveDialog.tsx:226 +#: src/components/live/EditLiveDialog.tsx:233 msgid "Remove live status" msgstr "" @@ -7568,8 +7623,8 @@ msgstr "" #: src/components/verification/VerificationRemovePrompt.tsx:46 #: src/components/verification/VerificationsDialog.tsx:252 -#: src/view/com/profile/ProfileMenu.tsx:362 -#: src/view/com/profile/ProfileMenu.tsx:365 +#: src/view/com/profile/ProfileMenu.tsx:406 +#: src/view/com/profile/ProfileMenu.tsx:409 msgid "Remove verification" msgstr "" @@ -7704,8 +7759,8 @@ msgstr "" msgid "Report" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:429 -#: src/view/com/profile/ProfileMenu.tsx:432 +#: src/view/com/profile/ProfileMenu.tsx:473 +#: src/view/com/profile/ProfileMenu.tsx:476 msgid "Report account" msgstr "" @@ -7983,8 +8038,8 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:662 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:667 -#: src/components/live/EditLiveDialog.tsx:216 -#: src/components/live/EditLiveDialog.tsx:223 +#: src/components/live/EditLiveDialog.tsx:202 +#: src/components/live/EditLiveDialog.tsx:209 #: src/components/StarterPack/QrCodeDialog.tsx:204 #: src/screens/Profile/Header/EditProfileDialog.tsx:236 #: src/screens/Profile/Header/EditProfileDialog.tsx:250 @@ -8134,11 +8189,11 @@ msgstr "" msgid "Search for feeds that you want to suggest to others." msgstr "" -#: src/screens/Search/Explore.tsx:357 +#: src/screens/Search/Explore.tsx:362 msgid "Search for more accounts" msgstr "" -#: src/screens/Search/Explore.tsx:434 +#: src/screens/Search/Explore.tsx:439 msgid "Search for more feeds" msgstr "" @@ -8164,8 +8219,8 @@ msgstr "" msgid "Search my posts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:272 -#: src/view/com/profile/ProfileMenu.tsx:275 +#: src/view/com/profile/ProfileMenu.tsx:289 +#: src/view/com/profile/ProfileMenu.tsx:292 msgid "Search posts" msgstr "" @@ -8283,7 +8338,7 @@ msgstr "" msgid "Select content languages" msgstr "" -#: src/components/live/GoLiveDialog.tsx:176 +#: src/components/live/GoLiveDialog.tsx:175 msgid "Select duration" msgstr "" @@ -8575,7 +8630,7 @@ msgstr "" msgid "Share a fun fact!" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:516 +#: src/view/com/profile/ProfileMenu.tsx:560 msgid "Share anyway" msgstr "" @@ -8623,8 +8678,8 @@ msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:171 #: src/screens/StarterPack/StarterPackScreen.tsx:613 #: src/screens/StarterPack/StarterPackScreen.tsx:621 -#: src/view/com/profile/ProfileMenu.tsx:252 -#: src/view/com/profile/ProfileMenu.tsx:265 +#: src/view/com/profile/ProfileMenu.tsx:269 +#: src/view/com/profile/ProfileMenu.tsx:282 msgid "Share via..." msgstr "" @@ -8684,6 +8739,11 @@ msgstr "" msgid "Show lists of users to select from" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx:27 +#: src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx:37 +msgid "Show live events in your Discover Feed" +msgstr "" + #: src/components/Post/ShowMoreTextButton.tsx:51 msgid "Show More" msgstr "" @@ -8738,6 +8798,11 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:60 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:170 +msgid "Show when you’re live" +msgstr "" + #: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Shows information about when this post was created" msgstr "" @@ -8885,7 +8950,7 @@ msgstr "" msgid "Some of your verifications are invalid." msgstr "" -#: src/components/FeedInterstitials.tsx:870 +#: src/components/FeedInterstitials.tsx:871 msgid "Some other feeds you might like" msgstr "" @@ -8942,8 +9007,8 @@ msgstr "" msgid "Sorry, we're unable to load account suggestions at this time." msgstr "" -#: src/App.native.tsx:126 -#: src/App.web.tsx:99 +#: src/App.native.tsx:131 +#: src/App.web.tsx:104 msgid "Sorry! Your session expired. Please sign in again." msgstr "" @@ -9022,7 +9087,7 @@ msgstr "" msgid "Starter pack is invalid" msgstr "" -#: src/screens/Search/Explore.tsx:625 +#: src/screens/Search/Explore.tsx:630 #: src/view/screens/Profile.tsx:240 msgid "Starter Packs" msgstr "" @@ -9058,6 +9123,10 @@ msgstr "" msgid "Storybook" msgstr "" +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:181 +msgid "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream." +msgstr "" + #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:117 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:123 #: src/components/live/GoLiveDisabledDialog.tsx:138 @@ -9121,7 +9190,7 @@ msgstr "" msgid "Successfully verified" msgstr "" -#: src/screens/Search/Explore.tsx:354 +#: src/screens/Search/Explore.tsx:359 msgid "Suggested Accounts" msgstr "" @@ -9256,8 +9325,8 @@ msgstr "" msgid "Tell us a little more" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:120 -#: src/view/shell/desktop/RightNav.tsx:121 +#: src/view/shell/desktop/RightNav.tsx:123 +#: src/view/shell/desktop/RightNav.tsx:124 msgid "Terms" msgstr "" @@ -9323,7 +9392,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391 -#: src/view/com/profile/ProfileMenu.tsx:492 +#: src/view/com/profile/ProfileMenu.tsx:536 msgid "The account will be able to interact with you after unblocking." msgstr "" @@ -9377,6 +9446,10 @@ msgstr "" msgid "The following labels were applied to your content." msgstr "" +#: src/components/live/GoLiveDialog.tsx:157 +msgid "The following services are enabled for your account: {allowedServices}" +msgstr "" + #: src/screens/ModerationInteractionSettings/index.tsx:42 msgid "The following settings will be used as your defaults when creating new posts. You can edit these for a specific post from the composer." msgstr "" @@ -9465,7 +9538,7 @@ msgstr "" msgid "There was an issue fetching notifications. Tap here to try again." msgstr "" -#: src/screens/Search/Explore.tsx:992 +#: src/screens/Search/Explore.tsx:999 #: src/view/com/posts/PostFeed.tsx:759 msgid "There was an issue fetching posts. Tap here to try again." msgstr "" @@ -9506,12 +9579,12 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:253 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:279 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:294 -#: src/view/com/profile/ProfileMenu.tsx:139 -#: src/view/com/profile/ProfileMenu.tsx:149 -#: src/view/com/profile/ProfileMenu.tsx:163 -#: src/view/com/profile/ProfileMenu.tsx:173 -#: src/view/com/profile/ProfileMenu.tsx:186 +#: src/view/com/profile/ProfileMenu.tsx:151 +#: src/view/com/profile/ProfileMenu.tsx:161 +#: src/view/com/profile/ProfileMenu.tsx:175 +#: src/view/com/profile/ProfileMenu.tsx:185 #: src/view/com/profile/ProfileMenu.tsx:198 +#: src/view/com/profile/ProfileMenu.tsx:210 msgid "There was an issue! {0}" msgstr "" @@ -9671,8 +9744,8 @@ msgstr "" msgid "This is an empty state" msgstr "" -#: src/components/live/EditLiveDialog.tsx:189 -#: src/components/live/GoLiveDialog.tsx:157 +#: src/components/live/EditLiveDialog.tsx:176 +#: src/components/live/GoLiveDialog.tsx:150 msgid "This is not a valid link" msgstr "" @@ -9732,7 +9805,7 @@ msgstr "" msgid "This post's author has disabled quote posts." msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:513 +#: src/view/com/profile/ProfileMenu.tsx:557 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." msgstr "" @@ -9744,6 +9817,10 @@ msgstr "" msgid "This service has not provided terms of service or a privacy policy." msgstr "" +#: src/components/live/queries.ts:38 +msgid "This service is not supported while the Live feature is in beta. Allowed services: {formatted}." +msgstr "" + #: src/screens/Settings/components/ChangeHandleDialog.tsx:467 msgid "This should create a domain record at:" msgstr "" @@ -9806,8 +9883,8 @@ msgstr "" msgid "Thread options" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:68 -#: src/screens/Settings/ContentAndMediaSettings.tsx:71 +#: src/screens/Settings/ContentAndMediaSettings.tsx:69 +#: src/screens/Settings/ContentAndMediaSettings.tsx:72 msgid "Thread preferences" msgstr "" @@ -10001,7 +10078,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394 #: src/screens/ProfileList/components/Header.tsx:171 #: src/screens/ProfileList/components/Header.tsx:178 -#: src/view/com/profile/ProfileMenu.tsx:504 +#: src/view/com/profile/ProfileMenu.tsx:548 msgid "Unblock" msgstr "" @@ -10012,14 +10089,14 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:261 #: src/components/dms/ConvoMenu.tsx:264 -#: src/view/com/profile/ProfileMenu.tsx:409 -#: src/view/com/profile/ProfileMenu.tsx:415 +#: src/view/com/profile/ProfileMenu.tsx:453 +#: src/view/com/profile/ProfileMenu.tsx:459 msgid "Unblock account" msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389 -#: src/view/com/profile/ProfileMenu.tsx:486 +#: src/view/com/profile/ProfileMenu.tsx:530 msgid "Unblock Account?" msgstr "" @@ -10028,6 +10105,11 @@ msgstr "" msgid "Unblock list" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:72 +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:78 +msgid "Undo" +msgstr "" + #: src/components/PostControls/BookmarkButton.tsx:42 msgctxt "Button label to undo saving/removing a post from saved posts." msgid "Undo" @@ -10047,8 +10129,8 @@ msgstr "" msgid "Unfollow {0}" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:292 -#: src/view/com/profile/ProfileMenu.tsx:302 +#: src/view/com/profile/ProfileMenu.tsx:309 +#: src/view/com/profile/ProfileMenu.tsx:319 msgid "Unfollow account" msgstr "" @@ -10107,8 +10189,8 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:685 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:691 -#: src/view/com/profile/ProfileMenu.tsx:388 -#: src/view/com/profile/ProfileMenu.tsx:394 +#: src/view/com/profile/ProfileMenu.tsx:432 +#: src/view/com/profile/ProfileMenu.tsx:438 msgid "Unmute account" msgstr "" @@ -10141,7 +10223,7 @@ msgstr "" msgid "Unpin feed" msgstr "" -#: src/components/FeedCard.tsx:320 +#: src/components/FeedCard.tsx:352 msgid "Unpin Feed" msgstr "" @@ -10290,8 +10372,8 @@ msgstr "" msgid "Use in-app browser" msgstr "" -#: src/screens/Settings/ContentAndMediaSettings.tsx:102 -#: src/screens/Settings/ContentAndMediaSettings.tsx:108 +#: src/screens/Settings/ContentAndMediaSettings.tsx:103 +#: src/screens/Settings/ContentAndMediaSettings.tsx:109 msgid "Use in-app browser to open links" msgstr "" @@ -10416,8 +10498,8 @@ msgstr "" #: src/components/verification/VerificationCreatePrompt.tsx:84 #: src/components/verification/VerificationCreatePrompt.tsx:86 -#: src/view/com/profile/ProfileMenu.tsx:372 -#: src/view/com/profile/ProfileMenu.tsx:375 +#: src/view/com/profile/ProfileMenu.tsx:416 +#: src/view/com/profile/ProfileMenu.tsx:419 msgid "Verify account" msgstr "" @@ -10922,7 +11004,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:51 #: src/view/com/auth/SplashScreen.web.tsx:103 #: src/view/com/composer/Composer.tsx:857 -#: src/view/com/feeds/ComposerPrompt.tsx:191 +#: src/view/com/feeds/ComposerPrompt.tsx:189 msgid "What's up?" msgstr "" @@ -11025,8 +11107,8 @@ msgstr "" msgid "Wrong DID returned from server. Received: {0}" msgstr "" -#: src/components/live/EditLiveDialog.tsx:153 -#: src/components/live/GoLiveDialog.tsx:126 +#: src/components/live/EditLiveDialog.tsx:152 +#: src/components/live/GoLiveDialog.tsx:131 msgid "www.mylivestream.tv" msgstr "" @@ -11084,19 +11166,15 @@ msgstr "" msgid "You are in line." msgstr "" -#: src/components/live/EditLiveDialog.tsx:119 -#: src/components/live/EditLiveDialog.tsx:124 +#: src/components/live/EditLiveDialog.tsx:118 +#: src/components/live/EditLiveDialog.tsx:123 msgid "You are Live" msgstr "" -#: src/components/live/queries.ts:211 +#: src/components/live/queries.ts:212 msgid "You are no longer live" msgstr "" -#: src/components/live/queries.ts:35 -msgid "You are not allowed to go live" -msgstr "" - #: src/view/com/composer/state/video.ts:411 msgid "You are not allowed to upload videos." msgstr "" @@ -11105,7 +11183,7 @@ msgstr "" msgid "You are not following anyone yet" msgstr "" -#: src/components/live/queries.ts:156 +#: src/components/live/queries.ts:157 msgid "You are now live!" msgstr "" @@ -11597,8 +11675,8 @@ msgstr "" #: src/Navigation.tsx:526 #: src/screens/Search/modules/ExploreInterestsCard.tsx:67 -#: src/screens/Settings/ContentAndMediaSettings.tsx:92 -#: src/screens/Settings/ContentAndMediaSettings.tsx:95 +#: src/screens/Settings/ContentAndMediaSettings.tsx:93 +#: src/screens/Settings/ContentAndMediaSettings.tsx:96 #: src/screens/Settings/InterestsSettings.tsx:46 msgid "Your interests" msgstr "" @@ -11612,6 +11690,10 @@ msgstr "" msgid "Your interests help us find what you like!" msgstr "" +#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:68 +msgid "Your live event preferences have been updated." +msgstr "" + #: src/components/dialogs/DeviceLocationRequestDialog.tsx:123 msgid "Your location data is not tracked and does not leave your device." msgstr ""