From 1380c5ac67c937b84dd5d7172fbb660c45d0c87c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 10 Jun 2025 23:29:39 +0300 Subject: [PATCH 01/49] use ref instead of source (#8471) --- src/state/unstable-post-source.tsx | 31 ++++++++++-------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx index 1fb4af2872..43aac6f4d0 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -1,4 +1,4 @@ -import {createContext, useCallback, useContext, useState} from 'react' +import {createContext, useCallback, useContext, useRef, useState} from 'react' import {type AppBskyFeedDefs} from '@atproto/api' import {type FeedDescriptor} from './queries/post-feed' @@ -22,30 +22,19 @@ const ConsumeUnstablePostSourceContext = createContext< >(() => undefined) export function Provider({children}: {children: React.ReactNode}) { - const [sources, setSources] = useState>(() => new Map()) + const sourcesRef = useRef>(new Map()) const setUnstablePostSource = useCallback((key: string, source: Source) => { - setSources(prev => { - const newMap = new Map(prev) - newMap.set(key, source) - return newMap - }) + sourcesRef.current.set(key, source) }, []) - const consumeUnstablePostSource = useCallback( - (uri: string) => { - const source = sources.get(uri) - if (source) { - setSources(prev => { - const newMap = new Map(prev) - newMap.delete(uri) - return newMap - }) - } - return source - }, - [sources], - ) + const consumeUnstablePostSource = useCallback((uri: string) => { + const source = sourcesRef.current.get(uri) + if (source) { + sourcesRef.current.delete(uri) + } + return source + }, []) return ( From 269105371b22f2de6e8017862a783aaec340948b Mon Sep 17 00:00:00 2001 From: Francisco Nascimento Date: Wed, 11 Jun 2025 14:21:31 +0100 Subject: [PATCH 02/49] Instant Feed Update on Mute or Moderation Action (#8463) * Implemented #2406: Instant Feed Update on Mute or Moderation Action Posts from muted or blocked users are now removed immediately from the feed. This is achieved by extending the usePostShadow hook to check if the post author is muted or blocked and return POST_TOMBSTONE accordingly. A unit test was also added to validate the new logic. Co-authored-by: Pedro Macedo * remove useless tests --------- Co-authored-by: Pedro Macedo Co-authored-by: Samuel Newman --- src/state/cache/post-shadow.ts | 12 ++++++++++-- src/state/queries/profile.ts | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index 923e5c0000..3f9644879b 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -14,6 +14,7 @@ import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/qu import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' +import {useProfileShadow} from './profile-shadow' import {castAsShadow, type Shadow} from './types' export type {Shadow} from './types' @@ -43,6 +44,10 @@ export function usePostShadow( setShadow(shadows.get(post)) } + const authorShadow = useProfileShadow(post.author) + const wasMuted = !!authorShadow.viewer?.muted + const wasBlocked = !!authorShadow.viewer?.blocking + useEffect(() => { function onUpdate() { setShadow(shadows.get(post)) @@ -54,15 +59,18 @@ export function usePostShadow( }, [post, setShadow]) return useMemo(() => { + if (wasMuted || wasBlocked) { + return POST_TOMBSTONE + } if (shadow) { return mergeShadow(post, shadow) } else { return castAsShadow(post) } - }, [post, shadow]) + }, [post, shadow, wasMuted, wasBlocked]) } -function mergeShadow( +export function mergeShadow( post: AppBskyFeedDefs.PostView, shadow: Partial, ): Shadow | typeof POST_TOMBSTONE { diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index eb65fef7c2..b0af57c4a7 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -499,9 +499,10 @@ function useProfileBlockMutation() { {subject: did, createdAt: new Date().toISOString()}, ) }, - onSuccess(_, {did}) { + onSuccess(data, {did}) { queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) resetProfilePostsQueries(queryClient, did, 1000) + updateProfileShadow(queryClient, did, {blockingUri: data.uri}) }, }) } @@ -523,6 +524,7 @@ function useProfileUnblockMutation() { }, onSuccess(_, {did}) { resetProfilePostsQueries(queryClient, did, 1000) + updateProfileShadow(queryClient, did, {blockingUri: undefined}) }, }) } From 7341294df6156afdf24ab43e1e27ebba94f265ad Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 11 Jun 2025 20:12:05 +0300 Subject: [PATCH 03/49] Fix using screen names in `Link` (#8473) * use our router in favour of useLinkBuilder * test feature using Home header feeds button * handle non-string params properly --- src/components/Link.tsx | 8 +++----- src/lib/routes/router.ts | 8 ++++---- src/lib/routes/types.ts | 2 +- src/view/com/home/HomeHeaderLayoutMobile.tsx | 14 +++----------- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index d73a3db4ae..49c9c52358 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -4,7 +4,6 @@ import {sanitizeUrl} from '@braintree/sanitize-url' import { type LinkProps as RNLinkProps, StackActions, - useLinkBuilder, } from '@react-navigation/native' import {BSKY_DOWNLOAD_URL} from '#/lib/constants' @@ -95,20 +94,19 @@ export function useLink({ shouldProxy?: boolean }) { const navigation = useNavigationDeduped() - const {buildHref} = useLinkBuilder() const href = useMemo(() => { return typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to.screen - ? buildHref(to.screen, to.params) + ? router.matchName(to.screen)?.build(to.params) : to.href ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href)) : undefined - }, [to, buildHref]) + }, [to]) if (!href) { throw new Error( - 'Link `to` prop must be a string or an object with `screen` and `params` properties', + 'Could not resolve screen. Link `to` prop must be a string or an object with `screen` and `params` properties', ) } diff --git a/src/lib/routes/router.ts b/src/lib/routes/router.ts index 45f9c85fdb..ba76b1bdac 100644 --- a/src/lib/routes/router.ts +++ b/src/lib/routes/router.ts @@ -1,4 +1,4 @@ -import {Route, RouteParams} from './types' +import {type Route, type RouteParams} from './types' export class Router { routes: [string, Route][] = [] @@ -45,7 +45,7 @@ function createRoute(pattern: string): Route { }) const matcherRe = new RegExp(`^${matcherReInternal}([?]|$)`, 'i') return { - match(path: string) { + match(path) { const {pathname, searchParams} = new URL(path, 'http://throwaway.com') const addedParams = Object.fromEntries(searchParams.entries()) @@ -55,10 +55,10 @@ function createRoute(pattern: string): Route { } return undefined }, - build(params: Record) { + build(params = {}) { const str = pattern.replace( /:([\w]+)/g, - (_m, name) => params[name] || 'undefined', + (_m, name) => params[encodeURIComponent(name)] || 'undefined', ) let hasQp = false diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 6f102d438a..f587423900 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -143,5 +143,5 @@ export type RouteParams = Record export type MatchResult = {params: RouteParams} export type Route = { match: (path: string) => MatchResult | undefined - build: (params: RouteParams) => string + build: (params?: Record) => string } diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index e48c2cc893..7a40604f43 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' @@ -56,13 +55,8 @@ export function HomeHeaderLayoutMobile({ { - emitSoftReset() - }} - onPressIn={() => { - playHaptic('Heavy') - }} - onPressOut={() => { playHaptic('Light') + emitSoftReset() }}> @@ -72,7 +66,7 @@ export function HomeHeaderLayoutMobile({ {hasSession && ( From 143d5f3b814f1ce707fdfc87dabff7af5349bd06 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Jun 2025 13:22:02 -0500 Subject: [PATCH 04/49] Post source handling updates (#8472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add debugs * Key post-source using URI with handle * Enhance * EnHANCE * ENHANCE * ENHANCEEEECEE * ᵉⁿʰᵃⁿᶜᵉ * enhance --- src/App.native.tsx | 19 ++- src/App.web.tsx | 11 +- src/logger/types.ts | 2 + src/state/feed-feedback.tsx | 8 +- src/state/unstable-post-source.tsx | 121 +++++++++++++------- src/view/com/post-thread/PostThread.tsx | 30 ++++- src/view/com/post-thread/PostThreadItem.tsx | 34 +++--- src/view/com/posts/PostFeedItem.tsx | 8 +- 8 files changed, 148 insertions(+), 85 deletions(-) diff --git a/src/App.native.tsx b/src/App.native.tsx index e3f85c0fe5..baab8c8385 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -58,7 +58,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source' import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -151,16 +150,14 @@ function InnerApp() { - - - - - - - - - + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 97ada61485..c5ec0473ce 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -48,7 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -132,12 +131,10 @@ function InnerApp() { - - - - - - + + + + diff --git a/src/logger/types.ts b/src/logger/types.ts index d14e21a9d4..88d8d9d93d 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -10,6 +10,8 @@ export enum LogContext { ConversationAgent = 'conversation-agent', DMsAgent = 'dms-agent', ReportDialog = 'report-dialog', + FeedFeedback = 'feed-feedback', + PostSource = 'post-source', /** * METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 225b495d3f..a718a761d5 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -12,7 +12,7 @@ import throttle from 'lodash.throttle' import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' -import {logger} from '#/logger' +import {Logger} from '#/logger' import { type FeedDescriptor, type FeedPostSliceItem, @@ -20,6 +20,8 @@ import { import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {useAgent} from './session' +const logger = Logger.create(Logger.Context.FeedFeedback) + export type StateContext = { enabled: boolean onItemSeen: (item: any) => void @@ -89,6 +91,7 @@ export function useFeedFeedback( } sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) throttledFlushAggregatedStats() + logger.debug('flushed') }, [agent, throttledFlushAggregatedStats, feed]) const sendToFeed = useMemo( @@ -141,6 +144,9 @@ export function useFeedFeedback( if (!enabled) { return } + logger.debug('sendInteraction', { + ...interaction, + }) if (!history.current.has(interaction)) { history.current.add(interaction) queue.current.add(toString(interaction)) diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx index 43aac6f4d0..ac126d79c6 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -1,62 +1,97 @@ -import {createContext, useCallback, useContext, useRef, useState} from 'react' -import {type AppBskyFeedDefs} from '@atproto/api' +import {useEffect, useId, useState} from 'react' +import {type AppBskyFeedDefs, AtUri} from '@atproto/api' -import {type FeedDescriptor} from './queries/post-feed' +import {Logger} from '#/logger' +import {type FeedDescriptor} from '#/state/queries/post-feed' /** - * For passing the source of the post (i.e. the original post, from the feed) to the threadview, - * without using query params. Deliberately unstable to avoid using query params, use for FeedFeedback - * and other ephemeral non-critical systems. + * Separate logger for better debugging */ +const logger = Logger.create(Logger.Context.PostSource) -type Source = { +export type PostSource = { post: AppBskyFeedDefs.FeedViewPost feed?: FeedDescriptor } -const SetUnstablePostSourceContext = createContext< - (key: string, source: Source) => void ->(() => {}) -const ConsumeUnstablePostSourceContext = createContext< - (uri: string) => Source | undefined ->(() => undefined) +/** + * A cache of sources that will be consumed by the post thread view. This is + * cleaned up any time a source is consumed. + */ +const transientSources = new Map() -export function Provider({children}: {children: React.ReactNode}) { - const sourcesRef = useRef>(new Map()) +/** + * A cache of sources that have been consumed by the post thread view. This is + * not cleaned up, but because we use a new ID for each post thread view that + * consumes a source, this is never reused unless a user navigates back to a + * post thread view that has not been dropped from memory. + */ +const consumedSources = new Map() - const setUnstablePostSource = useCallback((key: string, source: Source) => { - sourcesRef.current.set(key, source) - }, []) - - const consumeUnstablePostSource = useCallback((uri: string) => { - const source = sourcesRef.current.get(uri) - if (source) { - sourcesRef.current.delete(uri) - } - return source - }, []) - - return ( - - - {children} - - +/** + * For stashing the feed that the user was browsing when they clicked on a post. + * + * Used for FeedFeedback and other ephemeral non-critical systems. + */ +export function setUnstablePostSource(key: string, source: PostSource) { + assertValid( + key, + `setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, ) -} - -export function useSetUnstablePostSource() { - return useContext(SetUnstablePostSourceContext) + logger.debug('set', {key, source}) + transientSources.set(key, source) } /** - * DANGER - This hook is unstable and should only be used for FeedFeedback - * and other ephemeral non-critical systems. Does not change when the URI changes. + * This hook is unstable and should only be used for FeedFeedback and other + * ephemeral non-critical systems. Views that use this hook will continue to + * return a reference to the same source until those views are dropped from + * memory. */ -export function useUnstablePostSource(uri: string) { - const consume = useContext(ConsumeUnstablePostSourceContext) +export function useUnstablePostSource(key: string) { + const id = useId() + const [source] = useState(() => { + assertValid( + key, + `consumeUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, + ) + const source = consumedSources.get(id) || transientSources.get(key) + if (source) { + logger.debug('consume', {id, key, source}) + transientSources.delete(key) + consumedSources.set(id, source) + } + return source + }) + + useEffect(() => { + return () => { + consumedSources.delete(id) + logger.debug('cleanup', {id}) + } + }, [id]) - const [source] = useState(() => consume(uri)) return source } + +/** + * Builds a post source key. This (atm) is a URI where the `host` is the post + * author's handle, not DID. + */ +export function buildPostSourceKey(key: string, handle: string) { + const urip = new AtUri(key) + urip.host = handle + return urip.toString() +} + +/** + * Just a lil dev helper + */ +function assertValid(key: string, message: string) { + if (__DEV__) { + const urip = new AtUri(key) + if (urip.host.startsWith('did:')) { + throw new Error(message) + } + } +} diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index d974ce6b53..5bec9ced1a 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -22,6 +22,7 @@ import {ScrollProvider} from '#/lib/ScrollContext' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {useFeedFeedback} from '#/state/feed-feedback' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { fillThreadModerationCache, @@ -37,6 +38,7 @@ import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {useUnstablePostSource} from '#/state/unstable-post-source' import {List, type ListMethods} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' @@ -93,7 +95,7 @@ const keyExtractor = (item: RowItem) => { return item._reactKey } -export function PostThread({uri}: {uri: string | undefined}) { +export function PostThread({uri}: {uri: string}) { const {hasSession, currentAccount} = useSession() const {_} = useLingui() const t = useTheme() @@ -104,6 +106,8 @@ export function PostThread({uri}: {uri: string | undefined}) { HiddenRepliesState.Hide, ) const headerRef = React.useRef(null) + const anchorPostSource = useUnstablePostSource(uri) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const {data: preferences} = usePreferencesQuery() const { @@ -395,10 +399,18 @@ export function PostThread({uri}: {uri: string | undefined}) { ) const {openComposer} = useOpenComposer() - const onPressReply = React.useCallback(() => { + const onReplyToAnchor = React.useCallback(() => { if (thread?.type !== 'post') { return } + if (anchorPostSource) { + feedFeedback.sendInteraction({ + item: thread.post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, + }) + } openComposer({ replyTo: { uri: thread.post.uri, @@ -410,7 +422,14 @@ export function PostThread({uri}: {uri: string | undefined}) { }, onPost: onPostReply, }) - }, [openComposer, thread, onPostReply, threadModerationCache]) + }, [ + openComposer, + thread, + onPostReply, + threadModerationCache, + anchorPostSource, + feedFeedback, + ]) const canReply = !error && rootPost && !rootPost.viewer?.replyDisabled const hasParents = @@ -423,7 +442,7 @@ export function PostThread({uri}: {uri: string | undefined}) { return ( {!isMobile && ( - + )} ) @@ -511,6 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) { } onPostReply={onPostReply} hideTopBorder={index === 0 && !item.ctx.isParentLoading} + anchorPostSource={anchorPostSource} /> ) @@ -586,7 +606,7 @@ export function PostThread({uri}: {uri: string | undefined}) { /> {isMobile && canReply && hasSession && ( - + )} ) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8b39072ba6..576b195a06 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -40,7 +40,7 @@ import {useLanguagePrefs} from '#/state/preferences' import {type ThreadPost} from '#/state/queries/post-thread' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {useUnstablePostSource} from '#/state/unstable-post-source' +import {type PostSource} from '#/state/unstable-post-source' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' @@ -87,6 +87,7 @@ export function PostThreadItem({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record @@ -104,6 +105,7 @@ export function PostThreadItem({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: PostSource }) { const postShadowed = usePostShadow(post) const richText = useMemo( @@ -139,6 +141,7 @@ export function PostThreadItem({ onPostReply={onPostReply} hideTopBorder={hideTopBorder} threadgateRecord={threadgateRecord} + anchorPostSource={anchorPostSource} /> ) } @@ -184,6 +187,7 @@ let PostThreadItemLoaded = ({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: Shadow record: AppBskyFeedPost.Record @@ -202,10 +206,10 @@ let PostThreadItemLoaded = ({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: PostSource }): React.ReactNode => { const {currentAccount, hasSession} = useSession() - const source = useUnstablePostSource(post.uri) - const feedFeedback = useFeedFeedback(source?.feed, hasSession) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const t = useTheme() const pal = usePalette('default') @@ -276,12 +280,12 @@ let PostThreadItemLoaded = ({ ) const onPressReply = () => { - if (source) { + if (anchorPostSource && isHighlightedPost) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionReply', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } openComposer({ @@ -298,23 +302,23 @@ let PostThreadItemLoaded = ({ } const onOpenAuthor = () => { - if (source) { + if (anchorPostSource) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#clickthroughAuthor', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } } const onOpenEmbed = () => { - if (source) { + if (anchorPostSource) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#clickthroughEmbed', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } } @@ -325,7 +329,7 @@ let PostThreadItemLoaded = ({ const {isActive: live} = useActorStatus(post.author) - const reason = source?.post.reason + const reason = anchorPostSource?.post.reason const viaRepost = useMemo(() => { if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { return { @@ -550,8 +554,8 @@ let PostThreadItemLoaded = ({ onPostReply={onPostReply} logContext="PostThreadItem" threadgateRecord={threadgateRecord} - feedContext={source?.post?.feedContext} - reqId={source?.post?.reqId} + feedContext={anchorPostSource?.post?.feedContext} + reqId={anchorPostSource?.post?.reqId} viaRepost={viaRepost} /> diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index b9aa676737..fd0d1c707d 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -36,7 +36,10 @@ import {useFeedFeedbackContext} from '#/state/feed-feedback' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {useSetUnstablePostSource} from '#/state/unstable-post-source' +import { + buildPostSourceKey, + setUnstablePostSource, +} from '#/state/unstable-post-source' import {FeedNameText} from '#/view/com/util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' @@ -176,7 +179,6 @@ let FeedItemInner = ({ return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() - const unstableSetPostSource = useSetUnstablePostSource() const onPressReply = () => { sendInteraction({ @@ -232,7 +234,7 @@ let FeedItemInner = ({ reqId, }) unstableCacheProfileView(queryClient, post.author) - unstableSetPostSource(post.uri, { + setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { feed: feedDescriptor, post: { post, From 61004b887b0c7515837e051144b694fc7db5a1cc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Jun 2025 14:32:14 -0500 Subject: [PATCH 05/49] [Threads V2] Preliminary integration of unspecced V2 APIs (#8443) * WIP * Sorting working * Rough handling of hidden/muted * Better muted/hidden sorting and handling * Clarify some naming * Fix parents * Handle first reply under highlighted/composer * WIP RaW * WIP optimistic * Optimistic WIP * Little cleanup, inserting dupes * Re-org * Add in new optimistic insert logic * Update types * Sorta working linear view optimistic state * Simple working version, no pref for OP * Working optimistic reply insertions, preference for OP * Ensure deletes are coming through * WIP scroll handling * WIP scroll tweaks * Clean up scrolling * Clean up onPostSuccess * Add annotations * Fix highlighted post calc * WIP kill me * Update APIs * Nvm don't kill me * Fix optimistic insert * Handle read more cases in tree view * Basically working read more * Handle linear view * Reorg * More reorg * Split up thread post components * New reply tree layout * Fix up traversal metadata * Tighten some spacing * Use indent ya idiot * Some linear mode cleanup * Fix lines on read more items * Vibe coding to success * Almost there with read mores * Update APIs * Bump sdk * Update import * Checkpoint new traversal * Checkpoint cleanup * Checkpoint, need to fix blocked posts * Checkpoint: think we're good, needs more cleanup * Clean it up * Two passes only * Set to default params, update comment * Fix render bug on native * Checkpoint parent rendering, can opt for slower handling here * Clean up parent handling, reply handling * Fix read more extra space * Fix read more in linear view * Fix hidden reply handling, seen count, before/after calc * Update naming * Rename Slice to ThreadItem * Add basic post and anchor skeletons * Refactor client-side hidden * WIP hidden fetching * Update types * Clean up query a bit * Scrolling still broken * Ok maybe fix scrolling * Checkpoint move state into meta query * Don't load remote hidden items unless needed * skeleton view * Reset hidden items when params change * Split up traversal and avoid multiple passes * Clean up * Checkpoint: handling exhausted replies * Clean up traversal functions further * Clean up pagination * Limit optimistic reply depth * Handle optimistic insert in hidden replies * Share root query key for easier cache extraction * Make blurred posts not look like ass * Fix double deleted item * Make optimistic deleted state not look like crap in tree view * Fix parents traversal 4 real * Rename tree post * Make optimistic deletions of linear posts not look bad * Rename linear post components * Handle tombstone views * Rename read more component * Add moreParents handling * Align interaction states of read more * Fix read more on FF * Tree view skeleton * Reply composer skele * Remove hack for showing more replies * Checkpoint: sort change scrolling fixed * Checkpoint: learned new things, reset to base * Feature gate * Rename * Replace show more * Update settings screen * Update pkg and endpoint * Remove console * Eureka * Cleanup last commit * No tests atm * Remove scroll provider * Clean up callbacks, better error state * Remove todo * Remove todo * Remove todos * Format * Ok I think scrolling is solid * Add back mobile compose input * Ok need to compute headerHeight every time * Update comments * Ok button up web too * Threads v2 tweaks (#8467) * fix error screen collapsing * use personx icon for blocked posts * Remove height/width * Revert unused Header change * Clarify code * Relate consts to theme values * Remove debug code * Typo * Fix debounce of threads prefs * Update metadata comments, dev mode * Missed a spot * Clean up todo * Fix up no-unauthenticated posts * Truncate parents if no-unauth * Update getBranch docs * Remove debug code * Expand fetching in some cases * Clear scroll need for root post to fix jump bug * Fix reply composer skeleton state * Remove uneeded initialized value * Add profile shadow cache * Some metrics * prettier tweak * eslint ignore * Fix optimistic insertion * Typo * Rename, comment * Remove wait * Counter naming * Replies seen counter for moderated sub-trees * Remove borders on skeleton * Align tombstone with optimistic deletion state * Fix optimistic deletion for thread * Add tree view icon * Rename * Cleanup * Update settings copy * Header menu open metric * Bump package * Better reply prompt (#8474) * restyle reply prompt * hide bottom bar border for cleaner look * use new border hiding hook in DMs * create `transparentifyColor` function * adjust padding * fix padding in immersive lpayer * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Integrate post-source (cherry picked from commit fe053e9b38395a4fcb30a4367bc800f64ea84fe9) --------- Co-authored-by: Samuel Newman Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- ...arrowTopCircle_stroke2_corner0_rounded.svg | 1 + .../circlePlus_stroke2_corner0_rounded.svg | 1 + assets/icons/tree_stroke2_corner0_rounded.svg | 1 + package.json | 2 +- src/App.native.tsx | 19 +- src/App.web.tsx | 11 +- src/alf/atoms.ts | 4 + src/alf/util/__tests__/colors.test.ts | 48 ++ src/alf/util/colorGeneration.ts | 28 + src/components/Skeleton.tsx | 107 +++ src/components/icons/ArrowTopCircle.tsx | 5 + src/components/icons/CirclePlus.tsx | 5 + src/components/icons/Tree.tsx | 5 + src/lib/async/retry.ts | 13 +- src/lib/hooks/useCallOnce.ts | 20 + src/lib/hooks/useHideBottomBarBorder.tsx | 50 ++ src/lib/statsig/gates.ts | 1 + src/logger/metrics.ts | 9 + .../Messages/components/MessagesList.tsx | 3 + .../PostThread/components/HeaderDropdown.tsx | 106 +++ .../PostThread/components/ThreadError.tsx | 89 +++ .../components/ThreadItemAnchor.tsx | 706 ++++++++++++++++++ .../ThreadItemAnchorNoUnauthenticated.tsx | 32 + .../PostThread/components/ThreadItemPost.tsx | 405 ++++++++++ .../ThreadItemPostNoUnauthenticated.tsx | 74 ++ .../components/ThreadItemPostTombstone.tsx | 55 ++ .../components/ThreadItemReadMore.tsx | 107 +++ .../components/ThreadItemReadMoreUp.tsx | 89 +++ .../components/ThreadItemReplyComposer.tsx | 31 + .../components/ThreadItemShowOtherReplies.tsx | 59 ++ .../components/ThreadItemTreePost.tsx | 456 +++++++++++ src/screens/PostThread/const.ts | 7 + src/screens/PostThread/index.tsx | 577 ++++++++++++++ src/screens/Settings/ThreadPreferences.tsx | 136 +++- src/screens/VideoFeed/index.tsx | 5 +- src/state/cache/post-shadow.ts | 4 + src/state/cache/profile-shadow.ts | 2 + .../preferences/useThreadPreferences.ts | 179 +++++ src/state/queries/usePostThread/const.ts | 27 + src/state/queries/usePostThread/index.ts | 325 ++++++++ src/state/queries/usePostThread/queryCache.ts | 300 ++++++++ src/state/queries/usePostThread/traversal.ts | 539 +++++++++++++ src/state/queries/usePostThread/types.ts | 227 ++++++ src/state/queries/usePostThread/utils.ts | 170 +++++ src/state/queries/usePostThread/views.ts | 183 +++++ src/state/shell/composer/index.tsx | 9 + src/state/threadgate-hidden-replies.tsx | 14 + src/storage/hooks/dev-mode.ts | 14 + src/types/utils.ts | 5 + src/view/com/composer/Composer.tsx | 58 +- src/view/com/post-thread/PostThread.tsx | 44 +- .../post-thread/PostThreadComposePrompt.tsx | 76 +- src/view/com/post-thread/PostThreadItem.tsx | 7 + src/view/screens/PostThread.tsx | 18 +- src/view/shell/Composer.ios.tsx | 1 + src/view/shell/Composer.tsx | 1 + src/view/shell/Composer.web.tsx | 1 + src/view/shell/bottom-bar/BottomBar.tsx | 4 +- src/view/shell/bottom-bar/BottomBarWeb.tsx | 13 +- yarn.lock | 14 + 60 files changed, 5416 insertions(+), 86 deletions(-) create mode 100644 assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg create mode 100644 assets/icons/circlePlus_stroke2_corner0_rounded.svg create mode 100644 assets/icons/tree_stroke2_corner0_rounded.svg create mode 100644 src/alf/util/__tests__/colors.test.ts create mode 100644 src/components/Skeleton.tsx create mode 100644 src/components/icons/ArrowTopCircle.tsx create mode 100644 src/components/icons/CirclePlus.tsx create mode 100644 src/components/icons/Tree.tsx create mode 100644 src/lib/hooks/useCallOnce.ts create mode 100644 src/lib/hooks/useHideBottomBarBorder.tsx create mode 100644 src/screens/PostThread/components/HeaderDropdown.tsx create mode 100644 src/screens/PostThread/components/ThreadError.tsx create mode 100644 src/screens/PostThread/components/ThreadItemAnchor.tsx create mode 100644 src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx create mode 100644 src/screens/PostThread/components/ThreadItemPost.tsx create mode 100644 src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx create mode 100644 src/screens/PostThread/components/ThreadItemPostTombstone.tsx create mode 100644 src/screens/PostThread/components/ThreadItemReadMore.tsx create mode 100644 src/screens/PostThread/components/ThreadItemReadMoreUp.tsx create mode 100644 src/screens/PostThread/components/ThreadItemReplyComposer.tsx create mode 100644 src/screens/PostThread/components/ThreadItemShowOtherReplies.tsx create mode 100644 src/screens/PostThread/components/ThreadItemTreePost.tsx create mode 100644 src/screens/PostThread/const.ts create mode 100644 src/screens/PostThread/index.tsx create mode 100644 src/state/queries/preferences/useThreadPreferences.ts create mode 100644 src/state/queries/usePostThread/const.ts create mode 100644 src/state/queries/usePostThread/index.ts create mode 100644 src/state/queries/usePostThread/queryCache.ts create mode 100644 src/state/queries/usePostThread/traversal.ts create mode 100644 src/state/queries/usePostThread/types.ts create mode 100644 src/state/queries/usePostThread/utils.ts create mode 100644 src/state/queries/usePostThread/views.ts create mode 100644 src/types/utils.ts diff --git a/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg b/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e34b5eb38f --- /dev/null +++ b/assets/icons/arrowTopCircle_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circlePlus_stroke2_corner0_rounded.svg b/assets/icons/circlePlus_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..aa517a2240 --- /dev/null +++ b/assets/icons/circlePlus_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/tree_stroke2_corner0_rounded.svg b/assets/icons/tree_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b8488c8bef --- /dev/null +++ b/assets/icons/tree_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index ac2171a220..f0bc5bbe75 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.9", + "@atproto/api": "^0.15.14", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/App.native.tsx b/src/App.native.tsx index baab8c8385..25d186dcfb 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -72,6 +72,7 @@ import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' SplashScreen.preventAutoHideAsync() if (isIOS) { @@ -150,14 +151,16 @@ function InnerApp() { - - - - - - - + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index c5ec0473ce..fa8e24e53d 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -61,6 +61,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' /** * Begin geolocation ASAP @@ -131,10 +132,12 @@ function InnerApp() { - - - - + + + + + + diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 02ad98c5f0..79ec41679a 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -1051,4 +1051,8 @@ export const atoms = { transform: [], }, }) as {transform: Exclude}, + + pointer: web({ + cursor: 'pointer', + }), } as const diff --git a/src/alf/util/__tests__/colors.test.ts b/src/alf/util/__tests__/colors.test.ts new file mode 100644 index 0000000000..350b6ff4a4 --- /dev/null +++ b/src/alf/util/__tests__/colors.test.ts @@ -0,0 +1,48 @@ +import {jest} from '@jest/globals' + +import {logger} from '#/logger' +import {transparentifyColor} from '../colorGeneration' + +jest.mock('#/logger', () => ({ + logger: {warn: jest.fn()}, +})) + +describe('transparentifyColor', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('converts hsl() to hsla()', () => { + const result = transparentifyColor('hsl(120 100% 50%)', 0.5) + expect(result).toBe('hsla(120 100% 50%, 0.5)') + }) + + it('converts hsl() to hsla() - fully transparent', () => { + const result = transparentifyColor('hsl(120 100% 50%)', 0) + expect(result).toBe('hsla(120 100% 50%, 0)') + }) + + it('converts rgb() to rgba()', () => { + const result = transparentifyColor('rgb(255 0 0)', 0.75) + expect(result).toBe('rgba(255 0 0, 0.75)') + }) + + it('expands 3-digit hex and appends alpha channel', () => { + const result = transparentifyColor('#abc', 0.4) + expect(result).toBe('#aabbcc66') + }) + + it('appends alpha to 6-digit hex', () => { + const result = transparentifyColor('#aabbcc', 0.4) + expect(result).toBe('#aabbcc66') + }) + + it('returns the original string and warns for unsupported formats', () => { + const unsupported = 'blue' + const result = transparentifyColor(unsupported, 0.5) + expect(result).toBe(unsupported) + expect(logger.warn).toHaveBeenCalledWith( + `Could not make '${unsupported}' transparent`, + ) + }) +}) diff --git a/src/alf/util/colorGeneration.ts b/src/alf/util/colorGeneration.ts index 8d769b51b1..574ab0a496 100644 --- a/src/alf/util/colorGeneration.ts +++ b/src/alf/util/colorGeneration.ts @@ -1,3 +1,5 @@ +import {logger} from '#/logger' + export const BLUE_HUE = 211 export const RED_HUE = 346 export const GREEN_HUE = 152 @@ -19,3 +21,29 @@ export function generateScale(start: number, end: number) { export const defaultScale = generateScale(6, 100) // dim shifted 6% lighter export const dimScale = generateScale(12, 100) + +export function transparentifyColor(color: string, alpha: number) { + if (color.startsWith('hsl(')) { + return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})` + } else if (color.startsWith('rgb(')) { + return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})` + } else if (color.startsWith('#')) { + if (color.length === 7) { + const alphaHex = Math.round(alpha * 255).toString(16) + // Per MDN: If there is only one number, it is duplicated: e means ee + // https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color + return color.slice(0, 7) + alphaHex.padStart(2, alphaHex) + } else if (color.length === 4) { + // convert to 6-digit hex before adding alpha + const [r, g, b] = color.slice(1).split('') + const alphaHex = Math.round(alpha * 255).toString(16) + return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart( + 2, + alphaHex, + )}` + } + } else { + logger.warn(`Could not make '${color}' transparent`) + } + return color +} diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx new file mode 100644 index 0000000000..14c3177c54 --- /dev/null +++ b/src/components/Skeleton.tsx @@ -0,0 +1,107 @@ +import {type ReactNode} from 'react' +import {View} from 'react-native' + +import { + atoms as a, + flatten, + type TextStyleProp, + useAlf, + useTheme, + type ViewStyleProp, +} from '#/alf' +import {normalizeTextStyles} from '#/alf/typography' + +type SkeletonProps = { + blend?: boolean +} + +export function Text({blend, style}: TextStyleProp & SkeletonProps) { + const {fonts, flags, theme: t} = useAlf() + const {width, ...flattened} = flatten(style) + const {lineHeight = 14, ...rest} = normalizeTextStyles( + [a.text_sm, a.leading_snug, flattened], + { + fontScale: fonts.scaleMultiplier, + fontFamily: fonts.family, + flags, + }, + ) + return ( + + + + ) +} + +export function Circle({ + children, + size, + blend, + style, +}: ViewStyleProp & {children?: ReactNode; size: number} & SkeletonProps) { + const t = useTheme() + return ( + + {children} + + ) +} + +export function Pill({ + size, + blend, + style, +}: ViewStyleProp & {size: number} & SkeletonProps) { + const t = useTheme() + return ( + + ) +} + +export function Col({ + children, + style, +}: ViewStyleProp & {children?: React.ReactNode}) { + return {children} +} + +export function Row({ + children, + style, +}: ViewStyleProp & {children?: React.ReactNode}) { + return {children} +} diff --git a/src/components/icons/ArrowTopCircle.tsx b/src/components/icons/ArrowTopCircle.tsx new file mode 100644 index 0000000000..2d250367f3 --- /dev/null +++ b/src/components/icons/ArrowTopCircle.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ArrowTopCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z', +}) diff --git a/src/components/icons/CirclePlus.tsx b/src/components/icons/CirclePlus.tsx new file mode 100644 index 0000000000..690e77326e --- /dev/null +++ b/src/components/icons/CirclePlus.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CirclePlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/Tree.tsx b/src/components/icons/Tree.tsx new file mode 100644 index 0000000000..5c2c798720 --- /dev/null +++ b/src/components/icons/Tree.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Tree_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 2a2.998 2.998 0 0 1 1 5.825V8a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a3.98 3.98 0 0 1-2-.537V16a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a4 4 0 0 1-4-4V7.825A2.998 2.998 0 0 1 6 2Zm7 16a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm0-8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5ZM6 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z', +}) diff --git a/src/lib/async/retry.ts b/src/lib/async/retry.ts index abf78de55f..8a1729091d 100644 --- a/src/lib/async/retry.ts +++ b/src/lib/async/retry.ts @@ -1,17 +1,22 @@ +import {timeout} from '#/lib/async/timeout' import {isNetworkError} from '#/lib/strings/errors' export async function retry

( retries: number, - cond: (err: any) => boolean, - fn: () => Promise

, + shouldRetry: (err: any) => boolean, + action: () => Promise

, + delay?: number, ): Promise

{ let lastErr while (retries > 0) { try { - return await fn() + return await action() } catch (e: any) { lastErr = e - if (cond(e)) { + if (shouldRetry(e)) { + if (delay) { + await timeout(delay) + } retries-- continue } diff --git a/src/lib/hooks/useCallOnce.ts b/src/lib/hooks/useCallOnce.ts new file mode 100644 index 0000000000..fa01cf4aa3 --- /dev/null +++ b/src/lib/hooks/useCallOnce.ts @@ -0,0 +1,20 @@ +import {useCallback} from 'react' + +export enum OnceKey { + PreferencesThread = 'preferences:thread', +} + +const called: Record = { + [OnceKey.PreferencesThread]: false, +} + +export function useCallOnce(key: OnceKey) { + return useCallback( + (cb: () => void) => { + if (called[key] === true) return + called[key] = true + cb() + }, + [key], + ) +} diff --git a/src/lib/hooks/useHideBottomBarBorder.tsx b/src/lib/hooks/useHideBottomBarBorder.tsx new file mode 100644 index 0000000000..e21184fda4 --- /dev/null +++ b/src/lib/hooks/useHideBottomBarBorder.tsx @@ -0,0 +1,50 @@ +import {createContext, useCallback, useContext, useState} from 'react' +import {useFocusEffect} from '@react-navigation/native' + +type HideBottomBarBorderSetter = () => () => void + +const HideBottomBarBorderContext = createContext(false) +const HideBottomBarBorderSetterContext = + createContext(null) + +export function useHideBottomBarBorderSetter() { + const hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext) + if (!hideBottomBarBorder) { + throw new Error( + 'useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider', + ) + } + return hideBottomBarBorder +} + +export function useHideBottomBarBorderForScreen() { + const hideBorder = useHideBottomBarBorderSetter() + + useFocusEffect( + useCallback(() => { + const cleanup = hideBorder() + return () => cleanup() + }, [hideBorder]), + ) +} + +export function useHideBottomBarBorder() { + return useContext(HideBottomBarBorderContext) +} + +export function Provider({children}: {children: React.ReactNode}) { + const [refCount, setRefCount] = useState(0) + + const setter = useCallback(() => { + setRefCount(prev => prev + 1) + return () => setRefCount(prev => prev - 1) + }, []) + + return ( + + 0}> + {children} + + + ) +} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index c67bb60a3a..3b1106480d 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -6,6 +6,7 @@ export type Gate = | 'explore_show_suggested_feeds' | 'old_postonboarding' | 'onboarding_add_video_feed' + | 'post_threads_v2_unspecced' | 'remove_show_latest_button' | 'test_gate_1' | 'test_gate_2' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index d01a92825b..31af1be2b0 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -434,4 +434,13 @@ export type MetricEvents = { 'share:press:dmSelected': {} 'share:press:recentDm': {} 'share:press:embed': {} + + 'thread:click:showOtherReplies': {} + 'thread:preferences:load': { + [key: string]: any + } + 'thread:preferences:update': { + [key: string]: any + } + 'thread:click:headerMenuOpen': {} } diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index ce33ca3aa9..c84371f2c0 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -16,6 +16,7 @@ import { RichText, } from '@atproto/api' +import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' import {ScrollProvider} from '#/lib/ScrollContext' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import { @@ -106,6 +107,8 @@ export function MessagesList({ const getPost = useGetPost() const {embedUri, setEmbed} = useMessageEmbed() + useHideBottomBarBorderForScreen() + const flatListRef = useAnimatedRef() const [newMessagesPill, setNewMessagesPill] = useState({ diff --git a/src/screens/PostThread/components/HeaderDropdown.tsx b/src/screens/PostThread/components/HeaderDropdown.tsx new file mode 100644 index 0000000000..def3979b78 --- /dev/null +++ b/src/screens/PostThread/components/HeaderDropdown.tsx @@ -0,0 +1,106 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {HITSLOP_10} from '#/lib/constants' +import {logger} from '#/logger' +import {type ThreadPreferences} from '#/state/queries/preferences/useThreadPreferences' +import {Button, ButtonIcon} from '#/components/Button' +import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' +import * as Menu from '#/components/Menu' + +export function HeaderDropdown({ + sort, + view, + setSort, + setView, +}: Pick< + ThreadPreferences, + 'sort' | 'setSort' | 'view' | 'setView' +>): React.ReactNode { + const {_} = useLingui() + return ( + + + {({props: {onPress, ...props}}) => ( + + )} + + + + Show replies as + + + { + setView('linear') + }}> + + Linear + + + + { + setView('tree') + }}> + + Threaded + + + + + + + Reply sorting + + + { + setSort('top') + }}> + + Top replies first + + + + { + setSort('oldest') + }}> + + Oldest replies first + + + + { + setSort('newest') + }}> + + Newest replies first + + + + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadError.tsx b/src/screens/PostThread/components/ThreadError.tsx new file mode 100644 index 0000000000..e1ca23cf97 --- /dev/null +++ b/src/screens/PostThread/components/ThreadError.tsx @@ -0,0 +1,89 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {OUTER_SPACE} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise' +import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' + +export function ThreadError({ + error, + onRetry, +}: { + error: Error + onRetry: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const cleanError = useCleanError() + + const {title, message} = useMemo(() => { + let title = _(msg`Error loading post`) + let message = _(msg`Something went wrong. Please try again in a moment.`) + + const {raw, clean} = cleanError(error) + + if (error.message.startsWith('Post not found')) { + title = _(msg`Post not found`) + message = clean || raw || message + } + + return {title, message} + }, [_, error, cleanError]) + + return ( + + + + + + {title} + + + {message} + + + + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx new file mode 100644 index 0000000000..0aacd4e771 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -0,0 +1,706 @@ +import {memo, useCallback, useMemo} from 'react' +import {type GestureResponderEvent, Text as RNText, View} from 'react-native' +import { + AppBskyFeedDefs, + AppBskyFeedPost, + type AppBskyFeedThreadgate, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' +import {msg, Plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useActorStatus} from '#/lib/actor-status' +import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {useOpenLink} from '#/lib/hooks/useOpenLink' +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {niceDate} from '#/lib/strings/time' +import {s} from '#/lib/styles' +import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers' +import {logger} from '#/logger' +import { + POST_TOMBSTONE, + type Shadow, + usePostShadow, +} from '#/state/cache/post-shadow' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' +import {useLanguagePrefs} from '#/state/preferences' +import {type ThreadItem} from '#/state/queries/usePostThread/types' +import {useSession} from '#/state/session' +import {type OnPostSuccessData} from '#/state/shell/composer' +import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {type PostSource} from '#/state/unstable-post-source' +import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' +import {Link} from '#/view/com/util/Link' +import {formatCount} from '#/view/com/util/numeric/format' +import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import { + LINEAR_AVI_WIDTH, + OUTER_SPACE, + REPLY_LINE_WIDTH, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {colors} from '#/components/Admonition' +import {Button} from '#/components/Button' +import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import {InlineLinkText} from '#/components/Link' +import {ContentHider} from '#/components/moderation/ContentHider' +import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {type AppModerationCause} from '#/components/Pills' +import {PostControls} from '#/components/PostControls' +import * as Prompt from '#/components/Prompt' +import {RichText} from '#/components/RichText' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' +import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' +import {WhoCanReply} from '#/components/WhoCanReply' +import * as bsky from '#/types/bsky' + +export function ThreadItemAnchor({ + item, + onPostSuccess, + threadgateRecord, + postSource, +}: { + item: Extract + onPostSuccess?: (data: OnPostSuccessData) => void + threadgateRecord?: AppBskyFeedThreadgate.Record + postSource?: PostSource +}) { + const postShadow = usePostShadow(item.value.post) + const threadRootUri = item.value.post.record.reply?.root?.uri || item.uri + const isRoot = threadRootUri === item.uri + + if (postShadow === POST_TOMBSTONE) { + return + } + + return ( + + ) +} + +function ThreadItemAnchorDeleted({isRoot}: {isRoot: boolean}) { + const t = useTheme() + + return ( + <> + + + + + + + + + Post has been deleted + + + + + ) +} + +function ThreadItemAnchorParentReplyLine({isRoot}: {isRoot: boolean}) { + const t = useTheme() + + return !isRoot ? ( + + + + + + ) : null +} + +const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ + item, + isRoot, + postShadow, + onPostSuccess, + threadgateRecord, + postSource, +}: { + item: Extract + isRoot: boolean + postShadow: Shadow + onPostSuccess?: (data: OnPostSuccessData) => void + threadgateRecord?: AppBskyFeedThreadgate.Record + postSource?: PostSource +}) { + const t = useTheme() + const {_, i18n} = useLingui() + const {openComposer} = useOpenComposer() + const {currentAccount, hasSession} = useSession() + const feedFeedback = useFeedFeedback(postSource?.feed, hasSession) + + const post = item.value.post + const record = item.value.post.record + const moderation = item.moderation + const authorShadow = useProfileShadow(post.author) + const {isActive: live} = useActorStatus(post.author) + const richText = useMemo( + () => + new RichTextAPI({ + text: record.text, + facets: record.facets, + }), + [record], + ) + + const threadRootUri = record.reply?.root?.uri || post.uri + const authorHref = makeProfileLink(post.author) + const authorTitle = post.author.handle + const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did + + const likesHref = useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') + }, [post.uri, post.author]) + const repostsHref = useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') + }, [post.uri, post.author]) + const quotesHref = useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey, 'quotes') + }, [post.uri, post.author]) + + const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord, + }) + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { + const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) + const isControlledByViewer = + new AtUri(threadRootUri).host === currentAccount?.did + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: {type: 'user', did: currentAccount?.did}, + priority: 6, + }, + ] + : [] + }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) + const onlyFollowersCanReply = !!threadgateRecord?.allow?.find( + rule => rule.$type === 'app.bsky.feed.threadgate#followerRule', + ) + const showFollowButton = + currentAccount?.did !== post.author.did && !onlyFollowersCanReply + + const viaRepost = useMemo(() => { + const reason = postSource?.post.reason + + if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { + return { + uri: reason.uri, + cid: reason.cid, + } + } + }, [postSource]) + + const onPressReply = useCallback(() => { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation, + }, + onPostSuccess: onPostSuccess, + }) + + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }) + } + }, [ + openComposer, + post, + record, + onPostSuccess, + moderation, + postSource, + feedFeedback, + ]) + + const onOpenAuthor = () => { + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughAuthor', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }) + } + } + + const onOpenEmbed = () => { + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughEmbed', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }) + } + } + + return ( + <> + + + + + + + + + + {sanitizeDisplayName( + post.author.displayName || + sanitizeHandle(post.author.handle), + moderation.ui('displayName'), + )} + + + + + + + + + + {sanitizeHandle(post.author.handle, '@')} + + + + {showFollowButton && ( + + + + )} + + + + + + {richText?.text ? ( + + ) : undefined} + {post.embed && ( + + + + )} + + + {post.repostCount !== 0 || + post.likeCount !== 0 || + post.quoteCount !== 0 ? ( + // Show this section unless we're *sure* it has no engagement. + + {post.repostCount != null && post.repostCount !== 0 ? ( + + + + {formatCount(i18n, post.repostCount)} + {' '} + + + + ) : null} + {post.quoteCount != null && + post.quoteCount !== 0 && + !post.viewer?.embeddingDisabled ? ( + + + + {formatCount(i18n, post.quoteCount)} + {' '} + + + + ) : null} + {post.likeCount != null && post.likeCount !== 0 ? ( + + + + {formatCount(i18n, post.likeCount)} + {' '} + + + + ) : null} + + ) : null} + + + + + + + + + ) +}) + +function ExpandedPostDetails({ + post, + isThreadAuthor, +}: { + post: Extract['value']['post'] + isThreadAuthor: boolean +}) { + const t = useTheme() + const {_, i18n} = useLingui() + const openLink = useOpenLink() + const langPrefs = useLanguagePrefs() + + const translatorUrl = getTranslatorLink( + post.record?.text || '', + langPrefs.primaryLanguage, + ) + const needsTranslation = useMemo( + () => + Boolean( + langPrefs.primaryLanguage && + !isPostInLanguage(post, [langPrefs.primaryLanguage]), + ), + [post, langPrefs.primaryLanguage], + ) + + const onTranslatePress = useCallback( + (e: GestureResponderEvent) => { + e.preventDefault() + openLink(translatorUrl, true) + + if ( + bsky.dangerousIsType( + post.record, + AppBskyFeedPost.isRecord, + ) + ) { + logger.metric('translate', { + sourceLanguages: post.record.langs ?? [], + targetLanguage: langPrefs.primaryLanguage, + textLength: post.record.text.length, + }) + } + + return false + }, + [openLink, translatorUrl, langPrefs, post], + ) + + return ( + + + + + {niceDate(i18n, post.indexedAt)} + + + {needsTranslation && ( + <> + + · + + + + Translate + + + )} + + + ) +} + +function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { + const t = useTheme() + const {_, i18n} = useLingui() + const control = Prompt.usePromptControl() + + const indexedAt = new Date(post.indexedAt) + const createdAt = bsky.dangerousIsType( + post.record, + AppBskyFeedPost.isRecord, + ) + ? new Date(post.record.createdAt) + : new Date(post.indexedAt) + + // backdated if createdAt is 24 hours or more before indexedAt + const isBackdated = + indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000 + + if (!isBackdated) return null + + const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light + + return ( + <> + + + + + Archived post + + + + This post claims to have been created on{' '} + {niceDate(i18n, createdAt)}, + but was first seen by Bluesky on{' '} + {niceDate(i18n, indexedAt)}. + + + + + Bluesky cannot confirm the authenticity of the claimed date. + + + + {}} /> + + + + ) +} + +function getThreadAuthor( + post: AppBskyFeedDefs.PostView, + record: AppBskyFeedPost.Record, +): string { + if (!record.reply) { + return post.author.did + } + try { + return new AtUri(record.reply.root.uri).host + } catch { + return '' + } +} + +export function ThreadItemAnchorSkeleton() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx b/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx new file mode 100644 index 0000000000..c8477e211f --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx @@ -0,0 +1,32 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {atoms as a, useTheme} from '#/alf' +import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' + +export function ThreadItemAnchorNoUnauthenticated() { + const t = useTheme() + + return ( + + + + + + + + + + + + + + + You must sign in to view this post. + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx new file mode 100644 index 0000000000..1f63b10cd9 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -0,0 +1,405 @@ +import {memo, type ReactNode, useCallback, useMemo, useState} from 'react' +import {View} from 'react-native' +import { + type AppBskyFeedDefs, + type AppBskyFeedThreadgate, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useActorStatus} from '#/lib/actor-status' +import {MAX_POST_LINES} from '#/lib/constants' +import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {countLines} from '#/lib/strings/helpers' +import { + POST_TOMBSTONE, + type Shadow, + usePostShadow, +} from '#/state/cache/post-shadow' +import {type ThreadItem} from '#/state/queries/usePostThread/types' +import {useSession} from '#/state/session' +import {type OnPostSuccessData} from '#/state/shell/composer' +import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {TextLink} from '#/view/com/util/Link' +import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' +import {PostMeta} from '#/view/com/util/PostMeta' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import { + LINEAR_AVI_WIDTH, + OUTER_SPACE, + REPLY_LINE_WIDTH, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {PostHider} from '#/components/moderation/PostHider' +import {type AppModerationCause} from '#/components/Pills' +import {PostControls} from '#/components/PostControls' +import {RichText} from '#/components/RichText' +import * as Skele from '#/components/Skeleton' +import {SubtleWebHover} from '#/components/SubtleWebHover' +import {Text} from '#/components/Typography' + +export type ThreadItemPostProps = { + item: Extract + overrides?: { + moderation?: boolean + topBorder?: boolean + } + onPostSuccess?: (data: OnPostSuccessData) => void + threadgateRecord?: AppBskyFeedThreadgate.Record +} + +export function ThreadItemPost({ + item, + overrides, + onPostSuccess, + threadgateRecord, +}: ThreadItemPostProps) { + const postShadow = usePostShadow(item.value.post) + + if (postShadow === POST_TOMBSTONE) { + return + } + + return ( + + ) +} + +function ThreadItemPostDeleted({ + item, + overrides, +}: Pick) { + const t = useTheme() + + return ( + + + + + + + + + Post has been deleted + + + + + + ) +} + +const ThreadItemPostOuterWrapper = memo(function ThreadItemPostOuterWrapper({ + item, + overrides, + children, +}: Pick & { + children: ReactNode +}) { + const t = useTheme() + const showTopBorder = + !item.ui.showParentReplyLine && overrides?.topBorder !== true + + return ( + + {children} + + ) +}) + +/** + * Provides some space between posts as well as contains the reply line + */ +const ThreadItemPostParentReplyLine = memo( + function ThreadItemPostParentReplyLine({ + item, + }: Pick) { + const t = useTheme() + return ( + + + {item.ui.showParentReplyLine && ( + + )} + + + ) + }, +) + +const ThreadItemPostInner = memo(function ThreadItemPostInner({ + item, + postShadow, + overrides, + onPostSuccess, + threadgateRecord, +}: ThreadItemPostProps & { + postShadow: Shadow +}) { + const t = useTheme() + const pal = usePalette('default') + const {_} = useLingui() + const {openComposer} = useOpenComposer() + const {currentAccount} = useSession() + + const post = item.value.post + const record = item.value.post.record + const moderation = item.moderation + const richText = useMemo( + () => + new RichTextAPI({ + text: record.text, + facets: record.facets, + }), + [record], + ) + const [limitLines, setLimitLines] = useState( + () => countLines(richText?.text) >= MAX_POST_LINES, + ) + const threadRootUri = record.reply?.root?.uri || post.uri + const postHref = useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey) + }, [post.uri, post.author]) + const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord, + }) + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { + const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) + const isControlledByViewer = + new AtUri(threadRootUri).host === currentAccount?.did + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: {type: 'user', did: currentAccount?.did}, + priority: 6, + }, + ] + : [] + }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) + + const onPressReply = useCallback(() => { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation, + }, + onPostSuccess: onPostSuccess, + }) + }, [openComposer, post, record, onPostSuccess, moderation]) + + const onPressShowMore = useCallback(() => { + setLimitLines(false) + }, [setLimitLines]) + + const {isActive: live} = useActorStatus(post.author) + + return ( + + + + + + + + + + {(item.ui.showChildReplyLine || + item.ui.precedesChildReadMore) && ( + + )} + + + + + + + {richText?.text ? ( + + ) : undefined} + {limitLines ? ( + + ) : undefined} + {post.embed && ( + + + + )} + + + + + + + ) +}) + +function SubtleHover({children}: {children: ReactNode}) { + const { + state: hover, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + return ( + + + {children} + + ) +} + +export function ThreadItemPostSkeleton({index}: {index: number}) { + const even = index % 2 === 0 + return ( + + + + + + + + + + + + {even ? ( + <> + + + + ) : ( + + )} + + + + + + + + + + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx b/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx new file mode 100644 index 0000000000..552d8f813e --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx @@ -0,0 +1,74 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {type ThreadItem} from '#/state/queries/usePostThread/types' +import { + LINEAR_AVI_WIDTH, + OUTER_SPACE, + REPLY_LINE_WIDTH, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' + +export function ThreadItemPostNoUnauthenticated({ + item, +}: { + item: Extract +}) { + const t = useTheme() + + return ( + + + + {item.ui.showParentReplyLine && ( + + )} + + + + + + + + + You must sign in to view this post. + + + + {item.ui.showChildReplyLine && ( + + )} + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemPostTombstone.tsx b/src/screens/PostThread/components/ThreadItemPostTombstone.tsx new file mode 100644 index 0000000000..4f1ab450bb --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPostTombstone.tsx @@ -0,0 +1,55 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {LINEAR_AVI_WIDTH, OUTER_SPACE} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import {Text} from '#/components/Typography' + +export type ThreadItemPostTombstoneProps = { + type: 'not-found' | 'blocked' +} + +export function ThreadItemPostTombstone({type}: ThreadItemPostTombstoneProps) { + const t = useTheme() + const {_} = useLingui() + const {copy, Icon} = useMemo(() => { + switch (type) { + case 'blocked': + return {copy: _(msg`Post blocked`), Icon: PersonXIcon} + case 'not-found': + default: + return {copy: _(msg`Post not found`), Icon: TrashIcon} + } + }, [_, type]) + + return ( + + + + + + + {copy} + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemReadMore.tsx b/src/screens/PostThread/components/ThreadItemReadMore.tsx new file mode 100644 index 0000000000..22ae633951 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMore.tsx @@ -0,0 +1,107 @@ +import {memo} from 'react' +import {View} from 'react-native' +import {msg, Plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import { + type PostThreadParams, + type ThreadItem, +} from '#/state/queries/usePostThread' +import { + LINEAR_AVI_WIDTH, + REPLY_LINE_WIDTH, + TREE_AVI_WIDTH, + TREE_INDENT, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {CirclePlus_Stroke2_Corner0_Rounded as CirclePlus} from '#/components/icons/CirclePlus' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +export const ThreadItemReadMore = memo(function ThreadItemReadMore({ + item, + view, +}: { + item: Extract + view: PostThreadParams['view'] +}) { + const t = useTheme() + const {_} = useLingui() + const isTreeView = view === 'tree' + const indent = Math.max(0, item.depth - 1) + + const spacers = isTreeView + ? Array.from(Array(indent)).map((_, n: number) => { + const isSkipped = item.skippedIndentIndices.has(n) + return ( + + ) + }) + : null + + return ( + + {spacers} + + + {({hovered, pressed}) => { + const interacted = hovered || pressed + return ( + <> + + + + Read {item.moreReplies} more{' '} + + + + + ) + }} + + + ) +}) diff --git a/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx b/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx new file mode 100644 index 0000000000..da18a19e90 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMoreUp.tsx @@ -0,0 +1,89 @@ +import {memo} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {type ThreadItem} from '#/state/queries/usePostThread' +import { + LINEAR_AVI_WIDTH, + OUTER_SPACE, + REPLY_LINE_WIDTH, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {ArrowTopCircle_Stroke2_Corner0_Rounded as UpIcon} from '#/components/icons/ArrowTopCircle' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +export const ThreadItemReadMoreUp = memo(function ThreadItemReadMoreUp({ + item, +}: { + item: Extract +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + {({hovered, pressed}) => { + const interacted = hovered || pressed + return ( + + + + + + + Continue thread... + + + + + + + ) + }} + + ) +}) diff --git a/src/screens/PostThread/components/ThreadItemReplyComposer.tsx b/src/screens/PostThread/components/ThreadItemReplyComposer.tsx new file mode 100644 index 0000000000..f1862569ea --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReplyComposer.tsx @@ -0,0 +1,31 @@ +import {View} from 'react-native' + +import {OUTER_SPACE} from '#/screens/PostThread/const' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as Skele from '#/components/Skeleton' + +/* + * Wacky padding here is just replicating what we have in the actual + * `PostThreadComposePrompt` component + */ +export function ThreadItemReplyComposerSkeleton() { + const t = useTheme() + const {gtMobile} = useBreakpoints() + + return ( + + + + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemShowOtherReplies.tsx b/src/screens/PostThread/components/ThreadItemShowOtherReplies.tsx new file mode 100644 index 0000000000..e418375b65 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemShowOtherReplies.tsx @@ -0,0 +1,59 @@ +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {Text} from '#/components/Typography' + +export function ThreadItemShowOtherReplies({onPress}: {onPress: () => void}) { + const {_} = useLingui() + const t = useTheme() + const label = _(msg`Show more replies`) + + return ( + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx new file mode 100644 index 0000000000..d86d2ef6f7 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -0,0 +1,456 @@ +import React, {memo, useMemo} from 'react' +import {View} from 'react-native' +import { + type AppBskyFeedDefs, + type AppBskyFeedThreadgate, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {MAX_POST_LINES} from '#/lib/constants' +import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {countLines} from '#/lib/strings/helpers' +import { + POST_TOMBSTONE, + type Shadow, + usePostShadow, +} from '#/state/cache/post-shadow' +import {type ThreadItem} from '#/state/queries/usePostThread/types' +import {useSession} from '#/state/session' +import {type OnPostSuccessData} from '#/state/shell/composer' +import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {TextLink} from '#/view/com/util/Link' +import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' +import {PostMeta} from '#/view/com/util/PostMeta' +import { + OUTER_SPACE, + REPLY_LINE_WIDTH, + TREE_AVI_WIDTH, + TREE_INDENT, +} from '#/screens/PostThread/const' +import {atoms as a, useTheme} from '#/alf' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {PostHider} from '#/components/moderation/PostHider' +import {type AppModerationCause} from '#/components/Pills' +import {PostControls} from '#/components/PostControls' +import {RichText} from '#/components/RichText' +import * as Skele from '#/components/Skeleton' +import {SubtleWebHover} from '#/components/SubtleWebHover' +import {Text} from '#/components/Typography' + +/** + * Mimic the space in PostMeta + */ +const TREE_AVI_PLUS_SPACE = TREE_AVI_WIDTH + a.gap_xs.gap + +export function ThreadItemTreePost({ + item, + overrides, + onPostSuccess, + threadgateRecord, +}: { + item: Extract + overrides?: { + moderation?: boolean + topBorder?: boolean + } + onPostSuccess?: (data: OnPostSuccessData) => void + threadgateRecord?: AppBskyFeedThreadgate.Record +}) { + const postShadow = usePostShadow(item.value.post) + + if (postShadow === POST_TOMBSTONE) { + return + } + + return ( + + ) +} + +function ThreadItemTreePostDeleted({ + item, +}: { + item: Extract +}) { + const t = useTheme() + return ( + + + + + + Post has been deleted + + + {item.ui.isLastChild && !item.ui.precedesChildReadMore && ( + + )} + + + ) +} + +const ThreadItemTreePostOuterWrapper = memo( + function ThreadItemTreePostOuterWrapper({ + item, + children, + }: { + item: Extract + children: React.ReactNode + }) { + const t = useTheme() + const indents = Math.max(0, item.ui.indent - 1) + + return ( + + {Array.from(Array(indents)).map((_, n: number) => { + const isSkipped = item.ui.skippedIndentIndices.has(n) + return ( + + ) + })} + {children} + + ) + }, +) + +const ThreadItemTreePostInnerWrapper = memo( + function ThreadItemTreePostInnerWrapper({ + item, + children, + }: { + item: Extract + children: React.ReactNode + }) { + const t = useTheme() + return ( + + {item.ui.indent > 1 && ( + + )} + {children} + + ) + }, +) + +const ThreadItemTreeReplyChildReplyLine = memo( + function ThreadItemTreeReplyChildReplyLine({ + item, + }: { + item: Extract + }) { + const t = useTheme() + return ( + + {item.ui.showChildReplyLine && ( + + )} + + ) + }, +) + +const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ + item, + postShadow, + overrides, + onPostSuccess, + threadgateRecord, +}: { + item: Extract + postShadow: Shadow + overrides?: { + moderation?: boolean + topBorder?: boolean + } + onPostSuccess?: (data: OnPostSuccessData) => void + threadgateRecord?: AppBskyFeedThreadgate.Record +}): React.ReactNode { + const pal = usePalette('default') + const {_} = useLingui() + const {openComposer} = useOpenComposer() + const {currentAccount} = useSession() + + const post = item.value.post + const record = item.value.post.record + const moderation = item.moderation + const richText = useMemo( + () => + new RichTextAPI({ + text: record.text, + facets: record.facets, + }), + [record], + ) + const [limitLines, setLimitLines] = React.useState( + () => countLines(richText?.text) >= MAX_POST_LINES, + ) + const threadRootUri = record.reply?.root?.uri || post.uri + const postHref = React.useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey) + }, [post.uri, post.author]) + const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord, + }) + const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { + const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) + const isControlledByViewer = + new AtUri(threadRootUri).host === currentAccount?.did + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: {type: 'user', did: currentAccount?.did}, + priority: 6, + }, + ] + : [] + }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) + + const onPressReply = React.useCallback(() => { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation, + }, + onPostSuccess: onPostSuccess, + }) + }, [openComposer, post, record, onPostSuccess, moderation]) + + const onPressShowMore = React.useCallback(() => { + setLimitLines(false) + }, [setLimitLines]) + + return ( + + + + + + + + + + + + {richText?.text ? ( + + + + ) : undefined} + {limitLines ? ( + + ) : undefined} + {post.embed && ( + + + + )} + + + + + + + + + ) +}) + +function SubtleHover({children}: {children: React.ReactNode}) { + const { + state: hover, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + return ( + + + {children} + + ) +} + +export function ThreadItemTreePostSkeleton({index}: {index: number}) { + const t = useTheme() + const even = index % 2 === 0 + return ( + + + + + + + + + + + + {even ? ( + <> + + + + ) : ( + + )} + + + + + + + + + + + + + ) +} diff --git a/src/screens/PostThread/const.ts b/src/screens/PostThread/const.ts new file mode 100644 index 0000000000..cf559ac4e7 --- /dev/null +++ b/src/screens/PostThread/const.ts @@ -0,0 +1,7 @@ +import {tokens} from '#/alf' + +export const TREE_INDENT = tokens.space.lg +export const TREE_AVI_WIDTH = 24 +export const LINEAR_AVI_WIDTH = 42 +export const REPLY_LINE_WIDTH = 2 +export const OUTER_SPACE = tokens.space.lg diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx new file mode 100644 index 0000000000..a4f94851ad --- /dev/null +++ b/src/screens/PostThread/index.tsx @@ -0,0 +1,577 @@ +import {useCallback, useMemo, useRef, useState} from 'react' +import {useWindowDimensions, View} from 'react-native' +import Animated, {useAnimatedStyle} from 'react-native-reanimated' +import {Trans} from '@lingui/macro' + +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {useFeedFeedback} from '#/state/feed-feedback' +import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' +import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' +import {useSession} from '#/state/session' +import {type OnPostSuccessData} from '#/state/shell/composer' +import {useShellLayout} from '#/state/shell/shell-layout' +import {useUnstablePostSource} from '#/state/unstable-post-source' +import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' +import {List, type ListMethods} from '#/view/com/util/List' +import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown' +import {ThreadError} from '#/screens/PostThread/components/ThreadError' +import { + ThreadItemAnchor, + ThreadItemAnchorSkeleton, +} from '#/screens/PostThread/components/ThreadItemAnchor' +import {ThreadItemAnchorNoUnauthenticated} from '#/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated' +import { + ThreadItemPost, + ThreadItemPostSkeleton, +} from '#/screens/PostThread/components/ThreadItemPost' +import {ThreadItemPostNoUnauthenticated} from '#/screens/PostThread/components/ThreadItemPostNoUnauthenticated' +import {ThreadItemPostTombstone} from '#/screens/PostThread/components/ThreadItemPostTombstone' +import {ThreadItemReadMore} from '#/screens/PostThread/components/ThreadItemReadMore' +import {ThreadItemReadMoreUp} from '#/screens/PostThread/components/ThreadItemReadMoreUp' +import {ThreadItemReplyComposerSkeleton} from '#/screens/PostThread/components/ThreadItemReplyComposer' +import {ThreadItemShowOtherReplies} from '#/screens/PostThread/components/ThreadItemShowOtherReplies' +import { + ThreadItemTreePost, + ThreadItemTreePostSkeleton, +} from '#/screens/PostThread/components/ThreadItemTreePost' +import {atoms as a, native, platform, useBreakpoints, web} from '#/alf' +import * as Layout from '#/components/Layout' +import {ListFooter} from '#/components/Lists' + +const PARENT_CHUNK_SIZE = 5 +const CHILDREN_CHUNK_SIZE = 50 + +export function PostThread({uri}: {uri: string}) { + const {gtMobile} = useBreakpoints() + const {hasSession} = useSession() + const initialNumToRender = useInitialNumToRender() // TODO + const {height: windowHeight} = useWindowDimensions() + const anchorPostSource = useUnstablePostSource(uri) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) + + /* + * One query to rule them all + */ + const thread = usePostThread({anchor: uri}) + const anchor = useMemo(() => { + for (const item of thread.data.items) { + if (item.type === 'threadPost' && item.depth === 0) { + return item + } + } + return + }, [thread.data.items]) + + const {openComposer} = useOpenComposer() + const optimisticOnPostReply = useCallback( + (payload: OnPostSuccessData) => { + if (payload) { + const {replyToUri, posts} = payload + if (replyToUri && posts.length) { + thread.actions.insertReplies(replyToUri, posts) + } + } + }, + [thread], + ) + const onReplyToAnchor = useCallback(() => { + if (anchor?.type !== 'threadPost') { + return + } + const post = anchor.value.post + openComposer({ + replyTo: { + uri: anchor.uri, + cid: post.cid, + text: post.record.text, + author: post.author, + embed: post.embed, + moderation: anchor.moderation, + }, + onPostSuccess: optimisticOnPostReply, + }) + + if (anchorPostSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, + }) + } + }, [ + anchor, + openComposer, + optimisticOnPostReply, + anchorPostSource, + feedFeedback, + ]) + + const isRoot = !!anchor && anchor.value.post.record.reply === undefined + const canReply = !anchor?.value.post?.viewer?.replyDisabled + const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE) + const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE) + const totalParentCount = useRef(0) // recomputed below + const totalChildrenCount = useRef(thread.data.items.length) // recomputed below + const listRef = useRef(null) + const anchorRef = useRef(null) + const headerRef = useRef(null) + + /* + * On a cold load, parents are not prepended until the anchor post has + * rendered as the first item in the list. This gives us a consistent + * reference point for which to pin the anchor post to the top of the screen. + * + * We simulate a cold load any time the user changes the view or sort params + * so that this handling is consistent. + * + * On native, `maintainVisibleContentPosition={{minIndexForVisible: 0}}` gives + * us this for free, since the anchor post is the first item in the list. + * + * On web, `onContentSizeChange` is used to get ahead of next paint and handle + * this scrolling. + */ + const [deferParents, setDeferParents] = useState(true) + /** + * Used to flag whether we should scroll to the anchor post. On a cold load, + * this is always true. And when a user changes thread parameters, we also + * manually set this to true. + */ + const shouldHandleScroll = useRef(true) + /** + * Called any time the content size of the list changes, _just_ before paint. + * + * We want this to fire every time we change params (which will reset + * `deferParents` via `onLayout` on the anchor post, due to the key change), + * or click into a new post (which will result in a fresh `deferParents` + * hook). + * + * The result being: any intentional change in view by the user will result + * in the anchor being pinned as the first item. + */ + const onContentSizeChangeWebOnly = web(() => { + const list = listRef.current + const anchor = anchorRef.current as any as Element + const header = headerRef.current as any as Element + + if (list && anchor && header && shouldHandleScroll.current) { + const anchorOffsetTop = anchor.getBoundingClientRect().top + const headerHeight = header.getBoundingClientRect().height + + /* + * `deferParents` is `true` on a cold load, and always reset to + * `true` when params change via `prepareForParamsUpdate`. + * + * On a cold load or a push to a new post, on the first pass of this + * logic, the anchor post is the first item in the list. Therefore + * `anchorOffsetTop - headerHeight` will be 0. + * + * When a user changes thread params, on the first pass of this logic, + * the anchor post may not move (if there are no parents above it), or it + * may have gone off the screen above, because of the sudden lack of + * parents due to `deferParents === true`. This negative value (minus + * `headerHeight`) will result in a _negative_ `offset` value, which will + * scroll the anchor post _down_ to the top of the screen. + * + * However, `prepareForParamsUpdate` also resets scroll to `0`, so when a user + * changes params, the anchor post's offset will actually be equivalent + * to the `headerHeight` because of how the DOM is stacked on web. + * Therefore, `anchorOffsetTop - headerHeight` will once again be 0, + * which means the first pass in this case will result in no scroll. + * + * Then, once parents are prepended, this will fire again. Now, the + * `anchorOffsetTop` will be positive, which minus the header height, + * will give us a _positive_ offset, which will scroll the anchor post + * back _up_ to the top of the screen. + */ + list.scrollToOffset({ + offset: anchorOffsetTop - headerHeight, + }) + + /* + * After the second pass, `deferParents` will be `false`, and we need + * to ensure this doesn't run again until scroll handling is requested + * again via `shouldHandleScroll.current === true` and a params + * change via `prepareForParamsUpdate`. + * + * The `isRoot` here is needed because if we're looking at the anchor + * post, this handler will not fire after `deferParents` is set to + * `false`, since there are no parents to render above it. In this case, + * we want to make sure `shouldHandleScroll` is set to `false` so that + * subsequent size changes unrelated to a params change (like pagination) + * do not affect scroll. + */ + if (!deferParents || isRoot) shouldHandleScroll.current = false + } + }) + + /** + * Ditto the above, but for native. + */ + const onContentSizeChangeNativeOnly = native(() => { + const list = listRef.current + const anchor = anchorRef.current + + if (list && anchor && shouldHandleScroll.current) { + /* + * `prepareForParamsUpdate` is called any time the user changes thread params like + * `view` or `sort`, which sets `deferParents(true)` and resets the + * scroll to the top of the list. However, there is a split second + * where the top of the list is wherever the parents _just were_. So if + * there were parents, the anchor is not at the top of the list just + * prior to this handler being called. + * + * Once this handler is called, the anchor post is the first item in + * the list (because of `deferParents` being `true`), and so we can + * synchronously scroll the list back to the top of the list (which is + * 0 on native, no need to handle `headerHeight`). + */ + list.scrollToOffset({ + animated: false, + offset: 0, + }) + + /* + * After this first pass, `deferParents` will be `false`, and those + * will render in. However, the anchor post will retain its position + * because of `maintainVisibleContentPosition` handling on native. So we + * don't need to let this handler run again, like we do on web. + */ + shouldHandleScroll.current = false + } + }) + + /** + * Called any time the user changes thread params, such as `view` or `sort`. + * Prepares the UI for repositioning of the scroll so that the anchor post is + * always at the top after a params change. + * + * No need to handle max parents here, deferParents will handle that and we + * want it to re-render with the same items above the anchor. + */ + const prepareForParamsUpdate = useCallback(() => { + /** + * Truncate list so that anchor post is the first item in the list. Manual + * scroll handling on web is predicated on this, and on native, this allows + * `maintainVisibleContentPosition` to do its thing. + */ + setDeferParents(true) + // reset this to a lower value for faster re-render + setMaxChildrenCount(CHILDREN_CHUNK_SIZE) + // set flag + shouldHandleScroll.current = true + }, [setDeferParents, setMaxChildrenCount]) + + const setSortWrapped = useCallback( + (sort: string) => { + prepareForParamsUpdate() + thread.actions.setSort(sort) + }, + [thread, prepareForParamsUpdate], + ) + + const setViewWrapped = useCallback( + (view: ThreadViewOption) => { + prepareForParamsUpdate() + thread.actions.setView(view) + }, + [thread, prepareForParamsUpdate], + ) + + const onStartReached = () => { + if (thread.state.isFetching) return + // can be true after `prepareForParamsUpdate` is called + if (deferParents) return + // prevent any state mutations if we know we're done + if (maxParentCount >= totalParentCount.current) return + setMaxParentCount(n => n + PARENT_CHUNK_SIZE) + } + + const onEndReached = () => { + if (thread.state.isFetching) return + // can be true after `prepareForParamsUpdate` is called + if (deferParents) return + // prevent any state mutations if we know we're done + if (maxChildrenCount >= totalChildrenCount.current) return + setMaxChildrenCount(prev => prev + CHILDREN_CHUNK_SIZE) + } + + const slices = useMemo(() => { + const results: ThreadItem[] = [] + + if (!thread.data.items.length) return results + + /* + * Pagination hack, tracks the # of items below the anchor post. + */ + let childrenCount = 0 + + for (let i = 0; i < thread.data.items.length; i++) { + const item = thread.data.items[i] + /* + * Need to check `depth`, since not found or blocked posts are not + * `threadPost`s, but still have `depth`. + */ + const hasDepth = 'depth' in item + + /* + * Handle anchor post. + */ + if (hasDepth && item.depth === 0) { + results.push(item) + + // Recalculate total parents current index. + totalParentCount.current = i + // Recalculate total children using (length - 1) - current index. + totalChildrenCount.current = thread.data.items.length - 1 - i + + /* + * Walk up the parents, limiting by `maxParentCount` + */ + if (!deferParents) { + const start = i - 1 + if (start >= 0) { + const limit = Math.max(0, start - maxParentCount) + for (let pi = start; pi >= limit; pi--) { + results.unshift(thread.data.items[pi]) + } + } + } + } else { + // ignore any parent items + if (item.type === 'readMoreUp' || (hasDepth && item.depth < 0)) continue + // can exit early if we've reached the max children count + if (childrenCount > maxChildrenCount) break + + results.push(item) + childrenCount++ + } + } + + return results + }, [thread, deferParents, maxParentCount, maxChildrenCount]) + + const isTombstoneView = useMemo(() => { + if (slices.length > 1) return false + return slices.every( + s => s.type === 'threadPostBlocked' || s.type === 'threadPostNotFound', + ) + }, [slices]) + + const renderItem = useCallback( + ({item, index}: {item: ThreadItem; index: number}) => { + if (item.type === 'threadPost') { + if (item.depth < 0) { + return ( + + ) + } else if (item.depth === 0) { + return ( + /* + * Keep this view wrapped so that the anchor post is always index 0 + * in the list and `maintainVisibleContentPosition` can do its + * thing. + */ + + setDeferParents(false)} + /> + + + ) + } else { + if (thread.state.view === 'tree') { + return ( + 0, + }} + onPostSuccess={optimisticOnPostReply} + /> + ) + } else { + return ( + 0, + }} + onPostSuccess={optimisticOnPostReply} + /> + ) + } + } + } else if (item.type === 'threadPostNoUnauthenticated') { + if (item.depth < 0) { + return + } else if (item.depth === 0) { + return + } + } else if (item.type === 'readMore') { + return ( + + ) + } else if (item.type === 'readMoreUp') { + return + } else if (item.type === 'threadPostBlocked') { + return + } else if (item.type === 'threadPostNotFound') { + return + } else if (item.type === 'replyComposer') { + return ( + + {gtMobile && ( + + )} + + ) + } else if (item.type === 'showOtherReplies') { + return + } else if (item.type === 'skeleton') { + if (item.item === 'anchor') { + return + } else if (item.item === 'reply') { + if (thread.state.view === 'linear') { + return + } else { + return + } + } else if (item.item === 'replyComposer') { + return + } + } + return null + }, + [ + thread, + optimisticOnPostReply, + onReplyToAnchor, + gtMobile, + anchorPostSource, + ], + ) + + return ( + <> + + + + + Post + + + + + + + + {thread.state.error ? ( + + ) : ( + + } + initialNumToRender={initialNumToRender} + windowSize={11} + sideBorders={false} + /> + )} + + {!gtMobile && canReply && hasSession && ( + + )} + + ) +} + +function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) { + const {footerHeight} = useShellLayout() + + const animatedStyle = useAnimatedStyle(() => { + return { + bottom: footerHeight.get(), + } + }) + + return ( + + + + ) +} + +const keyExtractor = (item: ThreadItem) => { + return item.key +} diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx index 701d3d9e56..af3cf915f5 100644 --- a/src/screens/Settings/ThreadPreferences.tsx +++ b/src/screens/Settings/ThreadPreferences.tsx @@ -2,22 +2,156 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import { usePreferencesQuery, useSetThreadViewPreferencesMutation, } from '#/state/queries/preferences' +import { + normalizeSort, + normalizeView, + useThreadPreferences, +} from '#/state/queries/preferences/useThreadPreferences' import {atoms as a, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' +import {Tree_Stroke2_Corner0_Rounded as TreeIcon} from '#/components/icons/Tree' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' import * as SettingsList from './components/SettingsList' type Props = NativeStackScreenProps export function ThreadPreferencesScreen({}: Props) { + const gate = useGate() + + return gate('post_threads_v2_unspecced') ? ( + + ) : ( + + ) +} + +export function ThreadPreferencesV2() { + const t = useTheme() + const {_} = useLingui() + const { + sort, + setSort, + view, + setView, + prioritizeFollowedUsers, + setPrioritizeFollowedUsers, + } = useThreadPreferences({save: true}) + + return ( + + + + + + Thread Preferences + + + + + + + + + + Sort replies + + + + Sort replies to the same post by: + + setSort(normalizeSort(values[0]))}> + + + + + Top replies first + + + + + + Oldest replies first + + + + + + Newest replies first + + + + + + + + + + + Prioritize your Follows + + setPrioritizeFollowedUsers(value)} + style={[a.w_full, a.gap_md]}> + + + Show replies by people you follow before all other replies + + + + + + + + + + Tree view + + + setView(normalizeView({treeViewEnabled: value})) + } + style={[a.w_full, a.gap_md]}> + + Show post replies in a threaded tree view + + + + + + + + ) +} + +export function ThreadPreferencesV1() { const {_} = useLingui() const t = useTheme() diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 8a75751f72..495b3bc622 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -882,7 +882,10 @@ function Overlay({ player={player} seekingAnimationSV={seekingAnimationSV} scrollGesture={scrollGesture}> - + diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index 3f9644879b..90fddda2bd 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -14,6 +14,7 @@ import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/qu import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' +import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' import {useProfileShadow} from './profile-shadow' import {castAsShadow, type Shadow} from './types' export type {Shadow} from './types' @@ -157,6 +158,9 @@ function* findPostsInCache( yield node.post } } + for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { + yield post + } for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { yield post } diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index a1212d8a29..31bf55d132 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -21,6 +21,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery' +import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' import type * as bsky from '#/types/bsky' import {castAsShadow, type Shadow} from './types' @@ -167,6 +168,7 @@ function* findProfilesInCache( yield* findAllProfilesInListConvosQueryData(queryClient, did) yield* findAllProfilesInFeedsQueryData(queryClient, did) yield* findAllProfilesInPostThreadQueryData(queryClient, did) + yield* findAllProfilesInPostThreadV2QueryData(queryClient, did) yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did) } diff --git a/src/state/queries/preferences/useThreadPreferences.ts b/src/state/queries/preferences/useThreadPreferences.ts new file mode 100644 index 0000000000..dc3122a72a --- /dev/null +++ b/src/state/queries/preferences/useThreadPreferences.ts @@ -0,0 +1,179 @@ +import {useCallback, useMemo, useRef, useState} from 'react' +import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' +import debounce from 'lodash.debounce' + +import {OnceKey, useCallOnce} from '#/lib/hooks/useCallOnce' +import {logger} from '#/logger' +import { + usePreferencesQuery, + useSetThreadViewPreferencesMutation, +} from '#/state/queries/preferences' +import {type ThreadViewPreferences} from '#/state/queries/preferences/types' +import {type Literal} from '#/types/utils' + +export type ThreadSortOption = Literal< + AppBskyUnspeccedGetPostThreadV2.QueryParams['sort'], + string +> +export type ThreadViewOption = 'linear' | 'tree' +export type ThreadPreferences = { + isLoaded: boolean + isSaving: boolean + sort: ThreadSortOption + setSort: (sort: string) => void + view: ThreadViewOption + setView: (view: ThreadViewOption) => void + prioritizeFollowedUsers: boolean + setPrioritizeFollowedUsers: (prioritize: boolean) => void +} + +export function useThreadPreferences({ + save, +}: {save?: boolean} = {}): ThreadPreferences { + const {data: preferences} = usePreferencesQuery() + const serverPrefs = preferences?.threadViewPrefs + const once = useCallOnce(OnceKey.PreferencesThread) + + /* + * Create local state representations of server state + */ + const [sort, setSort] = useState(normalizeSort(serverPrefs?.sort || 'top')) + const [view, setView] = useState( + normalizeView({ + treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled, + }), + ) + const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( + !!serverPrefs?.prioritizeFollowedUsers, + ) + + /** + * If we get a server update, update local state + */ + const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs) + const isLoaded = !!prevServerPrefs + if (serverPrefs && prevServerPrefs !== serverPrefs) { + setPrevServerPrefs(serverPrefs) + + /* + * Update + */ + setSort(normalizeSort(serverPrefs.sort)) + setPrioritizeFollowedUsers(serverPrefs.prioritizeFollowedUsers) + setView( + normalizeView({ + treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, + }), + ) + + once(() => { + logger.metric('thread:preferences:load', { + sort: serverPrefs.sort, + view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', + prioritizeFollowedUsers: serverPrefs.prioritizeFollowedUsers, + }) + }) + } + + const userUpdatedPrefs = useRef(false) + const [isSaving, setIsSaving] = useState(false) + const {mutateAsync} = useSetThreadViewPreferencesMutation() + const savePrefs = useMemo(() => { + return debounce(async (prefs: ThreadViewPreferences) => { + try { + setIsSaving(true) + await mutateAsync(prefs) + logger.metric('thread:preferences:update', { + sort: prefs.sort, + view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', + prioritizeFollowedUsers: prefs.prioritizeFollowedUsers, + }) + } catch (e) { + logger.error('useThreadPreferences failed to save', { + safeMessage: e, + }) + } finally { + setIsSaving(false) + } + }, 4e3) + }, [mutateAsync]) + + if (save && userUpdatedPrefs.current) { + savePrefs({ + sort, + prioritizeFollowedUsers, + lab_treeViewEnabled: view === 'tree', + }) + userUpdatedPrefs.current = false + } + + const setSortWrapped = useCallback( + (next: string) => { + userUpdatedPrefs.current = true + setSort(normalizeSort(next)) + }, + [setSort], + ) + const setViewWrapped = useCallback( + (next: ThreadViewOption) => { + userUpdatedPrefs.current = true + setView(next) + }, + [setView], + ) + const setPrioritizeFollowedUsersWrapped = useCallback( + (next: boolean) => { + userUpdatedPrefs.current = true + setPrioritizeFollowedUsers(next) + }, + [setPrioritizeFollowedUsers], + ) + + return useMemo( + () => ({ + isLoaded, + isSaving, + sort, + setSort: setSortWrapped, + view, + setView: setViewWrapped, + prioritizeFollowedUsers, + setPrioritizeFollowedUsers: setPrioritizeFollowedUsersWrapped, + }), + [ + isLoaded, + isSaving, + sort, + setSortWrapped, + view, + setViewWrapped, + prioritizeFollowedUsers, + setPrioritizeFollowedUsersWrapped, + ], + ) +} + +/** + * Migrates user thread preferences from the old sort values to V2 + */ +export function normalizeSort(sort: string): ThreadSortOption { + switch (sort) { + case 'oldest': + return 'oldest' + case 'newest': + return 'newest' + default: + return 'top' + } +} + +/** + * Transforms existing treeViewEnabled preference into a ThreadViewOption + */ +export function normalizeView({ + treeViewEnabled, +}: { + treeViewEnabled: boolean +}): ThreadViewOption { + return treeViewEnabled ? 'tree' : 'linear' +} diff --git a/src/state/queries/usePostThread/const.ts b/src/state/queries/usePostThread/const.ts new file mode 100644 index 0000000000..9b74361307 --- /dev/null +++ b/src/state/queries/usePostThread/const.ts @@ -0,0 +1,27 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' + +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export const LINEAR_VIEW_BELOW = 10 + +/** + * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export const LINEAR_VIEW_BF = 1 + +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export const TREE_VIEW_BELOW = 4 + +/** + * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export const TREE_VIEW_BF = undefined + +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export const TREE_VIEW_BELOW_DESKTOP = 6 diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts new file mode 100644 index 0000000000..782888cfbe --- /dev/null +++ b/src/state/queries/usePostThread/index.ts @@ -0,0 +1,325 @@ +import {useCallback, useMemo, useState} from 'react' +import {useQuery, useQueryClient} from '@tanstack/react-query' + +import {isWeb} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences' +import { + LINEAR_VIEW_BELOW, + LINEAR_VIEW_BF, + TREE_VIEW_BELOW, + TREE_VIEW_BELOW_DESKTOP, + TREE_VIEW_BF, +} from '#/state/queries/usePostThread/const' +import { + createCacheMutator, + getThreadPlaceholder, +} from '#/state/queries/usePostThread/queryCache' +import { + buildThread, + sortAndAnnotateThreadItems, +} from '#/state/queries/usePostThread/traversal' +import { + createPostThreadOtherQueryKey, + createPostThreadQueryKey, + type ThreadItem, + type UsePostThreadQueryResult, +} from '#/state/queries/usePostThread/types' +import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' +import * as views from '#/state/queries/usePostThread/views' +import {useAgent, useSession} from '#/state/session' +import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {useBreakpoints} from '#/alf' + +export * from '#/state/queries/usePostThread/types' + +export function usePostThread({anchor}: {anchor?: string}) { + const qc = useQueryClient() + const agent = useAgent() + const {hasSession} = useSession() + const {gtPhone} = useBreakpoints() + const moderationOpts = useModerationOpts() + const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies() + const { + isLoaded: isThreadPreferencesLoaded, + sort, + setSort: baseSetSort, + view, + setView: baseSetView, + prioritizeFollowedUsers, + } = useThreadPreferences() + const below = useMemo(() => { + return view === 'linear' + ? LINEAR_VIEW_BELOW + : isWeb && gtPhone + ? TREE_VIEW_BELOW_DESKTOP + : TREE_VIEW_BELOW + }, [view, gtPhone]) + + const postThreadQueryKey = createPostThreadQueryKey({ + anchor, + sort, + view, + prioritizeFollowedUsers, + }) + const postThreadOtherQueryKey = createPostThreadOtherQueryKey({ + anchor, + prioritizeFollowedUsers, + }) + + const query = useQuery({ + enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, + queryKey: postThreadQueryKey, + async queryFn(ctx) { + const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ + anchor: anchor!, + branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, + below, + sort: sort, + prioritizeFollowedUsers: prioritizeFollowedUsers, + }) + + /* + * Initialize `ctx.meta` to track if we know we have additional replies + * we could fetch once we hit the end. + */ + ctx.meta = ctx.meta || { + hasOtherReplies: false, + } + + /* + * If we know we have additional replies, we'll set this to true. + */ + if (data.hasOtherReplies) { + ctx.meta.hasOtherReplies = true + } + + const result = { + thread: data.thread || [], + threadgate: data.threadgate, + hasOtherReplies: !!ctx.meta.hasOtherReplies, + } + + const record = getThreadgateRecord(result.threadgate) + if (result.threadgate && record) { + result.threadgate.record = record + } + + return result as UsePostThreadQueryResult + }, + placeholderData() { + if (!anchor) return + const placeholder = getThreadPlaceholder(qc, anchor) + /* + * Always return something here, even empty data, so that + * `isPlaceholderData` is always true, which we'll use to insert + * skeletons. + */ + const thread = placeholder ? [placeholder] : [] + return {thread, threadgate: undefined, hasOtherReplies: false} + }, + select(data) { + const record = getThreadgateRecord(data.threadgate) + if (data.threadgate && record) { + data.threadgate.record = record + } + return data + }, + }) + + const thread = useMemo(() => query.data?.thread || [], [query.data?.thread]) + const threadgate = useMemo( + () => query.data?.threadgate, + [query.data?.threadgate], + ) + const hasOtherThreadItems = useMemo( + () => !!query.data?.hasOtherReplies, + [query.data?.hasOtherReplies], + ) + const [otherItemsVisible, setOtherItemsVisible] = useState(false) + + /** + * Creates a mutator for the post thread cache. This is used to insert + * replies into the thread cache after posting. + */ + const mutator = useMemo( + () => + createCacheMutator({ + params: {view, below}, + postThreadQueryKey, + postThreadOtherQueryKey, + queryClient: qc, + }), + [qc, view, below, postThreadQueryKey, postThreadOtherQueryKey], + ) + + /** + * If we have additional items available from the server and the user has + * chosen to view them, start loading data + */ + const additionalQueryEnabled = hasOtherThreadItems && otherItemsVisible + const additionalItemsQuery = useQuery({ + enabled: additionalQueryEnabled, + queryKey: postThreadOtherQueryKey, + async queryFn() { + const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({ + anchor: anchor!, + prioritizeFollowedUsers, + }) + return data + }, + }) + const serverOtherThreadItems: ThreadItem[] = useMemo(() => { + if (!additionalQueryEnabled) return [] + if (additionalItemsQuery.isLoading) { + return Array.from({length: 2}).map((_, i) => + views.skeleton({ + key: `other-reply-${i}`, + item: 'reply', + }), + ) + } else if (additionalItemsQuery.isError) { + /* + * We could insert an special error component in here, but since these + * are optional additional replies, it's not critical that they're shown + * atm. + */ + return [] + } else if (additionalItemsQuery.data?.thread) { + const {threadItems} = sortAndAnnotateThreadItems( + additionalItemsQuery.data.thread, + { + view, + skipModerationHandling: true, + threadgateHiddenReplies: mergeThreadgateHiddenReplies( + threadgate?.record, + ), + moderationOpts: moderationOpts!, + }, + ) + return threadItems + } else { + return [] + } + }, [ + view, + additionalQueryEnabled, + additionalItemsQuery, + mergeThreadgateHiddenReplies, + moderationOpts, + threadgate?.record, + ]) + + /** + * Sets the sort order for the thread and resets the additional thread items + */ + const setSort: typeof baseSetSort = useCallback( + nextSort => { + setOtherItemsVisible(false) + baseSetSort(nextSort) + }, + [baseSetSort, setOtherItemsVisible], + ) + + /** + * Sets the view variant for the thread and resets the additional thread items + */ + const setView: typeof baseSetView = useCallback( + nextView => { + setOtherItemsVisible(false) + baseSetView(nextView) + }, + [baseSetView, setOtherItemsVisible], + ) + + /* + * This is the main thread response, sorted into separate buckets based on + * moderation, and annotated with all UI state needed for rendering. + */ + const {threadItems, otherThreadItems} = useMemo(() => { + return sortAndAnnotateThreadItems(thread, { + view: view, + threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record), + moderationOpts: moderationOpts!, + }) + }, [ + thread, + threadgate?.record, + mergeThreadgateHiddenReplies, + moderationOpts, + view, + ]) + + /* + * Take all three sets of thread items and combine them into a single thread, + * along with any other thread items required for rendering e.g. "Show more + * replies" or the reply composer. + */ + const items = useMemo(() => { + return buildThread({ + threadItems, + otherThreadItems, + serverOtherThreadItems, + isLoading: query.isPlaceholderData, + hasSession, + hasOtherThreadItems, + otherItemsVisible, + showOtherItems: () => setOtherItemsVisible(true), + }) + }, [ + threadItems, + otherThreadItems, + serverOtherThreadItems, + query.isPlaceholderData, + hasSession, + hasOtherThreadItems, + otherItemsVisible, + setOtherItemsVisible, + ]) + + return useMemo( + () => ({ + state: { + /* + * Copy in any query state that is useful + */ + isFetching: query.isFetching, + isPlaceholderData: query.isPlaceholderData, + error: query.error, + /* + * Other state + */ + sort, + view, + otherItemsVisible, + }, + data: { + items, + threadgate, + }, + actions: { + /* + * Copy in any query actions that are useful + */ + insertReplies: mutator.insertReplies, + refetch: query.refetch, + /* + * Other actions + */ + setSort, + setView, + }, + }), + [ + query, + mutator.insertReplies, + otherItemsVisible, + sort, + view, + setSort, + setView, + threadgate, + items, + ], + ) +} diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts new file mode 100644 index 0000000000..871033395f --- /dev/null +++ b/src/state/queries/usePostThread/queryCache.ts @@ -0,0 +1,300 @@ +import { + type $Typed, + type AppBskyActorDefs, + type AppBskyFeedDefs, + AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadOtherV2, + type AppBskyUnspeccedGetPostThreadV2, + AtUri, +} from '@atproto/api' +import {type QueryClient} from '@tanstack/react-query' + +import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' +import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' +import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' +import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' +import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' +import {getBranch} from '#/state/queries/usePostThread/traversal' +import { + type ApiThreadItem, + type createPostThreadOtherQueryKey, + type createPostThreadQueryKey, + type PostThreadParams, + postThreadQueryKeyRoot, +} from '#/state/queries/usePostThread/types' +import {getRootPostAtUri} from '#/state/queries/usePostThread/utils' +import {postViewToThreadPlaceholder} from '#/state/queries/usePostThread/views' +import {didOrHandleUriMatches, getEmbeddedPost} from '#/state/queries/util' +import {embedViewRecordToPostView} from '#/state/queries/util' + +export function createCacheMutator({ + queryClient, + postThreadQueryKey, + postThreadOtherQueryKey, + params, +}: { + queryClient: QueryClient + postThreadQueryKey: ReturnType + postThreadOtherQueryKey: ReturnType + params: Pick & {below: number} +}) { + return { + insertReplies( + parentUri: string, + replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], + ) { + /* + * Main thread query mutator. + */ + queryClient.setQueryData( + postThreadQueryKey, + data => { + if (!data) return + return { + ...data, + thread: mutator([ + ...data.thread, + ]), + } + }, + ) + + /* + * Additional replies query mutator. + */ + queryClient.setQueryData( + postThreadOtherQueryKey, + data => { + if (!data) return + return { + ...data, + thread: mutator([ + ...data.thread, + ]), + } + }, + ) + + function mutator(thread: ApiThreadItem[]): T[] { + for (let i = 0; i < thread.length; i++) { + const existingParent = thread[i] + if (!AppBskyUnspeccedDefs.isThreadItemPost(existingParent.value)) + continue + if (existingParent.uri !== parentUri) continue + + /* + * Update parent data + */ + existingParent.value.post = { + ...existingParent.value.post, + replyCount: (existingParent.value.post.replyCount || 0) + 1, + } + + const opDid = getRootPostAtUri(existingParent.value.post)?.host + const nextItem = thread.at(i + 1) + const isReplyToRoot = existingParent.depth === 0 + const isEndOfReplyChain = + !nextItem || nextItem.depth <= existingParent.depth + const firstReply = replies.at(0) + const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost( + firstReply?.value, + ) + ? opDid === firstReply.value.post.author.did + : false + + /* + * Always insert replies if the following conditions are met. + */ + const shouldAlwaysInsertReplies = + isReplyToRoot || + params.view === 'tree' || + (params.view === 'linear' && isEndOfReplyChain) + /* + * Maybe insert replies if the replier is the OP and certain conditions are met + */ + const shouldReplaceWithOPReplies = + !isReplyToRoot && params.view === 'linear' && opIsReplier + + if (shouldAlwaysInsertReplies || shouldReplaceWithOPReplies) { + const branch = getBranch(thread, i, existingParent.depth) + /* + * OP insertions replace other replies _in linear view_. + */ + const itemsToRemove = shouldReplaceWithOPReplies ? branch.length : 0 + const itemsToInsert = replies + .map((r, ri) => { + r.depth = existingParent.depth + 1 + ri + return r + }) + .filter(r => { + // Filter out replies that are too deep for our UI + return r.depth <= params.below + }) + + thread.splice(i + 1, itemsToRemove, ...itemsToInsert) + } + } + + return thread as T[] + } + }, + /** + * Unused atm, post shadow does the trick, but it would be nice to clean up + * the whole sub-tree on deletes. + */ + deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) { + queryClient.setQueryData( + postThreadQueryKey, + queryData => { + if (!queryData) return + + const thread = [...queryData.thread] + + for (let i = 0; i < thread.length; i++) { + const existingPost = thread[i] + if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue + + if (existingPost.uri === post.uri) { + const branch = getBranch(thread, i, existingPost.depth) + thread.splice(branch.start, branch.length) + break + } + } + + return { + ...queryData, + thread, + } + }, + ) + }, + } +} + +export function getThreadPlaceholder( + queryClient: QueryClient, + uri: string, +): $Typed | void { + let partial + for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { + /* + * Currently, the backend doesn't send full post info in some cases (for + * example, for quoted posts). We use missing `likeCount` as a way to + * detect that. In the future, we should fix this on the backend, which + * will let us always stop on the first result. + * + * TODO can we send in feeds and quotes? + */ + const hasAllInfo = item.value.post.likeCount != null + if (hasAllInfo) { + return item + } else { + // Keep searching, we might still find a full post in the cache. + partial = item + } + } + return partial +} + +export function* getThreadPlaceholderCandidates( + queryClient: QueryClient, + uri: string, +): Generator< + $Typed< + Omit & { + value: $Typed + } + >, + void +> { + /* + * Check post thread queries first + */ + for (const post of findAllPostsInQueryData(queryClient, uri)) { + yield postViewToThreadPlaceholder(post) + } + + /* + * Check notifications first. If you have a post in notifications, it's + * often due to a like or a repost, and we want to prioritize a post object + * with >0 likes/reposts over a stale version with no metrics in order to + * avoid a notification->post scroll jump. + */ + for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { + yield postViewToThreadPlaceholder(post) + } + for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { + yield postViewToThreadPlaceholder(post) + } + for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { + yield postViewToThreadPlaceholder(post) + } + for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { + yield postViewToThreadPlaceholder(post) + } + for (let post of findAllPostsInExploreFeedPreviewsQueryData( + queryClient, + uri, + )) { + yield postViewToThreadPlaceholder(post) + } +} + +export function* findAllPostsInQueryData( + queryClient: QueryClient, + uri: string, +): Generator { + const atUri = new AtUri(uri) + const queryDatas = + queryClient.getQueriesData({ + queryKey: [postThreadQueryKeyRoot], + }) + + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData) continue + + const {thread} = queryData + + for (const item of thread) { + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (didOrHandleUriMatches(atUri, item.value.post)) { + yield item.value.post + } + + const qp = getEmbeddedPost(item.value.post.embed) + if (qp && didOrHandleUriMatches(atUri, qp)) { + yield embedViewRecordToPostView(qp) + } + } + } + } +} + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = + queryClient.getQueriesData({ + queryKey: [postThreadQueryKeyRoot], + }) + + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData) continue + + const {thread} = queryData + + for (const item of thread) { + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (item.value.post.author.did === did) { + yield item.value.post.author + } + + const qp = getEmbeddedPost(item.value.post.embed) + if (qp && qp.author.did === did) { + yield qp.author + } + } + } + } +} diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts new file mode 100644 index 0000000000..fbae4ecdbf --- /dev/null +++ b/src/state/queries/usePostThread/traversal.ts @@ -0,0 +1,539 @@ +/* eslint-disable no-labels */ +import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api' + +import { + type ApiThreadItem, + type PostThreadParams, + type ThreadItem, + type TraversalMetadata, +} from '#/state/queries/usePostThread/types' +import { + getPostRecord, + getThreadPostNoUnauthenticatedUI, + getThreadPostUI, + getTraversalMetadata, + storeTraversalMetadata, +} from '#/state/queries/usePostThread/utils' +import * as views from '#/state/queries/usePostThread/views' + +export function sortAndAnnotateThreadItems( + thread: ApiThreadItem[], + { + threadgateHiddenReplies, + moderationOpts, + view, + skipModerationHandling, + }: { + threadgateHiddenReplies: Set + moderationOpts: ModerationOpts + view: PostThreadParams['view'] + /** + * Set to `true` in cases where we already know the moderation state of the + * post e.g. when fetching additional replies from the server. This will + * prevent additional sorting or nested-branch truncation, and all replies, + * regardless of moderation state, will be included in the resulting + * `threadItems` array. + */ + skipModerationHandling?: boolean + }, +) { + const threadItems: ThreadItem[] = [] + const otherThreadItems: ThreadItem[] = [] + const metadatas = new Map() + + traversal: for (let i = 0; i < thread.length; i++) { + const item = thread[i] + let parentMetadata: TraversalMetadata | undefined + let metadata: TraversalMetadata | undefined + + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + parentMetadata = metadatas.get( + getPostRecord(item.value.post).reply?.parent?.uri || '', + ) + metadata = getTraversalMetadata({ + item, + parentMetadata, + prevItem: thread.at(i - 1), + nextItem: thread.at(i + 1), + }) + storeTraversalMetadata(metadatas, metadata) + } + + if (item.depth < 0) { + /* + * Parents are ignored until we find the anchor post, then we walk + * _up_ from there. + */ + } else if (item.depth === 0) { + if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) { + threadItems.push(views.threadPostNoUnauthenticated(item)) + } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) { + threadItems.push(views.threadPostNotFound(item)) + } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) { + threadItems.push(views.threadPostBlocked(item)) + } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + const post = views.threadPost({ + uri: item.uri, + depth: item.depth, + value: item.value, + moderationOpts, + threadgateHiddenReplies, + }) + threadItems.push(post) + + parentTraversal: for (let pi = i - 1; pi >= 0; pi--) { + const parent = thread[pi] + + if ( + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value) + ) { + const post = views.threadPostNoUnauthenticated(parent) + post.ui = getThreadPostNoUnauthenticatedUI({ + depth: parent.depth, + // ignore for now + // prevItemDepth: thread[pi - 1]?.depth, + nextItemDepth: thread[pi + 1]?.depth, + }) + threadItems.unshift(post) + // for now, break parent traversal at first no-unauthed + break parentTraversal + } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) { + threadItems.unshift(views.threadPostNotFound(parent)) + break parentTraversal + } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) { + threadItems.unshift(views.threadPostBlocked(parent)) + break parentTraversal + } else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) { + threadItems.unshift( + views.threadPost({ + uri: parent.uri, + depth: parent.depth, + value: parent.value, + moderationOpts, + threadgateHiddenReplies, + }), + ) + } + } + } + } else if (item.depth > 0) { + /* + * The API does not send down any unavailable replies, so this will + * always be false (for now). If we ever wanted to tombstone them here, + * we could. + */ + const shouldBreak = + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) || + AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || + AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) + + if (shouldBreak) { + const branch = getBranch(thread, i, item.depth) + // could insert tombstone + i = branch.end + continue traversal + } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (parentMetadata) { + /* + * Set this value before incrementing the parent's repliesSeenCounter + */ + metadata!.replyIndex = parentMetadata.repliesIndexCounter + // Increment the parent's repliesIndexCounter + parentMetadata.repliesIndexCounter += 1 + } + + const post = views.threadPost({ + uri: item.uri, + depth: item.depth, + value: item.value, + moderationOpts, + threadgateHiddenReplies, + }) + + if (!post.isBlurred || skipModerationHandling) { + /* + * Not moderated, need to insert it + */ + threadItems.push(post) + + /* + * Update seen reply count of parent + */ + if (parentMetadata) { + parentMetadata.repliesSeenCounter += 1 + } + } else { + /* + * Moderated in some way, we're going to walk children + */ + const parent = post + const parentIsTopLevelReply = parent.depth === 1 + // get sub tree + const branch = getBranch(thread, i, item.depth) + + if (parentIsTopLevelReply) { + // push branch anchor into sorted array + otherThreadItems.push(parent) + // skip branch anchor in branch traversal + const startIndex = branch.start + 1 + + for (let ci = startIndex; ci <= branch.end; ci++) { + const child = thread[ci] + + if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) { + const childParentMetadata = metadatas.get( + getPostRecord(child.value.post).reply?.parent?.uri || '', + ) + const childMetadata = getTraversalMetadata({ + item: child, + prevItem: thread[ci - 1], + nextItem: thread[ci + 1], + parentMetadata: childParentMetadata, + }) + storeTraversalMetadata(metadatas, childMetadata) + if (childParentMetadata) { + /* + * Set this value before incrementing the parent's repliesIndexCounter + */ + childMetadata!.replyIndex = + childParentMetadata.repliesIndexCounter + childParentMetadata.repliesIndexCounter += 1 + } + + const childPost = views.threadPost({ + uri: child.uri, + depth: child.depth, + value: child.value, + moderationOpts, + threadgateHiddenReplies, + }) + + /* + * If a child is moderated in any way, drop it an its sub-branch + * entirely. To reveal these, the user must navigate to the + * parent post directly. + */ + if (childPost.isBlurred) { + ci = getBranch(thread, ci, child.depth).end + } else { + otherThreadItems.push(childPost) + + if (childParentMetadata) { + childParentMetadata.repliesSeenCounter += 1 + } + } + } else { + /* + * Drop the rest of the branch if we hit anything unexpected + */ + break + } + } + } + + /* + * Skip to next branch + */ + i = branch.end + continue traversal + } + } + } + } + + /* + * Both `threadItems` and `otherThreadItems` now need to be traversed again to fully compute + * UI state based on collected metadata. These arrays will be muted in situ. + */ + for (const subset of [threadItems, otherThreadItems]) { + for (let i = 0; i < subset.length; i++) { + const item = subset[i] + const prevItem = subset.at(i - 1) + const nextItem = subset.at(i + 1) + + if (item.type === 'threadPost') { + const metadata = metadatas.get(item.uri) + + if (metadata) { + if (metadata.parentMetadata) { + /* + * Track what's before/after now that we've applied moderation + */ + if (prevItem?.type === 'threadPost') + metadata.prevItemDepth = prevItem?.depth + if (nextItem?.type === 'threadPost') + metadata.nextItemDepth = nextItem?.depth + + /* + * We can now officially calculate `isLastSibling` and `isLastChild` + * based on the actual data that we've seen. + */ + metadata.isLastSibling = + metadata.replyIndex === + metadata.parentMetadata.repliesSeenCounter - 1 + metadata.isLastChild = + metadata.nextItemDepth === undefined || + metadata.nextItemDepth <= metadata.depth + + /* + * If this is the last sibling, it's implicitly part of the last + * branch of this sub-tree. + */ + if (metadata.isLastSibling) { + metadata.isPartOfLastBranchFromDepth = metadata.depth + + /** + * If the parent is part of the last branch of the sub-tree, so is the child. + */ + if (metadata.parentMetadata.isPartOfLastBranchFromDepth) { + metadata.isPartOfLastBranchFromDepth = + metadata.parentMetadata.isPartOfLastBranchFromDepth + } + } + + /* + * If this is the last sibling, and the parent has unhydrated replies, + * at some point down the line we will need to show a "read more". + */ + if ( + metadata.parentMetadata.repliesUnhydrated > 0 && + metadata.isLastSibling + ) { + metadata.upcomingParentReadMore = metadata.parentMetadata + } + + /* + * Copy in the parent's upcoming read more, if it exists. Once we + * reach the bottom, we'll insert a "read more" + */ + if (metadata.parentMetadata.upcomingParentReadMore) { + metadata.upcomingParentReadMore = + metadata.parentMetadata.upcomingParentReadMore + } + + /* + * Copy in the parent's skipped indents + */ + metadata.skippedIndentIndices = new Set([ + ...metadata.parentMetadata.skippedIndentIndices, + ]) + + /** + * If this is the last sibling, and the parent has no unhydrated + * replies, then we know we can skip an indent line. + */ + if ( + metadata.parentMetadata.repliesUnhydrated <= 0 && + metadata.isLastSibling + ) { + /** + * Depth is 2 more than the 0-index of the indent calculation + * bc of how we render these. So instead of handling that in the + * component, we just adjust that back to 0-index here. + */ + metadata.skippedIndentIndices.add(item.depth - 2) + } + } + + /* + * If this post has unhydrated replies, and it is the last child, then + * it itself needs a "read more" + */ + if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) { + metadata.precedesChildReadMore = true + subset.splice(i + 1, 0, views.readMore(metadata)) + i++ // skip next iteration + } + + /* + * Tree-view only. + * + * If there's an upcoming parent read more, this branch is part of the + * last branch of the sub-tree, and the item itself is the last child, + * insert the parent "read more". + */ + if ( + view === 'tree' && + metadata.upcomingParentReadMore && + metadata.isPartOfLastBranchFromDepth === + metadata.upcomingParentReadMore.depth && + metadata.isLastChild + ) { + subset.splice( + i + 1, + 0, + views.readMore(metadata.upcomingParentReadMore), + ) + i++ + } + + /** + * Only occurs for the first item in the thread, which may have + * additional parents not included in this request. + */ + if (item.value.moreParents) { + metadata.followsReadMoreUp = true + subset.splice(i, 0, views.readMoreUp(metadata)) + i++ + } + + /* + * Calculate the final UI state for the thread item. + */ + item.ui = getThreadPostUI(metadata) + } + } + } + } + + return { + threadItems, + otherThreadItems, + } +} + +export function buildThread({ + threadItems, + otherThreadItems, + serverOtherThreadItems, + isLoading, + hasSession, + otherItemsVisible, + hasOtherThreadItems, + showOtherItems, +}: { + threadItems: ThreadItem[] + otherThreadItems: ThreadItem[] + serverOtherThreadItems: ThreadItem[] + isLoading: boolean + hasSession: boolean + otherItemsVisible: boolean + hasOtherThreadItems: boolean + showOtherItems: () => void +}) { + /** + * `threadItems` is memoized here, so don't mutate it directly. + */ + const items = [...threadItems] + + if (isLoading) { + const anchorPost = items.at(0) + const hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost' + const skeletonReplies = hasAnchorFromCache + ? anchorPost.value.post.replyCount ?? 4 + : 4 + + if (!items.length) { + items.push( + views.skeleton({ + key: 'anchor-skeleton', + item: 'anchor', + }), + ) + } + + if (hasSession) { + // we might have this from cache + const replyDisabled = + hasAnchorFromCache && + anchorPost.value.post.viewer?.replyDisabled === true + + if (hasAnchorFromCache) { + if (!replyDisabled) { + items.push({ + type: 'replyComposer', + key: 'replyComposer', + }) + } + } else { + items.push( + views.skeleton({ + key: 'replyComposer', + item: 'replyComposer', + }), + ) + } + } + + for (let i = 0; i < skeletonReplies; i++) { + items.push( + views.skeleton({ + key: `anchor-skeleton-reply-${i}`, + item: 'reply', + }), + ) + } + } else { + for (let i = 0; i < items.length; i++) { + const item = items[i] + if ( + item.type === 'threadPost' && + item.depth === 0 && + !item.value.post.viewer?.replyDisabled && + hasSession + ) { + items.splice(i + 1, 0, { + type: 'replyComposer', + key: 'replyComposer', + }) + break + } + } + + if (otherThreadItems.length || hasOtherThreadItems) { + if (otherItemsVisible) { + items.push(...otherThreadItems) + items.push(...serverOtherThreadItems) + } else { + items.push({ + type: 'showOtherReplies', + key: 'showOtherReplies', + onPress: showOtherItems, + }) + } + } + } + + return items +} + +/** + * Get the start and end index of a "branch" of the thread. A "branch" is a + * parent and it's children (not siblings). Returned indices are inclusive of + * the parent and its last child. + * + * items[] (index, depth) + * └─┬ anchor ──────── (0, 0) + * ├─── branch ───── (1, 1) + * ├──┬ branch ───── (2, 1) (start) + * │ ├──┬ leaf ──── (3, 2) + * │ │ └── leaf ── (4, 3) + * │ └─── leaf ──── (5, 2) (end) + * ├─── branch ───── (6, 1) + * └─── branch ───── (7, 1) + * + * const { start: 2, end: 5, length: 3 } = getBranch(items, 2, 1) + */ +export function getBranch( + thread: ApiThreadItem[], + branchStartIndex: number, + branchStartDepth: number, +) { + let end = branchStartIndex + + for (let ci = branchStartIndex + 1; ci < thread.length; ci++) { + const next = thread[ci] + if (next.depth > branchStartDepth) { + end = ci + } else { + end = ci - 1 + break + } + } + + return { + start: branchStartIndex, + end, + length: end - branchStartIndex, + } +} diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts new file mode 100644 index 0000000000..2f370b0ab7 --- /dev/null +++ b/src/state/queries/usePostThread/types.ts @@ -0,0 +1,227 @@ +import { + type AppBskyFeedDefs, + type AppBskyFeedPost, + type AppBskyFeedThreadgate, + type AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadOtherV2, + type AppBskyUnspeccedGetPostThreadV2, + type ModerationDecision, +} from '@atproto/api' + +export type ApiThreadItem = + | AppBskyUnspeccedGetPostThreadV2.ThreadItem + | AppBskyUnspeccedGetPostThreadOtherV2.ThreadItem + +export const postThreadQueryKeyRoot = 'post-thread-v2' as const + +export const createPostThreadQueryKey = (props: PostThreadParams) => + [postThreadQueryKeyRoot, props] as const + +export const createPostThreadOtherQueryKey = ( + props: Omit & { + anchor?: string + }, +) => [postThreadQueryKeyRoot, 'other', props] as const + +export type PostThreadParams = Pick< + AppBskyUnspeccedGetPostThreadV2.QueryParams, + 'sort' | 'prioritizeFollowedUsers' +> & { + anchor?: string + view: 'tree' | 'linear' +} + +export type UsePostThreadQueryResult = { + hasOtherReplies: boolean + thread: AppBskyUnspeccedGetPostThreadV2.ThreadItem[] + threadgate?: Omit & { + record: AppBskyFeedThreadgate.Record + } +} + +export type ThreadItem = + | { + type: 'threadPost' + key: string + uri: string + depth: number + value: Omit & { + post: Omit & { + record: AppBskyFeedPost.Record + } + } + isBlurred: boolean + moderation: ModerationDecision + ui: { + isAnchor: boolean + showParentReplyLine: boolean + showChildReplyLine: boolean + indent: number + isLastChild: boolean + skippedIndentIndices: Set + precedesChildReadMore: boolean + } + } + | { + type: 'threadPostNoUnauthenticated' + key: string + uri: string + depth: number + value: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated + ui: { + showParentReplyLine: boolean + showChildReplyLine: boolean + } + } + | { + type: 'threadPostNotFound' + key: string + uri: string + depth: number + value: AppBskyUnspeccedDefs.ThreadItemNotFound + } + | { + type: 'threadPostBlocked' + key: string + uri: string + depth: number + value: AppBskyUnspeccedDefs.ThreadItemBlocked + } + | { + type: 'replyComposer' + key: string + } + | { + type: 'showOtherReplies' + key: string + onPress: () => void + } + | { + /* + * Read more replies, downwards in the thread. + */ + type: 'readMore' + key: string + depth: number + href: string + moreReplies: number + skippedIndentIndices: Set + } + | { + /* + * Read more parents, upwards in the thread. + */ + type: 'readMoreUp' + key: string + href: string + } + | { + type: 'skeleton' + key: string + item: 'anchor' | 'reply' | 'replyComposer' + } + +/** + * Metadata collected while traversing the raw data from the thread response. + * Some values here can be computed immediately, while others need to be + * computed during a second pass over the thread after we know things like + * total number of replies, the reply index, etc. + * + * The idea here is that these values should be objectively true in all cases, + * such that we can use them later — either individually on in composite — to + * drive rendering behaviors. + */ +export type TraversalMetadata = { + /** + * The depth of the post in the reply tree, where 0 is the root post. This is + * calculated on the server. + */ + depth: number + /** + * Indicates if this item is a "read more" link preceding this post that + * continues the thread upwards. + */ + followsReadMoreUp: boolean + /** + * Indicates if the post is the last reply beneath its parent post. + */ + isLastSibling: boolean + /** + * Indicates the post is the end-of-the-line for a given branch of replies. + */ + isLastChild: boolean + /** + * Indicates if the post is the left/lower-most branch of the reply tree. + * Value corresponds to the depth at which this branch started. + */ + isPartOfLastBranchFromDepth?: number + /** + * The depth of the slice immediately following this one, if it exists. + */ + nextItemDepth?: number + /** + * This is a live reference to the parent metadata object. Mutations to this + * are available for later use in children. + */ + parentMetadata?: TraversalMetadata + /** + * Populated during the final traversal of the thread. Denotes whether + * there is a "Read more" link for this item immediately following + * this item. + */ + precedesChildReadMore: boolean + /** + * The depth of the slice immediately preceding this one, if it exists. + */ + prevItemDepth?: number + /** + * Any data needed to be passed along to the "read more" items. Keep this + * trim for better memory usage. + */ + postData: { + uri: string + authorHandle: string + } + /** + * The total number of replies to this post, including those not hydrated + * and returned by the response. + */ + repliesCount: number + /** + * The number of replies to this post not hydrated and returned by the + * response. + */ + repliesUnhydrated: number + /** + * The number of replies that have been seen so far in the traversal. + * Excludes replies that are moderated in some way, since those are not + * "seen" on first load. Use `repliesIndexCounter` for the total number of + * replies that were hydrated in the response. + * + * After traversal, we can use this to calculate if we actually got all the + * replies we expected, or if some were blocked, etc. + */ + repliesSeenCounter: number + /** + * The total number of replies to this post hydrated in this response. Used + * for populating the `replyIndex` of the post by referencing this value on + * the parent. + */ + repliesIndexCounter: number + /** + * The index-0-based index of this reply in the parent post's replies. + */ + replyIndex: number + /** + * Each slice is responsible for rendering reply lines based on its depth. + * This value corresponds to any line indices that can be skipped e.g. + * because there are no further replies below this sub-tree to render. + */ + skippedIndentIndices: Set + /** + * Indicates and stores parent data IF that parent has additional unhydrated + * replies. This value is passed down to children along the left/lower-most + * branch of the tree. When the end is reached, a "read more" is inserted. + */ + upcomingParentReadMore?: TraversalMetadata +} diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts new file mode 100644 index 0000000000..b8ab340d87 --- /dev/null +++ b/src/state/queries/usePostThread/utils.ts @@ -0,0 +1,170 @@ +import { + type AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyFeedThreadgate, + AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, + AtUri, +} from '@atproto/api' + +import { + type ApiThreadItem, + type ThreadItem, + type TraversalMetadata, +} from '#/state/queries/usePostThread/types' +import {isDevMode} from '#/storage/hooks/dev-mode' +import * as bsky from '#/types/bsky' + +export function getThreadgateRecord( + view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'], +) { + return bsky.dangerousIsType( + view?.record, + AppBskyFeedThreadgate.isRecord, + ) + ? view?.record + : undefined +} + +export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) { + if ( + bsky.dangerousIsType( + post.record, + AppBskyFeedPost.isRecord, + ) + ) { + if (post.record.reply?.root?.uri) { + return new AtUri(post.record.reply.root.uri) + } + } +} + +export function getPostRecord(post: AppBskyFeedDefs.PostView) { + return post.record as AppBskyFeedPost.Record +} + +export function getTraversalMetadata({ + item, + prevItem, + nextItem, + parentMetadata, +}: { + item: ApiThreadItem + prevItem?: ApiThreadItem + nextItem?: ApiThreadItem + parentMetadata?: TraversalMetadata +}): TraversalMetadata { + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + throw new Error(`Expected thread item to be a post`) + } + const repliesCount = item.value.post.replyCount || 0 + const repliesUnhydrated = item.value.moreReplies || 0 + const metadata = { + depth: item.depth, + /* + * Unknown until after traversal + */ + isLastChild: false, + /* + * Unknown until after traversal + */ + isLastSibling: false, + /* + * If it's a top level reply, bc we render each top-level branch as a + * separate tree, it's implicitly part of the last branch. For subsequent + * replies, we'll override this after traversal. + */ + isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined, + nextItemDepth: nextItem?.depth, + parentMetadata, + prevItemDepth: prevItem?.depth, + /* + * Unknown until after traversal + */ + precedesChildReadMore: false, + /* + * Unknown until after traversal + */ + followsReadMoreUp: false, + postData: { + uri: item.uri, + authorHandle: item.value.post.author.handle, + }, + repliesCount, + repliesUnhydrated, + repliesSeenCounter: 0, + repliesIndexCounter: 0, + replyIndex: 0, + skippedIndentIndices: new Set(), + } + + if (isDevMode()) { + // @ts-ignore dev only for debugging + metadata.postData.text = getPostRecord(item.value.post).text + } + + return metadata +} + +export function storeTraversalMetadata( + metadatas: Map, + metadata: TraversalMetadata, +) { + metadatas.set(metadata.postData.uri, metadata) + + if (isDevMode()) { + // @ts-ignore dev only for debugging + metadatas.set(metadata.postData.text, metadata) + // @ts-ignore + window.__thread = metadatas + } +} + +export function getThreadPostUI({ + depth, + repliesCount, + prevItemDepth, + isLastChild, + skippedIndentIndices, + repliesSeenCounter, + repliesUnhydrated, + precedesChildReadMore, + followsReadMoreUp, +}: TraversalMetadata): Extract['ui'] { + const isReplyAndHasReplies = + depth > 0 && + repliesCount > 0 && + (repliesCount - repliesUnhydrated === repliesSeenCounter || + repliesSeenCounter > 0) + return { + isAnchor: depth === 0, + showParentReplyLine: + followsReadMoreUp || + (!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth), + showChildReplyLine: depth < 0 || isReplyAndHasReplies, + indent: depth, + /* + * If there are no slices below this one, or the next slice has a depth <= + * than the depth of this post, it's the last child of the reply tree. It + * is not necessarily the last leaf in the parent branch, since it could + * have another sibling. + */ + isLastChild, + skippedIndentIndices, + precedesChildReadMore: precedesChildReadMore ?? false, + } +} + +export function getThreadPostNoUnauthenticatedUI({ + depth, + prevItemDepth, +}: { + depth: number + prevItemDepth?: number + nextItemDepth?: number +}): Extract['ui'] { + return { + showChildReplyLine: depth < 0, + showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth), + } +} diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts new file mode 100644 index 0000000000..71acfc77bc --- /dev/null +++ b/src/state/queries/usePostThread/views.ts @@ -0,0 +1,183 @@ +import { + type $Typed, + type AppBskyFeedDefs, + type AppBskyFeedPost, + type AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, + AtUri, + moderatePost, + type ModerationOpts, +} from '@atproto/api' + +import {makeProfileLink} from '#/lib/routes/links' +import { + type ApiThreadItem, + type ThreadItem, + type TraversalMetadata, +} from '#/state/queries/usePostThread/types' + +export function threadPostNoUnauthenticated({ + uri, + depth, + value, +}: ApiThreadItem): Extract { + return { + type: 'threadPostNoUnauthenticated', + key: uri, + uri, + depth, + value: value as AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated, + // @ts-ignore populated by the traversal + ui: {}, + } +} + +export function threadPostNotFound({ + uri, + depth, + value, +}: ApiThreadItem): Extract { + return { + type: 'threadPostNotFound', + key: uri, + uri, + depth, + value: value as AppBskyUnspeccedDefs.ThreadItemNotFound, + } +} + +export function threadPostBlocked({ + uri, + depth, + value, +}: ApiThreadItem): Extract { + return { + type: 'threadPostBlocked', + key: uri, + uri, + depth, + value: value as AppBskyUnspeccedDefs.ThreadItemBlocked, + } +} + +export function threadPost({ + uri, + depth, + value, + moderationOpts, + threadgateHiddenReplies, +}: { + uri: string + depth: number + value: $Typed + moderationOpts: ModerationOpts + threadgateHiddenReplies: Set +}): Extract { + const moderation = moderatePost(value.post, moderationOpts) + const modui = moderation.ui('contentList') + const blurred = modui.blur || modui.filter + const muted = (modui.blurs[0] || modui.filters[0])?.type === 'muted' + const hiddenByThreadgate = threadgateHiddenReplies.has(uri) + const isBlurred = hiddenByThreadgate || blurred || muted + return { + type: 'threadPost', + key: uri, + uri, + depth, + value: { + ...value, + /* + * Do not spread anything here, load bearing for post shadow strict + * equality reference checks. + */ + post: value.post as Omit & { + record: AppBskyFeedPost.Record + }, + }, + isBlurred, + moderation, + // @ts-ignore populated by the traversal + ui: {}, + } +} + +export function readMore({ + depth, + repliesUnhydrated, + skippedIndentIndices, + postData, +}: TraversalMetadata): Extract { + const urip = new AtUri(postData.uri) + const href = makeProfileLink( + { + did: urip.host, + handle: postData.authorHandle, + }, + 'post', + urip.rkey, + ) + return { + type: 'readMore' as const, + key: `readMore:${postData.uri}`, + href, + moreReplies: repliesUnhydrated, + depth, + skippedIndentIndices, + } +} + +export function readMoreUp({ + postData, +}: TraversalMetadata): Extract { + const urip = new AtUri(postData.uri) + const href = makeProfileLink( + { + did: urip.host, + handle: postData.authorHandle, + }, + 'post', + urip.rkey, + ) + return { + type: 'readMoreUp' as const, + key: `readMoreUp:${postData.uri}`, + href, + } +} + +export function skeleton({ + key, + item, +}: Omit, 'type'>): Extract< + ThreadItem, + {type: 'skeleton'} +> { + return { + type: 'skeleton', + key, + item, + } +} + +export function postViewToThreadPlaceholder( + post: AppBskyFeedDefs.PostView, +): $Typed< + Omit & { + value: $Typed + } +> { + return { + $type: 'app.bsky.unspecced.getPostThreadV2#threadItem', + uri: post.uri, + depth: 0, // reset to 0 for highlighted post + value: { + $type: 'app.bsky.unspecced.defs#threadItemPost', + post, + opThread: false, + moreParents: false, + moreReplies: 0, + hiddenByThreadgate: false, + mutedByViewer: false, + }, + } +} diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx index ad07333beb..b317942480 100644 --- a/src/state/shell/composer/index.tsx +++ b/src/state/shell/composer/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import { type AppBskyActorDefs, type AppBskyFeedDefs, + type AppBskyUnspeccedGetPostThreadV2, type ModerationDecision, } from '@atproto/api' import {msg} from '@lingui/macro' @@ -24,9 +25,17 @@ export interface ComposerOptsPostRef { moderation?: ModerationDecision } +export type OnPostSuccessData = + | { + replyToUri?: string + posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[] + } + | undefined + export interface ComposerOpts { replyTo?: ComposerOptsPostRef onPost?: (postUri: string | undefined) => void + onPostSuccess?: (data: OnPostSuccessData) => void quote?: AppBskyFeedDefs.PostView mention?: string // handle of user to mention openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void diff --git a/src/state/threadgate-hidden-replies.tsx b/src/state/threadgate-hidden-replies.tsx index 60806f5706..9d116c7f9f 100644 --- a/src/state/threadgate-hidden-replies.tsx +++ b/src/state/threadgate-hidden-replies.tsx @@ -83,3 +83,17 @@ export function useMergedThreadgateHiddenReplies({ return set }, [uris, recentlyUnhiddenUris, threadgateRecord]) } + +export function useMergeThreadgateHiddenReplies() { + const {uris, recentlyUnhiddenUris} = useThreadgateHiddenReplyUris() + return React.useCallback( + (threadgate?: AppBskyFeedThreadgate.Record) => { + const set = new Set([...(threadgate?.hiddenReplies || []), ...uris]) + for (const uri of recentlyUnhiddenUris) { + set.delete(uri) + } + return set + }, + [uris, recentlyUnhiddenUris], + ) +} diff --git a/src/storage/hooks/dev-mode.ts b/src/storage/hooks/dev-mode.ts index 49eca3bb11..331825c48f 100644 --- a/src/storage/hooks/dev-mode.ts +++ b/src/storage/hooks/dev-mode.ts @@ -5,3 +5,17 @@ export function useDevMode() { return [devMode, setDevMode] as const } + +let cachedIsDevMode: boolean | undefined +/** + * Does not update when toggling dev mode on or off. This util simply retrieves + * the value and caches in memory indefinitely. So after an update, you'll need + * to reload the app so it can pull a fresh value from storage. + */ +export function isDevMode() { + if (__DEV__) return true + if (cachedIsDevMode === undefined) { + cachedIsDevMode = device.get(['devMode']) ?? false + } + return cachedIsDevMode +} diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000000..f64922a1f1 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,5 @@ +export type Literal = T extends A + ? string extends T + ? never + : T + : never diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 42f057803f..f5b29664ab 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -45,6 +45,7 @@ import {type ImagePickerAsset} from 'expo-image-picker' import { AppBskyFeedDefs, type AppBskyFeedGetPostThread, + AppBskyUnspeccedDefs, type BskyAgent, type RichText, } from '@atproto/api' @@ -55,6 +56,7 @@ import {useQueryClient} from '@tanstack/react-query' import * as apilib from '#/lib/api/index' import {EmbeddingDisabledError} from '#/lib/api/resolve' +import {retry} from '#/lib/async/retry' import {until} from '#/lib/async/until' import { MAX_GRAPHEME_LENGTH, @@ -87,7 +89,7 @@ import {useProfileQuery} from '#/state/queries/profile' import {type Gif} from '#/state/queries/tenor' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' -import {type ComposerOpts} from '#/state/shell/composer' +import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo' import { @@ -152,6 +154,7 @@ type Props = ComposerOpts export const ComposePost = ({ replyTo, onPost, + onPostSuccess, quote: initQuote, mention: initMention, openEmojiPicker, @@ -388,8 +391,10 @@ export const ComposePost = ({ setError('') setIsPublishing(true) - let postUri + let postUri: string | undefined + let postSuccessData: OnPostSuccessData try { + logger.info(`composer: posting...`) postUri = ( await apilib.post(agent, queryClient, { thread, @@ -398,16 +403,48 @@ export const ComposePost = ({ langs: toPostLanguages(langPrefs.postLanguage), }) ).uris[0] + + /* + * Wait for app view to have received the post(s). If this fails, it's + * ok, because the post _was_ actually published above. + */ try { - await whenAppViewReady(agent, postUri, res => { - const postedThread = res?.data?.thread - return AppBskyFeedDefs.isThreadViewPost(postedThread) - }) + if (postUri) { + logger.info(`composer: waiting for app view`) + + const posts = await retry( + 5, + _e => true, + async () => { + const res = await agent.app.bsky.unspecced.getPostThreadV2({ + anchor: postUri!, + above: false, + below: thread.posts.length - 1, + branchingFactor: 1, + }) + if (res.data.thread.length !== thread.posts.length) { + throw new Error(`composer: app view is not ready`) + } + if ( + !res.data.thread.every(p => + AppBskyUnspeccedDefs.isThreadItemPost(p.value), + ) + ) { + throw new Error(`composer: app view returned non-post items`) + } + return res.data.thread + }, + 1e3, + ) + postSuccessData = { + replyToUri: replyTo?.uri, + posts, + } + } } catch (waitErr: any) { - logger.error(waitErr, { - message: `Waiting for app view failed`, + logger.info(`composer: waiting for app view failed`, { + safeMessage: waitErr, }) - // Keep going because the post *was* published. } } catch (e: any) { logger.error(e, { @@ -465,12 +502,14 @@ export const ComposePost = ({ quotedThread.post.quoteCount !== initQuote.quoteCount ) { onPost?.(postUri) + onPostSuccess?.(postSuccessData) return true } return false }) } else { onPost?.(postUri) + onPostSuccess?.(postSuccessData) } onClose() Toast.show( @@ -489,6 +528,7 @@ export const ComposePost = ({ langPrefs.postLanguage, onClose, onPost, + onPostSuccess, initQuote, replyTo, setLangPrefs, diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 5bec9ced1a..94cc04f542 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,8 +1,7 @@ import React, {memo, useRef, useState} from 'react' -import {StyleSheet, useWindowDimensions, View} from 'react-native' -import {runOnJS} from 'react-native-reanimated' +import {useWindowDimensions, View} from 'react-native' +import {runOnJS, useAnimatedStyle} from 'react-native-reanimated' import Animated from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import { AppBskyFeedDefs, type AppBskyFeedThreadgate, @@ -13,11 +12,9 @@ import {useLingui} from '@lingui/react' import {HITSLOP_10} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {clamp} from '#/lib/numbers' import {ScrollProvider} from '#/lib/ScrollContext' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' @@ -37,6 +34,7 @@ import { import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' +import {useShellLayout} from '#/state/shell/shell-layout' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useUnstablePostSource} from '#/state/unstable-post-source' import {List, type ListMethods} from '#/view/com/util/List' @@ -301,11 +299,14 @@ export function PostThread({uri}: {uri: string}) { // maintainVisibleContentPosition and onContentSizeChange // to "hold onto" the correct row instead of the first one. + /* + * This is basically `!!parents.length`, see notes on `isParentLoading` + */ if (!highlightedPost.ctx.isParentLoading && !deferParents) { // When progressively revealing parents, rendering a placeholder // here will cause scrolling jumps. Don't add it unless you test it. // QT'ing this thread is a great way to test all the scrolling hacks: - // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o + // https://bsky.app/profile/samuel.bsky.team/post/3kjqhblh6qk2o // Everything is loaded let startIndex = Math.max(0, parents.length - maxParents) @@ -581,6 +582,9 @@ export function PostThread({uri}: {uri: string}) { onEndReached={onEndReached} onEndReachedThreshold={2} onScrollToTop={onScrollToTop} + /** + * @see https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition + */ maintainVisibleContentPosition={ isNative && hasParents ? MAINTAIN_VISIBLE_CONTENT_POSITION @@ -729,17 +733,16 @@ let ThreadMenu = ({ ThreadMenu = memo(ThreadMenu) function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) { - const safeAreaInsets = useSafeAreaInsets() - const fabMinimalShellTransform = useMinimalShellFabTransform() + const {footerHeight} = useShellLayout() + + const animatedStyle = useAnimatedStyle(() => { + return { + bottom: footerHeight.get(), + } + }) + return ( - + ) @@ -904,12 +907,3 @@ function hasBranchingReplies(node?: ThreadNode) { } return true } - -const styles = StyleSheet.create({ - prompt: { - // @ts-ignore web-only - position: isWeb ? 'fixed' : 'absolute', - left: 0, - right: 0, - }, -}) diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index 40acff3765..f45b16085f 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -1,20 +1,25 @@ -import {View} from 'react-native' +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {LinearGradient} from 'expo-linear-gradient' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {useHaptics} from '#/lib/haptics' +import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, ios, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf' +import {transparentifyColor} from '#/alf/util/colorGeneration' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Text} from '#/components/Typography' export function PostThreadComposePrompt({ onPressCompose, + style, }: { onPressCompose: () => void + style?: StyleProp }) { const {currentAccount} = useSession() const {data: profile} = useProfileQuery({did: currentAccount?.did}) @@ -28,29 +33,49 @@ export function PostThreadComposePrompt({ onOut: onHoverOut, } = useInteractionState() + useHideBottomBarBorderForScreen() + return ( - { - onPressCompose() - playHaptic('Light') - }} - onLongPress={ios(() => { - onPressCompose() - playHaptic('Heavy') - })} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut}> - + {!gtMobile && ( + + )} + { + onPressCompose() + playHaptic('Light') + }} + onLongPress={ios(() => { + onPressCompose() + playHaptic('Heavy') + })} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} style={[ a.flex_row, a.align_center, @@ -58,6 +83,7 @@ export function PostThreadComposePrompt({ a.gap_sm, a.rounded_full, (!gtMobile || hovered) && t.atoms.bg_contrast_25, + native([a.border, t.atoms.border_contrast_low]), a.transition_color, ]}> Write your reply - - + + ) } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 576b195a06..5184047cbb 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -39,6 +39,7 @@ import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {useLanguagePrefs} from '#/state/preferences' import {type ThreadPost} from '#/state/queries/post-thread' import {useSession} from '#/state/session' +import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {type PostSource} from '#/state/unstable-post-source' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' @@ -85,6 +86,7 @@ export function PostThreadItem({ hasPrecedingItem, overrideBlur, onPostReply, + onPostSuccess, hideTopBorder, threadgateRecord, anchorPostSource, @@ -103,6 +105,7 @@ export function PostThreadItem({ hasPrecedingItem: boolean overrideBlur: boolean onPostReply: (postUri: string | undefined) => void + onPostSuccess?: (data: OnPostSuccessData) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record anchorPostSource?: PostSource @@ -139,6 +142,7 @@ export function PostThreadItem({ hasPrecedingItem={hasPrecedingItem} overrideBlur={overrideBlur} onPostReply={onPostReply} + onPostSuccess={onPostSuccess} hideTopBorder={hideTopBorder} threadgateRecord={threadgateRecord} anchorPostSource={anchorPostSource} @@ -185,6 +189,7 @@ let PostThreadItemLoaded = ({ hasPrecedingItem, overrideBlur, onPostReply, + onPostSuccess, hideTopBorder, threadgateRecord, anchorPostSource, @@ -204,6 +209,7 @@ let PostThreadItemLoaded = ({ hasPrecedingItem: boolean overrideBlur: boolean onPostReply: (postUri: string | undefined) => void + onPostSuccess?: (data: OnPostSuccessData) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record anchorPostSource?: PostSource @@ -298,6 +304,7 @@ let PostThreadItemLoaded = ({ moderation, }, onPost: onPostReply, + onPostSuccess: onPostSuccess, }) } diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 1bad9b6cdf..cc611e0d63 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -1,28 +1,38 @@ -import React from 'react' +import {useCallback} from 'react' import {useFocusEffect} from '@react-navigation/native' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import {makeRecordUri} from '#/lib/strings/url-helpers' import {useSetMinimalShellMode} from '#/state/shell' import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' +import {PostThread} from '#/screens/PostThread' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps export function PostThreadScreen({route}: Props) { const setMinimalShellMode = useSetMinimalShellMode() + const gate = useGate() const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) useFocusEffect( - React.useCallback(() => { + useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) return ( - + {gate('post_threads_v2_unspecced') || __DEV__ ? ( + + ) : ( + + )} ) } diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 8b53f40416..393b8f80e6 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -37,6 +37,7 @@ export function Composer({}: {winHeight: number}) { cancelRef={ref} replyTo={state?.replyTo} onPost={state?.onPost} + onPostSuccess={state?.onPostSuccess} quote={state?.quote} mention={state?.mention} text={state?.text} diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index e40c3528b9..a17de6163d 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -49,6 +49,7 @@ export function Composer({winHeight}: {winHeight: number}) { { @@ -146,7 +148,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { style={[ styles.bottomBar, pal.view, - pal.border, + hideBorder ? {borderColor: pal.view.backgroundColor} : pal.border, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)}, footerMinimalShellTransform, ]} diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 7a320cb438..8dce85cd17 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -5,16 +5,18 @@ import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigationState} from '@react-navigation/native' +import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {useHomeBadge} from '#/state/home-badge' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useShellLayout} from '#/state/shell/shell-layout' import {useCloseAllActiveElements} from '#/state/util' import {Link} from '#/view/com/util/Link' import {Logo} from '#/view/icons/Logo' @@ -49,6 +51,8 @@ export function BottomBarWeb() { const footerMinimalShellTransform = useMinimalShellFooterTransform() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() + const {footerHeight} = useShellLayout() + const hideBorder = useHideBottomBarBorder() const iconWidth = 26 const unreadMessageCount = useUnreadMessageCount() @@ -74,9 +78,12 @@ export function BottomBarWeb() { styles.bottomBar, styles.bottomBarWeb, t.atoms.bg, - t.atoms.border_contrast_low, + hideBorder + ? {borderColor: t.atoms.bg.backgroundColor} + : t.atoms.border_contrast_low, footerMinimalShellTransform, - ]}> + ]} + onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}> {hasSession ? ( <> Date: Wed, 11 Jun 2025 22:33:43 +0300 Subject: [PATCH 06/49] disable avatar preview in searchable people list (#8453) --- src/components/dialogs/SearchablePeopleList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx index 26e20db57c..81655be0f5 100644 --- a/src/components/dialogs/SearchablePeopleList.tsx +++ b/src/components/dialogs/SearchablePeopleList.tsx @@ -397,6 +397,7 @@ function DefaultProfileCard({ Date: Wed, 11 Jun 2025 22:38:46 +0300 Subject: [PATCH 07/49] Video - remove `MediaInsetBorder` when fullscreen (#8476) * lift up useFullscreen, hide mediainsetborder when fullscreen * Revert "lift up useFullscreen, hide mediainsetborder when fullscreen" This reverts commit 66b17657197e26d9b4c5c951e7cc9eef66519d6d. * just move border outside of div that gets fullscreened --- .../util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx | 6 +++--- .../VideoEmbedInner/web-controls/VideoControls.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 77f6cd0a6c..ce3a7b2c90 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useId, useRef, useState} from 'react' +import {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' import {type AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/macro' @@ -28,7 +28,7 @@ export function VideoEmbedInnerWeb({ const videoRef = useRef(null) const [focused, setFocused] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) - const [hlsLoading, setHlsLoading] = React.useState(false) + const [hlsLoading, setHlsLoading] = useState(false) const figId = useId() const {_} = useLingui() @@ -101,8 +101,8 @@ export function VideoEmbedInnerWeb({ fullscreenRef={containerRef} hasSubtitleTrack={hasSubtitleTrack} /> - + ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx index 8e134d2217..6d14deafc0 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import {useCallback, useEffect, useRef, useState} from 'react' import {Pressable, View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' From 093454f5b21b8b2e6cbe7948c788ffc234b969fc Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 11 Jun 2025 23:19:26 +0300 Subject: [PATCH 08/49] hide keyboard when backgrounding (#8450) --- src/view/com/composer/Composer.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index f5b29664ab..17d0f94f7c 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -64,6 +64,7 @@ import { type SupportedMimeTypes, } from '#/lib/constants' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' +import {useAppState} from '#/lib/hooks/useAppState' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {usePalette} from '#/lib/hooks/usePalette' @@ -822,6 +823,8 @@ let ComposerPost = React.memo(function ComposerPost({ [post.id, onSelectVideo, onImageAdd, _], ) + useHideKeyboardOnBackground() + return ( { + if (isIOS) { + if (appState === 'inactive') { + Keyboard.dismiss() + } + } + }, [appState]) +} + const styles = StyleSheet.create({ topbarInner: { flexDirection: 'row', From 18b258d060e51af1890809cee271af5362e24207 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 12 Jun 2025 01:22:21 +0300 Subject: [PATCH 09/49] Loosen post source constraints (#8478) * Loosen post source constraints * logger warn if failed to find source * Tweak assertion logic --------- Co-authored-by: Eric Bailey --- src/state/unstable-post-source.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx index ac126d79c6..450f2c120b 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -34,7 +34,7 @@ const consumedSources = new Map() * Used for FeedFeedback and other ephemeral non-critical systems. */ export function setUnstablePostSource(key: string, source: PostSource) { - assertValid( + assertValidDevOnly( key, `setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, ) @@ -51,9 +51,10 @@ export function setUnstablePostSource(key: string, source: PostSource) { export function useUnstablePostSource(key: string) { const id = useId() const [source] = useState(() => { - assertValid( + assertValidDevOnly( key, - `consumeUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, + `consumeUnstablePostSource key should be a URI containing a handle, received ${key} — be sure to use buildPostSourceKey when setting the source`, + true, ) const source = consumedSources.get(id) || transientSources.get(key) if (source) { @@ -87,11 +88,15 @@ export function buildPostSourceKey(key: string, handle: string) { /** * Just a lil dev helper */ -function assertValid(key: string, message: string) { +function assertValidDevOnly(key: string, message: string, beChill = false) { if (__DEV__) { const urip = new AtUri(key) if (urip.host.startsWith('did:')) { - throw new Error(message) + if (beChill) { + logger.warn(message) + } else { + throw new Error(message) + } } } } From a26b20b56cd0ac80f625a5eb5136b805b9341e8d Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 12 Jun 2025 02:40:53 +0000 Subject: [PATCH 10/49] Nightly source-language update --- src/locale/locales/en/messages.po | 408 ++++++++++++++++++------------ 1 file changed, 253 insertions(+), 155 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 7f3d35dfce..8bb3dca9e5 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -74,8 +74,8 @@ msgstr "" msgid "{0, plural, one {# second} other {# seconds}}" msgstr "" -#: src/view/shell/bottom-bar/BottomBar.tsx:223 -#: src/view/shell/bottom-bar/BottomBar.tsx:255 +#: src/view/shell/bottom-bar/BottomBar.tsx:225 +#: src/view/shell/bottom-bar/BottomBar.tsx:257 #: src/view/shell/Drawer.tsx:487 msgid "{0, plural, one {# unread item} other {# unread items}}" msgstr "" @@ -95,7 +95,8 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:529 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:461 +#: src/view/com/post-thread/PostThreadItem.tsx:540 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -103,11 +104,13 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:513 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:445 +#: src/view/com/post-thread/PostThreadItem.tsx:524 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:495 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:427 +#: src/view/com/post-thread/PostThreadItem.tsx:506 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" @@ -366,7 +369,7 @@ msgstr "" msgid "{following} following" msgstr "" -#: src/components/dialogs/SearchablePeopleList.tsx:412 +#: src/components/dialogs/SearchablePeopleList.tsx:413 msgid "{handle} can't be messaged" msgstr "" @@ -387,7 +390,7 @@ msgstr "" msgid "{minutes, plural, one {# minute} other {# minutes}}" msgstr "" -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:270 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:277 msgid "{notificationCount, plural, one {# unread item} other {# unread items}}" msgstr "" @@ -647,7 +650,7 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:731 +#: src/view/com/composer/Composer.tsx:772 msgid "Add another post" msgstr "" @@ -678,7 +681,7 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/view/com/composer/Composer.tsx:1287 +#: src/view/com/composer/Composer.tsx:1330 msgid "Add new post" msgstr "" @@ -1060,12 +1063,15 @@ msgstr "" msgid "Apply default recommended feeds" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:945 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:627 +#: src/view/com/post-thread/PostThreadItem.tsx:956 msgid "Archived from {0}" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:914 -#: src/view/com/post-thread/PostThreadItem.tsx:953 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:596 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 +#: src/view/com/post-thread/PostThreadItem.tsx:925 +#: src/view/com/post-thread/PostThreadItem.tsx:964 msgid "Archived post" msgstr "" @@ -1101,11 +1107,11 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:682 +#: src/view/com/composer/Composer.tsx:723 msgid "Are you sure you'd like to discard this draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:861 +#: src/view/com/composer/Composer.tsx:904 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1279,7 +1285,7 @@ msgstr "" msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." msgstr "" -#: src/view/com/post-thread/PostThread.tsx:468 +#: src/view/com/post-thread/PostThread.tsx:488 msgid "Blocked post." msgstr "" @@ -1304,7 +1310,8 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:970 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:652 +#: src/view/com/post-thread/PostThreadItem.tsx:981 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1464,8 +1471,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:270 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 -#: src/view/com/composer/Composer.tsx:916 -#: src/view/com/composer/Composer.tsx:927 +#: src/view/com/composer/Composer.tsx:959 +#: src/view/com/composer/Composer.tsx:970 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/modals/ChangePassword.tsx:279 @@ -1518,7 +1525,7 @@ msgstr "" #: src/components/PostControls/index.tsx:101 #: src/components/PostControls/index.tsx:132 #: src/components/PostControls/index.tsx:160 -#: src/state/shell/composer/index.tsx:82 +#: src/state/shell/composer/index.tsx:91 msgid "Cannot interact with a blocked user" msgstr "" @@ -1592,7 +1599,7 @@ msgid "Changes hosting provider" msgstr "" #: src/Navigation.tsx:428 -#: src/view/shell/bottom-bar/BottomBar.tsx:219 +#: src/view/shell/bottom-bar/BottomBar.tsx:221 #: src/view/shell/desktop/LeftNav.tsx:553 #: src/view/shell/Drawer.tsx:455 msgid "Chat" @@ -1825,7 +1832,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:924 +#: src/view/com/composer/Composer.tsx:967 msgid "Closes post composer and discards post draft" msgstr "" @@ -1882,15 +1889,15 @@ msgstr "" msgid "Compose new post" msgstr "" -#: src/view/com/composer/Composer.tsx:825 +#: src/view/com/composer/Composer.tsx:868 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:34 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:67 msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:1669 +#: src/view/com/composer/Composer.tsx:1724 msgid "Compressing video..." msgstr "" @@ -2014,6 +2021,11 @@ msgstr "" msgid "Continue as {0} (currently signed in)" msgstr "" +#: src/screens/PostThread/components/ThreadItemReadMoreUp.tsx:27 +msgid "Continue thread" +msgstr "" + +#: src/screens/PostThread/components/ThreadItemReadMoreUp.tsx:63 #: src/view/com/post-thread/PostThreadLoadMore.tsx:60 msgid "Continue thread..." msgstr "" @@ -2196,10 +2208,10 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:55 #: src/view/com/auth/SplashScreen.web.tsx:117 -#: src/view/shell/bottom-bar/BottomBar.tsx:343 -#: src/view/shell/bottom-bar/BottomBar.tsx:348 -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:199 -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:204 +#: src/view/shell/bottom-bar/BottomBar.tsx:345 +#: src/view/shell/bottom-bar/BottomBar.tsx:350 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:206 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:211 #: src/view/shell/NavSignupCard.tsx:47 #: src/view/shell/NavSignupCard.tsx:52 msgid "Create account" @@ -2383,7 +2395,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:682 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:684 -#: src/view/com/composer/Composer.tsx:835 +#: src/view/com/composer/Composer.tsx:878 msgid "Delete post" msgstr "" @@ -2413,7 +2425,7 @@ msgstr "" msgid "Deleted Account" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:454 +#: src/view/com/post-thread/PostThread.tsx:474 msgid "Deleted post." msgstr "" @@ -2502,8 +2514,8 @@ msgid "Disabled" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:88 -#: src/view/com/composer/Composer.tsx:684 -#: src/view/com/composer/Composer.tsx:868 +#: src/view/com/composer/Composer.tsx:725 +#: src/view/com/composer/Composer.tsx:911 msgid "Discard" msgstr "" @@ -2511,11 +2523,11 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:681 +#: src/view/com/composer/Composer.tsx:722 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:860 +#: src/view/com/composer/Composer.tsx:903 msgid "Discard post?" msgstr "" @@ -2541,7 +2553,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/view/com/composer/Composer.tsx:1593 +#: src/view/com/composer/Composer.tsx:1648 msgid "Dismiss error" msgstr "" @@ -2651,7 +2663,7 @@ msgstr "" msgid "Double tap to close the dialog" msgstr "" -#: src/screens/VideoFeed/index.tsx:1077 +#: src/screens/VideoFeed/index.tsx:1080 msgid "Double tap to like" msgstr "" @@ -3021,11 +3033,15 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:1678 +#: src/view/com/composer/Composer.tsx:1733 #: src/view/com/util/error/ErrorScreen.tsx:42 msgid "Error" msgstr "" +#: src/screens/PostThread/components/ThreadError.tsx:26 +msgid "Error loading post" +msgstr "" + #: src/screens/Settings/components/ExportCarDialog.tsx:47 msgid "Error occurred while saving file" msgstr "" @@ -3105,7 +3121,7 @@ msgstr "" msgid "Expand or collapse the full post you are replying to" msgstr "" -#: src/screens/VideoFeed/index.tsx:962 +#: src/screens/VideoFeed/index.tsx:965 msgid "Expands or collapses post text" msgstr "" @@ -3114,7 +3130,7 @@ msgid "Expected uri to resolve to a record" msgstr "" #: src/screens/Settings/FollowingFeedPreferences.tsx:123 -#: src/screens/Settings/ThreadPreferences.tsx:137 +#: src/screens/Settings/ThreadPreferences.tsx:271 msgid "Experimental" msgstr "" @@ -3638,7 +3654,7 @@ msgstr "" msgid "From @{sanitizedAuthor}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:326 +#: src/view/com/posts/PostFeedItem.tsx:328 msgctxt "from-feed" msgid "From <0/>" msgstr "" @@ -3685,8 +3701,8 @@ msgstr "" #: src/screens/Messages/Inbox.tsx:228 #: src/screens/Profile/ProfileFeed/index.tsx:92 #: src/screens/VideoFeed/components/Header.tsx:163 -#: src/screens/VideoFeed/index.tsx:1138 -#: src/screens/VideoFeed/index.tsx:1142 +#: src/screens/VideoFeed/index.tsx:1141 +#: src/screens/VideoFeed/index.tsx:1145 #: src/view/com/auth/LoggedOut.tsx:72 #: src/view/screens/NotFound.tsx:57 #: src/view/screens/ProfileList.tsx:1038 @@ -3956,7 +3972,7 @@ msgstr "" #: src/Navigation.tsx:620 #: src/Navigation.tsx:640 -#: src/view/shell/bottom-bar/BottomBar.tsx:176 +#: src/view/shell/bottom-bar/BottomBar.tsx:178 #: src/view/shell/desktop/LeftNav.tsx:617 #: src/view/shell/Drawer.tsx:429 msgid "Home" @@ -3975,10 +3991,10 @@ msgstr "" msgid "Hot" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:67 -#: src/screens/Settings/ThreadPreferences.tsx:70 -#: src/view/com/post-thread/PostThread.tsx:655 -#: src/view/com/post-thread/PostThread.tsx:660 +#: src/screens/Settings/ThreadPreferences.tsx:201 +#: src/screens/Settings/ThreadPreferences.tsx:204 +#: src/view/com/post-thread/PostThread.tsx:679 +#: src/view/com/post-thread/PostThread.tsx:684 msgid "Hot replies first" msgstr "" @@ -4133,7 +4149,7 @@ msgstr "" msgid "Invalid handle. Please try a different one." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:339 +#: src/view/com/post-thread/PostThreadItem.tsx:350 msgid "Invalid or unsupported post record" msgstr "" @@ -4185,7 +4201,7 @@ msgstr "" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "" -#: src/view/com/composer/Composer.tsx:1612 +#: src/view/com/composer/Composer.tsx:1667 msgid "Job ID: {0}" msgstr "" @@ -4423,12 +4439,15 @@ msgstr "" msgid "Likes" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:232 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:454 +#: src/view/com/post-thread/PostThreadItem.tsx:242 msgid "Likes on this post" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:629 -#: src/view/com/post-thread/PostThread.tsx:634 +#: src/screens/PostThread/components/HeaderDropdown.tsx:47 +#: src/screens/PostThread/components/HeaderDropdown.tsx:52 +#: src/view/com/post-thread/PostThread.tsx:653 +#: src/view/com/post-thread/PostThread.tsx:658 msgid "Linear" msgstr "" @@ -4766,7 +4785,7 @@ msgstr "" msgid "Moderator has chosen to set a general warning on the content." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:721 +#: src/view/com/post-thread/PostThreadItem.tsx:732 msgid "More" msgstr "" @@ -4781,13 +4800,13 @@ msgstr "" msgid "More options" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:94 +#: src/screens/Settings/ThreadPreferences.tsx:228 msgid "Most-liked first" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:91 -#: src/view/com/post-thread/PostThread.tsx:685 -#: src/view/com/post-thread/PostThread.tsx:690 +#: src/screens/Settings/ThreadPreferences.tsx:225 +#: src/view/com/post-thread/PostThread.tsx:709 +#: src/view/com/post-thread/PostThread.tsx:714 msgid "Most-liked replies first" msgstr "" @@ -5046,10 +5065,14 @@ msgstr "" msgid "New User List" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:83 -#: src/screens/Settings/ThreadPreferences.tsx:86 -#: src/view/com/post-thread/PostThread.tsx:675 -#: src/view/com/post-thread/PostThread.tsx:680 +#: src/screens/PostThread/components/HeaderDropdown.tsx:93 +#: src/screens/PostThread/components/HeaderDropdown.tsx:98 +#: src/screens/Settings/ThreadPreferences.tsx:96 +#: src/screens/Settings/ThreadPreferences.tsx:99 +#: src/screens/Settings/ThreadPreferences.tsx:217 +#: src/screens/Settings/ThreadPreferences.tsx:220 +#: src/view/com/post-thread/PostThread.tsx:699 +#: src/view/com/post-thread/PostThread.tsx:704 msgid "Newest replies first" msgstr "" @@ -5263,7 +5286,7 @@ msgstr "" #: src/Navigation.tsx:630 #: src/view/screens/Notifications.tsx:128 -#: src/view/shell/bottom-bar/BottomBar.tsx:250 +#: src/view/shell/bottom-bar/BottomBar.tsx:252 #: src/view/shell/desktop/LeftNav.tsx:654 #: src/view/shell/Drawer.tsx:482 msgid "Notifications" @@ -5309,14 +5332,19 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/view/com/post-thread/PostThreadItem.tsx:975 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:657 +#: src/view/com/post-thread/PostThreadItem.tsx:986 msgid "Okay" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:75 -#: src/screens/Settings/ThreadPreferences.tsx:78 -#: src/view/com/post-thread/PostThread.tsx:665 -#: src/view/com/post-thread/PostThread.tsx:670 +#: src/screens/PostThread/components/HeaderDropdown.tsx:83 +#: src/screens/PostThread/components/HeaderDropdown.tsx:88 +#: src/screens/Settings/ThreadPreferences.tsx:88 +#: src/screens/Settings/ThreadPreferences.tsx:91 +#: src/screens/Settings/ThreadPreferences.tsx:209 +#: src/screens/Settings/ThreadPreferences.tsx:212 +#: src/view/com/post-thread/PostThread.tsx:689 +#: src/view/com/post-thread/PostThread.tsx:694 msgid "Oldest replies first" msgstr "" @@ -5328,15 +5356,15 @@ msgstr "" msgid "Onboarding reset" msgstr "" -#: src/view/com/composer/Composer.tsx:342 +#: src/view/com/composer/Composer.tsx:346 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:339 +#: src/view/com/composer/Composer.tsx:343 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:349 +#: src/view/com/composer/Composer.tsx:353 msgid "One or more videos is missing alt text." msgstr "" @@ -5392,7 +5420,7 @@ msgid "Open drawer menu" msgstr "" #: src/screens/Messages/components/MessageInput.web.tsx:181 -#: src/view/com/composer/Composer.tsx:1272 +#: src/view/com/composer/Composer.tsx:1315 msgid "Open emoji picker" msgstr "" @@ -5487,7 +5515,7 @@ msgstr "" msgid "Opens change handle dialog" msgstr "" -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:35 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:68 msgid "Opens composer" msgstr "" @@ -5495,7 +5523,7 @@ msgstr "" msgid "Opens device photo gallery" msgstr "" -#: src/view/com/composer/Composer.tsx:1273 +#: src/view/com/composer/Composer.tsx:1316 msgid "Opens emoji picker" msgstr "" @@ -5702,7 +5730,7 @@ msgstr "" msgid "Pin to your profile" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:407 +#: src/view/com/posts/PostFeedItem.tsx:409 msgid "Pinned" msgstr "" @@ -5877,22 +5905,27 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:987 +#: src/view/com/composer/Composer.tsx:1030 msgctxt "action" msgid "Post" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:540 +#: src/screens/PostThread/index.tsx:490 +#: src/view/com/post-thread/PostThread.tsx:561 msgctxt "description" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:985 +#: src/view/com/composer/Composer.tsx:1028 msgctxt "action" msgid "Post All" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:224 +#: src/screens/PostThread/components/ThreadItemPostTombstone.tsx:22 +msgid "Post blocked" +msgstr "" + +#: src/view/com/post-thread/PostThreadItem.tsx:234 msgid "Post by {0}" msgstr "" @@ -5912,11 +5945,14 @@ msgstr "" msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:133 +#: src/screens/PostThread/components/ThreadItemPost.tsx:112 +#: src/screens/PostThread/components/ThreadItemTreePost.tsx:109 #: src/screens/VideoFeed/index.tsx:529 msgid "Post has been deleted" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:269 +#: src/view/com/post-thread/PostThread.tsx:271 msgid "Post hidden" msgstr "" @@ -5947,8 +5983,10 @@ msgstr "" msgid "Post Languages" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:264 -#: src/view/com/post-thread/PostThread.tsx:276 +#: src/screens/PostThread/components/ThreadError.tsx:32 +#: src/screens/PostThread/components/ThreadItemPostTombstone.tsx:25 +#: src/view/com/post-thread/PostThread.tsx:266 +#: src/view/com/post-thread/PostThread.tsx:278 msgid "Post not found" msgstr "" @@ -6008,8 +6046,10 @@ msgstr "" msgid "Primary Language" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:112 -#: src/screens/Settings/ThreadPreferences.tsx:117 +#: src/screens/Settings/ThreadPreferences.tsx:110 +#: src/screens/Settings/ThreadPreferences.tsx:115 +#: src/screens/Settings/ThreadPreferences.tsx:246 +#: src/screens/Settings/ThreadPreferences.tsx:251 msgid "Prioritize your Follows" msgstr "" @@ -6041,7 +6081,7 @@ msgstr "" msgid "Privacy Policy" msgstr "" -#: src/view/com/composer/Composer.tsx:1675 +#: src/view/com/composer/Composer.tsx:1730 msgid "Processing video..." msgstr "" @@ -6055,7 +6095,7 @@ msgstr "" msgid "profile" msgstr "" -#: src/view/shell/bottom-bar/BottomBar.tsx:314 +#: src/view/shell/bottom-bar/BottomBar.tsx:316 #: src/view/shell/desktop/LeftNav.tsx:709 #: src/view/shell/Drawer.tsx:76 #: src/view/shell/Drawer.tsx:559 @@ -6081,22 +6121,22 @@ msgid "Public, sharable lists which can be used to drive feeds." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:967 +#: src/view/com/composer/Composer.tsx:1010 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:960 +#: src/view/com/composer/Composer.tsx:1003 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:945 +#: src/view/com/composer/Composer.tsx:988 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:952 +#: src/view/com/composer/Composer.tsx:995 msgid "Publish reply" msgstr "" @@ -6142,14 +6182,15 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:258 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:438 +#: src/view/com/post-thread/PostThreadItem.tsx:268 msgid "Quotes of this post" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:99 -#: src/screens/Settings/ThreadPreferences.tsx:102 -#: src/view/com/post-thread/PostThread.tsx:695 -#: src/view/com/post-thread/PostThread.tsx:700 +#: src/screens/Settings/ThreadPreferences.tsx:233 +#: src/screens/Settings/ThreadPreferences.tsx:236 +#: src/view/com/post-thread/PostThread.tsx:719 +#: src/view/com/post-thread/PostThread.tsx:724 msgid "Random (aka \"Poster's Roulette\")" msgstr "" @@ -6170,19 +6211,27 @@ msgstr "" msgid "Reactivate your account" msgstr "" +#: src/screens/PostThread/components/ThreadItemReadMore.tsx:92 +msgid "Read {0} more {1, plural, one {reply} other {replies}}" +msgstr "" + #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:158 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:173 msgid "Read blog post" msgstr "" -#: src/screens/VideoFeed/index.tsx:963 +#: src/screens/VideoFeed/index.tsx:966 msgid "Read less" msgstr "" -#: src/screens/VideoFeed/index.tsx:963 +#: src/screens/VideoFeed/index.tsx:966 msgid "Read more" msgstr "" +#: src/screens/PostThread/components/ThreadItemReadMore.tsx:71 +msgid "Read more replies" +msgstr "" + #: src/view/com/auth/SplashScreen.web.tsx:173 msgid "Read the Bluesky blog" msgstr "" @@ -6409,7 +6458,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:983 +#: src/view/com/composer/Composer.tsx:1026 msgctxt "action" msgid "Reply" msgstr "" @@ -6437,28 +6486,29 @@ msgstr "" msgid "Reply settings are chosen by the author of the thread" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:651 +#: src/screens/PostThread/components/HeaderDropdown.tsx:69 +#: src/view/com/post-thread/PostThread.tsx:675 msgid "Reply sorting" msgstr "" #: src/view/com/post/Post.tsx:205 -#: src/view/com/posts/PostFeedItem.tsx:605 +#: src/view/com/posts/PostFeedItem.tsx:607 msgctxt "description" msgid "Reply to <0><1/>" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:596 +#: src/view/com/posts/PostFeedItem.tsx:598 msgctxt "description" msgid "Reply to a blocked post" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:598 +#: src/view/com/posts/PostFeedItem.tsx:600 msgctxt "description" msgid "Reply to a post" msgstr "" #: src/view/com/post/Post.tsx:203 -#: src/view/com/posts/PostFeedItem.tsx:602 +#: src/view/com/posts/PostFeedItem.tsx:604 msgctxt "description" msgid "Reply to you" msgstr "" @@ -6587,20 +6637,21 @@ msgstr "" msgid "Reposted By" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:347 +#: src/view/com/posts/PostFeedItem.tsx:349 msgid "Reposted by {0}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:366 +#: src/view/com/posts/PostFeedItem.tsx:368 msgid "Reposted by <0><1/>" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:345 -#: src/view/com/posts/PostFeedItem.tsx:364 +#: src/view/com/posts/PostFeedItem.tsx:347 +#: src/view/com/posts/PostFeedItem.tsx:366 msgid "Reposted by you" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:237 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:420 +#: src/view/com/post-thread/PostThreadItem.tsx:247 msgid "Reposts of this post" msgstr "" @@ -6691,6 +6742,8 @@ msgstr "" #: src/screens/Messages/Inbox.tsx:197 #: src/screens/Onboarding/StepInterests/index.tsx:217 #: src/screens/Onboarding/StepInterests/index.tsx:220 +#: src/screens/PostThread/components/ThreadError.tsx:75 +#: src/screens/PostThread/components/ThreadError.tsx:81 #: src/screens/Signup/BackNextButtons.tsx:53 #: src/view/com/util/error/ErrorMessage.tsx:60 #: src/view/com/util/error/ErrorScreen.tsx:97 @@ -6715,7 +6768,7 @@ msgstr "" #: src/screens/Profile/ProfileFeed/index.tsx:93 #: src/screens/Settings/components/ChangeHandleDialog.tsx:559 -#: src/screens/VideoFeed/index.tsx:1139 +#: src/screens/VideoFeed/index.tsx:1142 #: src/view/screens/NotFound.tsx:60 #: src/view/screens/ProfileList.tsx:1039 msgid "Returns to previous page" @@ -6820,12 +6873,12 @@ msgstr "" msgid "Scroll to top" msgstr "" -#: src/components/dialogs/SearchablePeopleList.tsx:513 +#: src/components/dialogs/SearchablePeopleList.tsx:514 #: src/components/forms/SearchInput.tsx:34 #: src/components/forms/SearchInput.tsx:36 #: src/screens/Search/Shell.tsx:307 #: src/screens/Search/Shell.tsx:464 -#: src/view/shell/bottom-bar/BottomBar.tsx:196 +#: src/view/shell/bottom-bar/BottomBar.tsx:198 msgid "Search" msgstr "" @@ -6884,7 +6937,7 @@ msgstr "" msgid "Search posts" msgstr "" -#: src/components/dialogs/SearchablePeopleList.tsx:533 +#: src/components/dialogs/SearchablePeopleList.tsx:534 #: src/components/ProgressGuide/FollowDialog.tsx:702 msgid "Search profiles" msgstr "" @@ -6897,7 +6950,7 @@ msgstr "" msgid "Search..." msgstr "" -#: src/components/dialogs/SearchablePeopleList.tsx:534 +#: src/components/dialogs/SearchablePeopleList.tsx:535 #: src/components/ProgressGuide/FollowDialog.tsx:703 msgid "Searches for profiles" msgstr "" @@ -7304,9 +7357,11 @@ msgstr "" msgid "Show list anyway" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:681 +#: src/screens/PostThread/components/ThreadItemPost.tsx:318 +#: src/screens/PostThread/components/ThreadItemTreePost.tsx:364 +#: src/view/com/post-thread/PostThreadItem.tsx:692 #: src/view/com/post/Post.tsx:244 -#: src/view/com/posts/PostFeedItem.tsx:561 +#: src/view/com/posts/PostFeedItem.tsx:563 msgid "Show More" msgstr "" @@ -7315,10 +7370,18 @@ msgstr "" msgid "Show more like this" msgstr "" +#: src/screens/PostThread/components/ThreadItemShowOtherReplies.tsx:14 +msgid "Show more replies" +msgstr "" + #: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:22 msgid "Show muted replies" msgstr "" +#: src/screens/Settings/ThreadPreferences.tsx:143 +msgid "Show post replies in a threaded tree view" +msgstr "" + #: src/screens/Settings/FollowingFeedPreferences.tsx:104 #: src/screens/Settings/FollowingFeedPreferences.tsx:114 msgid "Show quote posts" @@ -7329,15 +7392,17 @@ msgstr "" msgid "Show replies" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:625 +#: src/screens/PostThread/components/HeaderDropdown.tsx:43 +#: src/view/com/post-thread/PostThread.tsx:649 msgid "Show replies as" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:151 +#: src/screens/Settings/ThreadPreferences.tsx:285 msgid "Show replies as threaded" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:126 +#: src/screens/Settings/ThreadPreferences.tsx:120 +#: src/screens/Settings/ThreadPreferences.tsx:260 msgid "Show replies by people you follow before all other replies" msgstr "" @@ -7364,7 +7429,8 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:916 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:598 +#: src/view/com/post-thread/PostThreadItem.tsx:927 msgid "Shows information about when this post was created" msgstr "" @@ -7386,10 +7452,10 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:69 #: src/view/com/auth/SplashScreen.web.tsx:123 #: src/view/com/auth/SplashScreen.web.tsx:131 -#: src/view/shell/bottom-bar/BottomBar.tsx:353 -#: src/view/shell/bottom-bar/BottomBar.tsx:358 -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:209 -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:214 +#: src/view/shell/bottom-bar/BottomBar.tsx:355 +#: src/view/shell/bottom-bar/BottomBar.tsx:360 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:216 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:221 #: src/view/shell/NavSignupCard.tsx:57 #: src/view/shell/NavSignupCard.tsx:62 msgid "Sign in" @@ -7528,6 +7594,10 @@ msgstr "" msgid "Something went wrong!" msgstr "" +#: src/screens/PostThread/components/ThreadError.tsx:27 +msgid "Something went wrong. Please try again in a moment." +msgstr "" + #: src/components/moderation/ReportDialog/index.tsx:178 msgid "Something went wrong. Please try again." msgstr "" @@ -7541,15 +7611,18 @@ msgstr "" msgid "Sorry! Your session expired. Please sign in again." msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:55 +#: src/screens/Settings/ThreadPreferences.tsx:68 +#: src/screens/Settings/ThreadPreferences.tsx:189 msgid "Sort replies" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:62 +#: src/screens/Settings/ThreadPreferences.tsx:75 +#: src/screens/Settings/ThreadPreferences.tsx:196 msgid "Sort replies by" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:59 +#: src/screens/Settings/ThreadPreferences.tsx:72 +#: src/screens/Settings/ThreadPreferences.tsx:193 msgid "Sort replies to the same post by:" msgstr "" @@ -7878,7 +7951,7 @@ msgstr "" msgid "That's all, folks!" msgstr "" -#: src/screens/VideoFeed/index.tsx:1111 +#: src/screens/VideoFeed/index.tsx:1114 msgid "That's everything!" msgstr "" @@ -7941,8 +8014,8 @@ msgstr "" 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 "" -#: src/view/com/post-thread/PostThread.tsx:265 -#: src/view/com/post-thread/PostThread.tsx:277 +#: src/view/com/post-thread/PostThread.tsx:267 +#: src/view/com/post-thread/PostThread.tsx:279 msgid "The post may have been deleted." msgstr "" @@ -8230,7 +8303,8 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:956 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:638 +#: src/view/com/post-thread/PostThreadItem.tsx:967 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -8238,7 +8312,7 @@ msgstr "" msgid "This post has an unknown type of threadgate on it. Your app may be out of date." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:163 +#: src/view/com/post-thread/PostThreadItem.tsx:170 msgid "This post has been deleted." msgstr "" @@ -8250,7 +8324,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:424 +#: src/view/com/composer/Composer.tsx:462 msgid "This post's author has disabled quote posts." msgstr "" @@ -8319,8 +8393,10 @@ msgstr "" msgid "This will remove your post from this quote post for all users, and replace it with a placeholder." msgstr "" -#: src/view/com/post-thread/PostThread.tsx:609 -#: src/view/com/post-thread/PostThread.tsx:612 +#: src/screens/PostThread/components/HeaderDropdown.tsx:23 +#: src/screens/PostThread/components/HeaderDropdown.tsx:26 +#: src/view/com/post-thread/PostThread.tsx:633 +#: src/view/com/post-thread/PostThread.tsx:636 msgid "Thread options" msgstr "" @@ -8329,16 +8405,19 @@ msgstr "" msgid "Thread preferences" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:45 +#: src/screens/Settings/ThreadPreferences.tsx:58 +#: src/screens/Settings/ThreadPreferences.tsx:179 msgid "Thread Preferences" msgstr "" -#: src/view/com/post-thread/PostThread.tsx:639 -#: src/view/com/post-thread/PostThread.tsx:644 +#: src/screens/PostThread/components/HeaderDropdown.tsx:57 +#: src/screens/PostThread/components/HeaderDropdown.tsx:62 +#: src/view/com/post-thread/PostThread.tsx:663 +#: src/view/com/post-thread/PostThread.tsx:668 msgid "Threaded" msgstr "" -#: src/screens/Settings/ThreadPreferences.tsx:142 +#: src/screens/Settings/ThreadPreferences.tsx:276 msgid "Threaded mode" msgstr "" @@ -8389,6 +8468,13 @@ msgstr "" msgid "Top" msgstr "" +#: src/screens/PostThread/components/HeaderDropdown.tsx:73 +#: src/screens/PostThread/components/HeaderDropdown.tsx:78 +#: src/screens/Settings/ThreadPreferences.tsx:80 +#: src/screens/Settings/ThreadPreferences.tsx:83 +msgid "Top replies first" +msgstr "" + #: src/Navigation.tsx:423 msgid "Topic" msgstr "" @@ -8397,11 +8483,18 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:145 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 -#: src/view/com/post-thread/PostThreadItem.tsx:878 -#: src/view/com/post-thread/PostThreadItem.tsx:881 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:560 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:563 +#: src/view/com/post-thread/PostThreadItem.tsx:889 +#: src/view/com/post-thread/PostThreadItem.tsx:892 msgid "Translate" msgstr "" +#: src/screens/Settings/ThreadPreferences.tsx:131 +#: src/screens/Settings/ThreadPreferences.tsx:136 +msgid "Tree view" +msgstr "" + #: src/view/shell/desktop/SidebarTrendingTopics.tsx:59 msgid "Trending" msgstr "" @@ -8631,7 +8724,7 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/Composer.tsx:769 +#: src/view/com/composer/Composer.tsx:810 msgid "Unsupported video type" msgstr "" @@ -8721,7 +8814,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:1672 +#: src/view/com/composer/Composer.tsx:1727 msgid "Uploading video..." msgstr "" @@ -8937,11 +9030,11 @@ msgstr "" msgid "Video Games" msgstr "" -#: src/screens/VideoFeed/index.tsx:1069 +#: src/screens/VideoFeed/index.tsx:1072 msgid "Video is paused" msgstr "" -#: src/screens/VideoFeed/index.tsx:1069 +#: src/screens/VideoFeed/index.tsx:1072 msgid "Video is playing" msgstr "" @@ -8953,7 +9046,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:1682 +#: src/view/com/composer/Composer.tsx:1737 msgid "Video uploaded" msgstr "" @@ -9062,7 +9155,7 @@ msgid "View your default post interaction settings" msgstr "" #: src/view/com/home/HomeHeaderLayout.web.tsx:56 -#: src/view/com/home/HomeHeaderLayoutMobile.tsx:77 +#: src/view/com/home/HomeHeaderLayoutMobile.tsx:71 msgid "View your feeds and explore more" msgstr "" @@ -9204,7 +9297,7 @@ msgstr "" msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "" -#: src/view/com/composer/Composer.tsx:421 +#: src/view/com/composer/Composer.tsx:459 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -9235,7 +9328,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.web.tsx:99 -#: src/view/com/composer/Composer.tsx:732 +#: src/view/com/composer/Composer.tsx:773 msgid "What's up?" msgstr "" @@ -9317,12 +9410,12 @@ msgstr "" msgid "Write a message" msgstr "" -#: src/view/com/composer/Composer.tsx:823 +#: src/view/com/composer/Composer.tsx:866 msgid "Write post" msgstr "" -#: src/view/com/composer/Composer.tsx:730 -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:69 +#: src/view/com/composer/Composer.tsx:771 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:95 msgid "Write your reply" msgstr "" @@ -9483,7 +9576,7 @@ msgstr "" msgid "You don't have any saved feeds." msgstr "" -#: src/view/com/post-thread/PostThread.tsx:271 +#: src/view/com/post-thread/PostThread.tsx:273 msgid "You have blocked the author or you have been blocked by the author." msgstr "" @@ -9603,6 +9696,11 @@ msgstr "" msgid "You must select at least one labeler for a report" msgstr "" +#: src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx:27 +#: src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx:47 +msgid "You must sign in to view this post." +msgstr "" + #: src/components/dialogs/EmailDialog/screens/Manage2FA/index.tsx:23 msgid "You need to verify your email address before you can enable email 2FA." msgstr "" @@ -9714,7 +9812,7 @@ msgstr "" msgid "You've reached your daily limit for video uploads (too many videos)" msgstr "" -#: src/screens/VideoFeed/index.tsx:1120 +#: src/screens/VideoFeed/index.tsx:1123 msgid "You've run out of videos to watch. Maybe it's a good time to take a break?" msgstr "" @@ -9834,11 +9932,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:481 +#: src/view/com/composer/Composer.tsx:521 msgid "Your post has been published" msgstr "" -#: src/view/com/composer/Composer.tsx:478 +#: src/view/com/composer/Composer.tsx:518 msgid "Your posts have been published" msgstr "" @@ -9850,7 +9948,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:480 +#: src/view/com/composer/Composer.tsx:520 msgid "Your reply has been published" msgstr "" From 477e5f4ecfaa0007aeed90b51274c78a730c1a9e Mon Sep 17 00:00:00 2001 From: hailey Date: Thu, 12 Jun 2025 10:46:22 -0700 Subject: [PATCH 11/49] new arch (#8295) Co-authored-by: Samuel Newman Co-authored-by: Charlotte Som Co-authored-by: Hailey --- __tests__/lib/images.test.ts | 51 +- app.config.js | 2 +- jest/jestSetup.js | 9 +- package.json | 78 +- ...t-native-paste-input+0.8.1.patch.disabled} | 111 ++- ...atch => @sentry+react-native+6.14.0.patch} | 0 ....patch => expo-media-library+17.1.7.patch} | 0 ...12.patch => expo-modules-core+2.4.0.patch} | 0 ...ch.md => expo-modules-core+2.4.0.patch.md} | 0 ....patch => expo-notifications+0.31.3.patch} | 0 ....md => expo-notifications+0.31.3.patch.md} | 0 ...28.12.patch => expo-updates+0.28.14.patch} | 0 ...patch.md => expo-updates+0.28.14.patch.md} | 0 ...0.79.2.patch => react-native+0.79.3.patch} | 49 +- ....patch.md => react-native+0.79.3.patch.md} | 0 ...2.patch => react-native-svg+15.12.0.patch} | 0 src/components/forms/TextField.tsx | 14 +- src/lib/hooks/useHandleRef.ts | 39 - src/lib/media/manip.ts | 87 +- src/screens/Profile/Header/Shell.tsx | 24 +- .../com/composer/text-input/TextInput.tsx | 14 +- src/view/com/pager/Pager.tsx | 222 ++--- src/view/com/pager/Pager.web.tsx | 49 +- src/view/com/pager/PagerWithHeader.tsx | 315 ++++--- src/view/com/profile/ProfileSubpageHeader.tsx | 24 +- src/view/com/util/Toast.web.tsx | 1 - src/view/com/util/images/AutoSizedImage.tsx | 22 +- src/view/com/util/images/Gallery.tsx | 16 +- src/view/com/util/images/ImageLayoutGrid.tsx | 20 +- src/view/com/util/post-embeds/index.tsx | 11 +- yarn.lock | 766 +++++++++--------- 31 files changed, 1013 insertions(+), 911 deletions(-) rename patches/{@mattermost+react-native-paste-input+0.7.1.patch => @mattermost+react-native-paste-input+0.8.1.patch.disabled} (50%) rename patches/{@sentry+react-native+6.10.0.patch => @sentry+react-native+6.14.0.patch} (100%) rename patches/{expo-media-library+17.1.6.patch => expo-media-library+17.1.7.patch} (100%) rename patches/{expo-modules-core+2.3.12.patch => expo-modules-core+2.4.0.patch} (100%) rename patches/{expo-modules-core+2.3.12.patch.md => expo-modules-core+2.4.0.patch.md} (100%) rename patches/{expo-notifications+0.31.1.patch => expo-notifications+0.31.3.patch} (100%) rename patches/{expo-notifications+0.31.1.patch.md => expo-notifications+0.31.3.patch.md} (100%) rename patches/{expo-updates+0.28.12.patch => expo-updates+0.28.14.patch} (100%) rename patches/{expo-updates+0.28.12.patch.md => expo-updates+0.28.14.patch.md} (100%) rename patches/{react-native+0.79.2.patch => react-native+0.79.3.patch} (70%) rename patches/{react-native+0.79.2.patch.md => react-native+0.79.3.patch.md} (100%) rename patches/{react-native-svg+15.11.2.patch => react-native-svg+15.12.0.patch} (100%) delete mode 100644 src/lib/hooks/useHandleRef.ts diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index a5acad25f6..c7a645d39c 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,10 +1,9 @@ -import {deleteAsync} from 'expo-file-system' +import {createDownloadResumable, deleteAsync} from 'expo-file-system' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' -import RNFetchBlob from 'rn-fetch-blob' import { downloadAndResize, - DownloadAndResizeOpts, + type DownloadAndResizeOpts, getResizedDimensions, } from '../../src/lib/media/manip' @@ -32,11 +31,12 @@ describe('downloadAndResize', () => { }) it('should return resized image for valid URI and options', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), - info: jest.fn().mockReturnValue({status: 200}), - flush: jest.fn(), + const mockedFetch = createDownloadResumable as jest.Mock + mockedFetch.mockReturnValue({ + cancelAsync: jest.fn(), + downloadAsync: jest + .fn() + .mockResolvedValue({uri: 'file://resized-image.jpg'}), }) const opts: DownloadAndResizeOpts = { @@ -50,13 +50,12 @@ describe('downloadAndResize', () => { const result = await downloadAndResize(opts) expect(result).toEqual(mockResizedImage) - expect(RNFetchBlob.config).toHaveBeenCalledWith({ - fileCache: true, - appendExt: 'jpeg', - }) - expect(RNFetchBlob.fetch).toHaveBeenCalledWith( - 'GET', - 'https://example.com/image.jpg', + expect(createDownloadResumable).toHaveBeenCalledWith( + opts.uri, + expect.anything(), + { + cache: true, + }, ) // First time it gets called is to get dimensions @@ -86,28 +85,6 @@ describe('downloadAndResize', () => { expect(result).toBeUndefined() }) - it('should return undefined for non-200 response', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image'), - info: jest.fn().mockReturnValue({status: 400}), - flush: jest.fn(), - }) - - const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(errorSpy).not.toHaveBeenCalled() - expect(result).toBeUndefined() - }) - it('should not downsize whenever dimensions are below the max dimensions', () => { const initialDimensionsOne = { width: 1200, diff --git a/app.config.js b/app.config.js index 0fa91f2ced..36af084158 100644 --- a/app.config.js +++ b/app.config.js @@ -219,7 +219,7 @@ module.exports = function (_config) { compileSdkVersion: 35, targetSdkVersion: 35, buildToolsVersion: '35.0.0', - newArchEnabled: false, + newArchEnabled: true, }, }, ], diff --git a/jest/jestSetup.js b/jest/jestSetup.js index d303225f6c..700d20afc0 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => { } }) -jest.mock('rn-fetch-blob', () => ({ - config: jest.fn().mockReturnThis(), - cancel: jest.fn(), - fetch: jest.fn(), -})) - jest.mock('expo-file-system', () => ({ getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}), deleteAsync: jest.fn(), + createDownloadResumable: jest.fn(), })) jest.mock('expo-image-manipulator', () => ({ @@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({ } } }), - requireNativeViewManager: jest.fn().mockImplementation(moduleName => { + requireNativeViewManager: jest.fn().mockImplementation(_ => { return () => null }), })) diff --git a/package.json b/package.json index f0bc5bbe75..38f574f42f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@emoji-mart/react": "^1.1.1", - "@expo/html-elements": "^0.12.4", + "@expo/html-elements": "^0.12.5", "@expo/webpack-config": "^19.0.1", "@floating-ui/dom": "^1.6.3", "@floating-ui/react-dom": "^2.0.8", @@ -85,12 +85,12 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@haileyok/bluesky-video": "0.2.6", + "@haileyok/bluesky-video": "0.3.1", "@ipld/dag-cbor": "^9.2.0", "@lingui/react": "^4.14.1", - "@mattermost/react-native-paste-input": "^0.7.1", - "@miblanchard/react-native-slider": "^2.3.1", - "@mozzius/expo-dynamic-app-icon": "^1.5.0", + "@mattermost/react-native-paste-input": "mattermost/react-native-paste-input", + "@miblanchard/react-native-slider": "^2.6.0", + "@mozzius/expo-dynamic-app-icon": "1.5.0", "@react-native-async-storage/async-storage": "2.1.2", "@react-native-menu/menu": "^1.2.3", "@react-native-picker/picker": "2.11.0", @@ -98,7 +98,7 @@ "@react-navigation/drawer": "^7.3.12", "@react-navigation/native": "^7.1.9", "@react-navigation/native-stack": "^7.3.13", - "@sentry/react-native": "~6.10.0", + "@sentry/react-native": "~6.14.0", "@tanstack/query-async-storage-persister": "^5.25.0", "@tanstack/react-query": "^5.8.1", "@tanstack/react-query-persist-client": "^5.25.0", @@ -130,33 +130,33 @@ "emoji-mart": "^5.5.2", "emoji-regex": "^10.4.0", "eventemitter3": "^5.0.1", - "expo": "^53.0.5", + "expo": "53.0.11", "expo-application": "~6.1.4", - "expo-blur": "~14.1.4", + "expo-blur": "~14.1.5", "expo-build-properties": "~0.14.6", - "expo-camera": "~16.1.6", + "expo-camera": "~16.1.8", "expo-clipboard": "~7.1.4", - "expo-dev-client": "~5.1.7", + "expo-dev-client": "~5.2.0", "expo-device": "~7.1.4", - "expo-file-system": "~18.1.8", - "expo-font": "~13.3.0", + "expo-file-system": "~18.1.10", + "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", - "expo-image": "~2.1.6", + "expo-image": "~2.2.1", "expo-image-crop-tool": "^0.1.8", - "expo-image-manipulator": "~13.1.5", + "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", - "expo-linear-gradient": "~14.1.4", - "expo-linking": "~7.1.4", + "expo-linear-gradient": "~14.1.5", + "expo-linking": "~7.1.5", "expo-localization": "~16.1.5", - "expo-media-library": "~17.1.6", - "expo-notifications": "~0.31.1", - "expo-screen-orientation": "~8.1.5", + "expo-media-library": "~17.1.7", + "expo-notifications": "~0.31.3", + "expo-screen-orientation": "~8.1.7", "expo-sharing": "~13.1.5", - "expo-splash-screen": "~0.30.8", - "expo-system-ui": "~5.0.7", + "expo-splash-screen": "~0.30.9", + "expo-system-ui": "~5.0.8", "expo-task-manager": "~13.1.5", - "expo-updates": "~0.28.12", - "expo-video": "~2.1.8", + "expo-updates": "~0.28.14", + "expo-video": "~2.2.1", "expo-web-browser": "~14.1.6", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", @@ -182,35 +182,34 @@ "react-image-crop": "^11.0.7", "react-is": "19", "react-keyed-flatten-children": "^5.0.0", - "react-native": "0.79.2", - "react-native-compressor": "1.11.0", + "react-native": "^0.79.3", + "react-native-compressor": "^1.11.0", "react-native-date-picker": "^5.0.12", - "react-native-drawer-layout": "^4.1.6", + "react-native-drawer-layout": "^4.1.8", "react-native-edge-to-edge": "^1.6.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "~1.11.0", "react-native-ios-context-menu": "^1.15.3", "react-native-keyboard-controller": "^1.17.1", "react-native-mmkv": "^2.12.2", - "react-native-pager-view": "6.7.1", + "react-native-pager-view": "^6.7.1", "react-native-progress": "bluesky-social/react-native-progress", "react-native-qrcode-styled": "^0.3.3", "react-native-reanimated": "~3.17.5", - "react-native-root-siblings": "^4.1.1", + "react-native-root-siblings": "^5.0.1", "react-native-safe-area-context": "5.4.0", "react-native-screens": "^4.11.1", - "react-native-svg": "15.11.2", + "react-native-svg": "15.12.0", "react-native-uitextview": "^1.4.0", "react-native-url-polyfill": "^1.3.0", "react-native-uuid": "^2.0.3", "react-native-view-shot": "^4.0.3", "react-native-web": "~0.20.0", "react-native-web-webview": "^1.0.2", - "react-native-webview": "13.13.5", + "react-native-webview": "^13.13.5", "react-remove-scroll-bar": "^2.3.8", "react-responsive": "^9.0.2", "react-textarea-autosize": "^8.5.3", - "rn-fetch-blob": "^0.12.0", "statsig-react-native-expo": "^4.6.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", @@ -227,8 +226,9 @@ "@lingui/cli": "^4.14.1", "@lingui/macro": "^4.14.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", - "@react-native/eslint-config": "^0.79.2", - "@react-native/typescript-config": "^0.79.2", + "@react-native/babel-preset": "0.79.3", + "@react-native/eslint-config": "^0.79.3", + "@react-native/typescript-config": "^0.79.3", "@sentry/webpack-plugin": "^3.2.2", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.2.0", @@ -247,6 +247,7 @@ "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.1.0-rc.1", "babel-preset-expo": "~13.1.11", + "browserslist": "^4.25.0", "eslint": "^8.19.0", "eslint-plugin-bsky-internal": "link:./eslint", "eslint-plugin-ft-flow": "^2.0.3", @@ -260,7 +261,7 @@ "husky": "^8.0.3", "is-ci": "^3.0.1", "jest": "^29.7.0", - "jest-expo": "~53.0.3", + "jest-expo": "~53.0.7", "jest-junit": "^16.0.0", "lint-staged": "^13.2.3", "lockfile-lint": "^4.14.0", @@ -275,11 +276,11 @@ }, "resolutions": { "@expo/image-utils": "0.6.3", - "@react-native/babel-preset": "0.79.2", - "@react-native/normalize-colors": "0.79.2", + "@react-native/babel-preset": "0.79.3", + "@react-native/normalize-colors": "0.79.3", "@types/react": "^18", "**/expo-constants": "17.0.3", - "**/expo-device": "7.0.1", + "**/expo-device": "7.1.4", "**/zod": "3.23.8", "**/multiformats": "9.9.0" }, @@ -359,7 +360,8 @@ ], "allowedUrls": [ "https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908", - "https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4" + "https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4", + "https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9" ], "emptyHostname": false, "validatePackageNames": true, diff --git a/patches/@mattermost+react-native-paste-input+0.7.1.patch b/patches/@mattermost+react-native-paste-input+0.8.1.patch.disabled similarity index 50% rename from patches/@mattermost+react-native-paste-input+0.7.1.patch rename to patches/@mattermost+react-native-paste-input+0.8.1.patch.disabled index f25b6a776e..a7f1461432 100644 --- a/patches/@mattermost+react-native-paste-input+0.7.1.patch +++ b/patches/@mattermost+react-native-paste-input+0.8.1.patch.disabled @@ -114,7 +114,7 @@ index e916023..5049c33 100644 } #pragma mark - UIScrollViewDelegate -@@ -62,7 +92,6 @@ +@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation { - (void)scrollViewDidScroll:(UIScrollView *)scrollView { RCTDirectEventBlock onScroll = self.onScroll; @@ -122,7 +122,7 @@ index e916023..5049c33 100644 if (onScroll) { CGPoint contentOffset = scrollView.contentOffset; CGSize contentSize = scrollView.contentSize; -@@ -71,22 +100,22 @@ +@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView onScroll(@{ @"contentOffset": @{ @@ -155,3 +155,110 @@ index e916023..5049c33 100644 }, @"zoomScale": @(scrollView.zoomScale ?: 1), }); +diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm +index dd50053..2ed7017 100644 +--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm ++++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm +@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & + const auto &newTextInputProps = static_cast(*props); + + // Traits: +- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) { +- [self _setMultiline:newTextInputProps.traits.multiline]; ++ if (newTextInputProps.multiline != oldTextInputProps.multiline) { ++ [self _setMultiline:newTextInputProps.multiline]; + } + + if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) { +@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection + return; + } + const auto &props = static_cast(*_props); +- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { ++ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { + [self textInputDidChange]; + _ignoreNextTextInputCall = YES; + } +@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe + - (SubmitBehavior)getSubmitBehavior + { + const auto &props = static_cast(*_props); +- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior; ++ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior; + + // We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline. + if (submitBehaviorDefaultable == SubmitBehavior::Default) { +- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit; ++ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit; + } + + return submitBehaviorDefaultable; +diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp +index 29e094f..7ef519a 100644 +--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp ++++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp +@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps( + const PropsParserContext &context, + const PasteTextInputProps &sourceProps, + const RawProps& rawProps) +- : ViewProps(context, sourceProps, rawProps), +- BaseTextProps(context, sourceProps, rawProps), ++ : BaseTextInputProps(context, sourceProps, rawProps), + traits(convertRawProp(context, rawProps, sourceProps.traits, {})), + smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})), + disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})), +@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul + ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const { + auto result = paragraphAttributes; + +- if (!traits.multiline) { ++ if (!multiline) { + result.maximumNumberOfLines = 1; + } + +diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h +index 723d00c..31cfe66 100644 +--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h ++++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -25,7 +26,7 @@ + + namespace facebook::react { + +-class PasteTextInputProps final : public ViewProps, public BaseTextProps { ++class PasteTextInputProps final : public BaseTextInputProps { + public: + PasteTextInputProps() = default; + PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps); +diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp +index 31e07e3..7f0ebfb 100644 +--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp ++++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp +@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded( + const auto& state = getStateData(); + + react_native_assert(textLayoutManager_); +- react_native_assert( +- (!state.layoutManager || state.layoutManager == textLayoutManager_) && +- "`StateData` refers to a different `TextLayoutManager`"); +- +- if (state.reactTreeAttributedString == reactTreeAttributedString && +- state.layoutManager == textLayoutManager_) { +- return; +- } + + auto newState = TextInputState{}; + newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString}; + newState.paragraphAttributes = getConcreteProps().paragraphAttributes; + newState.reactTreeAttributedString = reactTreeAttributedString; +- newState.layoutManager = textLayoutManager_; + newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount; + setStateData(std::move(newState)); + } diff --git a/patches/@sentry+react-native+6.10.0.patch b/patches/@sentry+react-native+6.14.0.patch similarity index 100% rename from patches/@sentry+react-native+6.10.0.patch rename to patches/@sentry+react-native+6.14.0.patch diff --git a/patches/expo-media-library+17.1.6.patch b/patches/expo-media-library+17.1.7.patch similarity index 100% rename from patches/expo-media-library+17.1.6.patch rename to patches/expo-media-library+17.1.7.patch diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.4.0.patch similarity index 100% rename from patches/expo-modules-core+2.3.12.patch rename to patches/expo-modules-core+2.4.0.patch diff --git a/patches/expo-modules-core+2.3.12.patch.md b/patches/expo-modules-core+2.4.0.patch.md similarity index 100% rename from patches/expo-modules-core+2.3.12.patch.md rename to patches/expo-modules-core+2.4.0.patch.md diff --git a/patches/expo-notifications+0.31.1.patch b/patches/expo-notifications+0.31.3.patch similarity index 100% rename from patches/expo-notifications+0.31.1.patch rename to patches/expo-notifications+0.31.3.patch diff --git a/patches/expo-notifications+0.31.1.patch.md b/patches/expo-notifications+0.31.3.patch.md similarity index 100% rename from patches/expo-notifications+0.31.1.patch.md rename to patches/expo-notifications+0.31.3.patch.md diff --git a/patches/expo-updates+0.28.12.patch b/patches/expo-updates+0.28.14.patch similarity index 100% rename from patches/expo-updates+0.28.12.patch rename to patches/expo-updates+0.28.14.patch diff --git a/patches/expo-updates+0.28.12.patch.md b/patches/expo-updates+0.28.14.patch.md similarity index 100% rename from patches/expo-updates+0.28.12.patch.md rename to patches/expo-updates+0.28.14.patch.md diff --git a/patches/react-native+0.79.2.patch b/patches/react-native+0.79.3.patch similarity index 70% rename from patches/react-native+0.79.2.patch rename to patches/react-native+0.79.3.patch index 609ae66178..6d465475eb 100644 --- a/patches/react-native+0.79.2.patch +++ b/patches/react-native+0.79.3.patch @@ -1,3 +1,32 @@ +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h +index 914a249..0deac55 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h +@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN + */ + @interface RCTPullToRefreshViewComponentView : RCTViewComponentView + ++- (void)beginRefreshingProgrammatically; ++ + @end + + NS_ASSUME_NONNULL_END +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +index d029337..0f63ea3 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition + } + } + +++ (BOOL)shouldBeRecycled ++{ ++ return NO; ++} ++ + @end + + Class RCTScrollViewCls(void) diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h index e9b330f..ec5f58c 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h @@ -15,7 +44,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont index 53bfd04..ff1b1ed 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -@@ -23,6 +23,7 @@ +@@ -23,6 +23,7 @@ @implementation RCTRefreshControl { UIColor *_titleColor; CGFloat _progressViewOffset; BOOL _hasMovedToWindow; @@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644 } - (instancetype)init -@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder) +@@ -58,6 +59,12 @@ - (void)layoutSubviews _isInitialRender = false; } @@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644 - (void)didMoveToWindow { [super didMoveToWindow]; -@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder) +@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged } } @@ -91,7 +120,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont index 40aaf9c..1c60164 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m -@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE() +@@ -22,11 +22,12 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) @@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644 RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { -diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m -index cd1e7eb..c1d0172 100644 ---- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m -+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m -@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) - RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) - RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) - RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) -+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets) - RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets) - RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL) - RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int) diff --git a/patches/react-native+0.79.2.patch.md b/patches/react-native+0.79.3.patch.md similarity index 100% rename from patches/react-native+0.79.2.patch.md rename to patches/react-native+0.79.3.patch.md diff --git a/patches/react-native-svg+15.11.2.patch b/patches/react-native-svg+15.12.0.patch similarity index 100% rename from patches/react-native-svg+15.11.2.patch rename to patches/react-native-svg+15.12.0.patch diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index e5fb338497..ea7f8e94ef 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -1,12 +1,12 @@ import React from 'react' import { - AccessibilityProps, + type AccessibilityProps, StyleSheet, TextInput, - TextInputProps, - TextStyle, + type TextInputProps, + type TextStyle, View, - ViewStyle, + type ViewStyle, } from 'react-native' import {HITSLOP_20} from '#/lib/constants' @@ -16,13 +16,13 @@ import { applyFonts, atoms as a, ios, - TextStyleProp, + type TextStyleProp, useAlf, useTheme, web, } from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {Props as SVGIconProps} from '#/components/icons/common' +import {type Props as SVGIconProps} from '#/components/icons/common' import {Text} from '#/components/Typography' const Context = React.createContext<{ @@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) { minWidth: 0, }, ios({paddingTop: 12, paddingBottom: 13}), - android(a.py_sm), + android(a.py_md), // fix for autofill styles covering border web({ paddingTop: 10, diff --git a/src/lib/hooks/useHandleRef.ts b/src/lib/hooks/useHandleRef.ts deleted file mode 100644 index 167ba270b6..0000000000 --- a/src/lib/hooks/useHandleRef.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {useState} from 'react' -import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated' - -export type HandleRef = { - (node: any): void - current: null | number -} - -// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions. -// Render it like , then pass `ref.current` to `measureHandle()` and such. -export function useHandleRef(): HandleRef { - return useState(() => { - const ref = (node: any) => { - if (node) { - ref.current = - node._nativeTag ?? - node.__nativeTag ?? - node.canonical?.nativeTag ?? - null - } else { - ref.current = null - } - } - ref.current = null - return ref - })[0] as HandleRef -} - -// When using this version, you need to read ref.current on the JS thread, and pass it to UI. -export function measureHandle( - current: number | null, -): MeasuredDimensions | null { - 'worklet' - if (current !== null) { - return measure((() => current) as AnimatedRef) - } else { - return null - } -} diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 62cbc55ac5..c7a429a243 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -1,8 +1,9 @@ -import {Image as RNImage, Share as RNShare} from 'react-native' +import {Image as RNImage} from 'react-native' import uuid from 'react-native-uuid' import { cacheDirectory, copyAsync, + createDownloadResumable, deleteAsync, EncodingType, getInfoAsync, @@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' import {Buffer} from 'buffer' -import RNFetchBlob from 'rn-fetch-blob' import {POST_IMG_MAX} from '#/lib/constants' import {logger} from '#/logger' @@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) { return } - let downloadRes + const path = createPath(appendExt) + try { - const downloadResPromise = RNFetchBlob.config({ - fileCache: true, - appendExt, - }).fetch('GET', opts.uri) - const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout) - downloadRes = await downloadResPromise - clearTimeout(to1) - - const status = downloadRes.info().status - if (status !== 200) { - return - } - - const localUri = normalizePath(downloadRes.path(), true) - return await doResize(localUri, opts) + await downloadImage(opts.uri, path, opts.timeout) + return await doResize(path, opts) } finally { - // TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey - if (downloadRes) { - downloadRes.flush() - } + safeDeleteAsync(path) } } @@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) { // TODO might need to give an error to the user in this case -prf return } - const downloadResponse = await RNFetchBlob.config({ - fileCache: true, - }).fetch('GET', uri) - // NOTE - // assuming PNG // we're currently relying on the fact our CDN only serves pngs // -prf - - let imagePath = downloadResponse.path() - imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) - - // NOTE - // for some reason expo-sharing refuses to work on iOS - // ...and visa versa - // -prf - if (isIOS) { - await RNShare.share({url: imagePath}) - } else { - await Sharing.shareAsync(imagePath, { - mimeType: 'image/png', - UTI: 'image/png', - }) - } - - safeDeleteAsync(imagePath) + const imageUri = await downloadImage(uri, createPath('png'), 5e3) + const imagePath = await moveToPermanentPath(imageUri, '.png') + safeDeleteAsync(imageUri) + await Sharing.shareAsync(imagePath, { + mimeType: 'image/png', + UTI: 'image/png', + }) } const ALBUM_NAME = 'Bluesky' @@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) { // assuming PNG // we're currently relying on the fact our CDN only serves pngs // -prf - const downloadResponse = await RNFetchBlob.config({ - fileCache: true, - }).fetch('GET', uri) - let imagePath = downloadResponse.path() - imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) + const imageUri = await downloadImage(uri, createPath('png'), 5e3) + const imagePath = await moveToPermanentPath(imageUri, '.png') // save try { @@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: { height: Math.round(originalDims.height * ratio), } } + +function createPath(ext: string) { + // cacheDirectory will never be null on native, so the null check here is not necessary except for typescript. + // we use a web-only function for downloadAndResize on web + return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}` +} + +async function downloadImage(uri: string, path: string, timeout: number) { + const dlResumable = createDownloadResumable(uri, path, {cache: true}) + + const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout) + + const dlRes = await dlResumable.downloadAsync() + clearTimeout(to1) + + if (!dlRes?.uri) { + throw new Error('Failed to download image - dlRes is undefined') + } + + return normalizePath(dlRes.uri) +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 9777c8cc78..53585c0947 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -1,9 +1,11 @@ -import React, {memo, useEffect} from 'react' +import {memo, useCallback, useEffect, useMemo} from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' -import { +import Animated, { + measure, type MeasuredDimensions, runOnJS, runOnUI, + useAnimatedRef, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' @@ -14,7 +16,6 @@ import {useNavigation} from '@react-navigation/native' import {useActorStatus} from '#/lib/actor-status' import {BACK_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' -import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' @@ -59,9 +60,9 @@ let ProfileHeaderShell = ({ const playHaptic = useHaptics() const liveStatusControl = useDialogControl() - const aviRef = useHandleRef() + const aviRef = useAnimatedRef() - const onPressBack = React.useCallback(() => { + const onPressBack = useCallback(() => { if (navigation.canGoBack()) { navigation.goBack() } else { @@ -69,7 +70,7 @@ let ProfileHeaderShell = ({ } }, [navigation]) - const _openLightbox = React.useCallback( + const _openLightbox = useCallback( (uri: string, thumbRect: MeasuredDimensions | null) => { openLightbox({ images: [ @@ -92,7 +93,7 @@ let ProfileHeaderShell = ({ [openLightbox], ) - const isMe = React.useMemo( + const isMe = useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], ) @@ -109,7 +110,7 @@ let ProfileHeaderShell = ({ } }, [live.isActive, profile.did]) - const onPressAvi = React.useCallback(() => { + const onPressAvi = useCallback(() => { if (live.isActive) { playHaptic('Light') logger.metric( @@ -122,10 +123,9 @@ let ProfileHeaderShell = ({ const modui = moderation.ui('avatar') const avatar = profile.avatar if (avatar && !(modui.blur && modui.noOverride)) { - const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measureHandle(aviHandle) + const rect = measure(aviRef) runOnJS(_openLightbox)(avatar, rect) })() } @@ -223,7 +223,7 @@ let ProfileHeaderShell = ({ styles.avi, profile.associated?.labeler && styles.aviLabeler, ]}> - + {live.isActive && } - + diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 6f5e812ed2..f927015af9 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -1,5 +1,5 @@ import React, { - ComponentProps, + type ComponentProps, forwardRef, useCallback, useMemo, @@ -7,16 +7,16 @@ import React, { useState, } from 'react' import { - NativeSyntheticEvent, + type NativeSyntheticEvent, Text as RNText, - TextInput as RNTextInput, - TextInputSelectionChangeEventData, + type TextInput as RNTextInput, + type TextInputSelectionChangeEventData, View, } from 'react-native' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import PasteInput, { - PastedFile, - PasteInputRef, + type PastedFile, + type PasteInputRef, // @ts-expect-error no types when installing from github } from '@mattermost/react-native-paste-input' import {POST_IMG_MAX} from '#/lib/constants' @@ -27,7 +27,7 @@ import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' import {useTheme} from '#/lib/ThemeContext' import {isAndroid, isNative} from '#/platform/detection' import { - LinkFacetMatch, + type LinkFacetMatch, suggestLinkCardUri, } from '#/view/com/composer/text-input/text-input-util' import {atoms as a, useAlf} from '#/alf' diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index f62bffc534..8cc3469032 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -1,16 +1,22 @@ -import React, {forwardRef, useCallback, useContext} from 'react' +import { + useCallback, + useContext, + useImperativeHandle, + useRef, + useState, +} from 'react' import {View} from 'react-native' import {DrawerGestureContext} from 'react-native-drawer-layout' import {Gesture, GestureDetector} from 'react-native-gesture-handler' import PagerView, { - PagerViewOnPageScrollEventData, - PagerViewOnPageSelectedEvent, - PagerViewOnPageSelectedEventData, - PageScrollStateChangedNativeEventData, + type PagerViewOnPageScrollEventData, + type PagerViewOnPageSelectedEvent, + type PagerViewOnPageSelectedEventData, + type PageScrollStateChangedNativeEventData, } from 'react-native-pager-view' import Animated, { runOnJS, - SharedValue, + type SharedValue, useEvent, useHandler, useSharedValue, @@ -36,8 +42,12 @@ export interface RenderTabBarFnProps { export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element interface Props { + ref?: React.Ref initialPage?: number renderTabBar: RenderTabBarFn + // tab pressed, yet to scroll to page + onTabPressed?: (index: number) => void + // scroll settled onPageSelected?: (index: number) => void onPageScrollStateChanged?: ( scrollState: 'idle' | 'dragging' | 'settling', @@ -47,114 +57,112 @@ interface Props { const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) -export const Pager = forwardRef>( - function PagerImpl( +export function Pager({ + ref, + children, + initialPage = 0, + renderTabBar, + onPageSelected: parentOnPageSelected, + onTabPressed: parentOnTabPressed, + onPageScrollStateChanged: parentOnPageScrollStateChanged, + testID, +}: React.PropsWithChildren) { + const [selectedPage, setSelectedPage] = useState(initialPage) + const pagerView = useRef(null) + + const [isIdle, setIsIdle] = useState(true) + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() + useFocusEffect( + useCallback(() => { + const canSwipeDrawer = selectedPage === 0 && isIdle + setDrawerSwipeDisabled(!canSwipeDrawer) + return () => { + setDrawerSwipeDisabled(false) + } + }, [setDrawerSwipeDisabled, selectedPage, isIdle]), + ) + + useImperativeHandle(ref, () => ({ + setPage: (index: number) => { + pagerView.current?.setPage(index) + }, + })) + + const onPageSelectedJSThread = useCallback( + (nextPosition: number) => { + setSelectedPage(nextPosition) + parentOnPageSelected?.(nextPosition) + }, + [setSelectedPage, parentOnPageSelected], + ) + + const onTabBarSelect = useCallback( + (index: number) => { + parentOnTabPressed?.(index) + pagerView.current?.setPage(index) + }, + [pagerView, parentOnTabPressed], + ) + + const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle') + const dragProgress = useSharedValue(selectedPage) + const didInit = useSharedValue(false) + const handlePageScroll = usePagerHandlers( { - children, - initialPage = 0, - renderTabBar, - onPageScrollStateChanged: parentOnPageScrollStateChanged, - onPageSelected: parentOnPageSelected, - testID, - }: React.PropsWithChildren, - ref, - ) { - const [selectedPage, setSelectedPage] = React.useState(initialPage) - const pagerView = React.useRef(null) - - const [isIdle, setIsIdle] = React.useState(true) - const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - useFocusEffect( - useCallback(() => { - const canSwipeDrawer = selectedPage === 0 && isIdle - setDrawerSwipeDisabled(!canSwipeDrawer) - return () => { - setDrawerSwipeDisabled(false) + onPageScroll(e: PagerViewOnPageScrollEventData) { + 'worklet' + if (didInit.get() === false) { + // On iOS, there's a spurious scroll event with 0 position + // even if a different page was supplied as the initial page. + // Ignore it and wait for the first confirmed selection instead. + return } - }, [setDrawerSwipeDisabled, selectedPage, isIdle]), - ) - - React.useImperativeHandle(ref, () => ({ - setPage: (index: number) => { - pagerView.current?.setPage(index) + dragProgress.set(e.offset + e.position) }, - })) - - const onPageSelectedJSThread = React.useCallback( - (nextPosition: number) => { - setSelectedPage(nextPosition) - parentOnPageSelected?.(nextPosition) + onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) { + 'worklet' + runOnJS(setIsIdle)(e.pageScrollState === 'idle') + if (dragState.get() === 'idle' && e.pageScrollState === 'settling') { + // This is a programmatic scroll on Android. + // Stay "idle" to match iOS and avoid confusing downstream code. + return + } + dragState.set(e.pageScrollState) + parentOnPageScrollStateChanged?.(e.pageScrollState) }, - [setSelectedPage, parentOnPageSelected], - ) - - const onTabBarSelect = React.useCallback( - (index: number) => { - pagerView.current?.setPage(index) + onPageSelected(e: PagerViewOnPageSelectedEventData) { + 'worklet' + didInit.set(true) + runOnJS(onPageSelectedJSThread)(e.position) }, - [pagerView], - ) + }, + [parentOnPageScrollStateChanged], + ) - const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle') - const dragProgress = useSharedValue(selectedPage) - const didInit = useSharedValue(false) - const handlePageScroll = usePagerHandlers( - { - onPageScroll(e: PagerViewOnPageScrollEventData) { - 'worklet' - if (didInit.get() === false) { - // On iOS, there's a spurious scroll event with 0 position - // even if a different page was supplied as the initial page. - // Ignore it and wait for the first confirmed selection instead. - return - } - dragProgress.set(e.offset + e.position) - }, - onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) { - 'worklet' - runOnJS(setIsIdle)(e.pageScrollState === 'idle') - if (dragState.get() === 'idle' && e.pageScrollState === 'settling') { - // This is a programmatic scroll on Android. - // Stay "idle" to match iOS and avoid confusing downstream code. - return - } - dragState.set(e.pageScrollState) - parentOnPageScrollStateChanged?.(e.pageScrollState) - }, - onPageSelected(e: PagerViewOnPageSelectedEventData) { - 'worklet' - didInit.set(true) - runOnJS(onPageSelectedJSThread)(e.position) - }, - }, - [parentOnPageScrollStateChanged], - ) + const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web + const nativeGesture = + Gesture.Native().requireExternalGestureToFail(drawerGesture) - const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web - const nativeGesture = - Gesture.Native().requireExternalGestureToFail(drawerGesture) - - return ( - - {renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - dragProgress, - dragState, - })} - - - {children} - - - - ) - }, -) + return ( + + {renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + dragProgress, + dragState, + })} + + + {children} + + + + ) +} function usePagerHandlers( handlers: { diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index c620e73e33..06aac169c8 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -1,8 +1,19 @@ -import React from 'react' +import { + Children, + useCallback, + useImperativeHandle, + useRef, + useState, +} from 'react' import {View} from 'react-native' import {flushSync} from 'react-dom' import {s} from '#/lib/styles' +import {atoms as a} from '#/alf' + +export interface PagerRef { + setPage: (index: number) => void +} export interface RenderTabBarFnProps { selectedPage: number @@ -12,30 +23,30 @@ export interface RenderTabBarFnProps { export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element interface Props { + ref?: React.Ref initialPage?: number renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void } -export const Pager = React.forwardRef(function PagerImpl( - { - children, - initialPage = 0, - renderTabBar, - onPageSelected, - }: React.PropsWithChildren, - ref, -) { - const [selectedPage, setSelectedPage] = React.useState(initialPage) - const scrollYs = React.useRef>([]) - const anchorRef = React.useRef(null) - React.useImperativeHandle(ref, () => ({ +export function Pager({ + ref, + children, + initialPage = 0, + renderTabBar, + onPageSelected, +}: React.PropsWithChildren) { + const [selectedPage, setSelectedPage] = useState(initialPage) + const scrollYs = useRef>([]) + const anchorRef = useRef(null) + + useImperativeHandle(ref, () => ({ setPage: (index: number) => { onTabBarSelect(index) }, })) - const onTabBarSelect = React.useCallback( + const onTabBarSelect = useCallback( (index: number) => { const scrollY = window.scrollY // We want to determine if the tabbar is already "sticking" at the top (in which @@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl( tabBarAnchor: , onSelect: e => onTabBarSelect(e), })} - {React.Children.map(children, (child, i) => ( - + {Children.map(children, (child, i) => ( + {child} ))} ) -}) +} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 1746d2ca13..57aaac0740 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,17 +1,16 @@ -import * as React from 'react' +import {memo, useCallback, useEffect, useRef, useState} from 'react' import { - LayoutChangeEvent, - NativeScrollEvent, - ScrollView, + type LayoutChangeEvent, + type NativeScrollEvent, + type ScrollView, StyleSheet, View, } from 'react-native' import Animated, { - AnimatedRef, - runOnJS, + type AnimatedRef, runOnUI, scrollTo, - SharedValue, + type SharedValue, useAnimatedRef, useAnimatedStyle, useSharedValue, @@ -20,9 +19,13 @@ import Animated, { import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {ScrollProvider} from '#/lib/ScrollContext' import {isIOS} from '#/platform/detection' -import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' +import { + Pager, + type PagerRef, + type RenderTabBarFnProps, +} from '#/view/com/pager/Pager' import {useTheme} from '#/alf' -import {ListMethods} from '../util/List' +import {type ListMethods} from '../util/List' import {PagerHeaderProvider} from './PagerHeaderContext' import {TabBar} from './TabBar' @@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams { } export interface PagerWithHeaderProps { + ref?: React.Ref testID?: string children: | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] @@ -49,97 +53,94 @@ export interface PagerWithHeaderProps { onCurrentPageSelected?: (index: number) => void allowHeaderOverScroll?: boolean } -export const PagerWithHeader = React.forwardRef( - function PageWithHeaderImpl( - { - children, - testID, +export function PagerWithHeader({ + ref, + children, + testID, + items, + isHeaderReady, + renderHeader, + initialPage, + onPageSelected, + onCurrentPageSelected, + allowHeaderOverScroll, +}: PagerWithHeaderProps) { + const [currentPage, setCurrentPage] = useState(0) + const [tabBarHeight, setTabBarHeight] = useState(0) + const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0) + const scrollY = useSharedValue(0) + const headerHeight = headerOnlyHeight + tabBarHeight + + // capture the header bar sizing + const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { + const height = evt.nativeEvent.layout.height + if (height > 0) { + // The rounding is necessary to prevent jumps on iOS + setTabBarHeight(Math.round(height * 2) / 2) + } + }) + const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { + if (height > 0) { + // The rounding is necessary to prevent jumps on iOS + setHeaderOnlyHeight(Math.round(height * 2) / 2) + } + }) + + const renderTabBar = useCallback( + (props: RenderTabBarFnProps) => { + return ( + + + + ) + }, + [ + headerOnlyHeight, items, isHeaderReady, renderHeader, - initialPage, - onPageSelected, + currentPage, onCurrentPageSelected, + onTabBarLayout, + onHeaderOnlyLayout, + scrollY, + testID, allowHeaderOverScroll, - }: PagerWithHeaderProps, - ref, - ) { - const [currentPage, setCurrentPage] = React.useState(0) - const [tabBarHeight, setTabBarHeight] = React.useState(0) - const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) - const scrollY = useSharedValue(0) - const headerHeight = headerOnlyHeight + tabBarHeight + ], + ) - // capture the header bar sizing - const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { - const height = evt.nativeEvent.layout.height - if (height > 0) { - // The rounding is necessary to prevent jumps on iOS - setTabBarHeight(Math.round(height * 2) / 2) - } - }) - const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { - if (height > 0) { - // The rounding is necessary to prevent jumps on iOS - setHeaderOnlyHeight(Math.round(height * 2) / 2) - } - }) + const scrollRefs = useSharedValue | null>>([]) + const registerRef = useCallback( + (scrollRef: AnimatedRef | null, atIndex: number) => { + scrollRefs.modify(refs => { + 'worklet' + refs[atIndex] = scrollRef + return refs + }) + }, + [scrollRefs], + ) - const renderTabBar = React.useCallback( - (props: RenderTabBarFnProps) => { - return ( - - - - ) - }, - [ - headerOnlyHeight, - items, - isHeaderReady, - renderHeader, - currentPage, - onCurrentPageSelected, - onTabBarLayout, - onHeaderOnlyLayout, - scrollY, - testID, - allowHeaderOverScroll, - ], - ) - - const scrollRefs = useSharedValue | null>>([]) - const registerRef = React.useCallback( - (scrollRef: AnimatedRef | null, atIndex: number) => { - scrollRefs.modify(refs => { - 'worklet' - refs[atIndex] = scrollRef - return refs - }) - }, - [scrollRefs], - ) - - const lastForcedScrollY = useSharedValue(0) - const adjustScrollForOtherPages = () => { + const lastForcedScrollY = useSharedValue(0) + const adjustScrollForOtherPages = useCallback( + (scrollState: 'idle' | 'dragging' | 'settling') => { 'worklet' + if (scrollState !== 'dragging') return const currentScrollY = scrollY.get() const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) if (lastForcedScrollY.get() !== forcedScrollY) { @@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef( } } } - } + }, + [currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY], + ) - const throttleTimeout = React.useRef | null>( - null, - ) - const queueThrottledOnScroll = useNonReactiveCallback(() => { - if (!throttleTimeout.current) { - throttleTimeout.current = setTimeout(() => { - throttleTimeout.current = null - runOnUI(adjustScrollForOtherPages)() - }, 80 /* Sync often enough you're unlikely to catch it unsynced */) + const onScrollWorklet = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + const nextScrollY = e.contentOffset.y + // HACK: onScroll is reporting some strange values on load (negative header height). + // Highly improbable that you'd be overscrolled by over 400px - + // in fact, I actually can't do it, so let's just ignore those. -sfn + const isPossiblyInvalid = + headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight + if (!isPossiblyInvalid) { + scrollY.set(nextScrollY) } - }) + }, + [scrollY, headerHeight], + ) - const onScrollWorklet = React.useCallback( - (e: NativeScrollEvent) => { - 'worklet' - const nextScrollY = e.contentOffset.y - // HACK: onScroll is reporting some strange values on load (negative header height). - // Highly improbable that you'd be overscrolled by over 400px - - // in fact, I actually can't do it, so let's just ignore those. -sfn - const isPossiblyInvalid = - headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight - if (!isPossiblyInvalid) { - scrollY.set(nextScrollY) - runOnJS(queueThrottledOnScroll)() - } - }, - [scrollY, queueThrottledOnScroll, headerHeight], - ) + const onPageSelectedInner = useCallback( + (index: number) => { + setCurrentPage(index) + onPageSelected?.(index) + }, + [onPageSelected, setCurrentPage], + ) - const onPageSelectedInner = React.useCallback( - (index: number) => { - setCurrentPage(index) - onPageSelected?.(index) - }, - [onPageSelected, setCurrentPage], - ) + const onTabPressed = useCallback(() => { + runOnUI(adjustScrollForOtherPages)('dragging') + }, [adjustScrollForOtherPages]) - return ( - - {toArray(children) - .filter(Boolean) - .map((child, i) => { - const isReady = - isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 - return ( - - - - ) - })} - - ) - }, -) + return ( + + {toArray(children) + .filter(Boolean) + .map((child, i) => { + const isReady = + isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 + return ( + + + + ) + })} + + ) +} let PagerTabBar = ({ currentPage, @@ -258,7 +253,7 @@ let PagerTabBar = ({ dragState: SharedValue<'idle' | 'dragging' | 'settling'> }): React.ReactNode => { const t = useTheme() - const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) + const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0) const headerTransform = useAnimatedStyle(() => { const translateY = Math.min( @@ -275,7 +270,7 @@ let PagerTabBar = ({ ], } }) - const headerRef = React.useRef(null) + const headerRef = useRef(null) return ( ) } -PagerTabBar = React.memo(PagerTabBar) +PagerTabBar = memo(PagerTabBar) function PagerItem({ headerHeight, @@ -348,7 +343,7 @@ function PagerItem({ }) { const scrollElRef = useAnimatedRef() - React.useEffect(() => { + useEffect(() => { registerRef(scrollElRef, index) return () => { registerRef(null, index) diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index b0cf4d10e4..02a6704bf9 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,23 +1,28 @@ import React from 'react' import {Pressable, View} from 'react-native' -import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' -import {AppBskyGraphDefs} from '@atproto/api' +import Animated, { + measure, + type MeasuredDimensions, + runOnJS, + runOnUI, + useAnimatedRef, +} from 'react-native-reanimated' +import {type AppBskyGraphDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {sanitizeHandle} from '#/lib/strings/handles' import {emitSoftReset} from '#/state/events' import {useLightboxControls} from '#/state/lightbox' import {TextLink} from '#/view/com/util/Link' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {Text} from '#/view/com/util/text/Text' -import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar' +import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar' import {StarterPack} from '#/components/icons/StarterPack' import * as Layout from '#/components/Layout' @@ -52,7 +57,7 @@ export function ProfileSubpageHeader({ const {openLightbox} = useLightboxControls() const pal = usePalette('default') const canGoBack = navigation.canGoBack() - const aviRef = useHandleRef() + const aviRef = useAnimatedRef() const _openLightbox = React.useCallback( (uri: string, thumbRect: MeasuredDimensions | null) => { @@ -81,10 +86,9 @@ export function ProfileSubpageHeader({ if ( avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) ) { - const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measureHandle(aviHandle) + const rect = measure(aviRef) runOnJS(_openLightbox)(avatar, rect) })() } @@ -111,7 +115,7 @@ export function ProfileSubpageHeader({ paddingBottom: 14, paddingHorizontal: isMobile ? 12 : 14, }}> - + )} - + {isLoading ? ( void + onPress?: ( + containerRef: AnimatedRef, + fetchedDims: Dimensions | null, + ) => void onLongPress?: () => void onPressIn?: () => void }) { const t = useTheme() const {_} = useLingui() const largeAlt = useLargeAltBadgeEnabled() - const containerRef = useHandleRef() + const containerRef = useAnimatedRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) let aspectRatio: number | undefined @@ -103,7 +109,7 @@ export function AutoSizedImage({ const hasAlt = !!image.alt const contents = ( - + ) : null} - + ) if (cropDisabled) { diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index cc3eda68d2..1d35c88c59 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,12 +1,12 @@ -import React from 'react' -import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import {Image, ImageStyle} from 'expo-image' -import {AppBskyEmbedImages} from '@atproto/api' +import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' +import {type AnimatedRef} from 'react-native-reanimated' +import {Image, type ImageStyle} from 'expo-image' +import {type AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import type React from 'react' -import {HandleRef} from '#/lib/hooks/useHandleRef' -import {Dimensions} from '#/lib/media/types' +import {type Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useTheme} from '#/alf' @@ -20,7 +20,7 @@ interface Props { index: number onPress?: ( index: number, - containerRefs: HandleRef[], + containerRefs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: EventFunction @@ -28,7 +28,7 @@ interface Props { imageStyle?: StyleProp viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp - containerRefs: HandleRef[] + containerRefs: AnimatedRef[] thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]> } diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 16ea9d453f..b91d7a7adb 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,18 +1,18 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AppBskyEmbedImages} from '@atproto/api' +import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' +import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated' +import {type AppBskyEmbedImages} from '@atproto/api' -import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useBreakpoints} from '#/alf' -import {Dimensions} from '../../lightbox/ImageViewing/@types' +import {type Dimensions} from '../../lightbox/ImageViewing/@types' import {GalleryItem} from './Gallery' interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: HandleRef[], + containerRefs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: HandleRef[], + containerRefs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const gap = props.gap const count = props.images.length - const containerRef1 = useHandleRef() - const containerRef2 = useHandleRef() - const containerRef3 = useHandleRef() - const containerRef4 = useHandleRef() + const containerRef1 = useAnimatedRef() + const containerRef2 = useAnimatedRef() + const containerRef3 = useAnimatedRef() + const containerRef4 = useAnimatedRef() const thumbDimsRef = React.useRef<(Dimensions | null)[]>([]) switch (count) { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 431baa2b22..4cf71f9486 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -7,6 +7,8 @@ import { type ViewStyle, } from 'react-native' import { + type AnimatedRef, + measure, type MeasuredDimensions, runOnJS, runOnUI, @@ -25,7 +27,6 @@ import { type ModerationDecision, } from '@atproto/api' -import {type HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useLightboxControls} from '#/state/lightbox' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -162,13 +163,15 @@ export function PostEmbeds({ } const onPress = ( index: number, - refs: HandleRef[], + refs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => { - const handles = refs.map(r => r.current) runOnUI(() => { 'worklet' - const rects = handles.map(measureHandle) + const rects: (MeasuredDimensions | null)[] = [] + for (const r of refs) { + rects.push(measure(r)) + } runOnJS(_openLightbox)(index, rects, fetchedDims) })() } diff --git a/yarn.lock b/yarn.lock index 9c6a368472..125c985ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3776,29 +3776,29 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== -"@expo/cli@0.24.10": - version "0.24.10" - resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.24.10.tgz#f23d150f011cdebd6f503343d9be3c6283fb4096" - integrity sha512-auPE4MSRdkJkHsWJk935VoqX/BGMKARgXOLtJybTFUi64K3MkvdUoBrujcn/QzXl5DTGhacFd9qxUeQss6/qwg== +"@expo/cli@0.24.14": + version "0.24.14" + resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.24.14.tgz#af2e7ea5a08e3574e868cb0ec2110e68d32672e0" + integrity sha512-o+QYyfIBhSRTgaywKTLJhm2Fg5PrSeUVCXS+uQySamgoMjLNhHa8QwE64mW/FmJr5hZLiqUEQxb60FK4JcyqXg== dependencies: "@0no-co/graphql.web" "^1.0.8" "@babel/runtime" "^7.20.0" "@expo/code-signing-certificates" "^0.0.5" - "@expo/config" "~11.0.7" + "@expo/config" "~11.0.10" "@expo/config-plugins" "~10.0.2" "@expo/devcert" "^1.1.2" "@expo/env" "~1.0.5" "@expo/image-utils" "^0.7.4" "@expo/json-file" "^9.1.4" - "@expo/metro-config" "~0.20.12" + "@expo/metro-config" "~0.20.14" "@expo/osascript" "^2.2.4" "@expo/package-manager" "^1.8.4" "@expo/plist" "^0.3.4" - "@expo/prebuild-config" "^9.0.5" + "@expo/prebuild-config" "^9.0.6" "@expo/spawn-async" "^1.7.2" "@expo/ws-tunnel" "^1.0.1" "@expo/xcpretty" "^4.3.0" - "@react-native/dev-middleware" "0.79.2" + "@react-native/dev-middleware" "0.79.3" "@urql/core" "^5.0.6" "@urql/exchange-retry" "^1.3.0" accepts "^1.3.8" @@ -3813,9 +3813,9 @@ debug "^4.3.4" env-editor "^0.4.1" freeport-async "^2.0.0" - getenv "^1.0.0" + getenv "^2.0.0" glob "^10.4.2" - lan-network "^0.1.4" + lan-network "^0.1.6" minimatch "^9.0.0" node-forge "^1.3.1" npm-package-arg "^11.0.0" @@ -3871,7 +3871,7 @@ xcode "^3.0.1" xml2js "0.6.0" -"@expo/config-plugins@~10.0.1", "@expo/config-plugins@~10.0.2": +"@expo/config-plugins@~10.0.2": version "10.0.2" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-10.0.2.tgz#040867991e9c8c527b4f5c13a47bcf040a7479fe" integrity sha512-TzUn3pPdpwCS0yYaSlZOClgDmCX8N4I2lfgitX5oStqmvpPtB+vqtdyqsVM02fQ2tlJIAqwBW+NHaHqqy8Jv7g== @@ -3926,6 +3926,11 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.3.tgz#d083d9b095972e89eee96c41d085feb5b92d2749" integrity sha512-V1e6CiM4TXtGxG/W2Msjp/QOx/vikLo5IUGMvEMjgAglBfGYx3PXfqsUb5aZDt6kqA3bDDwFuZoS5vNm/SYwSg== +"@expo/config-types@^53.0.4": + version "53.0.4" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.4.tgz#fe64fac734531ae883d18529b32586c23ffb1ceb" + integrity sha512-0s+9vFx83WIToEr0Iwy4CcmiUXa5BgwBmEjylBB2eojX5XAMm9mJvw9KpjAb8m7zq2G0Q6bRbeufkzgbipuNQg== + "@expo/config@~10.0.4": version "10.0.5" resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.5.tgz#2de75e3f5d46a55f9f5140b73e0913265e6a41c6" @@ -3945,14 +3950,14 @@ slugify "^1.3.4" sucrase "3.35.0" -"@expo/config@~11.0.6", "@expo/config@~11.0.7": - version "11.0.7" - resolved "https://registry.yarnpkg.com/@expo/config/-/config-11.0.7.tgz#e6a6071942854269825e2450c3a115c963a4fd56" - integrity sha512-pppH3Cy2IfituiYACMeW7cWYezcjmHKq7lDLfH1gMHT+zZ1QaYNs3EN6Kcc/QAXV//KFFhU0Qq4H/UrLuPp/yg== +"@expo/config@~11.0.10", "@expo/config@~11.0.9": + version "11.0.10" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-11.0.10.tgz#559d9425a4e0de4fab96ccac01ff40f5cebbc04b" + integrity sha512-8S8Krr/c5lnl0eF03tA2UGY9rGBhZcbWKz2UWw5dpL/+zstwUmog8oyuuC8aRcn7GiTQLlbBkxcMeT8sOGlhbA== dependencies: "@babel/code-frame" "~7.10.4" - "@expo/config-plugins" "~10.0.1" - "@expo/config-types" "^53.0.3" + "@expo/config-plugins" "~10.0.2" + "@expo/config-types" "^53.0.4" "@expo/json-file" "^9.1.4" deepmerge "^4.3.1" getenv "^1.0.0" @@ -4005,23 +4010,24 @@ dotenv-expand "~11.0.6" getenv "^1.0.0" -"@expo/fingerprint@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.12.4.tgz#d4cc4de50e7b6d4e03b0d38850d1e4a136b74c8c" - integrity sha512-HOJVvjiQYVHIouCOfFf4JRrQvBDIV/12GVG2iwbw1iGwmpQVkPgEXa9lN0f2yuS4J3QXHs73wr9jvuCjMmJlfw== +"@expo/fingerprint@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.13.0.tgz#5f5600122940ac381ed697743c10bdbddf6c55c1" + integrity sha512-3IwpH0p3uO8jrJSLOUNDzJVh7VEBod0emnCBq0hD72sy6ICmzauM6Xf4he+2Tip7fzImCJRd63GaehV+CCtpvA== dependencies: "@expo/spawn-async" "^1.7.2" arg "^5.0.2" chalk "^4.1.2" debug "^4.3.4" find-up "^5.0.0" - getenv "^1.0.0" + getenv "^2.0.0" + ignore "^5.3.1" minimatch "^9.0.0" p-limit "^3.1.0" resolve-from "^5.0.0" semver "^7.6.0" -"@expo/html-elements@^0.12.4": +"@expo/html-elements@^0.12.5": version "0.12.5" resolved "https://registry.yarnpkg.com/@expo/html-elements/-/html-elements-0.12.5.tgz#be7e7af9f2be6d3f1aa3ec2e7ae1c121c91a9aa1" integrity sha512-28KWO88YKykKU7ke5sEQs5TivFRMs1Aktz13xxgqAf5rTgb+lka0VKVt3W2fG7ksbUQ407rtUqz7SEAq298NvQ== @@ -4068,16 +4074,16 @@ json5 "^2.2.3" write-file-atomic "^2.3.0" -"@expo/metro-config@0.20.12", "@expo/metro-config@~0.20.12": - version "0.20.12" - resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.20.12.tgz#f6e2c33a305cb0ab8b0aa0dadafd6adf09058b9c" - integrity sha512-O9zaAF3gH76EXkwuQCpXLKC5dBy344/pqoszWmtOloKo4gJy74aNPUy2LRS57pDHyjZe1HjrxVkMcr7lZCIsag== +"@expo/metro-config@0.20.14", "@expo/metro-config@~0.20.14": + version "0.20.14" + resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.20.14.tgz#5abf8cd6454fe7f75c1f8529cf79619da32af82d" + integrity sha512-tYDDubuZycK+NX00XN7BMu73kBur/evOPcKfxc+UBeFfgN2EifOITtdwSUDdRsbtJ2OnXwMY1HfRUG3Lq3l4cw== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.5" "@babel/parser" "^7.20.0" "@babel/types" "^7.20.0" - "@expo/config" "~11.0.7" + "@expo/config" "~11.0.9" "@expo/env" "~1.0.5" "@expo/json-file" "~9.1.4" "@expo/spawn-async" "^1.7.2" @@ -4140,14 +4146,14 @@ base64-js "^1.2.3" xmlbuilder "^15.1.1" -"@expo/prebuild-config@^9.0.5": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-9.0.5.tgz#b8b864b5e19489a1f66442ae30d5d7295f658297" - integrity sha512-oiSVU5ePu9lsOvn5p4xplqjzPlcZHzKYwzuonTa9GCH1GxcOEIBsvMVQiHBXHtqvgV2dztjm34kdXV//+9jtCA== +"@expo/prebuild-config@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-9.0.6.tgz#f634e7b8f9ebebeaf2e7d2f2be46926c23834d2b" + integrity sha512-HDTdlMkTQZ95rd6EpvuLM+xkZV03yGLc38FqI37qKFLJtUN1WnYVaWsuXKoljd1OrVEVsHe6CfqKwaPZ52D56Q== dependencies: - "@expo/config" "~11.0.7" + "@expo/config" "~11.0.9" "@expo/config-plugins" "~10.0.2" - "@expo/config-types" "^53.0.3" + "@expo/config-types" "^53.0.4" "@expo/image-utils" "^0.7.4" "@expo/json-file" "^9.1.4" "@react-native/normalize-colors" "0.79.2" @@ -4416,10 +4422,10 @@ protobufjs "^7.2.5" yargs "^17.7.2" -"@haileyok/bluesky-video@0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.2.6.tgz#61bb4ff908498558fd2320f06ba0f74ea03598b4" - integrity sha512-IlzrTATD7ci/a+ehSA7pIhOvxxNTe35zdzBYt8EmVvKKafyeFhxGfDuuP5qxfQmHurcQMIL+3HeZgnAXwQQNmQ== +"@haileyok/bluesky-video@0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.3.1.tgz#c996d8433e8f1988f2e9644adf39366fb4305d00" + integrity sha512-TU5c0RCV1yWF/HEZA22E0qXjxNzu0SOuMZpvdo+QVrO4tQcja9k8nHBbKRmyj60Odke4HQMoTOfNDNq/xo2Grw== "@hapi/accept@^6.0.3": version "6.0.3" @@ -5119,12 +5125,11 @@ "@babel/runtime" "^7.20.13" "@lingui/core" "4.14.1" -"@mattermost/react-native-paste-input@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@mattermost/react-native-paste-input/-/react-native-paste-input-0.7.1.tgz#f14585030b992cf7c9bbd0921225eefa501756ba" - integrity sha512-kY8LKtqRX2T/rtn/HNrzTitijuATvyzd6yl5WNWOsszmyzNcssKStjjCTBup04CyMxfwutUU1CWrYUb3hQO7oA== +"@mattermost/react-native-paste-input@mattermost/react-native-paste-input": + version "0.8.1" + resolved "https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9" dependencies: - semver "7.6.0" + semver "7.6.3" "@messageformat/parser@^5.0.0": version "5.1.0" @@ -5133,12 +5138,12 @@ dependencies: moo "^0.5.1" -"@miblanchard/react-native-slider@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.3.1.tgz#79e0f1f9b1ce43ef25ee51ee9256c012e5dfa412" - integrity sha512-J/hZDBWmXq8fJeOnTVHqIUVDHshqMSpJVxJ4WqwuCBKl5Rke9OBYXIdkSlgi75OgtScAr8FKK5KNkDKHUf6JIg== +"@miblanchard/react-native-slider@^2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.6.0.tgz#9f78c805d637ffaff0e3e7429932d2995a67edc9" + integrity sha512-o7hk/f/8vkqh6QNR5L52m+ws846fQeD/qNCC9CCSRdBqjq66KiCgbxzlhRzKM/gbtxcvMYMIEEJ1yes5cr6I3A== -"@mozzius/expo-dynamic-app-icon@^1.5.0": +"@mozzius/expo-dynamic-app-icon@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@mozzius/expo-dynamic-app-icon/-/expo-dynamic-app-icon-1.5.0.tgz#c5f88c309965b6d6b89cfd5e2c00faa7bda736af" integrity sha512-yE2yEPO+HQmOqsX7cECh7/vu/LXnqhHGsVm3UiVi/3gaK8u5hAkPTNzZ0Qu6vnMwjPnY+uFbN6X+6Aj9c9yjMQ== @@ -6141,23 +6146,23 @@ resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-2.11.0.tgz#4587fbce6a382adedad74311e96ee10bb2b2d63a" integrity sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg== -"@react-native/assets-registry@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.79.2.tgz#731963e664c8543f5b277e56c058bde612b69f50" - integrity sha512-5h2Z7/+/HL/0h88s0JHOdRCW4CXMCJoROxqzHqxdrjGL6EBD1DdaB4ZqkCOEVSW4Vjhir5Qb97C8i/MPWEYPtg== +"@react-native/assets-registry@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.79.3.tgz#022218d55a5d9d221a6d176987ab0b35c10d388b" + integrity sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ== -"@react-native/babel-plugin-codegen@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.2.tgz#f3f86766a01487aaaa623ec62514af4c84400953" - integrity sha512-d+NB7Uosn2ZWd4O4+7ZkB6q1a+0z2opD/4+Bzhk/Tv6fc5FrSftK2Noqxvo3/bhbdGFVPxf0yvLE8et4W17x/Q== +"@react-native/babel-plugin-codegen@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.3.tgz#acad4acaead398a8c8bcdecbe44040aa0c2dc2d7" + integrity sha512-Zb8F4bSEKKZfms5n1MQ0o5mudDcpAINkKiFuFTU0PErYGjY3kZ+JeIP+gS6KCXsckxCfMEKQwqKicP/4DWgsZQ== dependencies: "@babel/traverse" "^7.25.3" - "@react-native/codegen" "0.79.2" + "@react-native/codegen" "0.79.3" -"@react-native/babel-preset@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.2.tgz#5a683a6efeea357a326f70c84a881be2bafbeae3" - integrity sha512-/HNu869oUq4FUXizpiNWrIhucsYZqu0/0spudJEzk9SEKar0EjVDP7zkg/sKK+KccNypDQGW7nFXT8onzvQ3og== +"@react-native/babel-preset@0.79.2", "@react-native/babel-preset@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.3.tgz#8ad6c149cd488fbc18d62983119bdcbfc15ff651" + integrity sha512-VHGNP02bDD2Ul1my0pLVwe/0dsEBHxR343ySpgnkCNEEm9C1ANQIL2wvnJrHZPcqfAkWfFQ8Ln3t+6fdm4A/Dg== dependencies: "@babel/core" "^7.25.2" "@babel/plugin-proposal-export-default-from" "^7.24.7" @@ -6200,15 +6205,15 @@ "@babel/plugin-transform-typescript" "^7.25.2" "@babel/plugin-transform-unicode-regex" "^7.24.7" "@babel/template" "^7.25.0" - "@react-native/babel-plugin-codegen" "0.79.2" + "@react-native/babel-plugin-codegen" "0.79.3" babel-plugin-syntax-hermes-parser "0.25.1" babel-plugin-transform-flow-enums "^0.0.2" react-refresh "^0.14.0" -"@react-native/codegen@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.2.tgz#75270d8162e78c02b0272396a3c6942e39e8703d" - integrity sha512-8JTlGLuLi1p8Jx2N/enwwEd7/2CfrqJpv90Cp77QLRX3VHF2hdyavRIxAmXMwN95k+Me7CUuPtqn2X3IBXOWYg== +"@react-native/codegen@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.3.tgz#49689132718c81a3b25426769bc6fd8fd2a0469f" + integrity sha512-CZejXqKch/a5/s/MO5T8mkAgvzCXgsTkQtpCF15kWR9HN8T+16k0CsN7TXAxXycltoxiE3XRglOrZNEa/TiZUQ== dependencies: glob "^7.1.1" hermes-parser "0.25.1" @@ -6216,12 +6221,12 @@ nullthrows "^1.1.1" yargs "^17.6.2" -"@react-native/community-cli-plugin@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.2.tgz#d3a0efbdfb554cf3a7e9bfb27865a7caeeeaa1b3" - integrity sha512-E+YEY2dL+68HyR2iahsZdyBKBUi9QyPyaN9vsnda1jNgCjNpSPk2yAF5cXsho+zKK5ZQna3JSeE1Kbi2IfGJbw== +"@react-native/community-cli-plugin@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz#84821d3401074e036ba05b8b6ca1ee122cb43e29" + integrity sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA== dependencies: - "@react-native/dev-middleware" "0.79.2" + "@react-native/dev-middleware" "0.79.3" chalk "^4.0.0" debug "^2.2.0" invariant "^2.2.4" @@ -6230,18 +6235,18 @@ metro-core "^0.82.0" semver "^7.1.3" -"@react-native/debugger-frontend@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.2.tgz#1377de6d9cabe5455bf332e06408167da5f60c19" - integrity sha512-cGmC7X6kju76DopSBNc+PRAEetbd7TWF9J9o84hOp/xL3ahxR2kuxJy0oJX8Eg8oehhGGEXTuMKHzNa3rDBeSg== +"@react-native/debugger-frontend@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz#9cb57d8e88c22552194ab5f6f257605b151bc5b3" + integrity sha512-ImNDuEeKH6lEsLXms3ZsgIrNF94jymfuhPcVY5L0trzaYNo9ZFE9Ni2/18E1IbfXxdeIHrCSBJlWD6CTm7wu5A== -"@react-native/dev-middleware@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.2.tgz#f09f1a75b4cd0b56dfd82a07bf41157a9c45619c" - integrity sha512-9q4CpkklsAs1L0Bw8XYCoqqyBSrfRALGEw4/r0EkR38Y/6fVfNfdsjSns0pTLO6h0VpxswK34L/hm4uK3MoLHw== +"@react-native/dev-middleware@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.3.tgz#3e315ef7516ebad60a4202b4094d84fedecb4064" + integrity sha512-x88+RGOyG71+idQefnQg7wLhzjn/Scs+re1O5vqCkTVzRAc/f7SdHMlbmECUxJPd08FqMcOJr7/X3nsJBrNuuw== dependencies: "@isaacs/ttlcache" "^1.4.1" - "@react-native/debugger-frontend" "0.79.2" + "@react-native/debugger-frontend" "0.79.3" chrome-launcher "^0.15.2" chromium-edge-launcher "^0.2.0" connect "^3.6.5" @@ -6252,14 +6257,14 @@ serve-static "^1.16.2" ws "^6.2.3" -"@react-native/eslint-config@^0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.79.2.tgz#b42c95fe2399aae84209356b0971dd68c4149e4e" - integrity sha512-ukb9qGvrFC/3YVlWVy/GGM+auKdGIsbbumCyfOYPfUdhHFWA/twz1zK4bJQmCZ38iINuTENYedRzoE+U57GclA== +"@react-native/eslint-config@^0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.79.3.tgz#a72352ce98a7d05b5686dc446ba933fee3b944a9" + integrity sha512-pvPXfXFoVfwqGEItIl7emv6bRTpi0NWv4jmb0ZBwSJ7+zM8bRRw7JwxLA+iwrevh0QAqrF66Nu5xl39ysWcavg== dependencies: "@babel/core" "^7.25.2" "@babel/eslint-parser" "^7.25.1" - "@react-native/eslint-plugin" "0.79.2" + "@react-native/eslint-plugin" "0.79.3" "@typescript-eslint/eslint-plugin" "^7.1.1" "@typescript-eslint/parser" "^7.1.1" eslint-config-prettier "^8.5.0" @@ -6270,35 +6275,35 @@ eslint-plugin-react-hooks "^4.6.0" eslint-plugin-react-native "^4.0.0" -"@react-native/eslint-plugin@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/eslint-plugin/-/eslint-plugin-0.79.2.tgz#23d18226bb4335404e6db561bf1a47ac7d1380ed" - integrity sha512-Abu+0OuwTje9E5eQOvYpTUuXvDgGjHeFhnfNVY9BXalBK8OrX20EFlonWvIbFqii136eS5KLEBm2Wjqk2V5XJg== +"@react-native/eslint-plugin@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/eslint-plugin/-/eslint-plugin-0.79.3.tgz#c1ac34d45b92f963b9af66e860c45cd891235741" + integrity sha512-6QZzCsV+Wc+HdOAMMoMqDea3SSzsvBBktGc/cqaLubKGiztTb22d+vtzZGWqCqUEVkhQKdK7qhWAs0kJPEbiXw== -"@react-native/gradle-plugin@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.79.2.tgz#d41d4e2c63baf688a2b47652c6260f2a2f1ec091" - integrity sha512-6MJFemrwR0bOT0QM+2BxX9k3/pvZQNmJ3Js5pF/6owsA0cUDiCO57otiEU8Fz+UywWEzn1FoQfOfQ8vt2GYmoA== +"@react-native/gradle-plugin@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz#69ba47ac406ccdb3b3829f311bd7c27e6fad7ebc" + integrity sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g== -"@react-native/js-polyfills@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.79.2.tgz#15eb4da0fe9e8d61d2980d08fd06b5f49e133b0f" - integrity sha512-IaY87Ckd4GTPMkO1/Fe8fC1IgIx3vc3q9Tyt/6qS3Mtk9nC0x9q4kSR5t+HHq0/MuvGtu8HpdxXGy5wLaM+zUw== +"@react-native/js-polyfills@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz#bf5614363f118c6bdf2f773c578e603c88d0425c" + integrity sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q== -"@react-native/normalize-colors@0.79.2", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.79.2.tgz#9ab70ca257c7411e4ab74cf7f91332c27d39cc6f" - integrity sha512-+b+GNrupWrWw1okHnEENz63j7NSMqhKeFMOyzYLBwKcprG8fqJQhDIGXfizKdxeIa5NnGSAevKL1Ev1zJ56X8w== +"@react-native/normalize-colors@0.79.2", "@react-native/normalize-colors@0.79.3", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz#e491937436a2c287707e24263308c818a66eb447" + integrity sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ== -"@react-native/typescript-config@^0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/typescript-config/-/typescript-config-0.79.2.tgz#02cb07db89ef80159b3c1b3e82e81b0c0d5ce908" - integrity sha512-krHAkkPRCOEhuqN3iwRUwIyE1rAnUQ9//huzUc1ukcoQ7Y4qFxM6amhNloAmYn4QH1Ay6o5At00VbEh2xoHISA== +"@react-native/typescript-config@^0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/typescript-config/-/typescript-config-0.79.3.tgz#21101b591c67ecef6ae5189f96a448b3bcaf1e9e" + integrity sha512-dqKAU8D3NkExthnpBOPZjZ/NGU5qqBaqZ12v9IlMqP9sVTWSuY1iswfzomp0AYeJBxo4ZpBNmrdlnQZYQSVWiw== -"@react-native/virtualized-lists@0.79.2": - version "0.79.2" - resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.79.2.tgz#ed5a419a30b7ddec978b7816ff698a9d85507e15" - integrity sha512-9G6ROJeP+rdw9Bvr5ruOlag11ET7j1z/En1riFFNo6W3xZvJY+alCuH1ttm12y9+zBm4n8jwCk4lGhjYaV4dKw== +"@react-native/virtualized-lists@0.79.3": + version "0.79.3" + resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz#4a2799017cd3795f519422f48b3c0bbc4739a245" + integrity sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA== dependencies: invariant "^2.2.4" nullthrows "^1.1.1" @@ -6412,6 +6417,11 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz#0c5f26e417b8f524924fa4531b82ad5603216e90" integrity sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q== +"@sentry/babel-plugin-component-annotate@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz#f47a7652e16f84556df82cbc38f0004bca1335d1" + integrity sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg== + "@sentry/browser@8.54.0": version "8.54.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.54.0.tgz#5487075908aac564892e689e1b6d233fdb314f5b" @@ -6442,70 +6452,75 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz#a32a4f226e717122b37d9969e8d4d0e14779f720" integrity sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg== -"@sentry/cli-darwin@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.4.tgz#029521d3052c644e3bac1c926e53d1e658b8cb28" - integrity sha512-PZV4Y97VDWBR4rIt0HkJfXaBXlebIN2s/FDzC3iHINZE5OG62CDFsnC4/lbGlf2/UZLDaGGIK7mYwSHhTvN+HQ== +"@sentry/cli-darwin@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz#e3d6feae4fadcfdf91db9c7b9c4689a66d3d8d19" + integrity sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg== "@sentry/cli-linux-arm64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz#1c06c83ff21f51ec23acf5be3b1f8c7553bf86b1" integrity sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw== -"@sentry/cli-linux-arm64@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.4.tgz#b5e2d2399764998e3d661f144aae0c3f3495d1f1" - integrity sha512-Ex8vRnryyzC/9e43daEmEqPS+9uirY/l6Hw2lAvhBblFaL7PTWNx52H+8GnYGd9Zy2H3rWNyBDYfHwnErg38zA== +"@sentry/cli-linux-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz#384c8e17f7e7dc007d164033d0e7c75aa83a2e9b" + integrity sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg== "@sentry/cli-linux-arm@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz#00cadc359ae3c051efb3e63873c033c61dbd1ca1" integrity sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg== -"@sentry/cli-linux-arm@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.4.tgz#286996c3969a553c07a74a2a67c6d3671e2c79b5" - integrity sha512-lBn0oeeg62h68/4Eo6zbPq99Idz5t0VRV48rEU/WKeM4MtQCvG/iGGQ3lBFW2yNiUBzXZIK9poXLEcgbwmcRVw== +"@sentry/cli-linux-arm@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz#b9d6f86f3934b4d9ced5b45a8158ff2ac2bdd25d" + integrity sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ== "@sentry/cli-linux-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz#3b817b715dd806c20dfbffd539725ad8089c310a" integrity sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ== -"@sentry/cli-linux-i686@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.4.tgz#03e72598dc37e96a99e4329e20db9e74df277f83" - integrity sha512-IBJg0aHjsLCL4LvcFa3cXIjA+4t5kPqBT9y+PoDu4goIFxYD8zl7mbUdGJutvJafTk8Akf4ss4JJXQBjg019zA== +"@sentry/cli-linux-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz#39e22beb84cfa26e11bdc198364315fdfb4da4d5" + integrity sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw== "@sentry/cli-linux-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz#ddf906bc3071cc79ce6e633eddcb76bb9068e688" integrity sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw== -"@sentry/cli-linux-x64@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.4.tgz#a3bc31a909f61029620e5d2ae0f8d8625ed8982a" - integrity sha512-gXI5OEiOSNiAEz7VCE6AZcAgHJ47mlgal3+NmbE8XcHmFOnyDws9FNie6PJAy8KZjXi3nqoBP9JVAbnmOix3uA== +"@sentry/cli-linux-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz#25cd3699297f9433835fb5edd42dad722c11f041" + integrity sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg== + +"@sentry/cli-win32-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz#50c7d29ea2169bdb4d98bbde81c5f7dac0dd3955" + integrity sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg== "@sentry/cli-win32-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz#9036085c7c6ce455ad45fda411c55ff39c06eb95" integrity sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw== -"@sentry/cli-win32-i686@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.4.tgz#0de663fc574f4bce2057e099c5b76b65f99a3bd6" - integrity sha512-vZuR3UPHKqOMniyrijrrsNwn9usaRysXq78F6WV0cL0ZyPLAmY+KBnTDSFk1Oig2pURnzaTm+RtcZu2fc8mlzg== +"@sentry/cli-win32-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz#201075c4aec37a3e797160e0b468641245437f0c" + integrity sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w== "@sentry/cli-win32-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz#7d6464b63f32c9f97fff428f246b1f039b402233" integrity sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw== -"@sentry/cli-win32-x64@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.4.tgz#f572a03084f3b1a4f355c1fbeb1339cb56ad91c9" - integrity sha512-OIBj3uaQ6nAERSm5Dcf8UIhyElEEwMNsZEEppQpN4IKl0mrwb/57AznM23Dvpu6GR8WGbVQUSolt879YZR5E9g== +"@sentry/cli-win32-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz#2075e9e1ea3c3609e0fa1a758ca033e94e1c600f" + integrity sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw== "@sentry/cli@2.42.2": version "2.42.2" @@ -6526,10 +6541,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/cli@2.42.4": - version "2.42.4" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.42.4.tgz#df6ac3e92a60a715b231873433894ed77e601086" - integrity sha512-BoSZDAWJiz/40tu6LuMDkSgwk4xTsq6zwqYoUqLU3vKBR/VsaaQGvu6EWxZXORthfZU2/5Agz0+t220cge6VQw== +"@sentry/cli@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.45.0.tgz#35feed7a2fee54faf25daed73001a2a2a3143396" + integrity sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -6537,27 +6552,28 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.42.4" - "@sentry/cli-linux-arm" "2.42.4" - "@sentry/cli-linux-arm64" "2.42.4" - "@sentry/cli-linux-i686" "2.42.4" - "@sentry/cli-linux-x64" "2.42.4" - "@sentry/cli-win32-i686" "2.42.4" - "@sentry/cli-win32-x64" "2.42.4" + "@sentry/cli-darwin" "2.45.0" + "@sentry/cli-linux-arm" "2.45.0" + "@sentry/cli-linux-arm64" "2.45.0" + "@sentry/cli-linux-i686" "2.45.0" + "@sentry/cli-linux-x64" "2.45.0" + "@sentry/cli-win32-arm64" "2.45.0" + "@sentry/cli-win32-i686" "2.45.0" + "@sentry/cli-win32-x64" "2.45.0" "@sentry/core@8.54.0": version "8.54.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.54.0.tgz#a2ebec965cadcb6de89e116689feeef79d5862a6" integrity sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q== -"@sentry/react-native@~6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-6.10.0.tgz#9efafb9b85870bd4c5189763edde30709b9f3213" - integrity sha512-B56vc+pnFHMiu3cabFb454v4qD0zObW6JVzJ5Gb6fIMdt93AFIJg10ZErzC+ump7xM4BOEROFFRuLiyvadvlPA== +"@sentry/react-native@~6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-6.14.0.tgz#bc6bdaf03860bb8946f8c30570a9abd82ed6cfc0" + integrity sha512-BBqixN6oV6tCNp1ABXfzvD531zxj1fUAH0HDPvOR/jX0h9f9pYfxCyI64B+DoQbVZKFsg8nte0QIHkZDhRAW9A== dependencies: - "@sentry/babel-plugin-component-annotate" "3.2.2" + "@sentry/babel-plugin-component-annotate" "3.4.0" "@sentry/browser" "8.54.0" - "@sentry/cli" "2.42.4" + "@sentry/cli" "2.45.0" "@sentry/core" "8.54.0" "@sentry/react" "8.54.0" "@sentry/types" "8.54.0" @@ -8731,6 +8747,33 @@ babel-preset-expo@~13.1.11: react-refresh "^0.14.2" resolve-from "^5.0.0" +babel-preset-expo@~13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.2.0.tgz#d4540009d07242e3c3d63184b7a34efda95e8e64" + integrity sha512-oNUeUZPMNRPmx/2jaKJLSQFP/MFI1M91vP+Gp+j8/FPl9p/ps603DNwCaRdcT/Vj3FfREdlIwRio1qDCjY0oAA== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/plugin-proposal-decorators" "^7.12.9" + "@babel/plugin-proposal-export-default-from" "^7.24.7" + "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-flow-strip-types" "^7.25.2" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-runtime" "^7.24.7" + "@babel/preset-react" "^7.22.15" + "@babel/preset-typescript" "^7.23.0" + "@react-native/babel-preset" "0.79.3" + babel-plugin-react-native-web "~0.19.13" + babel-plugin-syntax-hermes-parser "^0.25.1" + babel-plugin-transform-flow-enums "^0.0.2" + debug "^4.3.4" + react-refresh "^0.14.2" + resolve-from "^5.0.0" + babel-preset-jest@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" @@ -8749,11 +8792,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" - integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== - base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" @@ -8960,6 +8998,16 @@ browserslist@^4.24.0, browserslist@^4.24.2: node-releases "^2.0.18" update-browserslist-db "^1.1.1" +browserslist@^4.25.0: + version "4.25.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c" + integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA== + dependencies: + caniuse-lite "^1.0.30001718" + electron-to-chromium "^1.5.160" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -9102,6 +9150,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz" integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ== +caniuse-lite@^1.0.30001718: + version "1.0.30001722" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz#ec25a2b3085b25b9079b623db83c22a70882ce85" + integrity sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA== + cbor-extract@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" @@ -10340,6 +10393,11 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.777.tgz#f846fbba23fd11b3c6f97848cdda94896fdb8baf" integrity sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw== +electron-to-chromium@^1.5.160: + version "1.5.166" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz#3fff386ed473cc2169dbe2d3ace9592262601114" + integrity sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw== + electron-to-chromium@^1.5.41: version "1.5.51" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.51.tgz#bb99216fed4892d131a8585a8593b00739310163" @@ -11146,18 +11204,18 @@ expo-application@~6.1.4: resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-6.1.4.tgz#34ee2f7a86e3689f15961b296e82934e0f85afd6" integrity sha512-jXVZb3llTQ5j4C/I03GxKjujmhKex9Xo5JDZo/pRjScHSr4NoeMjPKWThyWVlWDM1v5YSEcsRJebVfTvq9SR5Q== -expo-asset@~11.1.4: - version "11.1.4" - resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.1.4.tgz#0258156f76c306521eb2a0d27e98d26258d12ad9" - integrity sha512-e3210sF0YHKRTCjVUOVmDAJ0Dk4vepL9RocKe36S7S+VthoCZwsBGLAM2LLvBa1SdmODF92AS0Nrcfi/1/VlbQ== +expo-asset@~11.1.5: + version "11.1.5" + resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.1.5.tgz#5cad3d781c9d0edec31b9b3adbba574eb4d5dd3e" + integrity sha512-GEQDCqC25uDBoXHEnXeBuwpeXvI+3fRGvtzwwt0ZKKzWaN+TgeF8H7c76p3Zi4DfBMFDcduM0CmOvJX+yCCLUQ== dependencies: "@expo/image-utils" "^0.7.4" - expo-constants "~17.1.4" + expo-constants "~17.1.5" -expo-blur@~14.1.4: - version "14.1.4" - resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.1.4.tgz#d246c0a224ce63321d022edfc0e6a8c5fa2cc865" - integrity sha512-55P9tK/RjJZEcu2tU7BqX3wmIOrGMOOkmHztJMMws+ZGHzvtjnPmT7dsQxhOU9vPj77oHnKetYHU2sik3iBcCw== +expo-blur@~14.1.5: + version "14.1.5" + resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.1.5.tgz#910712389e19286ccdc136275bf569f427aa05ef" + integrity sha512-CCLJHxN4eoAl06ESKT3CbMasJ98WsjF9ZQEJnuxtDb9ffrYbZ+g9ru84fukjNUOTtc8A8yXE5z8NgY1l0OMrmQ== expo-build-properties@~0.14.6: version "0.14.6" @@ -11167,10 +11225,10 @@ expo-build-properties@~0.14.6: ajv "^8.11.0" semver "^7.6.0" -expo-camera@~16.1.6: - version "16.1.6" - resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-16.1.6.tgz#9badbc3b93cab3386e3e70721d4f3c1983ab539c" - integrity sha512-caVSfoTUaayYhH5gicrXWCgBQIVrotPOH3jUDr4vhN5VQDB/+TWaY+le2nQtNXgQEz14Af+H/TNvYpvvNj5Ktg== +expo-camera@~16.1.8: + version "16.1.8" + resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-16.1.8.tgz#6c30dfb5c982795351f1053c36f048a11869e21b" + integrity sha512-NpBbkUhHG6cs2TNUQBFSEtXb5j1/kTPIhiuqBcHosZG2yb/8MuM/ii4McJaqfe/6pn0YPqkH4k0Uod11DOSLmw== dependencies: invariant "^2.2.4" @@ -11179,7 +11237,7 @@ expo-clipboard@~7.1.4: resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-7.1.4.tgz#f2cda0d3cbfd2d307aa85dd7ba6843d6bbaf4227" integrity sha512-NHhfKnrzb4o0PacUKD93ByadU0JmPBoFTFYbbFJZ9OAX6SImpSqG5gfrMUR3vVj4Qx9f1LpMcdAv5lBzv868ow== -expo-constants@17.0.3, expo-constants@^13.0.2, expo-constants@~17.1.4, expo-constants@~17.1.5: +expo-constants@17.0.3, expo-constants@^13.0.2, expo-constants@~17.1.5, expo-constants@~17.1.6: version "17.0.3" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.3.tgz#a05b38e0417d59759ece1642b4d483889e04dbda" integrity sha512-lnbcX2sAu8SucHXEXxSkhiEpqH+jGrf+TF+MO6sHWIESjwOUVVYlT8qYdjR9xbxWmqFtrI4KV44FkeJf2DaFjQ== @@ -11187,25 +11245,25 @@ expo-constants@17.0.3, expo-constants@^13.0.2, expo-constants@~17.1.4, expo-cons "@expo/config" "~10.0.4" "@expo/env" "~0.4.0" -expo-dev-client@~5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.1.7.tgz#b75d5c4650a2b19e8d5c4666a798a4150a7fbd68" - integrity sha512-/xcwNIeZIBA/y6Io7jv1ZbEG8XRUuAynIJyIGJvpMxf6hm7eEw8rEzhO9rZNk6H8bMjTjASs0Vf1bqIV6v3j6A== +expo-dev-client@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.2.0.tgz#a3d4f8c79fafe747ea8d0bfc6e3918b02011025c" + integrity sha512-7GgO3BGlFM016Zkp3c9bUbi35pubqKh8Z/iHC1arIvckEjDrLER+92zfUTFr49XLk2o64arItRPJyQL49pA/hg== dependencies: - expo-dev-launcher "5.1.10" - expo-dev-menu "6.1.9" + expo-dev-launcher "5.1.12" + expo-dev-menu "6.1.11" expo-dev-menu-interface "1.10.0" - expo-manifests "~0.16.4" + expo-manifests "~0.16.5" expo-updates-interface "~1.1.0" -expo-dev-launcher@5.1.10: - version "5.1.10" - resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-5.1.10.tgz#eaa8a7a4edcab557e6623f176e60d964606651ff" - integrity sha512-OW4k0efB6cWigYj1GlJGObMuMpg6DIwsAZkECsGqNU3U80zE7pBMPB0sy1xehFuTplO6F+dCTLg0hPPuqkSsTg== +expo-dev-launcher@5.1.12: + version "5.1.12" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-5.1.12.tgz#cad239327e258d84c4221a212ecd30a051f9097d" + integrity sha512-ALedYerjJtSiPa95l41zMAO/m1m1kgS39i2H0io+6Ix4OksYNhILNzMNB1qDht/oWt2yjLBvXfWULfs5+3vnaA== dependencies: ajv "8.11.0" - expo-dev-menu "6.1.8" - expo-manifests "~0.16.4" + expo-dev-menu "6.1.11" + expo-manifests "~0.16.5" resolve-from "^5.0.0" expo-dev-menu-interface@1.10.0: @@ -11213,28 +11271,14 @@ expo-dev-menu-interface@1.10.0: resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz#04671bda3c163d1d7b9438ce7095c3913a3f53f9" integrity sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg== -expo-dev-menu@6.1.8: - version "6.1.8" - resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.1.8.tgz#cec53379b76f5cb53e2e87a8c62555610a26e6bb" - integrity sha512-i8DW1OXvj4yxQuPP8p2AMGoYdnyhfTkoqWI/AiDYReh2viZv4kZlRloGAemV2bIQwswMq2GsvZehYrQPEK9QBw== +expo-dev-menu@6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.1.11.tgz#829118326bcd618aec3941cfbb5b32fd3dd72379" + integrity sha512-yrlDXGcqlbQX3Pgw/iPLRea7+pHFC17MdtkNaXYQ5K5u64mn9l4KZ2ZYUeQ8cKDG5l8ZdC4F9R9vfCJYFi82AA== dependencies: expo-dev-menu-interface "1.10.0" -expo-dev-menu@6.1.9: - version "6.1.9" - resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.1.9.tgz#897dcb4c49aed9c1f871b13e1359079f23981050" - integrity sha512-Uz02Bsc1xsYzjW4Ld+PxWLRNkbsoJYSbQtw/pZDSrJk5Hj869M4KQSOI8JpZ7WVlKEKkIRA8kBLepjhdFhq+Dg== - dependencies: - expo-dev-menu-interface "1.10.0" - -expo-device@7.0.1, expo-device@~4.1.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-7.0.1.tgz#3702fe8b4475eac63ed27f9d580ec8a78546e0d1" - integrity sha512-/3lk0f9wvle+6svHqWSCBC1B5NYFmXp1D7hmIyecJJVYRLwzrwwTDyNs76oG/UDU5Appdu8QyDKycsx2hqv71w== - dependencies: - ua-parser-js "^0.7.33" - -expo-device@~7.1.4: +expo-device@7.1.4, expo-device@~4.1.1, expo-device@~7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-7.1.4.tgz#84ae7c2520cc45f15a9cb0433ae1226c33f7a8ef" integrity sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q== @@ -11246,15 +11290,15 @@ expo-eas-client@~0.14.3: resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.14.3.tgz#3fc22378cc454953ecba88f70c16c20a74e0aa27" integrity sha512-BW2mSNEjFRFC8/CbkMQ3mfVhBdeZIjZhNfncw7PP80xEptLWhVjGTqwG8Usi0/yPpIu/YNYgop+XGMfhXyh9uA== -expo-file-system@~18.1.8: - version "18.1.8" - resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.1.8.tgz#caa0831b9826f568be36deb25aed835978e957b3" - integrity sha512-1HXpunpRMGnoIw0+f2urjUNaePAvac1X9wIwVRsGJTw7A2WHBFATRuFB7jUOhZac/qK1MDm0GZsggzoRi1oteQ== +expo-file-system@~18.1.10: + version "18.1.10" + resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.1.10.tgz#22f3bcc2c9a7edcd6bba5ece3c90a8467fda47be" + integrity sha512-SyaWg+HitScLuyEeSG9gMSDT0hIxbM9jiZjSBP9l9zMnwZjmQwsusE6+7qGiddxJzdOhTP4YGUfvEzeeS0YL3Q== -expo-font@~13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-13.3.0.tgz#139e6e1024e414afe1180ffed0add98aa8c8950b" - integrity sha512-TdbHoxCfLWN9Uvnqsrcak+5EkDCbNIWfgtNWx3JZ6sD9WYB7gvbS+Eu5YlZ85NvCOSJ9Khmw4mFQxEi2LUPZfQ== +expo-font@~13.3.1: + version "13.3.1" + resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-13.3.1.tgz#ed69ae14f263a4c447efb2615b60d9e045372e68" + integrity sha512-d+xrHYvSM9WB42wj8vP9OOFWyxed5R1evphfDb6zYBmC1dA9Hf89FpT7TNFtj2Bk3clTnpmVqQTCYbbA2P3CLg== dependencies: fontfaceobserver "^2.1.0" @@ -11273,10 +11317,10 @@ expo-image-loader@~5.1.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153" integrity sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q== -expo-image-manipulator@~13.1.5: - version "13.1.5" - resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-13.1.5.tgz#d657dceeb8ce8da9345a1903f1327b0fedfd08c1" - integrity sha512-V9cGJp0zVwAvAyL3w9JsLH8UEQjQZfFmwJM1l9/oXjlDrSDynQrPFwJq4VI8dCOJ+/nhYZK37yytFNG14Sqt4Q== +expo-image-manipulator@~13.1.7: + version "13.1.7" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-13.1.7.tgz#e891ce9b49d75962eafdf5b7d670116583379e76" + integrity sha512-DBy/Xdd0E/yFind14x36XmwfWuUxOHI/oH97/giKjjPaRc2dlyjQ3tuW3x699hX6gAs9Sixj5WEJ1qNf3c8sag== dependencies: expo-image-loader "~5.1.0" @@ -11287,10 +11331,10 @@ expo-image-picker@~16.1.4: dependencies: expo-image-loader "~5.1.0" -expo-image@~2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.1.6.tgz#f046fa631768e37326cc14c49de9113ceffa9e8b" - integrity sha512-AFQxeAI1iTXFZ4dMxUB+SOACGMQxEk+t7PvT3j4mrvffRFoOLfHZ4uJc8SAdDFJak7ByqKhCIPIEhL+1Deq4Sg== +expo-image@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.2.1.tgz#b4aa706a25f7e8902ac854a8da249caf4a90cd67" + integrity sha512-5ZSggMi0X2G9AN0aM+sdkCyyZ6YcWvGs9KYLYrRBVUN3ph6RBiu6mKGpaNN1TAscySRnH1eHbUE1H+Qeq7qm1g== expo-json-utils@~0.15.0: version "0.15.0" @@ -11302,17 +11346,17 @@ expo-keep-awake@~14.1.4: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz#80197728563e0e17523e5a606fbd6fbed9639503" integrity sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA== -expo-linear-gradient@~14.1.4: - version "14.1.4" - resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-14.1.4.tgz#fcb2b586cfd21dd5f3076de467f92b55d9c47cb2" - integrity sha512-bImj2qqIjnl+VHYGnIwan9LxmGvb8e4hFqHpxsPzUiK7Ady7uERrXPhJcyTKTxRf4RL2sQRDpoOKzBYNdQDmuw== +expo-linear-gradient@~14.1.5: + version "14.1.5" + resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz#414bf0c8145089087198d4dd5a419eb324af2a02" + integrity sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ== -expo-linking@~7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-7.1.4.tgz#7d398d99788d8d95b67d1ae463d24686cbeb1c1b" - integrity sha512-zLAbUzTB3+KGjqqLeIdhhkXayyN0qulHGjRI24X7W/0Mq/4oPbPZklKtCP0k7XOn/k4553m8OgJ7GPC03PlV9g== +expo-linking@~7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-7.1.5.tgz#99633892712d5442ddb1c6c3857346eb7a67119b" + integrity sha512-8g20zOpROW78bF+bLI4a3ZWj4ntLgM0rCewKycPL0jk9WGvBrBtFtwwADJgOiV1EurNp3lcquerXGlWS+SOQyA== dependencies: - expo-constants "~17.1.4" + expo-constants "~17.1.6" invariant "^2.2.4" expo-localization@~16.1.5: @@ -11322,23 +11366,23 @@ expo-localization@~16.1.5: dependencies: rtl-detect "^1.0.2" -expo-manifests@~0.16.4: - version "0.16.4" - resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.16.4.tgz#d1a648bab0068a2712cf49009a5f26377a585849" - integrity sha512-zB6ohgnsNbJDaLI/KRZQXxEHadhMJt+gA4LCqbiZQNa3P4FJq4JFRXPV6QQjgjJ998g9vY7eDCTduxTJYBqUaA== +expo-manifests@~0.16.5: + version "0.16.5" + resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.16.5.tgz#bb57ceff3db4eb74679d4a155b2ca2050375ce10" + integrity sha512-zLUeJogn2C7qOE75Zz7jcmJorMfIbSRR35ctspN0OK/Hq/+PAAptA8p9jNVC8xp/91uP9uI8f3xPhh+A11eR2A== dependencies: - "@expo/config" "~11.0.6" + "@expo/config" "~11.0.10" expo-json-utils "~0.15.0" -expo-media-library@~17.1.6: - version "17.1.6" - resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-17.1.6.tgz#118ada2d28139d540861b338acf1d088bc03d51f" - integrity sha512-Py8Y9wJlNXBZkhtJYy9acj0oRoUV09WXsZnjcvy6xZjpniPQIq0wkIkgS2DLhlYmMdjBrSux2TGp7Omi2WHp1g== +expo-media-library@~17.1.7: + version "17.1.7" + resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-17.1.7.tgz#71ed3d2d246d33410d6aecc335098a23519bd890" + integrity sha512-hLCoMvlhjtt+iYxPe71P1F6t06mYGysuNOfjQzDbbf64PCkglCZJYmywPyUSV1V5Hu9DhRj//gEg+Ki+7VWXog== -expo-modules-autolinking@2.1.9: - version "2.1.9" - resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.1.9.tgz#7bf8338d4b7a1b6e8eccab51634de9b339e90c04" - integrity sha512-54InfnWy1BR54IDZoawqdFAaF2lyLHe9J+2dZ7y91/36jVpBtAval39ZKt2IISFJZ7TVglsojl4P5BDcDGcvjQ== +expo-modules-autolinking@2.1.11: + version "2.1.11" + resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.1.11.tgz#efc2e756ccc8b9e0b927596ba074aefe31b5cbe4" + integrity sha512-KrWQo+cE4gWYNePBBhmHGVzf63gYV19ZLXe9EIH3GHTkViVzIX+Lp618H/7GxfawpN5kbhvilATH1QEKKnUUww== dependencies: "@expo/spawn-async" "^1.7.2" chalk "^4.1.0" @@ -11348,10 +11392,10 @@ expo-modules-autolinking@2.1.9: require-from-string "^2.0.2" resolve-from "^5.0.0" -expo-modules-core@2.3.12: - version "2.3.12" - resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.3.12.tgz#1c06402564c02b32f192adfe6946e671d8a95e79" - integrity sha512-bOm83mskw1S7xuDX50DlLdx68u0doQ6BZHSU2qTv8P1/5QYeAae3pCgFLq2hoptUNeMF7W+68ShJFTOHAe68BQ== +expo-modules-core@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.4.0.tgz#3081d62fadff913090cc5abfe46d9ec6b0e75789" + integrity sha512-Ko5eHBdvuMykjw9P9C9PF54/wBSsGOxaOjx92I5BwgKvEmUwN3UrXFV4CXzlLVbLfSYUQaLcB220xmPfgvT7Fg== dependencies: invariant "^2.2.4" @@ -11362,10 +11406,10 @@ expo-modules-core@^2.1.1: dependencies: invariant "^2.2.4" -expo-notifications@~0.31.1: - version "0.31.1" - resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.31.1.tgz#3be669ab78c6099d82f4fc3607f77f7d9e1805b6" - integrity sha512-g1CMi+3wUaMuX+tAU+Fhnfs5bJtJ3JVRXihO415aOInQkox+Mh79n38RlT6snLWCHjfSojQIxmydzUHt6ywTIQ== +expo-notifications@~0.31.3: + version "0.31.3" + resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.31.3.tgz#eb82c9975e26dcc4fa694b79970792c897ad8d16" + integrity sha512-AATxKoav5ZvwcRel2SKYNZc+EvOAKvAjxyBezC8y3J5fMNe/uKIhMzh3FN4fKdOi9ao/UBHkvLiUO2MqVnvBNg== dependencies: "@expo/image-utils" "^0.7.4" "@ide/backoff" "^1.0.0" @@ -11373,7 +11417,7 @@ expo-notifications@~0.31.1: assert "^2.0.0" badgin "^1.1.5" expo-application "~6.1.4" - expo-constants "~17.1.4" + expo-constants "~17.1.6" expo-pwa@0.0.127: version "0.0.127" @@ -11385,34 +11429,34 @@ expo-pwa@0.0.127: commander "2.20.0" update-check "1.5.3" -expo-screen-orientation@~8.1.5: - version "8.1.5" - resolved "https://registry.yarnpkg.com/expo-screen-orientation/-/expo-screen-orientation-8.1.5.tgz#447c19b8ad59d1d2cf0894900ee273836367ca0d" - integrity sha512-t2ss7a52f1VNrugmIYj1wlnGICUgrkqu2+J3Csew8ixczI8s6r6EXT4E4EeyQ/9WvcEUOOyh8TWC9EbbswPhUw== +expo-screen-orientation@~8.1.7: + version "8.1.7" + resolved "https://registry.yarnpkg.com/expo-screen-orientation/-/expo-screen-orientation-8.1.7.tgz#3751b441f2bfcbde798b1508c0ff9f099f4be911" + integrity sha512-nYwadYtdU6mMDk0MCHMPPPQtBoeFYJ2FspLRW+J35CMLqzE4nbpwGeiImfXzkvD94fpOCfI4KgLj5vGauC3pfA== expo-sharing@~13.1.5: version "13.1.5" resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-13.1.5.tgz#73d86cdcc037b46ddc82be224dfd3d6bceec497c" integrity sha512-X/5sAEiWXL2kdoGE3NO5KmbfcmaCWuWVZXHu8OQef7Yig4ZgHFkGD11HKJ5KqDrDg+SRZe4ISd6MxE7vGUgm4w== -expo-splash-screen@~0.30.8: - version "0.30.8" - resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.30.8.tgz#2e960ccff053bc8ace85eb56f7d6745e4ddfc6b6" - integrity sha512-2eh+uA543brfeG5HILXmtNKA7E2/pfywKzNumzy3Ef6OtDjYy6zJUGNSbhnZRbVEjUZo3/QNRs0JRBfY80okZg== +expo-splash-screen@~0.30.9: + version "0.30.9" + resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.30.9.tgz#8f2a86b3b802ea46065fc761ed60e77e81bdb84c" + integrity sha512-curHUaZxUTZ2dWvz32ao3xPv5mJr1LBqn5V8xm/IULAehB9RGCn8iKiROMN1PYebSG+56vPMuJmBm9P+ayvJpA== dependencies: - "@expo/prebuild-config" "^9.0.5" + "@expo/prebuild-config" "^9.0.6" expo-structured-headers@~4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-4.1.0.tgz#5475fc3f9559701cc755fd2d50605f8817d42ad0" integrity sha512-2X+aUNzC/qaw7/WyUhrVHNDB0uQ5rE12XA2H/rJXaAiYQSuOeU90ladaN0IJYV9I2XlhYrjXLktLXWbO7zgbag== -expo-system-ui@~5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-5.0.7.tgz#ccf047a689ab488d9bcda375afd063419578f494" - integrity sha512-ijSnSFA4VfuQc84N6WyCUNsKKTIyQb6QuC8q2zGvYC/sBXTMrOtZg0zrisQGzCRW+WhritQTiVqHlp3Ix9xDmQ== +expo-system-ui@~5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-5.0.8.tgz#1eaaa95cfa8b5e20750e5fb30918635a58276199" + integrity sha512-2sI7ALq3W8sKKa3FRW7PmuNznk+48cb1VzFy96vYZLZgTDZViz+fEJNdp1RHgLui/mAl3f8md1LneygSJvZ1EQ== dependencies: - "@react-native/normalize-colors" "0.79.2" + "@react-native/normalize-colors" "0.79.3" debug "^4.3.2" expo-task-manager@~13.1.5: @@ -11427,55 +11471,55 @@ expo-updates-interface@~1.1.0: resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz#62497d4647b381da9fdb68868ed180203ae737ef" integrity sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w== -expo-updates@~0.28.12: - version "0.28.12" - resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.28.12.tgz#abf6e0c593837b20af64feee31aa018e1a85b332" - integrity sha512-GUQNI7apaQa8mVLGyeUQZsSaY75lq3yIp2OO+gX0BZIeD5hr8ADZ0Gw2+9+uRSB70B7AWQklUCMBxvOsA2DVfg== +expo-updates@~0.28.14: + version "0.28.14" + resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.28.14.tgz#bc75b00c0744fec33ba36f8c96e7c86ed0d3c7f6" + integrity sha512-kxI428W7LGSdDWmN/ud5cIg8+SjmQ5XSaUrYauZ0DKsHm2qq1Lh+NYSUWLvYmps+Baalafe6mILmAX8ZnNg26Q== dependencies: "@expo/code-signing-certificates" "0.0.5" - "@expo/config" "~11.0.7" + "@expo/config" "~11.0.10" "@expo/config-plugins" "~10.0.2" "@expo/spawn-async" "^1.7.2" arg "4.1.0" chalk "^4.1.2" expo-eas-client "~0.14.3" - expo-manifests "~0.16.4" + expo-manifests "~0.16.5" expo-structured-headers "~4.1.0" expo-updates-interface "~1.1.0" glob "^10.4.2" ignore "^5.3.1" resolve-from "^5.0.0" -expo-video@~2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-2.1.8.tgz#507305ae2ea18f435a51ababd6b2dec21291d328" - integrity sha512-OEToLVEGLvfTq7ypjgOf1DUNGHLGxUiL/1K6WHwdew5tgFxiLBZesGQhKToyrSVPVmWaxMBSP4sQuwtTjWUpHg== +expo-video@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-2.2.1.tgz#d45357ee7a7da5a87c49177587183d1c7feabc57" + integrity sha512-dw3h0eMLK8WpY1Tnwsgrxx3sFqXiOujmurjGdr+RFG63ZurAze/H9uuKMVl3ps/ZNuK4q/2ifIiJudoFJfwKwA== expo-web-browser@~14.1.6: version "14.1.6" resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.1.6.tgz#26d66e641e6e96d155be6fa513e7e667a719a0b0" integrity sha512-/4P8eWqRyfXIMZna3acg320LXNA+P2cwyEVbjDX8vHnWU+UnOtyRKWy3XaAIyMPQ9hVjBNUQTh4MPvtnPRzakw== -expo@^53.0.5: - version "53.0.5" - resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.5.tgz#f8c30643c4e45d769934415ea9196531612cb657" - integrity sha512-A9nh7ojGcdsv05TCH39q/7RRQsTfgcQ3qD2CFzlADVW8jIIFNiIg80shE8DQiXHkeCKWT78Iy7iog2yIu4sq3Q== +expo@53.0.11: + version "53.0.11" + resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.11.tgz#66053862520ce2a6700d13346ebaf8210a68f24b" + integrity sha512-+QtvU+6VPd7/o4vmtwuRE/Li2rAiJtD25I6BOnoQSxphaWWaD0PdRQnIV3VQ0HESuJYRuKJ3DkAHNJ3jI6xwzA== dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.24.10" - "@expo/config" "~11.0.7" + "@expo/cli" "0.24.14" + "@expo/config" "~11.0.10" "@expo/config-plugins" "~10.0.2" - "@expo/fingerprint" "0.12.4" - "@expo/metro-config" "0.20.12" + "@expo/fingerprint" "0.13.0" + "@expo/metro-config" "0.20.14" "@expo/vector-icons" "^14.0.0" - babel-preset-expo "~13.1.11" - expo-asset "~11.1.4" - expo-constants "~17.1.5" - expo-file-system "~18.1.8" - expo-font "~13.3.0" + babel-preset-expo "~13.2.0" + expo-asset "~11.1.5" + expo-constants "~17.1.6" + expo-file-system "~18.1.10" + expo-font "~13.3.1" expo-keep-awake "~14.1.4" - expo-modules-autolinking "2.1.9" - expo-modules-core "2.3.12" + expo-modules-autolinking "2.1.11" + expo-modules-core "2.4.0" react-native-edge-to-edge "1.6.0" whatwg-url-without-unicode "8.0.0-3" @@ -12055,6 +12099,11 @@ getenv@^1.0.0: resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31" integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg== +getenv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0" + integrity sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ== + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -12079,18 +12128,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" - integrity sha512-f8c0rE8JiCxpa52kWPAOa3ZaYEnzofDzCQLCn3Vdk0Z5OVLq3BsRFJI4S4ykpeVW6QMGBUkMeUpoEgWnMTnw5Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.3.10: version "10.3.12" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" @@ -13469,12 +13506,12 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" -jest-expo@~53.0.3: - version "53.0.3" - resolved "https://registry.yarnpkg.com/jest-expo/-/jest-expo-53.0.3.tgz#094da673d5b7953565c4cc6c41af4337465ddfc9" - integrity sha512-aOJyy/i0wPAWrQpSVNTTFWjcNTbUE/0eXr2cpdM3WW57K3EB1sueuKj8j2tWlzeKz4bk3+DVRy8K44V/XMoy3Q== +jest-expo@~53.0.7: + version "53.0.7" + resolved "https://registry.yarnpkg.com/jest-expo/-/jest-expo-53.0.7.tgz#ab1c288940dcdf470c25b7b18e2ed99f03d26bc6" + integrity sha512-Uiu3ES0sWbsxpifQuBzXMI1/N9JygfJfwEby/Qw/OPndIQ1YeeIQqkbP52xn6UhdSM4qYQiteX3EjY8TfrZIoA== dependencies: - "@expo/config" "~11.0.7" + "@expo/config" "~11.0.10" "@expo/json-file" "^9.1.4" "@jest/create-cache-key-function" "^29.2.1" "@jest/globals" "^29.2.1" @@ -14061,10 +14098,10 @@ kysely@^0.23.4: resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.23.5.tgz#60c63d94e1c42cc0411be8aaa688a0f27405f514" integrity sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow== -lan-network@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.1.5.tgz#e781889b7bd4dbedd9126fff3ceddd809a83c3ff" - integrity sha512-CV3k7l8jW0Z1b+G41tB7JInVyJEKQzh/YPl2v9uXpZMusp0aa+rh3OqG77xWuX7+eVBa8PsdTuMznTAssF4qwg== +lan-network@^0.1.6: + version "0.1.7" + resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.1.7.tgz#9fcb9967c6d951f10b2f9a9ffabe4a312d63f69d" + integrity sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ== lande@^1.0.10: version "1.0.10" @@ -14846,7 +14883,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -15097,6 +15134,11 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + nodemailer-html-to-text@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz#91b959491fef8f7d91796047abb728aa86d4a12b" @@ -15755,7 +15797,7 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picocolors@^1.1.0: +picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -16770,7 +16812,7 @@ react-keyed-flatten-children@^5.0.0: resolved "https://registry.yarnpkg.com/react-keyed-flatten-children/-/react-keyed-flatten-children-5.0.0.tgz#3024fc8819f7b60fc5039b527f133d9ac3a02a82" integrity sha512-XA5ah02sZAeDrbz4Lusd4acqG5q5BtVwPHWierruVhrgX6CMCldbGcTZZM14cQZ+GWq+tzRzEpsCvnTtLODvjw== -react-native-compressor@1.11.0: +react-native-compressor@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/react-native-compressor/-/react-native-compressor-1.11.0.tgz#e297fa650b09cc754392153c38ae2ca510aee024" integrity sha512-XaI0U2CtlW6ZYjwdQ4jdpnJa3C9CD1pc1a4jiUMtnUxWtCqgT7PNjOiEqlYLLwwGTvorXXNuby5In1yy7Vdmhg== @@ -16787,20 +16829,13 @@ react-native-dotenv@^3.4.11: dependencies: dotenv "^16.4.5" -react-native-drawer-layout@^4.1.10: +react-native-drawer-layout@^4.1.10, react-native-drawer-layout@^4.1.8: version "4.1.10" resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.10.tgz#9007cb747767ca8e1c9c3337671ad35ed95ad4d9" integrity sha512-wejQo0F+EffCkOkRh+DP6ENWMB+aWEHkXV8Pd564PmtoySZLUsV/ksYrh/mrufh7T7EuvGT8+fNHz7mMRYftWg== dependencies: use-latest-callback "^0.2.3" -react-native-drawer-layout@^4.1.6: - version "4.1.7" - resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.7.tgz#1c741c9bf9c739d6672201692e4ba4839ca0c8ff" - integrity sha512-KeTGZsNEDbOmgo8ICwr1vBmvWjRrRsvbLc2IAfQnW5h5UtxVZVRxY4QaN84BSBQPXm6tQ6AXfII8TCXCv3c0Ew== - dependencies: - use-latest-callback "^0.2.3" - react-native-edge-to-edge@1.6.0, react-native-edge-to-edge@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz#2ba63b941704a7f713e298185c26cde4d9e4b973" @@ -16858,10 +16893,10 @@ react-native-mmkv@^2.12.2: resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== -react-native-pager-view@6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.7.1.tgz#60d52dedbcc92ee7037a13287ebeed5f74e49df7" - integrity sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw== +react-native-pager-view@^6.7.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.8.1.tgz#fa0ec09ea7c44190c7c013d75dd09fdc17b96100" + integrity sha512-XIyVEMhwq7sZqM7GobOJZXxFCfdFgVNq/CFB2rZIRNRSVPJqE1k1fsc8xfQKfdzsp6Rpt6I7VOIvhmP7/YHdVg== react-native-progress@bluesky-social/react-native-progress: version "5.0.0" @@ -16895,10 +16930,10 @@ react-native-reanimated@~3.17.5: invariant "^2.2.4" react-native-is-edge-to-edge "1.1.7" -react-native-root-siblings@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/react-native-root-siblings/-/react-native-root-siblings-4.1.1.tgz#b7742db7634a87f507eb99a5fd699c4f10c46ab0" - integrity sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA== +react-native-root-siblings@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-native-root-siblings/-/react-native-root-siblings-5.0.1.tgz#97e050e5155228f65810fb1c466ff8e769c5272c" + integrity sha512-Ay3k/fBj6ReUkWX5WNS+oEAcgPLEGOK8n7K/L7D85mf3xvd8rm/b4spsv26E4HlFzluVx5HKbxEt9cl0wQ1u3g== react-native-safe-area-context@5.4.0: version "5.4.0" @@ -16914,10 +16949,10 @@ react-native-screens@^4.11.1: react-native-is-edge-to-edge "^1.1.7" warn-once "^0.1.0" -react-native-svg@15.11.2: - version "15.11.2" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.11.2.tgz#7540e8e1eabc4dcd3b1e35ada5a1d9f1b96d37c4" - integrity sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw== +react-native-svg@15.12.0: + version "15.12.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.12.0.tgz#0e2d476961e8b07f8c549fe4489c99b5130dc150" + integrity sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A== dependencies: css-select "^5.1.0" css-tree "^1.1.3" @@ -16968,27 +17003,27 @@ react-native-web@~0.20.0: postcss-value-parser "^4.2.0" styleq "^0.1.3" -react-native-webview@13.13.5: - version "13.13.5" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.13.5.tgz#4ef5f9310ddff5747f884a6655228ec9c7d52c73" - integrity sha512-MfC2B+woL4Hlj2WCzcb1USySKk+SteXnUKmKktOk/H/AQy5+LuVdkPKm8SknJ0/RxaxhZ48WBoTRGaqgR137hw== +react-native-webview@^13.13.5: + version "13.15.0" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.15.0.tgz#b6d2f8d8dd65897db76659ddd8198d2c74ec5a79" + integrity sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ== dependencies: escape-string-regexp "^4.0.0" invariant "2.2.4" -react-native@0.79.2: - version "0.79.2" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.2.tgz#f1a53099701c1736d09e441eb79f97cfc90dd202" - integrity sha512-AnGzb56JvU5YCL7cAwg10+ewDquzvmgrMddiBM0GAWLwQM/6DJfGd2ZKrMuKKehHerpDDZgG+EY64gk3x3dEkw== +react-native@^0.79.3: + version "0.79.3" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.3.tgz#16580ca202016c75e3c61116fcfe3b30f6d762fc" + integrity sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg== dependencies: "@jest/create-cache-key-function" "^29.7.0" - "@react-native/assets-registry" "0.79.2" - "@react-native/codegen" "0.79.2" - "@react-native/community-cli-plugin" "0.79.2" - "@react-native/gradle-plugin" "0.79.2" - "@react-native/js-polyfills" "0.79.2" - "@react-native/normalize-colors" "0.79.2" - "@react-native/virtualized-lists" "0.79.2" + "@react-native/assets-registry" "0.79.3" + "@react-native/codegen" "0.79.3" + "@react-native/community-cli-plugin" "0.79.3" + "@react-native/gradle-plugin" "0.79.3" + "@react-native/js-polyfills" "0.79.3" + "@react-native/normalize-colors" "0.79.3" + "@react-native/virtualized-lists" "0.79.3" abort-controller "^3.0.0" anser "^1.4.9" ansi-regex "^5.0.0" @@ -17541,14 +17576,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rn-fetch-blob@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba" - integrity sha512-+QnR7AsJ14zqpVVUbzbtAjq0iI8c9tCg49tIoKO2ezjzRunN7YL6zFSFSWZm6d+mE/l9r+OeDM3jmb2tBb2WbA== - dependencies: - base-64 "0.1.0" - glob "7.0.6" - roarr@^7.0.4: version "7.15.1" resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f" @@ -17736,12 +17763,10 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -semver@7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" +semver@7.6.3, semver@^7.1.3, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== semver@^5.5.0, semver@^5.6.0: version "5.7.2" @@ -17753,11 +17778,6 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.3, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" @@ -19372,6 +19392,14 @@ update-browserslist-db@^1.1.1: escalade "^3.2.0" picocolors "^1.1.0" +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + update-check@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.3.tgz#45240fcfb8755a7c7fa68bbdd9eda026a41639ed" From 8a5a7db1466f20af90809a6fe7c07bb0e1e000ac Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 12 Jun 2025 21:21:16 +0300 Subject: [PATCH 12/49] rm browserslist (#8481) --- package.json | 1 - yarn.lock | 35 +---------------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/package.json b/package.json index 38f574f42f..f490f108f0 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,6 @@ "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.1.0-rc.1", "babel-preset-expo": "~13.1.11", - "browserslist": "^4.25.0", "eslint": "^8.19.0", "eslint-plugin-bsky-internal": "link:./eslint", "eslint-plugin-ft-flow": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 125c985ea5..d9dbc18063 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8998,16 +8998,6 @@ browserslist@^4.24.0, browserslist@^4.24.2: node-releases "^2.0.18" update-browserslist-db "^1.1.1" -browserslist@^4.25.0: - version "4.25.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c" - integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA== - dependencies: - caniuse-lite "^1.0.30001718" - electron-to-chromium "^1.5.160" - node-releases "^2.0.19" - update-browserslist-db "^1.1.3" - bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -9150,11 +9140,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz" integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ== -caniuse-lite@^1.0.30001718: - version "1.0.30001722" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz#ec25a2b3085b25b9079b623db83c22a70882ce85" - integrity sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA== - cbor-extract@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" @@ -10393,11 +10378,6 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.777.tgz#f846fbba23fd11b3c6f97848cdda94896fdb8baf" integrity sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw== -electron-to-chromium@^1.5.160: - version "1.5.166" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz#3fff386ed473cc2169dbe2d3ace9592262601114" - integrity sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw== - electron-to-chromium@^1.5.41: version "1.5.51" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.51.tgz#bb99216fed4892d131a8585a8593b00739310163" @@ -15134,11 +15114,6 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - nodemailer-html-to-text@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz#91b959491fef8f7d91796047abb728aa86d4a12b" @@ -15797,7 +15772,7 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -19392,14 +19367,6 @@ update-browserslist-db@^1.1.1: escalade "^3.2.0" picocolors "^1.1.0" -update-browserslist-db@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" - integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - update-check@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.3.tgz#45240fcfb8755a7c7fa68bbdd9eda026a41639ed" From 18b4fcf8eb1ec4d27c1245726a4dbf50c534f651 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 12 Jun 2025 23:16:52 +0300 Subject: [PATCH 13/49] Revert "Instant Feed Update on Mute or Moderation Action" (#8482) --- src/state/cache/post-shadow.ts | 12 ++---------- src/state/queries/profile.ts | 4 +--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index 90fddda2bd..d7f1eb8b93 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -15,7 +15,6 @@ import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/q import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' -import {useProfileShadow} from './profile-shadow' import {castAsShadow, type Shadow} from './types' export type {Shadow} from './types' @@ -45,10 +44,6 @@ export function usePostShadow( setShadow(shadows.get(post)) } - const authorShadow = useProfileShadow(post.author) - const wasMuted = !!authorShadow.viewer?.muted - const wasBlocked = !!authorShadow.viewer?.blocking - useEffect(() => { function onUpdate() { setShadow(shadows.get(post)) @@ -60,18 +55,15 @@ export function usePostShadow( }, [post, setShadow]) return useMemo(() => { - if (wasMuted || wasBlocked) { - return POST_TOMBSTONE - } if (shadow) { return mergeShadow(post, shadow) } else { return castAsShadow(post) } - }, [post, shadow, wasMuted, wasBlocked]) + }, [post, shadow]) } -export function mergeShadow( +function mergeShadow( post: AppBskyFeedDefs.PostView, shadow: Partial, ): Shadow | typeof POST_TOMBSTONE { diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index b0af57c4a7..eb65fef7c2 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -499,10 +499,9 @@ function useProfileBlockMutation() { {subject: did, createdAt: new Date().toISOString()}, ) }, - onSuccess(data, {did}) { + onSuccess(_, {did}) { queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) resetProfilePostsQueries(queryClient, did, 1000) - updateProfileShadow(queryClient, did, {blockingUri: data.uri}) }, }) } @@ -524,7 +523,6 @@ function useProfileUnblockMutation() { }, onSuccess(_, {did}) { resetProfilePostsQueries(queryClient, did, 1000) - updateProfileShadow(queryClient, did, {blockingUri: undefined}) }, }) } From ba0f5a9bdef5bd0447ded23cab1af222b65511cc Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:41:11 +0000 Subject: [PATCH 14/49] Nightly source-language update --- src/locale/locales/en/messages.po | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 8bb3dca9e5..1b2b03a19d 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -4469,11 +4469,11 @@ msgstr "" msgid "List by {0}" msgstr "" -#: src/view/com/profile/ProfileSubpageHeader.tsx:156 +#: src/view/com/profile/ProfileSubpageHeader.tsx:160 msgid "List by <0/>" msgstr "" -#: src/view/com/profile/ProfileSubpageHeader.tsx:154 +#: src/view/com/profile/ProfileSubpageHeader.tsx:158 msgid "List by you" msgstr "" @@ -4740,12 +4740,12 @@ msgstr "" msgid "Moderation list by {0}" msgstr "" -#: src/view/com/profile/ProfileSubpageHeader.tsx:169 +#: src/view/com/profile/ProfileSubpageHeader.tsx:173 msgid "Moderation list by <0/>" msgstr "" #: src/view/com/modals/UserAddRemoveLists.tsx:220 -#: src/view/com/profile/ProfileSubpageHeader.tsx:167 +#: src/view/com/profile/ProfileSubpageHeader.tsx:171 msgid "Moderation list by you" msgstr "" @@ -5905,17 +5905,17 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1030 -msgctxt "action" -msgid "Post" -msgstr "" - #: src/screens/PostThread/index.tsx:490 #: src/view/com/post-thread/PostThread.tsx:561 msgctxt "description" msgid "Post" msgstr "" +#: src/view/com/composer/Composer.tsx:1030 +msgctxt "action" +msgid "Post" +msgstr "" + #: src/view/com/composer/Composer.tsx:1028 msgctxt "action" msgid "Post All" @@ -7681,12 +7681,12 @@ msgstr "" msgid "Starter pack by {0}" msgstr "" -#: src/view/com/profile/ProfileSubpageHeader.tsx:182 +#: src/view/com/profile/ProfileSubpageHeader.tsx:186 msgid "Starter pack by <0/>" msgstr "" #: src/components/StarterPack/StarterPackCard.tsx:89 -#: src/view/com/profile/ProfileSubpageHeader.tsx:180 +#: src/view/com/profile/ProfileSubpageHeader.tsx:184 msgid "Starter pack by you" msgstr "" @@ -9126,7 +9126,7 @@ msgstr "" msgid "View profile" msgstr "" -#: src/view/com/profile/ProfileSubpageHeader.tsx:119 +#: src/view/com/profile/ProfileSubpageHeader.tsx:123 msgid "View the avatar" msgstr "" @@ -9171,8 +9171,8 @@ msgstr "" msgid "View your verifications" msgstr "" -#: src/view/com/util/images/AutoSizedImage.tsx:199 -#: src/view/com/util/images/AutoSizedImage.tsx:221 +#: src/view/com/util/images/AutoSizedImage.tsx:205 +#: src/view/com/util/images/AutoSizedImage.tsx:227 msgid "Views full image" msgstr "" From 45f0f7eefecae1922c2f30d4e7760d2b93b1ae56 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Jun 2025 12:05:41 -0500 Subject: [PATCH 15/49] Port post embeds to new arch (#7408) * Direct port of embeds to new arch (cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6) * Re-org * Split out ListEmbed and FeedEmbed * Split out ImageEmbed * DRY up a bit * Port over ExternalLinkEmbed * Port over Player and Gif embeds * Migrate ComposerReplyTo * Replace other usages of old post-embeds * Migrate view contexts * Copy pasta VideoEmbed * Copy pasta GifEmbed * Swap in new file location * Clean up * Fix up native * Add back in correct moderation on List and Feed embeds * Format * Prettier * delete old video utils * move bandwidth-estimate.ts * Remove log * Add LazyQuoteEmbed for composer use * Clean up unused things * Remove remaining items * Prettier * Fix imports * Handle nested quotes same as prod * Add back silenced error handling * Fix lint --------- Co-authored-by: Samuel Newman --- bskylink/src/routes/index.ts | 4 +- bskylink/src/routes/redirect.ts | 4 +- bskylink/src/routes/root.ts | 4 +- src/App.native.tsx | 2 +- src/App.web.tsx | 4 +- src/alf/util/systemUI.ts | 2 +- src/components/ContextMenu/Backdrop.tsx | 2 +- src/components/FeedInterstitials.tsx | 18 +- src/components/Layout/Header/index.tsx | 8 +- src/components/Menu/context.tsx | 2 +- .../Post/Embed/ExternalEmbed/ExternalGif.tsx} | 2 +- .../Embed/ExternalEmbed/ExternalPlayer.tsx} | 2 +- .../Post/Embed/ExternalEmbed/Gif.tsx} | 0 .../Post/Embed/ExternalEmbed/index.tsx} | 10 +- src/components/Post/Embed/FeedEmbed.tsx | 52 +++ src/components/Post/Embed/ImageEmbed.tsx | 106 ++++++ src/components/Post/Embed/LazyQuoteEmbed.tsx | 37 ++ src/components/Post/Embed/ListEmbed.tsx | 42 +++ src/components/Post/Embed/PostPlaceholder.tsx | 33 ++ .../VideoEmbed}/ActiveVideoWebContext.tsx | 0 .../VideoEmbedInner/TimeIndicator.tsx | 0 .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 2 +- .../VideoEmbedInnerNative.web.tsx | 0 .../VideoEmbedInnerWeb.native.tsx | 0 .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 0 .../VideoEmbedInner/VideoFallback.tsx | 0 .../VideoEmbedInner/bandwidth-estimate.ts | 0 .../web-controls/ControlButton.tsx | 2 +- .../VideoEmbedInner/web-controls/Scrubber.tsx | 0 .../web-controls/VideoControls.native.tsx | 0 .../web-controls/VideoControls.tsx | 0 .../web-controls/VolumeControl.tsx | 2 +- .../VideoEmbedInner/web-controls/utils.tsx | 6 +- .../Embed/VideoEmbed}/VideoVolumeContext.tsx | 0 .../Post/Embed/VideoEmbed/index.tsx} | 4 +- .../Post/Embed/VideoEmbed/index.web.tsx} | 10 +- src/components/Post/Embed/index.tsx | 332 +++++++++++++++++ src/components/Post/Embed/types.ts | 25 ++ src/components/dms/ActionsWrapper.tsx | 2 +- src/components/dms/MessageItemEmbed.tsx | 9 +- src/components/hooks/dates.ts | 4 +- src/components/moderation/PostHider.tsx | 16 +- src/locale/helpers.ts | 2 +- src/screens/Login/LoginForm.tsx | 4 +- src/screens/Onboarding/StepProfile/index.tsx | 6 +- .../components/ThreadItemAnchor.tsx | 4 +- .../PostThread/components/ThreadItemPost.tsx | 4 +- .../components/ThreadItemTreePost.tsx | 4 +- src/screens/Profile/Header/Handle.tsx | 4 +- src/screens/Signup/StepInfo/Policies.tsx | 4 +- .../StarterPack/StarterPackLandingScreen.tsx | 2 +- src/screens/StarterPack/Wizard/State.tsx | 4 +- src/screens/Takendown.tsx | 2 +- src/screens/VideoFeed/components/Scrubber.tsx | 2 +- src/state/messages/events/agent.ts | 8 +- src/state/queries/postgate/util.ts | 7 +- src/state/session/types.ts | 4 +- src/state/threadgate-hidden-replies.tsx | 2 +- src/view/com/composer/Composer.tsx | 15 +- src/view/com/composer/ComposerReplyTo.tsx | 13 +- src/view/com/composer/ExternalEmbed.tsx | 37 +- .../com/composer/ExternalEmbedRemoveBtn.tsx | 13 +- src/view/com/composer/GifAltText.tsx | 8 +- src/view/com/composer/labels/LabelsBtn.tsx | 6 +- .../composer/photos/ImageAltTextDialog.tsx | 6 +- .../com/composer/photos/OpenCameraBtn.tsx | 2 +- src/view/com/post-thread/PostThreadItem.tsx | 6 +- src/view/com/post/Post.tsx | 4 +- src/view/com/posts/PostFeedItem.tsx | 5 +- src/view/com/util/Views.web.tsx | 8 +- src/view/com/util/images/Gallery.tsx | 2 +- src/view/com/util/images/ImageLayoutGrid.tsx | 2 +- src/view/com/util/post-embeds/QuoteEmbed.tsx | 337 ------------------ src/view/com/util/post-embeds/index.tsx | 327 ----------------- src/view/com/util/post-embeds/types.ts | 9 - 75 files changed, 812 insertions(+), 799 deletions(-) rename src/{view/com/util/post-embeds/ExternalGifEmbed.tsx => components/Post/Embed/ExternalEmbed/ExternalGif.tsx} (99%) rename src/{view/com/util/post-embeds/ExternalPlayerEmbed.tsx => components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx} (99%) rename src/{view/com/util/post-embeds/GifEmbed.tsx => components/Post/Embed/ExternalEmbed/Gif.tsx} (100%) rename src/{view/com/util/post-embeds/ExternalLinkEmbed.tsx => components/Post/Embed/ExternalEmbed/index.tsx} (94%) create mode 100644 src/components/Post/Embed/FeedEmbed.tsx create mode 100644 src/components/Post/Embed/ImageEmbed.tsx create mode 100644 src/components/Post/Embed/LazyQuoteEmbed.tsx create mode 100644 src/components/Post/Embed/ListEmbed.tsx create mode 100644 src/components/Post/Embed/PostPlaceholder.tsx rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/ActiveVideoWebContext.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/TimeIndicator.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/VideoEmbedInnerNative.tsx (98%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/VideoEmbedInnerNative.web.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/VideoEmbedInnerWeb.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/VideoFallback.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/bandwidth-estimate.ts (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/ControlButton.tsx (93%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/Scrubber.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/VideoControls.native.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/VideoControls.tsx (100%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/VolumeControl.tsx (97%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoEmbedInner/web-controls/utils.tsx (96%) rename src/{view/com/util/post-embeds => components/Post/Embed/VideoEmbed}/VideoVolumeContext.tsx (100%) rename src/{view/com/util/post-embeds/VideoEmbed.tsx => components/Post/Embed/VideoEmbed/index.tsx} (96%) rename src/{view/com/util/post-embeds/VideoEmbed.web.tsx => components/Post/Embed/VideoEmbed/index.web.tsx} (97%) create mode 100644 src/components/Post/Embed/index.tsx create mode 100644 src/components/Post/Embed/types.ts delete mode 100644 src/view/com/util/post-embeds/QuoteEmbed.tsx delete mode 100644 src/view/com/util/post-embeds/index.tsx delete mode 100644 src/view/com/util/post-embeds/types.ts diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts index 9fd20d276b..d0122ff8bf 100644 --- a/bskylink/src/routes/index.ts +++ b/bskylink/src/routes/index.ts @@ -1,6 +1,6 @@ -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {default as createShortLink} from './createShortLink.js' import {default as health} from './health.js' import {default as redirect} from './redirect.js' diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts index 468d250192..7d68e4245d 100644 --- a/bskylink/src/routes/redirect.ts +++ b/bskylink/src/routes/redirect.ts @@ -2,9 +2,9 @@ import assert from 'node:assert' import {DAY, SECOND} from '@atproto/common' import escapeHTML from 'escape-html' -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {handler} from './util.js' const INTERNAL_IP_REGEX = new RegExp( diff --git a/bskylink/src/routes/root.ts b/bskylink/src/routes/root.ts index 12bdf15155..8c6c4afc3b 100644 --- a/bskylink/src/routes/root.ts +++ b/bskylink/src/routes/root.ts @@ -1,6 +1,6 @@ -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {handler} from './util.js' export default function (ctx: AppContext, app: Express) { diff --git a/src/App.native.tsx b/src/App.native.tsx index 25d186dcfb..81d4a870e9 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -59,7 +59,6 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {TestCtrls} from '#/view/com/testing/TestCtrls' -import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' @@ -69,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' diff --git a/src/App.web.tsx b/src/App.web.tsx index fa8e24e53d..b706774fdc 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -48,8 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' -import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' @@ -60,6 +58,8 @@ import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' +import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' diff --git a/src/alf/util/systemUI.ts b/src/alf/util/systemUI.ts index c973e10ea6..9e5769c4c6 100644 --- a/src/alf/util/systemUI.ts +++ b/src/alf/util/systemUI.ts @@ -1,7 +1,7 @@ import * as SystemUI from 'expo-system-ui' import {isAndroid} from '#/platform/detection' -import {Theme} from '../types' +import {type Theme} from '../types' export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) { if (isAndroid) { diff --git a/src/components/ContextMenu/Backdrop.tsx b/src/components/ContextMenu/Backdrop.tsx index 027bf9849a..37fcebf493 100644 --- a/src/components/ContextMenu/Backdrop.tsx +++ b/src/components/ContextMenu/Backdrop.tsx @@ -2,7 +2,7 @@ import {Pressable} from 'react-native' import Animated, { Extrapolation, interpolate, - SharedValue, + type SharedValue, useAnimatedStyle, } from 'react-native-reanimated' import {msg} from '@lingui/macro' diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 6ecc3f5a87..a92e7be7f2 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,24 +1,30 @@ import React from 'react' import {View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' -import {AppBskyFeedDefs, AtUri} from '@atproto/api' +import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' -import {FeedDescriptor} from '#/state/queries/post-feed' +import {type FeedDescriptor} from '#/state/queries/post-feed' import {useProfilesQuery} from '#/state/queries/profile' import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' -import {SeenPost} from '#/state/userActionHistory' +import {type SeenPost} from '#/state/userActionHistory' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' +import { + atoms as a, + useBreakpoints, + useTheme, + type ViewStyleProp, + web, +} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' @@ -27,7 +33,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' -import * as bsky from '#/types/bsky' +import type * as bsky from '#/types/bsky' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 300 diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index 44faa96498..d68f4bd1d2 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -1,24 +1,24 @@ import {createContext, useCallback, useContext} from 'react' -import {GestureResponderEvent, Keyboard, View} from 'react-native' +import {type GestureResponderEvent, Keyboard, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {HITSLOP_30} from '#/lib/constants' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {isIOS} from '#/platform/detection' import {useSetDrawerOpen} from '#/state/shell' import { atoms as a, platform, - TextStyleProp, + type TextStyleProp, useBreakpoints, useGutters, useLayoutBreakpoints, useTheme, web, } from '#/alf' -import {Button, ButtonIcon, ButtonProps} from '#/components/Button' +import {Button, ButtonIcon, type ButtonProps} from '#/components/Button' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import { diff --git a/src/components/Menu/context.tsx b/src/components/Menu/context.tsx index d810a03de4..076bc81511 100644 --- a/src/components/Menu/context.tsx +++ b/src/components/Menu/context.tsx @@ -1,6 +1,6 @@ import React from 'react' -import type {ContextType, ItemContextType} from '#/components/Menu/types' +import {type ContextType, type ItemContextType} from '#/components/Menu/types' export const Context = React.createContext(null) diff --git a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx similarity index 99% rename from src/view/com/util/post-embeds/ExternalGifEmbed.tsx rename to src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx index 39c1d109e1..8a12f0374d 100644 --- a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx @@ -14,7 +14,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {Fill} from '#/components/Fill' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -export function ExternalGifEmbed({ +export function ExternalGif({ link, params, }: { diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx similarity index 99% rename from src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx rename to src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index e78abdf176..7f6d533408 100644 --- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -25,12 +25,12 @@ import {NavigationProp} from '#/lib/routes/types' import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' +import {EventStopper} from '#/view/com/util/EventStopper' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {Fill} from '#/components/Fill' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -import {EventStopper} from '../EventStopper' interface ShouldStartLoadRequest { url: string diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx similarity index 100% rename from src/view/com/util/post-embeds/GifEmbed.tsx rename to src/components/Post/Embed/ExternalEmbed/Gif.tsx diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx similarity index 94% rename from src/view/com/util/post-embeds/ExternalLinkEmbed.tsx rename to src/components/Post/Embed/ExternalEmbed/index.tsx index 7ca11f60d0..714eaecd63 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import {toNiceDomain} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' -import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' -import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' -import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' import {atoms as a, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Link} from '#/components/Link' import {Text} from '#/components/Typography' +import {ExternalGif} from './ExternalGif' +import {ExternalPlayer} from './ExternalPlayer' +import {GifEmbed} from './Gif' -export const ExternalLinkEmbed = ({ +export const ExternalEmbed = ({ link, onOpen, style, @@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({ ) : undefined} {embedPlayerParams?.isGif ? ( - + ) : embedPlayerParams ? ( ) : undefined} diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx new file mode 100644 index 0000000000..fad4cd4d8b --- /dev/null +++ b/src/components/Post/Embed/FeedEmbed.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import {StyleSheet} from 'react-native' +import {moderateFeedGenerator} from '@atproto/api' + +import {usePalette} from '#/lib/hooks/usePalette' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' +import {ContentHider} from '#/components/moderation/ContentHider' +import {type EmbedType} from '#/types/bsky/post' +import {type CommonProps} from './types' + +export function FeedEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'feed'> +}) { + const pal = usePalette('default') + return ( + + ) +} + +export function ModeratedFeedEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'feed'> +}) { + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return moderationOpts + ? moderateFeedGenerator(embed.view, moderationOpts) + : undefined + }, [embed.view, moderationOpts]) + return ( + + + + ) +} + +const styles = StyleSheet.create({ + customFeedOuter: { + borderWidth: StyleSheet.hairlineWidth, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12, + }, +}) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx new file mode 100644 index 0000000000..030d237a03 --- /dev/null +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -0,0 +1,106 @@ +import {InteractionManager, View} from 'react-native' +import { + type AnimatedRef, + measure, + type MeasuredDimensions, + runOnJS, + runOnUI, +} from 'react-native-reanimated' +import {Image} from 'expo-image' + +import {useLightboxControls} from '#/state/lightbox' +import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' +import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' +import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' +import {atoms as a} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' +import {type EmbedType} from '#/types/bsky/post' +import {type CommonProps} from './types' + +export function ImageEmbed({ + embed, + ...rest +}: CommonProps & { + embed: EmbedType<'images'> +}) { + const {openLightbox} = useLightboxControls() + const {images} = embed.view + + if (images.length > 0) { + const items = images.map(img => ({ + uri: img.fullsize, + thumbUri: img.thumb, + alt: img.alt, + dimensions: img.aspectRatio ?? null, + })) + const _openLightbox = ( + index: number, + thumbRects: (MeasuredDimensions | null)[], + fetchedDims: (Dimensions | null)[], + ) => { + openLightbox({ + images: items.map((item, i) => ({ + ...item, + thumbRect: thumbRects[i] ?? null, + thumbDimensions: fetchedDims[i] ?? null, + type: 'image', + })), + index, + }) + } + const onPress = ( + index: number, + refs: AnimatedRef[], + fetchedDims: (Dimensions | null)[], + ) => { + runOnUI(() => { + 'worklet' + const rects: (MeasuredDimensions | null)[] = [] + for (const r of refs) { + rects.push(measure(r)) + } + runOnJS(_openLightbox)(index, rects, fetchedDims) + })() + } + const onPressIn = (_: number) => { + InteractionManager.runAfterInteractions(() => { + Image.prefetch(items.map(i => i.uri)) + }) + } + + if (images.length === 1) { + const image = images[0] + return ( + + onPress(0, [containerRef], [dims])} + onPressIn={() => onPressIn(0)} + hideBadge={ + rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + } + /> + + ) + } + + return ( + + + + ) + } +} diff --git a/src/components/Post/Embed/LazyQuoteEmbed.tsx b/src/components/Post/Embed/LazyQuoteEmbed.tsx new file mode 100644 index 0000000000..fdc1c63091 --- /dev/null +++ b/src/components/Post/Embed/LazyQuoteEmbed.tsx @@ -0,0 +1,37 @@ +import {useMemo} from 'react' +import {View} from 'react-native' + +import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' +import {useResolveLinkQuery} from '#/state/queries/resolve-link' +import {atoms as a, useTheme} from '#/alf' +import {QuoteEmbed} from '#/components/Post/Embed' + +export function LazyQuoteEmbed({uri}: {uri: string}) { + const t = useTheme() + const {data} = useResolveLinkQuery(uri) + + const view = useMemo(() => { + if (!data || data.type !== 'record' || data.kind !== 'post') return + return createEmbedViewRecordFromPost(data.view) + }, [data]) + + return view ? ( + + ) : ( + + ) +} diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx new file mode 100644 index 0000000000..dc79a75798 --- /dev/null +++ b/src/components/Post/Embed/ListEmbed.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import {View} from 'react-native' +import {moderateUserList} from '@atproto/api' + +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {atoms as a, useTheme} from '#/alf' +import * as ListCard from '#/components/ListCard' +import {ContentHider} from '#/components/moderation/ContentHider' +import {EmbedType} from '#/types/bsky/post' +import {CommonProps} from './types' + +export function ListEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'list'> +}) { + const t = useTheme() + return ( + + + + ) +} + +export function ModeratedListEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'list'> +}) { + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return moderationOpts + ? moderateUserList(embed.view, moderationOpts) + : undefined + }, [embed.view, moderationOpts]) + return ( + + + + ) +} diff --git a/src/components/Post/Embed/PostPlaceholder.tsx b/src/components/Post/Embed/PostPlaceholder.tsx new file mode 100644 index 0000000000..8402340269 --- /dev/null +++ b/src/components/Post/Embed/PostPlaceholder.tsx @@ -0,0 +1,33 @@ +import {StyleSheet, View} from 'react-native' + +import {usePalette} from '#/lib/hooks/usePalette' +import {InfoCircleIcon} from '#/lib/icons' +import {Text} from '#/view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' + +export function PostPlaceholder({children}: {children: React.ReactNode}) { + const t = useTheme() + const pal = usePalette('default') + return ( + + + + {children} + + + ) +} + +const styles = StyleSheet.create({ + errorContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderRadius: 8, + marginTop: 8, + paddingVertical: 14, + paddingHorizontal: 14, + borderWidth: StyleSheet.hairlineWidth, + }, +}) diff --git a/src/view/com/util/post-embeds/ActiveVideoWebContext.tsx b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx similarity index 100% rename from src/view/com/util/post-embeds/ActiveVideoWebContext.tsx rename to src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx similarity index 98% rename from src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 8b44f54483..88879d45a7 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react' import {HITSLOP_30} from '#/lib/constants' import {useAutoplayDisabled} from '#/state/preferences' -import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext' import {atoms as a, useTheme} from '#/alf' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' @@ -15,6 +14,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {TimeIndicator} from './TimeIndicator' export const VideoEmbedInnerNative = React.forwardRef( diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/bandwidth-estimate.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.ts similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/bandwidth-estimate.ts rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.ts diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx similarity index 93% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx index 6510464453..1b69a3e253 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx @@ -1,8 +1,8 @@ import React from 'react' import {SvgProps} from 'react-native-svg' +import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {atoms as a, useTheme, web} from '#/alf' -import {PressableWithHover} from '../../../PressableWithHover' export function ControlButton({ active, diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.native.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx similarity index 97% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx index 90ffb9e6b1..e0b6880757 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx @@ -8,7 +8,7 @@ import {isSafari, isTouchDevice} from '#/lib/browser' import {atoms as a} from '#/alf' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' -import {useVideoVolumeState} from '../../VideoVolumeContext' +import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {ControlButton} from './ControlButton' export function VolumeControl({ diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx similarity index 96% rename from src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx rename to src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx index 108814ea2b..320f61a5f8 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx @@ -1,9 +1,9 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import {type RefObject, useCallback, useEffect, useRef, useState} from 'react' import {isSafari} from '#/lib/browser' -import {useVideoVolumeState} from '../../VideoVolumeContext' +import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' -export function useVideoElement(ref: React.RefObject) { +export function useVideoElement(ref: RefObject) { const [playing, setPlaying] = useState(false) const [muted, setMuted] = useState(true) const [currentTime, setCurrentTime] = useState(0) diff --git a/src/view/com/util/post-embeds/VideoVolumeContext.tsx b/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.tsx similarity index 100% rename from src/view/com/util/post-embeds/VideoVolumeContext.tsx rename to src/components/Post/Embed/VideoEmbed/VideoVolumeContext.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx similarity index 96% rename from src/view/com/util/post-embeds/VideoEmbed.tsx rename to src/components/Post/Embed/VideoEmbed/index.tsx index b45027089a..fe29ecad63 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -5,13 +5,13 @@ import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' -import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -import {ErrorBoundary} from '../ErrorBoundary' +import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx similarity index 97% rename from src/view/com/util/post-embeds/VideoEmbed.web.tsx rename to src/components/Post/Embed/VideoEmbed/index.web.tsx index b0ded67548..53adc3b6aa 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -5,16 +5,16 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isFirefox} from '#/lib/browser' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' +import {atoms as a} from '#/alf' +import {useIsWithinMessage} from '#/components/dms/MessageContext' +import {useFullscreen} from '#/components/hooks/useFullscreen' import { HLSUnsupportedError, VideoEmbedInnerWeb, VideoNotFoundError, -} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' -import {atoms as a} from '#/alf' -import {useIsWithinMessage} from '#/components/dms/MessageContext' -import {useFullscreen} from '#/components/hooks/useFullscreen' -import {ErrorBoundary} from '../ErrorBoundary' +} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb' import {useActiveVideoWeb} from './ActiveVideoWebContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx new file mode 100644 index 0000000000..ace85dc984 --- /dev/null +++ b/src/components/Post/Embed/index.tsx @@ -0,0 +1,332 @@ +import React from 'react' +import {View} from 'react-native' +import { + type $Typed, + type AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, + moderatePost, + RichText as RichTextAPI, +} from '@atproto/api' +import {Trans} from '@lingui/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {unstableCacheProfileView} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {Link} from '#/view/com/util/Link' +import {PostMeta} from '#/view/com/util/PostMeta' +import {atoms as a, useTheme} from '#/alf' +import {ContentHider} from '#/components/moderation/ContentHider' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {RichText} from '#/components/RichText' +import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import {SubtleWebHover} from '#/components/SubtleWebHover' +import * as bsky from '#/types/bsky' +import { + type Embed as TEmbed, + type EmbedType, + parseEmbed, +} from '#/types/bsky/post' +import {ExternalEmbed} from './ExternalEmbed' +import {ModeratedFeedEmbed} from './FeedEmbed' +import {ImageEmbed} from './ImageEmbed' +import {ModeratedListEmbed} from './ListEmbed' +import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder' +import { + type CommonProps, + type EmbedProps, + PostEmbedViewContext, + QuoteEmbedViewContext, +} from './types' +import {VideoEmbed} from './VideoEmbed' + +export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' + +export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { + const embed = parseEmbed(rawEmbed) + + switch (embed.type) { + case 'images': + case 'link': + case 'video': { + return + } + case 'feed': + case 'list': + case 'starter_pack': + case 'labeler': + case 'post': + case 'post_not_found': + case 'post_blocked': + case 'post_detached': { + return + } + case 'post_with_media': { + return ( + + + + + ) + } + default: { + return null + } + } +} + +function MediaEmbed({ + embed, + ...rest +}: CommonProps & { + embed: TEmbed +}) { + switch (embed.type) { + case 'images': { + return ( + + + + ) + } + case 'link': { + return ( + + + + ) + } + case 'video': { + return ( + + + + ) + } + default: { + return null + } + } +} + +function RecordEmbed({ + embed, + ...rest +}: CommonProps & { + embed: TEmbed +}) { + switch (embed.type) { + case 'feed': { + return ( + + + + ) + } + case 'list': { + return ( + + + + ) + } + case 'starter_pack': { + return ( + + + + ) + } + case 'labeler': { + // not implemented + return null + } + case 'post': { + if (rest.isWithinQuote && !rest.allowNestedQuotes) { + return null + } + + return ( + + ) + } + case 'post_not_found': { + return ( + + Deleted + + ) + } + case 'post_blocked': { + return ( + + Blocked + + ) + } + case 'post_detached': { + return + } + default: { + return null + } + } +} + +export function PostDetachedEmbed({ + embed, +}: { + embed: EmbedType<'post_detached'> +}) { + const {currentAccount} = useSession() + const isViewerOwner = currentAccount?.did + ? embed.view.uri.includes(currentAccount.did) + : false + + return ( + + {isViewerOwner ? ( + Removed by you + ) : ( + Removed by author + )} + + ) +} + +/* + * Nests parent `Embed` component and therefore must live in this file to avoid + * circular imports. + */ +export function QuoteEmbed({ + embed, + onOpen, + style, + isWithinQuote: parentIsWithinQuote, + allowNestedQuotes: parentAllowNestedQuotes, +}: Omit & { + embed: EmbedType<'post'> + viewContext?: QuoteEmbedViewContext +}) { + const moderationOpts = useModerationOpts() + const quote = React.useMemo<$Typed>( + () => ({ + ...embed.view, + $type: 'app.bsky.feed.defs#postView', + record: embed.view.value, + embed: embed.view.embeds?.[0], + }), + [embed], + ) + const moderation = React.useMemo(() => { + return moderationOpts ? moderatePost(quote, moderationOpts) : undefined + }, [quote, moderationOpts]) + + const t = useTheme() + const queryClient = useQueryClient() + const pal = usePalette('default') + const itemUrip = new AtUri(quote.uri) + const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) + const itemTitle = `Post by ${quote.author.handle}` + + const richText = React.useMemo(() => { + if ( + !bsky.dangerousIsType( + quote.record, + AppBskyFeedPost.isRecord, + ) + ) + return undefined + const {text, facets} = quote.record + return text.trim() + ? new RichTextAPI({text: text, facets: facets}) + : undefined + }, [quote.record]) + + const onBeforePress = React.useCallback(() => { + unstableCacheProfileView(queryClient, quote.author) + onOpen?.() + }, [queryClient, quote.author, onOpen]) + + const [hover, setHover] = React.useState(false) + return ( + { + setHover(true) + }} + onPointerLeave={() => { + setHover(false) + }}> + + + + + + + {moderation ? ( + + ) : null} + {richText ? ( + + ) : null} + {quote.embed && ( + + )} + + + + ) +} diff --git a/src/components/Post/Embed/types.ts b/src/components/Post/Embed/types.ts new file mode 100644 index 0000000000..b719d00b4e --- /dev/null +++ b/src/components/Post/Embed/types.ts @@ -0,0 +1,25 @@ +import {type StyleProp, type ViewStyle} from 'react-native' +import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' + +export enum PostEmbedViewContext { + ThreadHighlighted = 'ThreadHighlighted', + Feed = 'Feed', + FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', +} + +export enum QuoteEmbedViewContext { + FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, +} + +export type CommonProps = { + moderation?: ModerationDecision + onOpen?: () => void + style?: StyleProp + viewContext?: PostEmbedViewContext + isWithinQuote?: boolean + allowNestedQuotes?: boolean +} + +export type EmbedProps = CommonProps & { + embed?: AppBskyFeedDefs.PostView['embed'] +} diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index 120a5f8ad9..eb9f0a09a4 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {ChatBskyConvoDefs} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index f1c6189d06..6390300c1a 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -1,15 +1,16 @@ import React from 'react' import {useWindowDimensions, View} from 'react-native' -import {AppBskyEmbedRecord} from '@atproto/api' +import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {atoms as a, native, tokens, useTheme, web} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed' +import {Embed} from '#/components/Post/Embed' import {MessageContextProvider} from './MessageContext' let MessageItemEmbed = ({ embed, }: { - embed: AppBskyEmbedRecord.View + embed: $Typed }): React.ReactNode => { const t = useTheme() const screen = useWindowDimensions() @@ -32,7 +33,7 @@ let MessageItemEmbed = ({ }), ]}> - - - - diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 17d0f94f7c..de060c6c22 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -72,7 +72,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {mimeToExt} from '#/lib/media/video/util' import {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' -import {colors, s} from '#/lib/styles' +import {colors} from '#/lib/styles' import {logger} from '#/logger' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {useDialogStateControlContext} from '#/state/dialogs' @@ -97,6 +97,7 @@ import { ExternalEmbedGif, ExternalEmbedLink, } from '#/view/com/composer/ExternalEmbed' +import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' import {GifAltTextDialog} from '#/view/com/composer/GifAltText' import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn' import {Gallery} from '#/view/com/composer/photos/Gallery' @@ -116,7 +117,6 @@ import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn' import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' -import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -125,6 +125,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' import * as Prompt from '#/components/Prompt' import {Text as NewText} from '#/components/Typography' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' @@ -1149,13 +1150,17 @@ function ComposerEmbeds({ )} {embed.quote?.uri ? ( - - + + {canRemoveQuote && ( - dispatch({type: 'embed_remove_quote'})} /> + dispatch({type: 'embed_remove_quote'})} + style={{top: 16}} + /> )} diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 0ced143597..acab84f659 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -13,12 +13,13 @@ import {useLingui} from '@lingui/react' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {type ComposerOptsPostRef} from '#/state/shell/composer' -import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' +import {QuoteEmbed} from '#/components/Post/Embed' import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' +import {parseEmbed} from '#/types/bsky/post' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const t = useTheme() @@ -51,6 +52,12 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { } return null }, [embed]) + const parsedQuoteEmbed = quoteEmbed + ? parseEmbed({ + $type: 'app.bsky.embed.record#view', + ...quoteEmbed, + }) + : null const images = useMemo(() => { if (AppBskyEmbedImages.isView(embed)) { @@ -124,7 +131,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { )} - {showFull && quoteEmbed && } + {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( + + )} ) diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index d819b28b72..e4bdabac32 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -1,19 +1,20 @@ import React from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' +import {type StyleProp, View, type ViewStyle} from 'react-native' import {cleanError} from '#/lib/strings/errors' import { useResolveGifQuery, useResolveLinkQuery, } from '#/state/queries/resolve-link' -import {Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/tenor' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' -import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed' import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' +import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' +import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' +import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard' import {Text} from '#/components/Typography' -import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds' export const ExternalEmbedGif = ({ onRemove, @@ -44,7 +45,7 @@ export const ExternalEmbedGif = ({ {linkInfo ? ( - + ) : error ? ( @@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({ if (data) { if (data.type === 'external') { return ( - ) } else if (data.kind === 'feed') { - return + return ( + + ) } else if (data.kind === 'list') { - return + return ( + + ) } else if (data.kind === 'starter-pack') { return } diff --git a/src/view/com/composer/ExternalEmbedRemoveBtn.tsx b/src/view/com/composer/ExternalEmbedRemoveBtn.tsx index 92102f8478..1e363d0184 100644 --- a/src/view/com/composer/ExternalEmbedRemoveBtn.tsx +++ b/src/view/com/composer/ExternalEmbedRemoveBtn.tsx @@ -2,22 +2,27 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) { +export function ExternalEmbedRemoveBtn({ + onRemove, + style, +}: {onRemove: () => void} & ViewStyleProp) { + const t = useTheme() const {_} = useLingui() return ( - + diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index 4d2539c4e3..ceee17eaa0 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -6,23 +6,23 @@ import {useLingui} from '@lingui/react' import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants' import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import { - EmbedPlayerParams, + type EmbedPlayerParams, parseEmbedPlayerFromUrl, } from '#/lib/strings/embed-player' import {isAndroid} from '#/platform/detection' import {useResolveGifQuery} from '#/state/queries/resolve-link' -import {Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/tenor' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {DialogControlProps} from '#/components/Dialog' +import {type DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif' import {Text} from '#/components/Typography' -import {GifEmbed} from '../util/post-embeds/GifEmbed' import {AltTextReminder} from './photos/Gallery' export function GifAltTextDialog({ diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 9548ed0655..902d89b7bb 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -4,10 +4,10 @@ import {useLingui} from '@lingui/react' import { ADULT_CONTENT_LABELS, - AdultSelfLabel, + type AdultSelfLabel, OTHER_SELF_LABELS, - OtherSelfLabel, - SelfLabel, + type OtherSelfLabel, + type SelfLabel, } from '#/lib/moderation' import {isWeb} from '#/platform/detection' import {atoms as a, native, useTheme, web} from '#/alf' diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index c0ce32af31..724149937c 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ImageStyle, useWindowDimensions, View} from 'react-native' +import {type ImageStyle, useWindowDimensions, View} from 'react-native' import {Image} from 'expo-image' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,12 +7,12 @@ import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' import {enforceLen} from '#/lib/strings/helpers' import {isAndroid, isWeb} from '#/platform/detection' -import {ComposerImage} from '#/state/gallery' +import {type ComposerImage} from '#/state/gallery' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {DialogControlProps} from '#/components/Dialog' +import {type DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Text} from '#/components/Typography' diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 1c9440eb16..8bd1aa27b0 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions' import {openCamera} from '#/lib/media/picker' import {logger} from '#/logger' import {isMobileWeb, isNative} from '#/platform/detection' -import {ComposerImage, createComposerImage} from '#/state/gallery' +import {type ComposerImage, createComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5184047cbb..15f5539c9d 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -46,7 +46,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' import {formatCount} from '#/view/com/util/numeric/format' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -62,6 +61,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -465,7 +465,7 @@ let PostThreadItemLoaded = ({ ) : undefined} {post.embed && ( - - ) : undefined} {post.embed ? ( - - void diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index b91d7a7adb..757d952a19 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -3,8 +3,8 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {type AppBskyEmbedImages} from '@atproto/api' -import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useBreakpoints} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {type Dimensions} from '../../lightbox/ImageViewing/@types' import {GalleryItem} from './Gallery' diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx deleted file mode 100644 index f788af1f86..0000000000 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import React from 'react' -import { - StyleProp, - StyleSheet, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import { - AppBskyEmbedExternal, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - AppBskyFeedDefs, - AppBskyFeedPost, - moderatePost, - ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' -import {AtUri} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' - -import {HITSLOP_20} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' -import {InfoCircleIcon} from '#/lib/icons' -import {makeProfileLink} from '#/lib/routes/links' -import {s} from '#/lib/styles' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {precacheProfile} from '#/state/queries/profile' -import {useResolveLinkQuery} from '#/state/queries/resolve-link' -import {useSession} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {RichText} from '#/components/RichText' -import {SubtleWebHover} from '#/components/SubtleWebHover' -import * as bsky from '#/types/bsky' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {PostAlerts} from '../../../../components/moderation/PostAlerts' -import {Link} from '../Link' -import {PostMeta} from '../PostMeta' -import {Text} from '../text/Text' -import {PostEmbeds} from '.' -import {QuoteEmbedViewContext} from './types' - -export function MaybeQuoteEmbed({ - embed, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - embed: AppBskyEmbedRecord.View - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const t = useTheme() - const pal = usePalette('default') - const {currentAccount} = useSession() - if ( - AppBskyEmbedRecord.isViewRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.value) && - AppBskyFeedPost.validateRecord(embed.record.value).success - ) { - return ( - - ) - } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { - return ( - - - - Blocked - - - ) - } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { - return ( - - - - Deleted - - - ) - } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { - const isViewerOwner = currentAccount?.did - ? embed.record.uri.includes(currentAccount.did) - : false - return ( - - - - {isViewerOwner ? ( - Removed by you - ) : ( - Removed by author - )} - - - ) - } - return null -} - -function QuoteEmbedModerated({ - viewRecord, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - viewRecord: AppBskyEmbedRecord.ViewRecord - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const moderationOpts = useModerationOpts() - const postView = React.useMemo( - () => viewRecordToPostView(viewRecord), - [viewRecord], - ) - const moderation = React.useMemo(() => { - return moderationOpts ? moderatePost(postView, moderationOpts) : undefined - }, [postView, moderationOpts]) - - return ( - - ) -} - -export function QuoteEmbed({ - quote, - moderation, - onOpen, - style, - allowNestedQuotes, -}: { - quote: AppBskyFeedDefs.PostView - moderation?: ModerationDecision - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const t = useTheme() - const queryClient = useQueryClient() - const pal = usePalette('default') - const itemUrip = new AtUri(quote.uri) - const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) - const itemTitle = `Post by ${quote.author.handle}` - - const richText = React.useMemo(() => { - if ( - !bsky.dangerousIsType( - quote.record, - AppBskyFeedPost.isRecord, - ) - ) - return undefined - const {text, facets} = quote.record - return text.trim() - ? new RichTextAPI({text: text, facets: facets}) - : undefined - }, [quote.record]) - - const embed = React.useMemo(() => { - const e = quote.embed - - if (allowNestedQuotes) { - return e - } else { - if ( - AppBskyEmbedImages.isView(e) || - AppBskyEmbedExternal.isView(e) || - AppBskyEmbedVideo.isView(e) - ) { - return e - } else if ( - AppBskyEmbedRecordWithMedia.isView(e) && - (AppBskyEmbedImages.isView(e.media) || - AppBskyEmbedExternal.isView(e.media) || - AppBskyEmbedVideo.isView(e.media)) - ) { - return e.media - } - } - }, [quote.embed, allowNestedQuotes]) - - const onBeforePress = React.useCallback(() => { - precacheProfile(queryClient, quote.author) - onOpen?.() - }, [queryClient, quote.author, onOpen]) - - const [hover, setHover] = React.useState(false) - return ( - { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> - - - - - - - {moderation ? ( - - ) : null} - {richText ? ( - - ) : null} - {embed && } - - - - ) -} - -export function QuoteX({onRemove}: {onRemove: () => void}) { - const {_} = useLingui() - return ( - - - - ) -} - -export function LazyQuoteEmbed({uri}: {uri: string}) { - const {data} = useResolveLinkQuery(uri) - const moderationOpts = useModerationOpts() - if (!data || data.type !== 'record' || data.kind !== 'post') { - return null - } - const moderation = moderationOpts - ? moderatePost(data.view, moderationOpts) - : undefined - return -} - -function viewRecordToPostView( - viewRecord: AppBskyEmbedRecord.ViewRecord, -): AppBskyFeedDefs.PostView { - const {value, embeds, ...rest} = viewRecord - return { - ...rest, - $type: 'app.bsky.feed.defs#postView', - record: value, - embed: embeds?.[0], - } -} - -const styles = StyleSheet.create({ - errorContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - borderRadius: 8, - marginTop: 8, - paddingVertical: 14, - paddingHorizontal: 14, - borderWidth: StyleSheet.hairlineWidth, - }, - alert: { - marginBottom: 6, - }, -}) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx deleted file mode 100644 index 4cf71f9486..0000000000 --- a/src/view/com/util/post-embeds/index.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react' -import { - InteractionManager, - type StyleProp, - StyleSheet, - View, - type ViewStyle, -} from 'react-native' -import { - type AnimatedRef, - measure, - type MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' -import {Image} from 'expo-image' -import { - AppBskyEmbedExternal, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - AppBskyFeedDefs, - AppBskyGraphDefs, - moderateFeedGenerator, - moderateUserList, - type ModerationDecision, -} from '@atproto/api' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useLightboxControls} from '#/state/lightbox' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' -import {atoms as a, useTheme} from '#/alf' -import * as ListCard from '#/components/ListCard' -import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {type Dimensions} from '../../lightbox/ImageViewing/@types' -import {AutoSizedImage} from '../images/AutoSizedImage' -import {ImageLayoutGrid} from '../images/ImageLayoutGrid' -import {ExternalLinkEmbed} from './ExternalLinkEmbed' -import {MaybeQuoteEmbed} from './QuoteEmbed' -import {PostEmbedViewContext, QuoteEmbedViewContext} from './types' -import {VideoEmbed} from './VideoEmbed' - -export * from './types' - -type Embed = - | AppBskyEmbedRecord.View - | AppBskyEmbedImages.View - | AppBskyEmbedVideo.View - | AppBskyEmbedExternal.View - | AppBskyEmbedRecordWithMedia.View - | {$type: string; [k: string]: unknown} - -export function PostEmbeds({ - embed, - moderation, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - embed?: Embed - moderation?: ModerationDecision - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: PostEmbedViewContext -}) { - const {openLightbox} = useLightboxControls() - - // quote post with media - // = - if (AppBskyEmbedRecordWithMedia.isView(embed)) { - return ( - - - - - ) - } - - if (AppBskyEmbedRecord.isView(embed)) { - // custom feed embed (i.e. generator view) - if (AppBskyFeedDefs.isGeneratorView(embed.record)) { - return ( - - - - ) - } - - // list embed - if (AppBskyGraphDefs.isListView(embed.record)) { - return ( - - - - ) - } - - // starter pack embed - if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { - return ( - - - - ) - } - - // quote post - // = - return ( - - ) - } - - // image embed - // = - if (AppBskyEmbedImages.isView(embed)) { - const {images} = embed - - if (images.length > 0) { - const items = embed.images.map(img => ({ - uri: img.fullsize, - thumbUri: img.thumb, - alt: img.alt, - dimensions: img.aspectRatio ?? null, - })) - const _openLightbox = ( - index: number, - thumbRects: (MeasuredDimensions | null)[], - fetchedDims: (Dimensions | null)[], - ) => { - openLightbox({ - images: items.map((item, i) => ({ - ...item, - thumbRect: thumbRects[i] ?? null, - thumbDimensions: fetchedDims[i] ?? null, - type: 'image', - })), - index, - }) - } - const onPress = ( - index: number, - refs: AnimatedRef[], - fetchedDims: (Dimensions | null)[], - ) => { - runOnUI(() => { - 'worklet' - const rects: (MeasuredDimensions | null)[] = [] - for (const r of refs) { - rects.push(measure(r)) - } - runOnJS(_openLightbox)(index, rects, fetchedDims) - })() - } - const onPressIn = (_: number) => { - InteractionManager.runAfterInteractions(() => { - Image.prefetch(items.map(i => i.uri)) - }) - } - - if (images.length === 1) { - const image = images[0] - return ( - - - - onPress(0, [containerRef], [dims]) - } - onPressIn={() => onPressIn(0)} - hideBadge={ - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } - /> - - - ) - } - - return ( - - - - - - ) - } - } - - // external link embed - // = - if (AppBskyEmbedExternal.isView(embed)) { - const link = embed.external - return ( - - - - ) - } - - // video embed - // = - if (AppBskyEmbedVideo.isView(embed)) { - return ( - - - - ) - } - - return -} - -export function MaybeFeedCard({view}: {view: AppBskyFeedDefs.GeneratorView}) { - const pal = usePalette('default') - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return moderationOpts - ? moderateFeedGenerator(view, moderationOpts) - : undefined - }, [view, moderationOpts]) - - return ( - - - - ) -} - -export function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) { - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return moderationOpts ? moderateUserList(view, moderationOpts) : undefined - }, [view, moderationOpts]) - const t = useTheme() - - return ( - - - - - - ) -} - -const styles = StyleSheet.create({ - altContainer: { - backgroundColor: 'rgba(0, 0, 0, 0.75)', - borderRadius: 6, - paddingHorizontal: 6, - paddingVertical: 3, - position: 'absolute', - right: 6, - bottom: 6, - }, - alt: { - color: 'white', - fontSize: 7, - fontWeight: '600', - }, - customFeedOuter: { - borderWidth: StyleSheet.hairlineWidth, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 12, - }, -}) diff --git a/src/view/com/util/post-embeds/types.ts b/src/view/com/util/post-embeds/types.ts deleted file mode 100644 index 08e9032768..0000000000 --- a/src/view/com/util/post-embeds/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum PostEmbedViewContext { - ThreadHighlighted = 'ThreadHighlighted', - Feed = 'Feed', - FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', -} - -export enum QuoteEmbedViewContext { - FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, -} From d2de074dc4cac54a694ef2fa5af037a1d5de1cf3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Jun 2025 13:37:47 -0500 Subject: [PATCH 16/49] Disable default stack traces that are causing issues (#8487) --- src/logger/sentry/setup/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/logger/sentry/setup/index.ts b/src/logger/sentry/setup/index.ts index 3819211f3c..f05a7fc833 100644 --- a/src/logger/sentry/setup/index.ts +++ b/src/logger/sentry/setup/index.ts @@ -38,4 +38,12 @@ init({ */ `Network request failed`, ], + /** + * Does not affect traces of error events or other logs, just disables + * automatically attaching stack traces to events. This helps us group events + * and prevents explosions of separate issues. + * + * @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace + */ + attachStacktrace: false, }) From 7cd607f523b715bdea4b01d9203610e764b4fbe3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Jun 2025 15:11:42 -0500 Subject: [PATCH 17/49] Bump API SDK to fix `and/or` mute words bug (#8488) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f490f108f0..691a6e94d0 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.14", + "@atproto/api": "^0.15.15", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/yarn.lock b/yarn.lock index d9dbc18063..fb126ae730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,10 +63,10 @@ "@atproto/xrpc" "^0.7.0" "@atproto/xrpc-server" "^0.7.18" -"@atproto/api@^0.15.14": - version "0.15.14" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.14.tgz#41ff6ce2e7603119a005b7b5ce8e64551ec84879" - integrity sha512-FHEMAdscG+r2OFcZUIzPyTDpwzRAyinRsIIaTcuqe0MgZWF4CEGNAKPos0IbecBzMxTOzUHE18dQDKhoXMdgvg== +"@atproto/api@^0.15.15": + version "0.15.15" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.15.tgz#a506a1a26f3dfef9adb77234f451c0e784b071e7" + integrity sha512-Wn8jv76pCvffnkNj68w0CGZ3PT4DJGM8DUZnYq9kEW2im6jbRBYI0yYrHNhSiE92A5Ox0HjL2jMhalsI2p9VlQ== dependencies: "@atproto/common-web" "^0.4.2" "@atproto/lexicon" "^0.4.11" From ed9691511beb26bdb799bbcb9a973a8b8df3433c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 14 Jun 2025 00:41:12 +0300 Subject: [PATCH 18/49] Hover card on anchor displayName/handle (#8479) * add hover to anchor display name / handle * use newer link component * Wrap using a single hover element --------- Co-authored-by: Eric Bailey --- src/components/ProfileHoverCard/types.ts | 1 - src/components/RichText.tsx | 10 +-- .../components/ThreadItemAnchor.tsx | 85 +++++++++++-------- src/view/com/post/Post.tsx | 2 +- src/view/com/posts/PostFeedItem.tsx | 4 +- src/view/com/util/PostMeta.tsx | 2 +- 6 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/components/ProfileHoverCard/types.ts b/src/components/ProfileHoverCard/types.ts index 37087dc95a..01ef0fce71 100644 --- a/src/components/ProfileHoverCard/types.ts +++ b/src/components/ProfileHoverCard/types.ts @@ -3,6 +3,5 @@ import type React from 'react' export type ProfileHoverCardProps = { children: React.ReactElement did: string - inline?: boolean disable?: boolean } diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index d501f4287b..6493e23421 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,14 +1,14 @@ import React from 'react' -import {TextStyle} from 'react-native' +import {type TextStyle} from 'react-native' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' import {toShortUrl} from '#/lib/strings/url-helpers' -import {atoms as a, flatten, TextStyleProp} from '#/alf' +import {atoms as a, flatten, type TextStyleProp} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' -import {InlineLinkText, LinkProps} from '#/components/Link' +import {InlineLinkText, type LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichTextTag} from '#/components/RichTextTag' -import {Text, TextProps} from '#/components/Typography' +import {Text, type TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} @@ -105,7 +105,7 @@ export function RichText({ !disableLinks ) { els.push( - + { @@ -321,42 +319,57 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ live={live} onBeforePress={onOpenAuthor} /> - - - - - {sanitizeDisplayName( + + + + - + onPress={onOpenAuthor}> + + {sanitizeDisplayName( + post.author.displayName || + sanitizeHandle(post.author.handle), + moderation.ui('displayName'), + )} + + - - + + + + + + + + {sanitizeHandle(post.author.handle, '@')} + + - - - {sanitizeHandle(post.author.handle, '@')} - - - + {showFollowButton && ( @@ -417,7 +430,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ t.atoms.border_contrast_low, ]}> {post.repostCount != null && post.repostCount !== 0 ? ( - + @@ -435,7 +448,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ {post.quoteCount != null && post.quoteCount !== 0 && !post.viewer?.embeddingDisabled ? ( - + @@ -451,7 +464,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ ) : null} {post.likeCount != null && post.likeCount !== 0 ? ( - + diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index d92ea6a9dc..19017f30f4 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -204,7 +204,7 @@ function PostInner({ ) : ( Reply to{' '} - + Reposted by{' '} - + Reply to{' '} - + { )} - + Date: Sat, 14 Jun 2025 00:43:17 +0300 Subject: [PATCH 19/49] Use Button instead of TextLink for show more button (#8480) * use button instead of TextLink for show more * Match post text size, provide interaction feedback * Move to new Post components dir * Prettier --------- Co-authored-by: Eric Bailey --- .../Post/Embed/ExternalEmbed/ExternalGif.tsx | 10 +++- .../Embed/ExternalEmbed/ExternalPlayer.tsx | 11 ++-- .../Post/Embed/ExternalEmbed/Gif.tsx | 8 +-- src/components/Post/Embed/ListEmbed.tsx | 4 +- .../VideoEmbedInner/TimeIndicator.tsx | 2 +- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 4 +- .../VideoEmbedInner/VideoFallback.tsx | 2 +- .../web-controls/ControlButton.tsx | 4 +- .../VideoEmbedInner/web-controls/Scrubber.tsx | 3 +- .../web-controls/VolumeControl.tsx | 3 +- .../Post/Embed/VideoEmbed/index.tsx | 2 +- .../Post/Embed/VideoEmbed/index.web.tsx | 5 +- src/components/Post/ShowMoreTextButton.tsx | 56 +++++++++++++++++++ .../PostThread/components/ThreadItemPost.tsx | 40 ++++++------- .../components/ThreadItemTreePost.tsx | 38 ++++++------- src/screens/VideoFeed/components/Scrubber.tsx | 6 +- .../notifications/NotificationFeedItem.tsx | 4 +- src/view/com/post-thread/PostThreadItem.tsx | 17 +++--- src/view/com/post/Post.tsx | 39 +++++-------- src/view/com/posts/PostFeedItem.tsx | 27 +++------ 20 files changed, 160 insertions(+), 125 deletions(-) create mode 100644 src/components/Post/ShowMoreTextButton.tsx diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx index 8a12f0374d..0c8f30d2b9 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx @@ -1,11 +1,15 @@ import React from 'react' -import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native' +import { + ActivityIndicator, + type GestureResponderEvent, + Pressable, +} from 'react-native' import {Image} from 'expo-image' -import {AppBskyEmbedExternal} from '@atproto/api' +import {type AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {EmbedPlayerParams} from '#/lib/strings/embed-player' +import {type EmbedPlayerParams} from '#/lib/strings/embed-player' import {isIOS, isNative, isWeb} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' import {atoms as a, useTheme} from '#/alf' diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index 7f6d533408..392cdd8a10 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ActivityIndicator, - GestureResponderEvent, + type GestureResponderEvent, Pressable, StyleSheet, useWindowDimensions, @@ -16,13 +16,16 @@ import Animated, { import {useSafeAreaInsets} from 'react-native-safe-area-context' import {WebView} from 'react-native-webview' import {Image} from 'expo-image' -import {AppBskyEmbedExternal} from '@atproto/api' +import {type AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {NavigationProp} from '#/lib/routes/types' -import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' +import {type NavigationProp} from '#/lib/routes/types' +import { + type EmbedPlayerParams, + getPlayerAspect, +} from '#/lib/strings/embed-player' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' import {EventStopper} from '#/view/com/util/EventStopper' diff --git a/src/components/Post/Embed/ExternalEmbed/Gif.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx index a839294f1c..8e84997314 100644 --- a/src/components/Post/Embed/ExternalEmbed/Gif.tsx +++ b/src/components/Post/Embed/ExternalEmbed/Gif.tsx @@ -1,17 +1,17 @@ import React from 'react' import { Pressable, - StyleProp, + type StyleProp, StyleSheet, TouchableOpacity, View, - ViewStyle, + type ViewStyle, } from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {HITSLOP_20} from '#/lib/constants' -import {EmbedPlayerParams} from '#/lib/strings/embed-player' +import {type EmbedPlayerParams} from '#/lib/strings/embed-player' import {isWeb} from '#/platform/detection' import {useAutoplayDisabled} from '#/state/preferences' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' @@ -22,7 +22,7 @@ import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {GifView} from '../../../../../modules/expo-bluesky-gif-view' -import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' +import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' function PlaybackControls({ onPress, diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx index dc79a75798..82685d2715 100644 --- a/src/components/Post/Embed/ListEmbed.tsx +++ b/src/components/Post/Embed/ListEmbed.tsx @@ -6,8 +6,8 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {atoms as a, useTheme} from '#/alf' import * as ListCard from '#/components/ListCard' import {ContentHider} from '#/components/moderation/ContentHider' -import {EmbedType} from '#/types/bsky/post' -import {CommonProps} from './types' +import {type EmbedType} from '#/types/bsky/post' +import {type CommonProps} from './types' export function ListEmbed({ embed, diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx index 95401309f4..67af7618c3 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx @@ -1,4 +1,4 @@ -import {StyleProp, ViewStyle} from 'react-native' +import {type StyleProp, type ViewStyle} from 'react-native' import {View} from 'react-native' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 88879d45a7..351e9f3056 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -1,6 +1,6 @@ import React, {useRef} from 'react' -import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import {AppBskyEmbedVideo} from '@atproto/api' +import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' +import {type AppBskyEmbedVideo} from '@atproto/api' import {BlueskyVideoView} from '@haileyok/bluesky-video' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx index 1b46163cce..37b44751d1 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx @@ -1,7 +1,7 @@ -import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import type React from 'react' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx index 1b69a3e253..9b0c963eaf 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import {SvgProps} from 'react-native-svg' +import {type SvgProps} from 'react-native-svg' +import type React from 'react' import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {atoms as a, useTheme, web} from '#/alf' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx index 96960bad47..d84a90fa62 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx @@ -1,7 +1,8 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import {useCallback, useEffect, useRef, useState} from 'react' import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import type React from 'react' import {isFirefox, isTouchDevice} from '#/lib/browser' import {clamp} from '#/lib/numbers' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx index e0b6880757..ec5f23fc07 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx @@ -1,8 +1,9 @@ -import React, {useCallback} from 'react' +import {useCallback} from 'react' import {View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import type React from 'react' import {isSafari, isTouchDevice} from '#/lib/browser' import {atoms as a} from '#/alf' diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index fe29ecad63..8cb78ff70b 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useState} from 'react' import {ActivityIndicator, View} from 'react-native' import {ImageBackground} from 'expo-image' -import {AppBskyEmbedVideo} from '@atproto/api' +import {type AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index 53adc3b6aa..7f601af47b 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -1,8 +1,9 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import {useCallback, useEffect, useRef, useState} from 'react' import {View} from 'react-native' -import {AppBskyEmbedVideo} from '@atproto/api' +import {type AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import type React from 'react' import {isFirefox} from '#/lib/browser' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' diff --git a/src/components/Post/ShowMoreTextButton.tsx b/src/components/Post/ShowMoreTextButton.tsx new file mode 100644 index 0000000000..bc6db55b9e --- /dev/null +++ b/src/components/Post/ShowMoreTextButton.tsx @@ -0,0 +1,56 @@ +import {useCallback, useMemo} from 'react' +import {LayoutAnimation, type TextStyle} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {HITSLOP_10} from '#/lib/constants' +import {atoms as a, flatten, type TextStyleProp, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {Text} from '#/components/Typography' + +export function ShowMoreTextButton({ + onPress: onPressProp, + style, +}: TextStyleProp & {onPress: () => void}) { + const t = useTheme() + const {_} = useLingui() + + const onPress = useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + onPressProp() + }, [onPressProp]) + + const textStyle = useMemo(() => { + return flatten([a.leading_snug, a.text_sm, style]) as TextStyle & { + fontSize: number + lineHeight: number + } + }, [style]) + + return ( + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index 9393a6d1bb..4337397f84 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -6,13 +6,11 @@ import { AtUri, RichText as RichTextAPI, } from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {Trans} from '@lingui/macro' import {useActorStatus} from '#/lib/actor-status' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' import { @@ -24,7 +22,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types' import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {TextLink} from '#/view/com/util/Link' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import { @@ -40,6 +37,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' +import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' @@ -187,8 +185,6 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ postShadow: Shadow }) { const t = useTheme() - const pal = usePalette('default') - const {_} = useLingui() const {openComposer} = useOpenComposer() const {currentAccount} = useSession() @@ -304,22 +300,22 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ additionalCauses={additionalPostAlerts} /> {richText?.text ? ( - - ) : undefined} - {limitLines ? ( - + <> + + {limitLines && ( + + )} + ) : undefined} {post.embed && ( diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index ac659a6e06..a8ffb76f46 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -1,4 +1,4 @@ -import React, {memo, useMemo} from 'react' +import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyFeedDefs, @@ -6,12 +6,10 @@ import { AtUri, RichText as RichTextAPI, } from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {Trans} from '@lingui/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' import { @@ -23,7 +21,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types' import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {TextLink} from '#/view/com/util/Link' import {PostMeta} from '#/view/com/util/PostMeta' import { OUTER_SPACE, @@ -39,6 +36,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' +import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' @@ -255,8 +253,6 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode { - const pal = usePalette('default') - const {_} = useLingui() const {openComposer} = useOpenComposer() const {currentAccount} = useSession() @@ -271,18 +267,18 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ }), [record], ) - const [limitLines, setLimitLines] = React.useState( + const [limitLines, setLimitLines] = useState( () => countLines(richText?.text) >= MAX_POST_LINES, ) const threadRootUri = record.reply?.root?.uri || post.uri - const postHref = React.useMemo(() => { + const postHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ threadgateRecord, }) - const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) const isControlledByViewer = new AtUri(threadRootUri).host === currentAccount?.did @@ -297,7 +293,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ : [] }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) - const onPressReply = React.useCallback(() => { + const onPressReply = useCallback(() => { openComposer({ replyTo: { uri: post.uri, @@ -311,7 +307,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ }) }, [openComposer, post, record, onPostSuccess, moderation]) - const onPressShowMore = React.useCallback(() => { + const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) @@ -348,7 +344,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ additionalCauses={additionalPostAlerts} /> {richText?.text ? ( - + <> - - ) : undefined} - {limitLines ? ( - + {limitLines && ( + + )} + ) : undefined} {post.embed && ( diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index 29cc4b278a..69e68ec9e3 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -3,13 +3,13 @@ import {View} from 'react-native' import { Gesture, GestureDetector, - NativeGesture, + type NativeGesture, } from 'react-native-gesture-handler' import Animated, { interpolate, runOnJS, runOnUI, - SharedValue, + type SharedValue, useAnimatedReaction, useAnimatedStyle, useSharedValue, @@ -20,7 +20,7 @@ import { useSafeAreaInsets, } from 'react-native-safe-area-context' import {useEventListener} from 'expo' -import {VideoPlayer} from 'expo-video' +import {type VideoPlayer} from 'expo-video' import {tokens} from '#/alf' import {atoms as a} from '#/alf' diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 0a460e77b6..85f67919ac 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -30,6 +30,7 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {MAX_POST_LINES} from '#/lib/constants' import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' @@ -918,7 +919,8 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { {text?.length > 0 && ( + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]} + numberOfLines={MAX_POST_LINES}> {text} )} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 15f5539c9d..592224ff54 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -44,7 +44,7 @@ import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replie import {type PostSource} from '#/state/unstable-post-source' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {Link, TextLink} from '#/view/com/util/Link' +import {Link} from '#/view/com/util/Link' import {formatCount} from '#/view/com/util/numeric/format' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -62,6 +62,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' +import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {PostControls} from '#/components/PostControls' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -685,16 +686,14 @@ let PostThreadItemLoaded = ({ authorHandle={post.author.handle} shouldProxyLinks={true} /> + {limitLines && ( + + )} ) : undefined} - {limitLines ? ( - - ) : undefined} {post.embed && ( countLines(richText?.text) >= MAX_POST_LINES, @@ -128,7 +127,7 @@ function PostInner({ replyAuthorDid = urip.hostname } - const onPressReply = React.useCallback(() => { + const onPressReply = useCallback(() => { openComposer({ replyTo: { uri: post.uri, @@ -141,18 +140,18 @@ function PostInner({ }) }, [openComposer, post, record, moderation]) - const onPressShowMore = React.useCallback(() => { + const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) - const onBeforePress = React.useCallback(() => { + const onBeforePress = useCallback(() => { precacheProfile(queryClient, post.author) }, [queryClient, post.author]) const {currentAccount} = useSession() const isMe = replyAuthorDid === currentAccount?.did - const [hover, setHover] = React.useState(false) + const [hover, setHover] = useState(false) return ( {richText.text ? ( - + + {limitLines && ( + + )} ) : undefined} - {limitLines ? ( - - ) : undefined} {post.embed ? ( { - const pal = usePalette('default') - const {_} = useLingui() const {currentAccount} = useSession() const [limitLines, setLimitLines] = useState( () => countLines(richText.text) >= MAX_POST_LINES, @@ -547,7 +546,7 @@ let PostContent = ({ additionalCauses={additionalPostAlerts} /> {richText.text ? ( - + <> - - ) : undefined} - {limitLines ? ( - + {limitLines && ( + + )} + ) : undefined} {postEmbed ? ( @@ -689,13 +683,6 @@ const styles = StyleSheet.create({ marginTop: 6, marginBottom: 6, }, - postTextContainer: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - paddingBottom: 2, - overflow: 'hidden', - }, contentHiderChild: { marginTop: 6, }, From 45e64757d01977783be904fc7d7761bd88e7914c Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:39:43 +0000 Subject: [PATCH 20/49] Nightly source-language update --- src/locale/locales/en/messages.po | 432 +++++++++++++++--------------- 1 file changed, 212 insertions(+), 220 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 1b2b03a19d..088d04852c 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -95,8 +95,8 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:461 -#: src/view/com/post-thread/PostThreadItem.tsx:540 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:474 +#: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -104,13 +104,13 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:445 -#: src/view/com/post-thread/PostThreadItem.tsx:524 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:458 +#: src/view/com/post-thread/PostThreadItem.tsx:525 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:427 -#: src/view/com/post-thread/PostThreadItem.tsx:506 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:440 +#: src/view/com/post-thread/PostThreadItem.tsx:507 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" @@ -145,7 +145,7 @@ msgstr "" msgid "{0} joined this week" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:201 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:202 msgid "{0} of {1}" msgstr "" @@ -212,155 +212,155 @@ msgstr "" msgid "{estimatedTimeMins, plural, one {minute} other {minutes}}" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:335 +#: src/view/com/notifications/NotificationFeedItem.tsx:336 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} followed you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:361 +#: src/view/com/notifications/NotificationFeedItem.tsx:362 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:254 +#: src/view/com/notifications/NotificationFeedItem.tsx:255 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:466 +#: src/view/com/notifications/NotificationFeedItem.tsx:467 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:439 +#: src/view/com/notifications/NotificationFeedItem.tsx:440 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} removed their verifications from your account" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:278 +#: src/view/com/notifications/NotificationFeedItem.tsx:279 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:490 +#: src/view/com/notifications/NotificationFeedItem.tsx:491 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:385 +#: src/view/com/notifications/NotificationFeedItem.tsx:386 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} signed up with your starter pack" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:414 +#: src/view/com/notifications/NotificationFeedItem.tsx:415 msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} verified you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:347 +#: src/view/com/notifications/NotificationFeedItem.tsx:348 msgid "{firstAuthorLink} followed you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:324 +#: src/view/com/notifications/NotificationFeedItem.tsx:325 msgid "{firstAuthorLink} followed you back" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:373 +#: src/view/com/notifications/NotificationFeedItem.tsx:374 msgid "{firstAuthorLink} liked your custom feed" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:266 +#: src/view/com/notifications/NotificationFeedItem.tsx:267 msgid "{firstAuthorLink} liked your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:478 +#: src/view/com/notifications/NotificationFeedItem.tsx:479 msgid "{firstAuthorLink} liked your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:451 +#: src/view/com/notifications/NotificationFeedItem.tsx:452 msgid "{firstAuthorLink} removed their verification from your account" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:290 +#: src/view/com/notifications/NotificationFeedItem.tsx:291 msgid "{firstAuthorLink} reposted your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:502 +#: src/view/com/notifications/NotificationFeedItem.tsx:503 msgid "{firstAuthorLink} reposted your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:397 +#: src/view/com/notifications/NotificationFeedItem.tsx:398 msgid "{firstAuthorLink} signed up with your starter pack" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:426 +#: src/view/com/notifications/NotificationFeedItem.tsx:427 msgid "{firstAuthorLink} verified you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:328 +#: src/view/com/notifications/NotificationFeedItem.tsx:329 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} followed you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:354 +#: src/view/com/notifications/NotificationFeedItem.tsx:355 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:247 +#: src/view/com/notifications/NotificationFeedItem.tsx:248 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:459 +#: src/view/com/notifications/NotificationFeedItem.tsx:460 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:432 +#: src/view/com/notifications/NotificationFeedItem.tsx:433 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} removed their verifications from your account" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:271 +#: src/view/com/notifications/NotificationFeedItem.tsx:272 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:483 +#: src/view/com/notifications/NotificationFeedItem.tsx:484 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:378 +#: src/view/com/notifications/NotificationFeedItem.tsx:379 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} signed up with your starter pack" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:407 +#: src/view/com/notifications/NotificationFeedItem.tsx:408 msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} verified you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:333 +#: src/view/com/notifications/NotificationFeedItem.tsx:334 msgid "{firstAuthorName} followed you" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:323 +#: src/view/com/notifications/NotificationFeedItem.tsx:324 msgid "{firstAuthorName} followed you back" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:359 +#: src/view/com/notifications/NotificationFeedItem.tsx:360 msgid "{firstAuthorName} liked your custom feed" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:252 +#: src/view/com/notifications/NotificationFeedItem.tsx:253 msgid "{firstAuthorName} liked your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:464 +#: src/view/com/notifications/NotificationFeedItem.tsx:465 msgid "{firstAuthorName} liked your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:437 +#: src/view/com/notifications/NotificationFeedItem.tsx:438 msgid "{firstAuthorName} removed their verification from your account" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:276 +#: src/view/com/notifications/NotificationFeedItem.tsx:277 msgid "{firstAuthorName} reposted your post" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:488 +#: src/view/com/notifications/NotificationFeedItem.tsx:489 msgid "{firstAuthorName} reposted your repost" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:383 +#: src/view/com/notifications/NotificationFeedItem.tsx:384 msgid "{firstAuthorName} signed up with your starter pack" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:412 +#: src/view/com/notifications/NotificationFeedItem.tsx:413 msgid "{firstAuthorName} verified you" msgstr "" @@ -650,7 +650,7 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:772 +#: src/view/com/composer/Composer.tsx:773 msgid "Add another post" msgstr "" @@ -681,7 +681,7 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/view/com/composer/Composer.tsx:1330 +#: src/view/com/composer/Composer.tsx:1335 msgid "Add new post" msgstr "" @@ -826,9 +826,9 @@ msgstr "" msgid "Already signed in as @{0}" msgstr "" +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:186 #: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/photos/Gallery.tsx:187 -#: src/view/com/util/post-embeds/GifEmbed.tsx:186 msgid "ALT" msgstr "" @@ -842,7 +842,7 @@ msgstr "" msgid "Alt text" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:191 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:191 msgid "Alt Text" msgstr "" @@ -871,7 +871,7 @@ msgstr "" msgid "An error has occurred" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:420 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:420 msgid "An error occurred" msgstr "" @@ -887,11 +887,11 @@ msgstr "" msgid "An error occurred while generating your starter pack. Want to try again?" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbed.tsx:160 +#: src/components/Post/Embed/VideoEmbed/index.tsx:160 msgid "An error occurred while loading the video. Please try again later." msgstr "" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:198 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:199 msgid "An error occurred while loading the video. Please try again." msgstr "" @@ -961,7 +961,7 @@ msgstr "" msgid "Animals" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:149 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:149 msgid "Animated GIF" msgstr "" @@ -1063,15 +1063,15 @@ msgstr "" msgid "Apply default recommended feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:627 -#: src/view/com/post-thread/PostThreadItem.tsx:956 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:640 +#: src/view/com/post-thread/PostThreadItem.tsx:955 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:596 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 -#: src/view/com/post-thread/PostThreadItem.tsx:925 -#: src/view/com/post-thread/PostThreadItem.tsx:964 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:609 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:648 +#: src/view/com/post-thread/PostThreadItem.tsx:924 +#: src/view/com/post-thread/PostThreadItem.tsx:963 msgid "Archived post" msgstr "" @@ -1107,11 +1107,11 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:723 +#: src/view/com/composer/Composer.tsx:724 msgid "Are you sure you'd like to discard this draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:904 +#: src/view/com/composer/Composer.tsx:905 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1263,7 +1263,7 @@ msgstr "" msgid "Block User" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:84 +#: src/components/Post/Embed/index.tsx:180 msgid "Blocked" msgstr "" @@ -1310,8 +1310,8 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:652 -#: src/view/com/post-thread/PostThreadItem.tsx:981 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:665 +#: src/view/com/post-thread/PostThreadItem.tsx:980 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1370,23 +1370,23 @@ msgstr "" msgid "Books" msgstr "" -#: src/components/FeedInterstitials.tsx:373 +#: src/components/FeedInterstitials.tsx:379 msgid "Browse more accounts on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:511 +#: src/components/FeedInterstitials.tsx:517 msgid "Browse more feeds on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:353 -#: src/components/FeedInterstitials.tsx:356 -#: src/components/FeedInterstitials.tsx:492 -#: src/components/FeedInterstitials.tsx:495 +#: src/components/FeedInterstitials.tsx:359 +#: src/components/FeedInterstitials.tsx:362 +#: src/components/FeedInterstitials.tsx:498 +#: src/components/FeedInterstitials.tsx:501 msgid "Browse more suggestions" msgstr "" -#: src/components/FeedInterstitials.tsx:381 -#: src/components/FeedInterstitials.tsx:520 +#: src/components/FeedInterstitials.tsx:387 +#: src/components/FeedInterstitials.tsx:526 msgid "Browse more suggestions on the Explore page" msgstr "" @@ -1471,8 +1471,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:270 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 -#: src/view/com/composer/Composer.tsx:959 -#: src/view/com/composer/Composer.tsx:970 +#: src/view/com/composer/Composer.tsx:960 +#: src/view/com/composer/Composer.tsx:971 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/modals/ChangePassword.tsx:279 @@ -1767,6 +1767,7 @@ msgstr "" #: src/components/live/EditLiveDialog.tsx:235 #: src/components/NewskieDialog.tsx:146 #: src/components/NewskieDialog.tsx:153 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:197 #: src/components/ProgressGuide/FollowDialog.tsx:386 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:117 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:123 @@ -1774,7 +1775,6 @@ msgstr "" #: src/components/verification/VerifierDialog.tsx:144 #: src/view/com/modals/ChangePassword.tsx:279 #: src/view/com/modals/ChangePassword.tsx:282 -#: src/view/com/util/post-embeds/GifEmbed.tsx:197 msgid "Close" msgstr "" @@ -1832,7 +1832,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:967 +#: src/view/com/composer/Composer.tsx:968 msgid "Closes post composer and discards post draft" msgstr "" @@ -1845,11 +1845,11 @@ msgstr "" msgid "Closes viewer for header image" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:537 +#: src/view/com/notifications/NotificationFeedItem.tsx:538 msgid "Collapse list of users" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:748 +#: src/view/com/notifications/NotificationFeedItem.tsx:749 msgid "Collapses list of users for a given notification" msgstr "" @@ -1889,7 +1889,7 @@ msgstr "" msgid "Compose new post" msgstr "" -#: src/view/com/composer/Composer.tsx:868 +#: src/view/com/composer/Composer.tsx:869 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -1897,7 +1897,7 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:1724 +#: src/view/com/composer/Composer.tsx:1729 msgid "Compressing video..." msgstr "" @@ -2395,7 +2395,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:682 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:684 -#: src/view/com/composer/Composer.tsx:878 +#: src/view/com/composer/Composer.tsx:879 msgid "Delete post" msgstr "" @@ -2416,7 +2416,7 @@ msgstr "" msgid "Delete this post?" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:94 +#: src/components/Post/Embed/index.tsx:173 msgid "Deleted" msgstr "" @@ -2500,7 +2500,7 @@ msgstr "" msgid "Disable haptic feedback" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:386 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:386 msgid "Disable subtitles" msgstr "" @@ -2514,8 +2514,8 @@ msgid "Disabled" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:88 -#: src/view/com/composer/Composer.tsx:725 -#: src/view/com/composer/Composer.tsx:911 +#: src/view/com/composer/Composer.tsx:726 +#: src/view/com/composer/Composer.tsx:912 msgid "Discard" msgstr "" @@ -2523,11 +2523,11 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:722 +#: src/view/com/composer/Composer.tsx:723 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:903 +#: src/view/com/composer/Composer.tsx:904 msgid "Discard post?" msgstr "" @@ -2553,7 +2553,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/view/com/composer/Composer.tsx:1648 +#: src/view/com/composer/Composer.tsx:1653 msgid "Dismiss error" msgstr "" @@ -2895,7 +2895,7 @@ msgstr "" msgid "Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website." msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx:58 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx:58 msgid "Embedded video player" msgstr "" @@ -2931,7 +2931,7 @@ msgstr "" msgid "Enable priority notifications" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:387 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:387 msgid "Enable subtitles" msgstr "" @@ -2984,7 +2984,7 @@ msgstr "" msgid "Enter Code" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:405 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405 msgid "Enter fullscreen" msgstr "" @@ -3025,7 +3025,7 @@ msgstr "" msgid "Enter your username and password" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:135 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:135 msgid "Enters full screen" msgstr "" @@ -3033,7 +3033,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:1733 +#: src/view/com/composer/Composer.tsx:1738 #: src/view/com/util/error/ErrorScreen.tsx:42 msgid "Error" msgstr "" @@ -3093,7 +3093,7 @@ msgstr "" msgid "Excludes users you follow" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:404 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:404 msgid "Exit fullscreen" msgstr "" @@ -3113,14 +3113,18 @@ msgstr "" msgid "Expand alt text" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:538 +#: src/view/com/notifications/NotificationFeedItem.tsx:539 msgid "Expand list of users" msgstr "" -#: src/view/com/composer/ComposerReplyTo.tsx:84 +#: src/view/com/composer/ComposerReplyTo.tsx:91 msgid "Expand or collapse the full post you are replying to" msgstr "" +#: src/components/Post/ShowMoreTextButton.tsx:32 +msgid "Expand post text" +msgstr "" + #: src/screens/VideoFeed/index.tsx:965 msgid "Expands or collapses post text" msgstr "" @@ -3654,7 +3658,7 @@ msgstr "" msgid "From @{sanitizedAuthor}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:328 +#: src/view/com/posts/PostFeedItem.tsx:330 msgctxt "from-feed" msgid "From <0/>" msgstr "" @@ -3753,7 +3757,7 @@ msgstr "" msgid "Go live for" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:204 +#: src/view/com/notifications/NotificationFeedItem.tsx:205 msgid "Go to {firstAuthorName}'s profile" msgstr "" @@ -3863,7 +3867,7 @@ msgstr "" #: src/components/interstitials/TrendingVideos.tsx:140 #: src/components/moderation/ContentHider.tsx:200 #: src/components/moderation/LabelPreference.tsx:135 -#: src/components/moderation/PostHider.tsx:124 +#: src/components/moderation/PostHider.tsx:134 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 #: src/lib/moderation/useLabelBehaviorDescription.ts:15 #: src/lib/moderation/useLabelBehaviorDescription.ts:20 @@ -3873,7 +3877,7 @@ msgstr "" msgid "Hide" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:755 +#: src/view/com/notifications/NotificationFeedItem.tsx:756 msgctxt "action" msgid "Hide" msgstr "" @@ -3924,7 +3928,7 @@ msgstr "" msgid "Hide trending videos?" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:746 +#: src/view/com/notifications/NotificationFeedItem.tsx:747 msgid "Hide user list" msgstr "" @@ -3934,7 +3938,7 @@ msgid "Hide verification badges" msgstr "" #: src/components/moderation/ContentHider.tsx:151 -#: src/components/moderation/PostHider.tsx:79 +#: src/components/moderation/PostHider.tsx:89 msgid "Hides the content" msgstr "" @@ -4149,7 +4153,7 @@ msgstr "" msgid "Invalid handle. Please try a different one." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:350 +#: src/view/com/post-thread/PostThreadItem.tsx:351 msgid "Invalid or unsupported post record" msgstr "" @@ -4201,7 +4205,7 @@ msgstr "" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "" -#: src/view/com/composer/Composer.tsx:1667 +#: src/view/com/composer/Composer.tsx:1672 msgid "Job ID: {0}" msgstr "" @@ -4313,7 +4317,7 @@ msgstr "" msgid "Learn more about the moderation applied to this content." msgstr "" -#: src/components/moderation/PostHider.tsx:100 +#: src/components/moderation/PostHider.tsx:110 #: src/components/moderation/ScreenHider.tsx:127 msgid "Learn more about this warning" msgstr "" @@ -4439,8 +4443,8 @@ msgstr "" msgid "Likes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:454 -#: src/view/com/post-thread/PostThreadItem.tsx:242 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:467 +#: src/view/com/post-thread/PostThreadItem.tsx:243 msgid "Likes on this post" msgstr "" @@ -4785,7 +4789,7 @@ msgstr "" msgid "Moderator has chosen to set a general warning on the content." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:732 +#: src/view/com/post-thread/PostThreadItem.tsx:731 msgid "More" msgstr "" @@ -4818,8 +4822,8 @@ msgstr "" msgid "Music" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:156 -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:95 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96 msgctxt "video" msgid "Mute" msgstr "" @@ -5332,8 +5336,8 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:657 -#: src/view/com/post-thread/PostThreadItem.tsx:986 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:670 +#: src/view/com/post-thread/PostThreadItem.tsx:985 msgid "Okay" msgstr "" @@ -5356,15 +5360,15 @@ msgstr "" msgid "Onboarding reset" msgstr "" -#: src/view/com/composer/Composer.tsx:346 +#: src/view/com/composer/Composer.tsx:347 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:343 +#: src/view/com/composer/Composer.tsx:344 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:353 +#: src/view/com/composer/Composer.tsx:354 msgid "One or more videos is missing alt text." msgstr "" @@ -5420,7 +5424,7 @@ msgid "Open drawer menu" msgstr "" #: src/screens/Messages/components/MessageInput.web.tsx:181 -#: src/view/com/composer/Composer.tsx:1315 +#: src/view/com/composer/Composer.tsx:1320 msgid "Open emoji picker" msgstr "" @@ -5437,7 +5441,7 @@ msgstr "" msgid "Open full emoji list" msgstr "" -#: src/view/com/util/post-embeds/ExternalLinkEmbed.tsx:79 +#: src/components/Post/Embed/ExternalEmbed/index.tsx:79 msgid "Open link to {niceUrl}" msgstr "" @@ -5523,7 +5527,7 @@ msgstr "" msgid "Opens device photo gallery" msgstr "" -#: src/view/com/composer/Composer.tsx:1316 +#: src/view/com/composer/Composer.tsx:1321 msgid "Opens emoji picker" msgstr "" @@ -5561,7 +5565,7 @@ msgstr "" msgid "Opens the linked website" msgstr "" -#: src/view/com/notifications/NotificationFeedItem.tsx:849 +#: src/view/com/notifications/NotificationFeedItem.tsx:850 #: src/view/com/util/UserAvatar.tsx:581 msgid "Opens this profile" msgstr "" @@ -5663,13 +5667,13 @@ msgstr "" msgid "Password updated!" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:43 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:369 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369 msgid "Pause" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:320 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:320 msgid "Pause video" msgstr "" @@ -5730,7 +5734,7 @@ msgstr "" msgid "Pin to your profile" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:409 +#: src/view/com/posts/PostFeedItem.tsx:411 msgid "Pinned" msgstr "" @@ -5747,38 +5751,38 @@ msgstr "" msgid "Pinned to your feeds" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:43 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:370 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:43 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:140 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:370 msgid "Play" msgstr "" -#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:107 +#: src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx:111 msgid "Play {0}" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbed.tsx:134 -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:321 +#: src/components/Post/Embed/VideoEmbed/index.tsx:134 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321 msgid "Play video" msgstr "" -#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:58 +#: src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx:61 msgid "Play Video" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:42 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:42 msgid "Plays or pauses the GIF" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:141 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:141 msgid "Plays or pauses the video" msgstr "" -#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:106 +#: src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx:110 msgid "Plays the GIF" msgstr "" -#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:59 +#: src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx:62 msgid "Plays the video" msgstr "" @@ -5911,12 +5915,12 @@ msgctxt "description" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1030 +#: src/view/com/composer/Composer.tsx:1031 msgctxt "action" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1028 +#: src/view/com/composer/Composer.tsx:1029 msgctxt "action" msgid "Post All" msgstr "" @@ -5925,7 +5929,7 @@ msgstr "" msgid "Post blocked" msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:234 +#: src/view/com/post-thread/PostThreadItem.tsx:235 msgid "Post by {0}" msgstr "" @@ -5945,9 +5949,9 @@ msgstr "" msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:133 -#: src/screens/PostThread/components/ThreadItemPost.tsx:112 -#: src/screens/PostThread/components/ThreadItemTreePost.tsx:109 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:132 +#: src/screens/PostThread/components/ThreadItemPost.tsx:110 +#: src/screens/PostThread/components/ThreadItemTreePost.tsx:107 #: src/screens/VideoFeed/index.tsx:529 msgid "Post has been deleted" msgstr "" @@ -6081,7 +6085,7 @@ msgstr "" msgid "Privacy Policy" msgstr "" -#: src/view/com/composer/Composer.tsx:1730 +#: src/view/com/composer/Composer.tsx:1735 msgid "Processing video..." msgstr "" @@ -6121,22 +6125,22 @@ msgid "Public, sharable lists which can be used to drive feeds." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1010 +#: src/view/com/composer/Composer.tsx:1011 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1003 +#: src/view/com/composer/Composer.tsx:1004 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:988 +#: src/view/com/composer/Composer.tsx:989 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:995 +#: src/view/com/composer/Composer.tsx:996 msgid "Publish reply" msgstr "" @@ -6182,8 +6186,8 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:438 -#: src/view/com/post-thread/PostThreadItem.tsx:268 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:451 +#: src/view/com/post-thread/PostThreadItem.tsx:269 msgid "Quotes of this post" msgstr "" @@ -6306,7 +6310,7 @@ msgstr "" msgid "Remove account" msgstr "" -#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:15 +#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:19 msgid "Remove attachment" msgstr "" @@ -6374,10 +6378,6 @@ msgstr "" msgid "Remove profile" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:290 -msgid "Remove quote" -msgstr "" - #: src/components/PostControls/RepostButton.tsx:140 #: src/components/PostControls/RepostButton.tsx:150 msgid "Remove repost" @@ -6406,11 +6406,11 @@ msgstr "" msgid "Remove your verification for this account?" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:110 +#: src/components/Post/Embed/index.tsx:208 msgid "Removed by author" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:108 +#: src/components/Post/Embed/index.tsx:206 msgid "Removed by you" msgstr "" @@ -6437,10 +6437,6 @@ msgstr "" msgid "Removed verification" msgstr "" -#: src/view/com/util/post-embeds/QuoteEmbed.tsx:291 -msgid "Removes quoted post" -msgstr "" - #: src/view/com/posts/FeedShutdownMsg.tsx:129 #: src/view/com/posts/FeedShutdownMsg.tsx:133 msgid "Replace with Discover" @@ -6458,7 +6454,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1026 +#: src/view/com/composer/Composer.tsx:1027 msgctxt "action" msgid "Reply" msgstr "" @@ -6491,24 +6487,24 @@ msgstr "" msgid "Reply sorting" msgstr "" -#: src/view/com/post/Post.tsx:205 -#: src/view/com/posts/PostFeedItem.tsx:607 +#: src/view/com/post/Post.tsx:204 +#: src/view/com/posts/PostFeedItem.tsx:602 msgctxt "description" msgid "Reply to <0><1/>" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:598 +#: src/view/com/posts/PostFeedItem.tsx:593 msgctxt "description" msgid "Reply to a blocked post" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:600 +#: src/view/com/posts/PostFeedItem.tsx:595 msgctxt "description" msgid "Reply to a post" msgstr "" -#: src/view/com/post/Post.tsx:203 -#: src/view/com/posts/PostFeedItem.tsx:604 +#: src/view/com/post/Post.tsx:202 +#: src/view/com/posts/PostFeedItem.tsx:599 msgctxt "description" msgid "Reply to you" msgstr "" @@ -6637,21 +6633,21 @@ msgstr "" msgid "Reposted By" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:349 +#: src/view/com/posts/PostFeedItem.tsx:351 msgid "Reposted by {0}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:368 +#: src/view/com/posts/PostFeedItem.tsx:370 msgid "Reposted by <0><1/>" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:347 -#: src/view/com/posts/PostFeedItem.tsx:366 +#: src/view/com/posts/PostFeedItem.tsx:349 +#: src/view/com/posts/PostFeedItem.tsx:368 msgid "Reposted by you" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:420 -#: src/view/com/post-thread/PostThreadItem.tsx:247 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:433 +#: src/view/com/post-thread/PostThreadItem.tsx:248 msgid "Reposts of this post" msgstr "" @@ -6734,6 +6730,8 @@ msgstr "" #: src/components/Error.tsx:65 #: src/components/Lists.tsx:110 #: src/components/moderation/ReportDialog/index.tsx:229 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:57 #: src/components/StarterPack/ProfileStarterPacks.tsx:346 #: src/screens/Login/LoginForm.tsx:323 #: src/screens/Login/LoginForm.tsx:330 @@ -6747,8 +6745,6 @@ msgstr "" #: src/screens/Signup/BackNextButtons.tsx:53 #: src/view/com/util/error/ErrorMessage.tsx:60 #: src/view/com/util/error/ErrorScreen.tsx:97 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx:55 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx:57 msgid "Retry" msgstr "" @@ -6859,8 +6855,8 @@ msgstr "" #: src/components/dms/ChatEmptyPill.tsx:33 #: src/components/NewskieDialog.tsx:105 -#: src/view/com/notifications/NotificationFeedItem.tsx:694 -#: src/view/com/notifications/NotificationFeedItem.tsx:719 +#: src/view/com/notifications/NotificationFeedItem.tsx:695 +#: src/view/com/notifications/NotificationFeedItem.tsx:720 msgid "Say hello!" msgstr "" @@ -6987,7 +6983,7 @@ msgstr "" msgid "See this guide" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:194 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:195 msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause" msgstr "" @@ -7315,11 +7311,11 @@ msgstr "" #: src/components/moderation/ContentHider.tsx:200 #: src/components/moderation/LabelPreference.tsx:137 -#: src/components/moderation/PostHider.tsx:124 +#: src/components/moderation/PostHider.tsx:134 msgid "Show" msgstr "" -#: src/view/com/util/post-embeds/GifEmbed.tsx:178 +#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:178 msgid "Show alt text" msgstr "" @@ -7357,11 +7353,7 @@ msgstr "" msgid "Show list anyway" msgstr "" -#: src/screens/PostThread/components/ThreadItemPost.tsx:318 -#: src/screens/PostThread/components/ThreadItemTreePost.tsx:364 -#: src/view/com/post-thread/PostThreadItem.tsx:692 -#: src/view/com/post/Post.tsx:244 -#: src/view/com/posts/PostFeedItem.tsx:563 +#: src/components/Post/ShowMoreTextButton.tsx:51 msgid "Show More" msgstr "" @@ -7429,8 +7421,8 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:598 -#: src/view/com/post-thread/PostThreadItem.tsx:927 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:611 +#: src/view/com/post-thread/PostThreadItem.tsx:926 msgid "Shows information about when this post was created" msgstr "" @@ -7439,7 +7431,7 @@ msgid "Shows other accounts you can switch to" msgstr "" #: src/components/moderation/ContentHider.tsx:152 -#: src/components/moderation/PostHider.tsx:79 +#: src/components/moderation/PostHider.tsx:89 msgid "Shows the content" msgstr "" @@ -7522,7 +7514,7 @@ msgstr "" msgid "Signed in as @{0}" msgstr "" -#: src/components/FeedInterstitials.tsx:337 +#: src/components/FeedInterstitials.tsx:343 msgid "Similar accounts" msgstr "" @@ -7553,7 +7545,7 @@ msgstr "" msgid "Some of your verifications are invalid." msgstr "" -#: src/components/FeedInterstitials.tsx:474 +#: src/components/FeedInterstitials.tsx:480 msgid "Some other feeds you might like" msgstr "" @@ -7775,7 +7767,7 @@ msgstr "" msgid "Suggested Accounts" msgstr "" -#: src/components/FeedInterstitials.tsx:339 +#: src/components/FeedInterstitials.tsx:345 msgid "Suggested for you" msgstr "" @@ -8303,8 +8295,8 @@ 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:638 -#: src/view/com/post-thread/PostThreadItem.tsx:967 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:651 +#: src/view/com/post-thread/PostThreadItem.tsx:966 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -8312,7 +8304,7 @@ msgstr "" msgid "This post has an unknown type of threadgate on it. Your app may be out of date." msgstr "" -#: src/view/com/post-thread/PostThreadItem.tsx:170 +#: src/view/com/post-thread/PostThreadItem.tsx:171 msgid "This post has been deleted." msgstr "" @@ -8324,7 +8316,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:462 +#: src/view/com/composer/Composer.tsx:463 msgid "This post's author has disabled quote posts." msgstr "" @@ -8425,7 +8417,7 @@ msgstr "" msgid "Threads Preferences" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx:34 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx:34 msgid "Time remaining: {0, plural, one {# second} other {# seconds}}" msgstr "" @@ -8458,7 +8450,7 @@ msgstr "" msgid "Toggle to enable or disable adult content" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:158 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:158 msgid "Toggles the sound" msgstr "" @@ -8483,10 +8475,10 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:145 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:560 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:563 -#: src/view/com/post-thread/PostThreadItem.tsx:889 -#: src/view/com/post-thread/PostThreadItem.tsx:892 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:573 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:576 +#: src/view/com/post-thread/PostThreadItem.tsx:888 +#: src/view/com/post-thread/PostThreadItem.tsx:891 msgid "Translate" msgstr "" @@ -8628,8 +8620,8 @@ msgstr "" msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:155 -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:94 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:95 msgctxt "video" msgid "Unmute" msgstr "" @@ -8663,7 +8655,7 @@ msgstr "" msgid "Unmute thread" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:318 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:318 msgid "Unmute video" msgstr "" @@ -8724,7 +8716,7 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/Composer.tsx:810 +#: src/view/com/composer/Composer.tsx:811 msgid "Unsupported video type" msgstr "" @@ -8814,7 +8806,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:1727 +#: src/view/com/composer/Composer.tsx:1732 msgid "Uploading video..." msgstr "" @@ -9007,8 +8999,8 @@ msgstr "" msgid "Version {appVersion}" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:134 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:134 msgid "Video" msgstr "" @@ -9038,7 +9030,7 @@ msgstr "" msgid "Video is playing" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:191 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:192 msgid "Video not found." msgstr "" @@ -9046,11 +9038,11 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:1737 +#: src/view/com/composer/Composer.tsx:1742 msgid "Video uploaded" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 msgid "Video: {0}" msgstr "" @@ -9071,7 +9063,7 @@ msgstr "" #: src/screens/Profile/components/ProfileFeedHeader.tsx:454 #: src/screens/Search/components/SearchProfileCard.tsx:36 #: src/screens/VideoFeed/index.tsx:790 -#: src/view/com/notifications/NotificationFeedItem.tsx:545 +#: src/view/com/notifications/NotificationFeedItem.tsx:546 msgid "View {0}'s profile" msgstr "" @@ -9185,7 +9177,7 @@ msgstr "" msgid "Visit Site" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:80 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:81 msgid "Volume" msgstr "" @@ -9297,7 +9289,7 @@ msgstr "" msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "" -#: src/view/com/composer/Composer.tsx:459 +#: src/view/com/composer/Composer.tsx:460 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -9328,7 +9320,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.web.tsx:99 -#: src/view/com/composer/Composer.tsx:773 +#: src/view/com/composer/Composer.tsx:774 msgid "What's up?" msgstr "" @@ -9410,11 +9402,11 @@ msgstr "" msgid "Write a message" msgstr "" -#: src/view/com/composer/Composer.tsx:866 +#: src/view/com/composer/Composer.tsx:867 msgid "Write post" msgstr "" -#: src/view/com/composer/Composer.tsx:771 +#: src/view/com/composer/Composer.tsx:772 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:95 msgid "Write your reply" msgstr "" @@ -9848,7 +9840,7 @@ msgstr "" msgid "Your birth date" msgstr "" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:195 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:196 msgid "Your browser does not support the video format. Please try a different browser." msgstr "" @@ -9932,11 +9924,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:521 +#: src/view/com/composer/Composer.tsx:522 msgid "Your post has been published" msgstr "" -#: src/view/com/composer/Composer.tsx:518 +#: src/view/com/composer/Composer.tsx:519 msgid "Your posts have been published" msgstr "" @@ -9948,7 +9940,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:520 +#: src/view/com/composer/Composer.tsx:521 msgid "Your reply has been published" msgstr "" From 9fb2a63466fda3389a5b6715ca109a14933db9c8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 14 Jun 2025 12:47:05 -0500 Subject: [PATCH 21/49] Run lint-staged without concurrency (#8489) --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc601..c8fec63e2a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx lint-staged +npx lint-staged --concurrent false From 2c8dd12281afb52f27fea809f16c2dbea94ce493 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 14 Jun 2025 12:47:16 -0500 Subject: [PATCH 22/49] Use post shadow (#8491) --- src/screens/PostThread/components/ThreadItemAnchor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index f6bc5871c8..907fb9a7be 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -180,7 +180,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ const {currentAccount, hasSession} = useSession() const feedFeedback = useFeedFeedback(postSource?.feed, hasSession) - const post = item.value.post + const post = postShadow const record = item.value.post.record const moderation = item.moderation const authorShadow = useProfileShadow(post.author) From 5c50e102055ec7bfbed08a970b4388ae95d4963a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 16 Jun 2025 18:20:17 +0300 Subject: [PATCH 23/49] Align avatar in reply prompt (#8501) * align avi in reply prompt * update skellie --- .../components/ThreadItemReplyComposer.tsx | 23 +++++-------------- .../post-thread/PostThreadComposePrompt.tsx | 13 ++++------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/screens/PostThread/components/ThreadItemReplyComposer.tsx b/src/screens/PostThread/components/ThreadItemReplyComposer.tsx index f1862569ea..d93612be8e 100644 --- a/src/screens/PostThread/components/ThreadItemReplyComposer.tsx +++ b/src/screens/PostThread/components/ThreadItemReplyComposer.tsx @@ -1,30 +1,19 @@ import {View} from 'react-native' -import {OUTER_SPACE} from '#/screens/PostThread/const' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import * as Skele from '#/components/Skeleton' -/* - * Wacky padding here is just replicating what we have in the actual - * `PostThreadComposePrompt` component - */ export function ThreadItemReplyComposerSkeleton() { const t = useTheme() const {gtMobile} = useBreakpoints() + if (!gtMobile) return null + return ( - - - - + + + + ) diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index f45b16085f..dc05617258 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -38,15 +38,10 @@ export function PostThreadComposePrompt({ return ( {!gtMobile && ( @@ -87,7 +82,7 @@ export function PostThreadComposePrompt({ a.transition_color, ]}> From 585dbebb693ac5799ee6cbedd918cf8fae01254d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 16 Jun 2025 18:32:51 +0300 Subject: [PATCH 24/49] Fix long-press loophole for disabled quote posts (#8502) * fix loophole for disabled quote posts * show dialog instead --- src/components/PostControls/RepostButton.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/PostControls/RepostButton.tsx b/src/components/PostControls/RepostButton.tsx index db63a7383a..31438c6bd3 100644 --- a/src/components/PostControls/RepostButton.tsx +++ b/src/components/PostControls/RepostButton.tsx @@ -40,6 +40,17 @@ let RepostButton = ({ const requireAuth = useRequireAuth() const dialogControl = Dialog.useDialogControl() + const onPress = () => requireAuth(() => dialogControl.open()) + + const onLongPress = () => + requireAuth(() => { + if (embeddingDisabled) { + dialogControl.open() + } else { + onQuote() + } + }) + return ( <> requireAuth(() => dialogControl.open())} - onLongPress={() => requireAuth(() => onQuote())} + onPress={onPress} + onLongPress={onLongPress} label={ isReposted ? _( From c2b71a6a9e668a8084c113825ba3ead58d1d300f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 16 Jun 2025 10:39:41 -0500 Subject: [PATCH 25/49] Fix v2 tree view bug caused by moderation settings (#8503) * Use actual index, not seen index * Handle edge case where last sibling is moderated --- src/state/queries/usePostThread/traversal.ts | 32 +++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index fbae4ecdbf..124591125f 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -265,12 +265,36 @@ export function sortAndAnnotateThreadItems( metadata.nextItemDepth = nextItem?.depth /* - * We can now officially calculate `isLastSibling` and `isLastChild` - * based on the actual data that we've seen. + * Item is the last "sibling" if we know for sure we're out of + * replies on the parent (even though this item itself may have its + * own reply branches). + */ + const isLastSiblingByCounts = + metadata.replyIndex === + metadata.parentMetadata.repliesIndexCounter - 1 + + /* + * Item can also be the last "sibling" if we know we don't have a + * next item, OR if that next item's depth is less than this item's + * depth (meaning it's a sibling of the parent, not a child of this + * item). + */ + const isImplicitlyLastSibling = + metadata.nextItemDepth === undefined || + metadata.nextItemDepth < metadata.depth + + /* + * Ok now we can set the last sibling state. */ metadata.isLastSibling = - metadata.replyIndex === - metadata.parentMetadata.repliesSeenCounter - 1 + isLastSiblingByCounts || isImplicitlyLastSibling + + /* + * Item is the last "child" in a branch if there is no next item, + * or if the next item's depth is less than this item's depth (a + * sibling of the parent) or equal to this item's depth (a sibling + * of this item) + */ metadata.isLastChild = metadata.nextItemDepth === undefined || metadata.nextItemDepth <= metadata.depth From c16d5ce8213ac10f0c3530fe97c561c5262b0d27 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 16 Jun 2025 20:00:38 +0300 Subject: [PATCH 26/49] Fix misuse of Promise.all (#8222) --- src/screens/Search/Explore.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 7c9e2a25dc..92eed25aa4 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -301,19 +301,19 @@ export function Explore({ const onPTR = useCallback(async () => { setIsPTR(true) await Promise.all([ - await qc.resetQueries({ + qc.resetQueries({ queryKey: createGetTrendsQueryKey(), }), - await qc.resetQueries({ + qc.resetQueries({ queryKey: createSuggestedStarterPacksQueryKey(), }), - await qc.resetQueries({ + qc.resetQueries({ queryKey: [getSuggestedUsersQueryKeyRoot], }), - await qc.resetQueries({ + qc.resetQueries({ queryKey: [useActorSearchPaginatedQueryKeyRoot], }), - await qc.resetQueries({ + qc.resetQueries({ queryKey: createGetSuggestedFeedsQueryKey(), }), ]) From 7dc6bb57a6666db3e507630c13448487acceadc5 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 17 Jun 2025 02:41:29 +0000 Subject: [PATCH 27/49] Nightly source-language update --- src/locale/locales/en/messages.po | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 088d04852c..33dc6d5669 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -1457,7 +1457,7 @@ msgstr "" #: src/components/live/GoLiveDialog.tsx:247 #: src/components/live/GoLiveDialog.tsx:253 #: src/components/Menu/index.tsx:350 -#: src/components/PostControls/RepostButton.tsx:198 +#: src/components/PostControls/RepostButton.tsx:209 #: src/components/Prompt.tsx:143 #: src/components/Prompt.tsx:145 #: src/screens/Deactivated.tsx:158 @@ -1506,7 +1506,7 @@ msgstr "" msgid "Cancel profile editing" msgstr "" -#: src/components/PostControls/RepostButton.tsx:192 +#: src/components/PostControls/RepostButton.tsx:203 msgid "Cancel quote post" msgstr "" @@ -1893,7 +1893,7 @@ msgstr "" msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:67 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:62 msgid "Compose reply" msgstr "" @@ -5519,7 +5519,7 @@ msgstr "" msgid "Opens change handle dialog" msgstr "" -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:68 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:63 msgid "Opens composer" msgstr "" @@ -6156,8 +6156,8 @@ msgstr "" msgid "QR code saved to your camera roll!" msgstr "" -#: src/components/PostControls/RepostButton.tsx:163 -#: src/components/PostControls/RepostButton.tsx:186 +#: src/components/PostControls/RepostButton.tsx:174 +#: src/components/PostControls/RepostButton.tsx:197 #: src/components/PostControls/RepostButton.web.tsx:78 #: src/components/PostControls/RepostButton.web.tsx:85 msgid "Quote post" @@ -6171,8 +6171,8 @@ msgstr "" msgid "Quote post was successfully detached" msgstr "" -#: src/components/PostControls/RepostButton.tsx:162 -#: src/components/PostControls/RepostButton.tsx:184 +#: src/components/PostControls/RepostButton.tsx:173 +#: src/components/PostControls/RepostButton.tsx:195 #: src/components/PostControls/RepostButton.web.tsx:77 #: src/components/PostControls/RepostButton.web.tsx:84 msgid "Quote posts disabled" @@ -6378,8 +6378,8 @@ msgstr "" msgid "Remove profile" msgstr "" -#: src/components/PostControls/RepostButton.tsx:140 -#: src/components/PostControls/RepostButton.tsx:150 +#: src/components/PostControls/RepostButton.tsx:151 +#: src/components/PostControls/RepostButton.tsx:161 msgid "Remove repost" msgstr "" @@ -6606,8 +6606,8 @@ msgstr "" msgid "Report this user" msgstr "" -#: src/components/PostControls/RepostButton.tsx:141 #: src/components/PostControls/RepostButton.tsx:152 +#: src/components/PostControls/RepostButton.tsx:163 msgctxt "action" msgid "Repost" msgstr "" @@ -6618,11 +6618,11 @@ msgid "Repost" msgstr "" #. Accessibility label for the repost button when the post has not been reposted, verb form followed by number of reposts and noun form -#: src/components/PostControls/RepostButton.tsx:65 +#: src/components/PostControls/RepostButton.tsx:76 msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" -#: src/components/PostControls/RepostButton.tsx:133 +#: src/components/PostControls/RepostButton.tsx:144 #: src/components/PostControls/RepostButton.web.tsx:43 #: src/components/PostControls/RepostButton.web.tsx:97 #: src/screens/StarterPack/StarterPackScreen.tsx:561 @@ -8581,7 +8581,7 @@ msgid "Undo repost" msgstr "" #. Accessibility label for the repost button when the post has been reposted, verb followed by number of reposts and noun -#: src/components/PostControls/RepostButton.tsx:55 +#: src/components/PostControls/RepostButton.tsx:66 msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" @@ -9407,7 +9407,7 @@ msgid "Write post" msgstr "" #: src/view/com/composer/Composer.tsx:772 -#: src/view/com/post-thread/PostThreadComposePrompt.tsx:95 +#: src/view/com/post-thread/PostThreadComposePrompt.tsx:90 msgid "Write your reply" msgstr "" From 21989b558bd074bf84ac08c174d7a411fda1ffb7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Jun 2025 12:37:14 +0300 Subject: [PATCH 28/49] Granular notification settings (#8484) * add mockup screen * add notification index screen * add redirect screen * upgrade sdk * new icons * add new screens * make router typesafe, finish adding screens * add routes to go server * load settings * push notif settings * improve web * fix lockfile lint * no $type on preferences * prompt to enable push notifications * fix reply prefs * space out options * fix copy error * Update RepostsOnRepostsNotificationSettings.tsx * only send minimal diff to putPrefs * fix yarn.lock * Update Navigation.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Settings/NotificationSettings/index.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add description to `syncOthers` --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- .../bellRinging_stroke2_corner0_rounded.svg | 1 + .../likeRepost_stroke2_corner2_rounded.svg | 1 + .../phoneHaptic_stroke2_corner2_rounded.svg | 1 + .../repostRepost_stroke2_corner2_rounded.svg | 1 + bskyweb/cmd/bskyweb/server.go | 11 + package.json | 2 +- src/Navigation.tsx | 99 +++++- src/components/icons/BellRinging.tsx | 5 + src/components/icons/Heart2.tsx | 4 + src/components/icons/Phone.tsx | 4 + src/components/icons/Repost.tsx | 4 + src/lib/routes/router.ts | 6 +- src/lib/routes/types.ts | 24 +- src/routes.ts | 26 +- src/screens/Messages/Settings.tsx | 4 +- ...ngsInterests.tsx => InterestsSettings.tsx} | 5 +- .../Settings/LegacyNotificationSettings.tsx | 21 ++ src/screens/Settings/NotificationSettings.tsx | 98 ------ .../LikeNotificationSettings.tsx | 60 ++++ .../LikesOnRepostsNotificationSettings.tsx | 65 ++++ .../MentionNotificationSettings.tsx | 63 ++++ .../MiscellaneousNotificationSettings.tsx | 68 ++++ .../NewFollowerNotificationSettings.tsx | 63 ++++ .../QuoteNotificationSettings.tsx | 60 ++++ .../ReplyNotificationSettings.tsx | 66 ++++ .../RepostNotificationSettings.tsx | 63 ++++ .../RepostsOnRepostsNotificationSettings.tsx | 66 ++++ .../components/ItemTextWithSubtitle.tsx | 34 ++ .../components/PreferenceControls.tsx | 194 ++++++++++++ .../Settings/NotificationSettings/index.tsx | 293 ++++++++++++++++++ src/screens/Settings/Settings.tsx | 9 + src/state/queries/notifications/settings.ts | 99 +++--- src/view/screens/Notifications.tsx | 2 +- yarn.lock | 199 ++++++------ 34 files changed, 1434 insertions(+), 287 deletions(-) create mode 100644 assets/icons/bellRinging_stroke2_corner0_rounded.svg create mode 100644 assets/icons/likeRepost_stroke2_corner2_rounded.svg create mode 100644 assets/icons/phoneHaptic_stroke2_corner2_rounded.svg create mode 100644 assets/icons/repostRepost_stroke2_corner2_rounded.svg create mode 100644 src/components/icons/BellRinging.tsx rename src/screens/Settings/{SettingsInterests.tsx => InterestsSettings.tsx} (96%) create mode 100644 src/screens/Settings/LegacyNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx create mode 100644 src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx create mode 100644 src/screens/Settings/NotificationSettings/index.tsx diff --git a/assets/icons/bellRinging_stroke2_corner0_rounded.svg b/assets/icons/bellRinging_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d49a59a01a --- /dev/null +++ b/assets/icons/bellRinging_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/likeRepost_stroke2_corner2_rounded.svg b/assets/icons/likeRepost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..f5d2da35bc --- /dev/null +++ b/assets/icons/likeRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..ebcf89b46e --- /dev/null +++ b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/repostRepost_stroke2_corner2_rounded.svg b/assets/icons/repostRepost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..caec8c1029 --- /dev/null +++ b/assets/icons/repostRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index f419212cc9..ef796920d6 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -278,6 +278,17 @@ func serve(cctx *cli.Context) error { e.GET("/settings/content-and-media", server.WebGeneric) e.GET("/settings/interests", server.WebGeneric) e.GET("/settings/about", server.WebGeneric) + e.GET("/settings/notifications", server.WebGeneric) + e.GET("/settings/notifications/replies", server.WebGeneric) + e.GET("/settings/notifications/mentions", server.WebGeneric) + e.GET("/settings/notifications/quotes", server.WebGeneric) + e.GET("/settings/notifications/likes", server.WebGeneric) + e.GET("/settings/notifications/reposts", server.WebGeneric) + e.GET("/settings/notifications/new-followers", server.WebGeneric) + e.GET("/settings/notifications/likes-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/reposts-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/activity", server.WebGeneric) + e.GET("/settings/notifications/miscellaneous", server.WebGeneric) e.GET("/settings/app-icon", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) diff --git a/package.json b/package.json index 691a6e94d0..fe38554141 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.133", + "@atproto/dev-env": "^0.3.142", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 2f26c09711..3bf1ace852 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -90,11 +90,10 @@ import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' +import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' -import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' -import {SettingsInterests} from '#/screens/Settings/SettingsInterests' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, @@ -110,6 +109,17 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' +import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' +import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' +import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' +import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' +import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' +import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' +import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' +import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' +import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' +import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' const navigationRef = createNavigationContainerRef() @@ -380,6 +390,83 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + NotificationSettingsScreen} + options={{title: title(msg`Notification settings`), requireAuth: true}} + /> + ReplyNotificationSettingsScreen} + options={{ + title: title(msg`Reply notifications`), + requireAuth: true, + }} + /> + MentionNotificationSettingsScreen} + options={{ + title: title(msg`Mention notifications`), + requireAuth: true, + }} + /> + QuoteNotificationSettingsScreen} + options={{ + title: title(msg`Quote notifications`), + requireAuth: true, + }} + /> + LikeNotificationSettingsScreen} + options={{ + title: title(msg`Like notifications`), + requireAuth: true, + }} + /> + RepostNotificationSettingsScreen} + options={{ + title: title(msg`Repost notifications`), + requireAuth: true, + }} + /> + NewFollowerNotificationSettingsScreen} + options={{ + title: title(msg`New follower notifications`), + requireAuth: true, + }} + /> + LikesOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Likes on your reposts notifications`), + requireAuth: true, + }} + /> + RepostsOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Reposts on your reposts notifications`), + requireAuth: true, + }} + /> + MiscellaneousNotificationSettingsScreen} + options={{ + title: title(msg`Miscellaneous notifications`), + requireAuth: true, + }} + /> ContentAndMediaSettingsScreen} @@ -389,8 +476,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> SettingsInterests} + name="InterestsSettings" + getComponent={() => InterestsSettingsScreen} options={{ title: title(msg`Your interests`), requireAuth: true, @@ -438,8 +525,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> NotificationSettingsScreen} + name="LegacyNotificationSettings" + getComponent={() => LegacyNotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> > { routes: [string, Route][] = [] - constructor(description: Record) { + constructor(description: Record) { for (const [screen, pattern] of Object.entries(description)) { if (typeof pattern === 'string') { this.routes.push([screen, createRoute(pattern)]) @@ -14,7 +14,7 @@ export class Router { } } - matchName(name: string): Route | undefined { + matchName(name: keyof T | (string & {})): Route | undefined { for (const [screenName, route] of this.routes) { if (screenName === name) { return route diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index f587423900..c92be34c23 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -52,7 +52,18 @@ export type CommonNavigatorParams = { AccountSettings: undefined PrivacyAndSecuritySettings: undefined ContentAndMediaSettings: undefined - SettingsInterests: undefined + NotificationSettings: undefined + ReplyNotificationSettings: undefined + MentionNotificationSettings: undefined + QuoteNotificationSettings: undefined + LikeNotificationSettings: undefined + RepostNotificationSettings: undefined + NewFollowerNotificationSettings: undefined + LikesOnRepostsNotificationSettings: undefined + RepostsOnRepostsNotificationSettings: undefined + ActivityNotificationSettings: undefined + MiscellaneousNotificationSettings: undefined + InterestsSettings: undefined AboutSettings: undefined AppIconSettings: undefined Search: {q?: string} @@ -61,7 +72,7 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesSettings: undefined MessagesInbox: undefined - NotificationSettings: undefined + LegacyNotificationSettings: undefined Feeds: undefined Start: {name: string; rkey: string} StarterPack: {name: string; rkey: string; new?: boolean} @@ -104,8 +115,6 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined Notifications: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -118,15 +127,8 @@ export type AllNavigatorParams = CommonNavigatorParams & { NotificationsTab: undefined Notifications: undefined MyProfileTab: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} - Start: {name: string; rkey: string} - StarterPack: {name: string; rkey: string; new?: boolean} - StarterPackShort: {code: string} - StarterPackWizard: undefined - StarterPackEdit: {rkey?: string} } // NOTE diff --git a/src/routes.ts b/src/routes.ts index 60bb65dd5a..b66a0ae53f 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,11 +1,17 @@ import {Router} from '#/lib/routes/router' +import {type FlatNavigatorParams} from './lib/routes/types' -export const router = new Router({ +type AllNavigatableRoutes = Omit< + FlatNavigatorParams, + 'NotFound' | 'SharedPreferencesTester' +> + +export const router = new Router({ Home: '/', Search: '/search', Feeds: '/feeds', Notifications: '/notifications', - NotificationSettings: '/notifications/settings', + LegacyNotificationSettings: '/notifications/settings', Settings: '/settings', Lists: '/lists', // moderation @@ -42,13 +48,25 @@ export const router = new Router({ AccessibilitySettings: '/settings/accessibility', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', - // new settings AccountSettings: '/settings/account', PrivacyAndSecuritySettings: '/settings/privacy-and-security', ContentAndMediaSettings: '/settings/content-and-media', - SettingsInterests: '/settings/interests', + InterestsSettings: '/settings/interests', AboutSettings: '/settings/about', AppIconSettings: '/settings/app-icon', + NotificationSettings: '/settings/notifications', + ReplyNotificationSettings: '/settings/notifications/replies', + MentionNotificationSettings: '/settings/notifications/mentions', + QuoteNotificationSettings: '/settings/notifications/quotes', + LikeNotificationSettings: '/settings/notifications/likes', + RepostNotificationSettings: '/settings/notifications/reposts', + NewFollowerNotificationSettings: '/settings/notifications/new-followers', + LikesOnRepostsNotificationSettings: + '/settings/notifications/likes-on-reposts', + RepostsOnRepostsNotificationSettings: + '/settings/notifications/reposts-on-reposts', + ActivityNotificationSettings: '/settings/notifications/activity', + MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', // support Support: '/support', PrivacyPolicy: '/support/privacy', diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index f37e7a9ba1..0b8c88b9dd 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -2,9 +2,9 @@ import {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {isNative} from '#/platform/detection' import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery} from '#/state/queries/profile' diff --git a/src/screens/Settings/SettingsInterests.tsx b/src/screens/Settings/InterestsSettings.tsx similarity index 96% rename from src/screens/Settings/SettingsInterests.tsx rename to src/screens/Settings/InterestsSettings.tsx index 42259e9b68..746315f7b8 100644 --- a/src/screens/Settings/SettingsInterests.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -2,9 +2,11 @@ import {useMemo, useState} from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' import debounce from 'lodash.debounce' +import {type CommonNavigatorParams} from '#/lib/routes/types' import { preferencesQueryKey, usePreferencesQuery, @@ -24,7 +26,8 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -export function SettingsInterests() { +type Props = NativeStackScreenProps +export function InterestsSettingsScreen({}: Props) { const t = useTheme() const gutters = useGutters(['base']) const {data: preferences} = usePreferencesQuery() diff --git a/src/screens/Settings/LegacyNotificationSettings.tsx b/src/screens/Settings/LegacyNotificationSettings.tsx new file mode 100644 index 0000000000..a9ef5d9831 --- /dev/null +++ b/src/screens/Settings/LegacyNotificationSettings.tsx @@ -0,0 +1,21 @@ +import {useCallback} from 'react' +import {useFocusEffect} from '@react-navigation/native' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LegacyNotificationSettings' +> +export function LegacyNotificationSettingsScreen({navigation}: Props) { + useFocusEffect( + useCallback(() => { + navigation.replace('NotificationSettings') + }, [navigation]), + ) + + return null +} diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx deleted file mode 100644 index ebb230c2ca..0000000000 --- a/src/screens/Settings/NotificationSettings.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import {Text} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' -import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings' -import {atoms as a} from '#/alf' -import {Admonition} from '#/components/Admonition' -import {Error} from '#/components/Error' -import * as Toggle from '#/components/forms/Toggle' -import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' -import * as Layout from '#/components/Layout' -import {Loader} from '#/components/Loader' -import * as SettingsList from './components/SettingsList' - -type Props = NativeStackScreenProps -export function NotificationSettingsScreen({}: Props) { - const {_} = useLingui() - - const { - data, - isError: isQueryError, - refetch, - } = useNotificationFeedQuery({ - filter: 'all', - }) - const serverPriority = data?.pages.at(0)?.priority - - const { - mutate: onChangePriority, - isPending: isMutationPending, - variables, - } = useNotificationSettingsMutation() - - const priority = isMutationPending - ? variables[0] === 'enabled' - : serverPriority - - return ( - - - - - - Notification Settings - - - - - - {isQueryError ? ( - - ) : ( - - - - - Notification filters - - - - - Enable priority notifications - - {!data ? : } - - - - - - - Experimental: When this - preference is enabled, you'll only receive reply and quote - notifications from users you follow. We'll continue to add - more controls here over time. - - - - - )} - - - ) -} diff --git a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx new file mode 100644 index 0000000000..f726ab558a --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Heart2_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikeNotificationSettings' +> +export function LikeNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Likes} + subtitleText={ + Get notifications when people like your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx new file mode 100644 index 0000000000..08a05d468f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx @@ -0,0 +1,65 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikesOnRepostsNotificationSettings' +> +export function LikesOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Likes on your reposts} + subtitleText={ + + Get notifications when people like posts that you've reposted. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx new file mode 100644 index 0000000000..0a770157e9 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MentionNotificationSettings' +> +export function MentionNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Mentions} + subtitleText={ + Get notifications when people mention you. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx new file mode 100644 index 0000000000..a0fe65ecf6 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx @@ -0,0 +1,68 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MiscellaneousNotificationSettings' +> +export function MiscellaneousNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Everything else} + subtitleText={ + + Notifications for everything else, such as when someone joins + via one of your starter packs. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx new file mode 100644 index 0000000000..dd603a52f5 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'NewFollowerNotificationSettings' +> +export function NewFollowerNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + New followers} + subtitleText={ + Get notifications when people follow you. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx new file mode 100644 index 0000000000..afb3df90f5 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'QuoteNotificationSettings' +> +export function QuoteNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Quotes} + subtitleText={ + Get notifications when people quote your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx new file mode 100644 index 0000000000..b3e7c6cff2 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'ReplyNotificationSettings' +> +export function ReplyNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Replies} + subtitleText={ + + Get notifications when people reply to your posts. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx new file mode 100644 index 0000000000..aa9e4e32fa --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostNotificationSettings' +> +export function RepostNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Reposts} + subtitleText={ + Get notifications when people repost your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx new file mode 100644 index 0000000000..13fec61682 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostsOnRepostsNotificationSettings' +> +export function RepostsOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Reposts of your reposts} + subtitleText={ + + Get notifications when people repost posts that you've + reposted. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx new file mode 100644 index 0000000000..217fc33b95 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx @@ -0,0 +1,34 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' +import * as SettingsList from '../../components/SettingsList' + +export function ItemTextWithSubtitle({ + titleText, + subtitleText, + bold = false, + showSkeleton = false, +}: { + titleText: React.ReactNode + subtitleText: React.ReactNode + bold?: boolean + showSkeleton?: boolean +}) { + const t = useTheme() + return ( + + + {titleText} + + {showSkeleton ? ( + + ) : ( + + {subtitleText} + + )} + + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx new file mode 100644 index 0000000000..336e086950 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -0,0 +1,194 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky/notification/defs' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings' +import {atoms as a, platform, useTheme} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' +import {Divider} from '../../components/SettingsList' + +export function PreferenceControls({ + name, + syncOthers, + preference, + allowDisableInApp = true, +}: { + name: Exclude + /** + * Keep other prefs in sync with `name`. For use in the "everything else" category + * which groups starterpack joins + verified + unverified notifications into a single toggle. + */ + syncOthers?: Exclude[] + preference?: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp?: boolean +}) { + if (!preference) + return ( + + + + ) + + return ( + + ) +} + +export function Inner({ + name, + syncOthers = [], + preference, + allowDisableInApp, +}: { + name: Exclude + syncOthers?: Exclude[] + preference: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {mutate} = useNotificationSettingsUpdateMutation() + + const channels = useMemo(() => { + const arr = [] + if (preference.list) arr.push('list') + if (preference.push) arr.push('push') + return arr + }, [preference]) + + const onChangeChannels = (change: string[]) => { + const newPreference = { + ...preference, + list: change.includes('list'), + push: change.includes('push'), + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + const onChangeFilter = ([change]: string[]) => { + if (change !== 'all' && change !== 'follows') + throw new Error('Invalid filter') + + const newPreference = { + ...preference, + filter: change, + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + return ( + + + + + + Push notifications + + + + {allowDisableInApp && ( + + + In-app notifications + + + + )} + + + {'filter' in preference && ( + <> + + From + + + + + 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + Everyone + + + + + 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + People I follow + + + + + + )} + + ) +} diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx new file mode 100644 index 0000000000..a4f6dede05 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -0,0 +1,293 @@ +import {useEffect} from 'react' +import {Linking, View} from 'react-native' +import * as Notification from 'expo-notifications' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQuery, useQueryClient} from '@tanstack/react-query' + +import {useAppState} from '#/lib/hooks/useAppState' +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {isAndroid, isIOS, isWeb} from '#/platform/detection' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +// import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' +import { + Heart2_Stroke2_Corner0_Rounded as HeartIcon, + LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon, +} from '#/components/icons/Heart2' +import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import { + Repost_Stroke2_Corner2_Rounded as RepostIcon, + RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, +} from '#/components/icons/Repost' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' + +const RQKEY = ['notification-permissions'] + +type Props = NativeStackScreenProps +export function NotificationSettingsScreen({}: Props) { + const {_} = useLingui() + const queryClient = useQueryClient() + const {data: settings, isError} = useNotificationSettingsQuery() + + const {data: permissions, refetch} = useQuery({ + queryKey: RQKEY, + queryFn: async () => { + if (isWeb) return null + return await Notification.getPermissionsAsync() + }, + }) + + const appState = useAppState() + useEffect(() => { + if (appState === 'active') { + refetch() + } + }, [appState, refetch]) + + const onRequestPermissions = async () => { + if (isWeb) return + if (permissions?.canAskAgain) { + const response = await Notification.requestPermissionsAsync() + queryClient.setQueryData(RQKEY, response) + } else { + if (isAndroid) { + try { + await Linking.sendIntent( + 'android.settings.APP_NOTIFICATION_SETTINGS', + [ + { + key: 'android.provider.extra.APP_PACKAGE', + value: 'xyz.blueskyweb.app', + }, + ], + ) + } catch { + Linking.openSettings() + } + } else if (isIOS) { + Linking.openSettings() + } + } + } + + return ( + + + + + + Notifications + + + + + + + {permissions && !permissions.granted && ( + <> + + + + Enable push notifications + + + + + )} + {isError && ( + + + Failed to load notification settings. + + + )} + + + + Replies} + subtitleText={} + showSkeleton={!settings} + /> + + + + Mentions} + subtitleText={} + showSkeleton={!settings} + /> + + + + Quotes} + subtitleText={} + showSkeleton={!settings} + /> + + + + Likes} + subtitleText={} + showSkeleton={!settings} + /> + + + + Reposts} + subtitleText={} + showSkeleton={!settings} + /> + + + + New followers} + subtitleText={} + showSkeleton={!settings} + /> + + {/* + + + Activity alerts} + subtitleText={ + + } + showSkeleton={!settings} + /> + */} + + + Likes on your reposts} + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + Reposts of your reposts} + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + Everything else} + // technically a bundle of several settings, but since they're set together + // and are most likely in sync we'll just show the state of one of them + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + + + ) +} + +function SettingPreview({ + preference, +}: { + preference?: + | AppBskyNotificationDefs.Preference + | AppBskyNotificationDefs.FilterablePreference +}) { + const {_} = useLingui() + if (!preference) { + return null + } else { + if ('filter' in preference) { + if (preference.filter === 'all') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, Everyone`) + } else if (preference.list) { + return _(msg`In-app, Everyone`) + } else if (preference.push) { + return _(msg`Push, Everyone`) + } + } else if (preference.filter === 'follows') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, People you follow`) + } else if (preference.list) { + return _(msg`In-app, People you follow`) + } else if (preference.push) { + return _(msg`Push, People you follow`) + } + } + } else { + if (preference.list && preference.push) { + return _(msg`In-app, Push`) + } else if (preference.list) { + return _(msg`In-app`) + } else if (preference.push) { + return _(msg`Push`) + } + } + } + + return _(msg`Off`) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 9f36c27acc..6310c7c3c8 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -36,6 +36,7 @@ import {AvatarStackWithFetch} from '#/components/AvatarStack' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' @@ -180,6 +181,14 @@ export function SettingsScreen({}: Props) { Moderation + + + + Notifications + + diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index 2ac42aa328..9661bed1be 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -1,72 +1,63 @@ -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMutation, useQueryClient} from '@tanstack/react-query' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {t} from '@lingui/macro' +import { + type QueryClient, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' -import {until} from '#/lib/async/until' import {logger} from '#/logger' -import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' -import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' import {useAgent} from '#/state/session' import * as Toast from '#/view/com/util/Toast' -export function useNotificationSettingsMutation() { - const {_} = useLingui() +const RQKEY_ROOT = 'notification-settings' +const RQKEY = [RQKEY_ROOT] + +export function useNotificationSettingsQuery() { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY, + queryFn: async () => { + const response = await agent.app.bsky.notification.getPreferences() + return response.data.preferences + }, + }) +} +export function useNotificationSettingsUpdateMutation() { const agent = useAgent() const queryClient = useQueryClient() return useMutation({ - mutationFn: async (keys: string[]) => { - const enabled = keys[0] === 'enabled' - - await agent.api.app.bsky.notification.putPreferences({ - priority: enabled, - }) - - await until( - 5, // 5 tries - 1e3, // 1s delay between tries - res => res.data.priority === enabled, - () => agent.api.app.bsky.notification.listNotifications({limit: 1}), + mutationFn: async ( + update: Partial, + ) => { + const response = await agent.app.bsky.notification.putPreferencesV2( + update, ) - - eagerlySetCachedPriority(queryClient, enabled) + return response.data.preferences }, - onError: err => { - logger.error('Failed to save notification preferences', { - safeMessage: err, - }) - Toast.show( - _(msg`Failed to save notification preferences, please try again`), - 'xmark', - ) + onMutate: update => { + optimisticUpdateNotificationSettings(queryClient, update) }, - onSuccess: () => { - Toast.show(_(msg({message: 'Preference saved', context: 'toast'}))) - }, - onSettled: () => { - invalidateCachedUnreadPage() - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) + onError: e => { + logger.error('Could not update notification settings', {message: e}) + queryClient.invalidateQueries({queryKey: RQKEY}) + Toast.show(t`Could not update notification settings`, 'xmark') }, }) } -function eagerlySetCachedPriority( - queryClient: ReturnType, - enabled: boolean, +function optimisticUpdateNotificationSettings( + queryClient: QueryClient, + update: Partial, ) { - function updateData(old: any) { - if (!old) return old - return { - ...old, - pages: old.pages.map((page: any) => { - return { - ...page, - priority: enabled, - } - }), - } - } - queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) - queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) + queryClient.setQueryData( + RQKEY, + (old?: AppBskyNotificationDefs.Preferences) => { + if (!old) return old + return {...old, ...update} + }, + ) } diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index ace0de2aed..528d6be870 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -130,7 +130,7 @@ export function NotificationsScreen({}: Props) { Date: Tue, 17 Jun 2025 13:09:15 -0700 Subject: [PATCH 29/49] add fabric package/lock files (#8509) --- .github/workflows/build-submit-android.yml | 4 ++++ .github/workflows/bundle-deploy-eas-update.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index f75c95052e..cff99f9c7e 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -52,6 +52,10 @@ jobs: distribution: 'temurin' java-version: '17' + - name: "Use upgraded MMKV for Fabric" + run: | + sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt + - name: ⚙️ Install dependencies run: yarn install diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 5a4702f964..cd62b4e8c5 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -275,6 +275,10 @@ jobs: distribution: 'temurin' java-version: '17' + - name: "Use upgraded MMKV for Fabric" + run: | + sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt + - name: ⚙️ Install dependencies run: yarn install From f1f9ca960681b5257429b4b4c2a02cccc29e4fd6 Mon Sep 17 00:00:00 2001 From: hailey Date: Tue, 17 Jun 2025 13:19:07 -0700 Subject: [PATCH 30/49] facepalm (#8510) --- .github/workflows/build-submit-android.yml | 2 +- .github/workflows/bundle-deploy-eas-update.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index cff99f9c7e..8a8388ee80 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -54,7 +54,7 @@ jobs: - name: "Use upgraded MMKV for Fabric" run: | - sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt + sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json - name: ⚙️ Install dependencies run: yarn install diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index cd62b4e8c5..ab497954c6 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -277,7 +277,7 @@ jobs: - name: "Use upgraded MMKV for Fabric" run: | - sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt + sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json - name: ⚙️ Install dependencies run: yarn install From 9cf457acf8495c1287e366f677e1516847e4d739 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 18 Jun 2025 00:12:59 +0300 Subject: [PATCH 31/49] copy tweak (#8506) --- src/Navigation.tsx | 4 ++-- .../LikesOnRepostsNotificationSettings.tsx | 2 +- src/screens/Settings/NotificationSettings/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 3bf1ace852..f1a9c569d0 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -447,7 +447,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="LikesOnRepostsNotificationSettings" getComponent={() => LikesOnRepostsNotificationSettingsScreen} options={{ - title: title(msg`Likes on your reposts notifications`), + title: title(msg`Likes of your reposts notifications`), requireAuth: true, }} /> @@ -455,7 +455,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="RepostsOnRepostsNotificationSettings" getComponent={() => RepostsOnRepostsNotificationSettingsScreen} options={{ - title: title(msg`Reposts on your reposts notifications`), + title: title(msg`Reposts of your reposts notifications`), requireAuth: true, }} /> diff --git a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx index 08a05d468f..c72e8c7578 100644 --- a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx +++ b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx @@ -38,7 +38,7 @@ export function LikesOnRepostsNotificationSettingsScreen({}: Props) { Likes on your reposts} + titleText={Likes of your reposts} subtitleText={ Get notifications when people like posts that you've reposted. diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx index a4f6dede05..7593635760 100644 --- a/src/screens/Settings/NotificationSettings/index.tsx +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -199,13 +199,13 @@ export function NotificationSettingsScreen({}: Props) { */} Likes on your reposts} + titleText={Likes of your reposts} subtitleText={ } From 619fa0d0bbac80100aefeb926ca7098f0644d0d8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 17 Jun 2025 18:12:37 -0500 Subject: [PATCH 32/49] Delete some old dialogs (#8512) * Delete old EditProfile dialog * Delete old email verification dialogs --- src/components/dialogs/ChangeEmailDialog.tsx | 259 ------------- src/components/dialogs/VerifyEmailDialog.tsx | 360 ------------------- src/view/com/modals/EditProfile.tsx | 335 ----------------- 3 files changed, 954 deletions(-) delete mode 100644 src/components/dialogs/ChangeEmailDialog.tsx delete mode 100644 src/components/dialogs/VerifyEmailDialog.tsx delete mode 100644 src/view/com/modals/EditProfile.tsx diff --git a/src/components/dialogs/ChangeEmailDialog.tsx b/src/components/dialogs/ChangeEmailDialog.tsx deleted file mode 100644 index 93397bae93..0000000000 --- a/src/components/dialogs/ChangeEmailDialog.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import {useState} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {cleanError} from '#/lib/strings/errors' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {atoms as a, useBreakpoints, web} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -export function ChangeEmailDialog({ - control, - verifyEmailControl, -}: { - control: Dialog.DialogControlProps - verifyEmailControl: Dialog.DialogControlProps -}) { - return ( - - - - - ) -} - -export function Inner({ - verifyEmailControl, -}: { - verifyEmailControl: Dialog.DialogControlProps -}) { - const {_} = useLingui() - const {currentAccount} = useSession() - const agent = useAgent() - const control = Dialog.useDialogContext() - const {gtMobile} = useBreakpoints() - - const [currentStep, setCurrentStep] = useState< - 'StepOne' | 'StepTwo' | 'StepThree' - >('StepOne') - const [email, setEmail] = useState('') - const [confirmationCode, setConfirmationCode] = useState('') - const [isProcessing, setIsProcessing] = useState(false) - const [error, setError] = useState('') - - const currentEmail = currentAccount?.email || '(no email)' - const uiStrings = { - StepOne: { - title: _(msg`Change Your Email`), - message: '', - }, - StepTwo: { - title: _(msg`Security Step Required`), - message: _( - msg`An email has been sent to your previous address, ${currentEmail}. It includes a confirmation code which you can enter below.`, - ), - }, - StepThree: { - title: _(msg`Email Updated!`), - message: _( - msg`Your email address has been updated but it is not yet verified. As a next step, please verify your new email.`, - ), - }, - } - - const onRequestChange = async () => { - if (email === currentAccount?.email) { - setError( - _( - msg`The email address you entered is the same as your current email address.`, - ), - ) - return - } - setError('') - setIsProcessing(true) - try { - const res = await agent.com.atproto.server.requestEmailUpdate() - if (res.data.tokenRequired) { - setCurrentStep('StepTwo') - } else { - await agent.com.atproto.server.updateEmail({email: email.trim()}) - await agent.resumeSession(agent.session!) - setCurrentStep('StepThree') - } - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onConfirm = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.updateEmail({ - email: email.trim(), - token: confirmationCode.trim(), - }) - await agent.resumeSession(agent.session!) - setCurrentStep('StepThree') - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onVerify = async () => { - control.close(() => { - verifyEmailControl.open() - }) - } - - return ( - - - - - - {uiStrings[currentStep].title} - - {error ? ( - - - - ) : null} - {currentStep === 'StepOne' ? ( - - - Enter your new email address below. - - - - - - ) : ( - - {uiStrings[currentStep].message} - - )} - - {currentStep === 'StepTwo' ? ( - - - Confirmation code - - - - - - ) : null} - - {currentStep === 'StepOne' ? ( - <> - - - - ) : currentStep === 'StepTwo' ? ( - <> - - - - ) : currentStep === 'StepThree' ? ( - <> - - - - ) : null} - - - - ) -} diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx deleted file mode 100644 index b8d1cd1925..0000000000 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import {useState} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {cleanError} from '#/lib/strings/errors' -import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' -import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' -import {InlineLinkText} from '#/components/Link' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' -import {ChangeEmailDialog} from './ChangeEmailDialog' - -export function VerifyEmailDialog({ - control, - onCloseWithoutVerifying, - onCloseAfterVerifying, - reasonText, - changeEmailControl, - reminder, -}: { - control: Dialog.DialogControlProps - onCloseWithoutVerifying?: () => void - onCloseAfterVerifying?: () => void - reasonText?: string - /** - * if a changeEmailControl for a ChangeEmailDialog is not provided, - * this component will create one for you. Using this prop - * helps reduce duplication, since these dialogs are often used together. - */ - changeEmailControl?: Dialog.DialogControlProps - reminder?: boolean -}) { - const agent = useAgent() - const fallbackChangeEmailControl = Dialog.useDialogControl() - - const [didVerify, setDidVerify] = useState(false) - - return ( - <> - { - if (!didVerify) { - onCloseWithoutVerifying?.() - return - } - - try { - await agent.resumeSession(agent.session!) - onCloseAfterVerifying?.() - } catch (e: unknown) { - logger.error(String(e)) - return - } - }}> - - - - {!changeEmailControl && ( - - )} - - ) -} - -export function Inner({ - setDidVerify, - reasonText, - changeEmailControl, - reminder, -}: { - setDidVerify: (value: boolean) => void - reasonText?: string - changeEmailControl: Dialog.DialogControlProps - reminder?: boolean -}) { - const control = Dialog.useDialogContext() - const {_} = useLingui() - const {currentAccount} = useSession() - const agent = useAgent() - const {gtMobile} = useBreakpoints() - const t = useTheme() - - const [currentStep, setCurrentStep] = useState< - 'Reminder' | 'StepOne' | 'StepTwo' | 'StepThree' - >(reminder ? 'Reminder' : 'StepOne') - const [confirmationCode, setConfirmationCode] = useState('') - const [isProcessing, setIsProcessing] = useState(false) - const [error, setError] = useState('') - - const uiStrings = { - Reminder: { - title: _(msg`Please Verify Your Email`), - message: _( - msg`Your email has not yet been verified. This is an important security step which we recommend.`, - ), - }, - StepOne: { - title: _(msg`Verify Your Email`), - message: '', - }, - StepTwo: { - title: _(msg`Enter Code`), - message: _( - msg`An email has been sent! Please enter the confirmation code included in the email below.`, - ), - }, - StepThree: { - title: _(msg`Success!`), - message: _(msg`Thank you! Your email has been successfully verified.`), - }, - } - - const onSendEmail = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.requestEmailConfirmation() - setCurrentStep('StepTwo') - } catch (e: unknown) { - setError(cleanError(e)) - } finally { - setIsProcessing(false) - } - } - - const onVerifyEmail = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.confirmEmail({ - email: (currentAccount?.email || '').trim(), - token: confirmationCode.trim(), - }) - } catch (e: unknown) { - setError(cleanError(String(e))) - setIsProcessing(false) - return - } - - setIsProcessing(false) - setDidVerify(true) - setCurrentStep('StepThree') - } - - return ( - - - {currentStep === 'Reminder' && ( - - - - )} - - - {uiStrings[currentStep].title} - - {error ? ( - - - - ) : null} - {currentStep === 'StepOne' ? ( - - {reasonText ? ( - - {reasonText} - - Don't have access to{' '} - - {currentAccount?.email} - - ?{' '} - { - e.preventDefault() - control.close(() => { - changeEmailControl.open() - }) - return false - }}> - Change your email address - - . - - - ) : ( - - - You'll receive an email at{' '} - - {currentAccount?.email} - {' '} - to verify it's you. - {' '} - { - e.preventDefault() - control.close(() => { - changeEmailControl.open() - }) - return false - }}> - Need to change it? - - - )} - - ) : ( - - {uiStrings[currentStep].message} - - )} - - {currentStep === 'StepTwo' ? ( - - - Confirmation Code - - - - - - ) : null} - - {currentStep === 'Reminder' ? ( - <> - - - - ) : currentStep === 'StepOne' ? ( - <> - - - - ) : currentStep === 'StepTwo' ? ( - <> - - - - ) : currentStep === 'StepThree' ? ( - - ) : null} - - - - ) -} diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx deleted file mode 100644 index cb1552fe57..0000000000 --- a/src/view/com/modals/EditProfile.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import {useCallback, useState} from 'react' -import { - ActivityIndicator, - KeyboardAvoidingView, - ScrollView, - StyleSheet, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import Animated, {FadeOut} from 'react-native-reanimated' -import {LinearGradient} from 'expo-linear-gradient' -import {type AppBskyActorDefs} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' -import {compressIfNeeded} from '#/lib/media/manip' -import {type PickerImage} from '#/lib/media/picker.shared' -import {cleanError} from '#/lib/strings/errors' -import {enforceLen} from '#/lib/strings/helpers' -import {colors, gradients, s} from '#/lib/styles' -import {useTheme} from '#/lib/ThemeContext' -import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {useProfileUpdateMutation} from '#/state/queries/profile' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {EditableUserAvatar} from '#/view/com/util/UserAvatar' -import {UserBanner} from '#/view/com/util/UserBanner' -import {Admonition} from '#/components/Admonition' -import {InlineLinkText} from '#/components/Link' -import {useSimpleVerificationState} from '#/components/verification' -import {ErrorMessage} from '../util/error/ErrorMessage' - -const AnimatedTouchableOpacity = - Animated.createAnimatedComponent(TouchableOpacity) - -export const snapPoints = ['fullscreen'] - -export function Component({ - profile, - onUpdate, -}: { - profile: AppBskyActorDefs.ProfileViewDetailed - onUpdate?: () => void -}) { - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - const {closeModal} = useModalControls() - const updateMutation = useProfileUpdateMutation() - const [imageError, setImageError] = useState('') - const initialDisplayName = profile.displayName || '' - const [displayName, setDisplayName] = useState( - profile.displayName || '', - ) - const [description, setDescription] = useState( - profile.description || '', - ) - const [userBanner, setUserBanner] = useState( - profile.banner, - ) - const [userAvatar, setUserAvatar] = useState( - profile.avatar, - ) - const [newUserBanner, setNewUserBanner] = useState< - PickerImage | undefined | null - >() - const [newUserAvatar, setNewUserAvatar] = useState< - PickerImage | undefined | null - >() - const onPressCancel = () => { - closeModal() - } - const onSelectNewAvatar = useCallback( - async (img: PickerImage | null) => { - setImageError('') - if (img === null) { - setNewUserAvatar(null) - setUserAvatar(null) - return - } - try { - const finalImg = await compressIfNeeded(img, 1000000) - setNewUserAvatar(finalImg) - setUserAvatar(finalImg.path) - } catch (e: any) { - setImageError(cleanError(e)) - } - }, - [setNewUserAvatar, setUserAvatar, setImageError], - ) - - const onSelectNewBanner = useCallback( - async (img: PickerImage | null) => { - setImageError('') - if (!img) { - setNewUserBanner(null) - setUserBanner(null) - return - } - try { - const finalImg = await compressIfNeeded(img, 1000000) - setNewUserBanner(finalImg) - setUserBanner(finalImg.path) - } catch (e: any) { - setImageError(cleanError(e)) - } - }, - [setNewUserBanner, setUserBanner, setImageError], - ) - - const onPressSave = useCallback(async () => { - setImageError('') - try { - await updateMutation.mutateAsync({ - profile, - updates: { - displayName, - description, - }, - newUserAvatar, - newUserBanner, - }) - Toast.show(_(msg({message: 'Profile updated', context: 'toast'}))) - onUpdate?.() - closeModal() - } catch (e: any) { - logger.error('Failed to update user profile', {message: String(e)}) - } - }, [ - updateMutation, - profile, - onUpdate, - closeModal, - displayName, - description, - newUserAvatar, - newUserBanner, - setImageError, - _, - ]) - const verification = useSimpleVerificationState({ - profile, - }) - - return ( - - - - Edit my profile - - - - - - - - {updateMutation.isError && ( - - - - )} - {imageError !== '' && ( - - - - )} - - - - Display Name - - - setDisplayName(enforceLen(v, MAX_DISPLAY_NAME)) - } - accessible={true} - accessibilityLabel={_(msg`Display name`)} - accessibilityHint={_(msg`Edit your display name`)} - /> - - {verification.isVerified && - verification.role === 'default' && - displayName !== initialDisplayName && ( - - - - You are verified. You will lose your verification status - if you change your display name.{' '} - - Learn more. - - - - - )} - - - - Description - - setDescription(enforceLen(v, MAX_DESCRIPTION))} - accessible={true} - accessibilityLabel={_(msg`Description`)} - accessibilityHint={_(msg`Edit your profile description`)} - /> - - {updateMutation.isPending ? ( - - - - ) : ( - - - - Save Changes - - - - )} - {!updateMutation.isPending && ( - - - - Cancel - - - - )} - - - - ) -} - -const styles = StyleSheet.create({ - title: { - textAlign: 'center', - fontWeight: '600', - fontSize: 24, - marginBottom: 18, - }, - label: { - fontWeight: '600', - paddingHorizontal: 4, - paddingBottom: 4, - marginTop: 20, - }, - form: { - paddingHorizontal: 14, - }, - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 16, - }, - textArea: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 12, - paddingTop: 10, - fontSize: 16, - height: 120, - textAlignVertical: 'top', - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - marginBottom: 10, - }, - avi: { - position: 'absolute', - top: 80, - left: 24, - width: 84, - height: 84, - borderWidth: 2, - borderRadius: 42, - }, - photos: { - marginBottom: 36, - marginHorizontal: -14, - }, - errorContainer: {marginTop: 20}, -}) From afd3d2829f692f5c480cfa019eed691b8b69d0bd Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:40:54 +0000 Subject: [PATCH 33/49] Nightly source-language update --- src/locale/locales/en/messages.po | 674 ++++++++++++++++-------------- 1 file changed, 357 insertions(+), 317 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 33dc6d5669..e668726899 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -464,10 +464,6 @@ msgstr "" msgid "<0>{date} at {time}" msgstr "" -#: src/screens/Settings/NotificationSettings.tsx:85 -msgid "<0>Experimental: When this preference is enabled, you'll only receive reply and quote notifications from users you follow. We'll continue to add more controls here over time." -msgstr "" - #: src/screens/StarterPack/Wizard/index.tsx:440 msgid "<0>You and<1> <2>{0} are included in your starter pack" msgstr "" @@ -501,10 +497,10 @@ msgstr "" msgid "A new form of verification" msgstr "" -#: src/Navigation.tsx:403 +#: src/Navigation.tsx:490 #: src/screens/Settings/AboutSettings.tsx:75 -#: src/screens/Settings/Settings.tsx:225 -#: src/screens/Settings/Settings.tsx:228 +#: src/screens/Settings/Settings.tsx:234 +#: src/screens/Settings/Settings.tsx:237 msgid "About" msgstr "" @@ -523,20 +519,20 @@ msgid "Accept Request" msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:46 -#: src/screens/Settings/Settings.tsx:201 -#: src/screens/Settings/Settings.tsx:204 +#: src/screens/Settings/Settings.tsx:210 +#: src/screens/Settings/Settings.tsx:213 msgid "Accessibility" msgstr "" -#: src/Navigation.tsx:355 +#: src/Navigation.tsx:365 msgid "Accessibility Settings" msgstr "" -#: src/Navigation.tsx:371 +#: src/Navigation.tsx:381 #: src/screens/Login/LoginForm.tsx:194 #: src/screens/Settings/AccountSettings.tsx:48 -#: src/screens/Settings/Settings.tsx:163 -#: src/screens/Settings/Settings.tsx:166 +#: src/screens/Settings/Settings.tsx:164 +#: src/screens/Settings/Settings.tsx:167 msgid "Account" msgstr "" @@ -567,11 +563,11 @@ msgstr "" msgid "Account Muted by List" msgstr "" -#: src/screens/Settings/Settings.tsx:505 +#: src/screens/Settings/Settings.tsx:514 msgid "Account options" msgstr "" -#: src/screens/Settings/Settings.tsx:541 +#: src/screens/Settings/Settings.tsx:550 msgid "Account removed from quick access" msgstr "" @@ -643,8 +639,8 @@ msgstr "" msgid "Add alt text (optional)" msgstr "" -#: src/screens/Settings/Settings.tsx:445 -#: src/screens/Settings/Settings.tsx:448 +#: src/screens/Settings/Settings.tsx:454 +#: src/screens/Settings/Settings.tsx:457 #: src/view/shell/desktop/LeftNav.tsx:260 #: src/view/shell/desktop/LeftNav.tsx:264 msgid "Add another account" @@ -772,7 +768,6 @@ msgstr "" msgid "Advanced" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:143 #: src/components/dialogs/EmailDialog/screens/Update.tsx:223 msgid "alice@example.com" msgstr "" @@ -859,14 +854,6 @@ msgstr "" msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:59 -msgid "An email has been sent to your previous address, {currentEmail}. It includes a confirmation code which you can enter below." -msgstr "" - -#: src/components/dialogs/VerifyEmailDialog.tsx:120 -msgid "An email has been sent! Please enter the confirmation code included in the email below." -msgstr "" - #: src/components/dialogs/GifSelect.tsx:265 msgid "An error has occurred" msgstr "" @@ -978,7 +965,7 @@ msgstr "" msgid "Anybody can interact" msgstr "" -#: src/Navigation.tsx:411 +#: src/Navigation.tsx:498 #: src/screens/Settings/AppIconSettings/index.tsx:67 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23 @@ -1015,7 +1002,7 @@ msgstr "" msgid "App passwords" msgstr "" -#: src/Navigation.tsx:323 +#: src/Navigation.tsx:333 #: src/screens/Settings/AppPasswords.tsx:51 msgid "App Passwords" msgstr "" @@ -1051,10 +1038,10 @@ msgstr "" msgid "Appeal this decision" msgstr "" -#: src/Navigation.tsx:363 +#: src/Navigation.tsx:373 #: src/screens/Settings/AppearanceSettings.tsx:85 -#: src/screens/Settings/Settings.tsx:193 -#: src/screens/Settings/Settings.tsx:196 +#: src/screens/Settings/Settings.tsx:202 +#: src/screens/Settings/Settings.tsx:205 msgid "Appearance" msgstr "" @@ -1271,7 +1258,7 @@ msgstr "" msgid "Blocked accounts" msgstr "" -#: src/Navigation.tsx:164 +#: src/Navigation.tsx:174 #: src/view/screens/ModerationBlockedAccounts.tsx:104 msgid "Blocked Accounts" msgstr "" @@ -1468,7 +1455,7 @@ msgstr "" #: src/screens/Settings/AppIconSettings/index.tsx:225 #: src/screens/Settings/components/ChangeHandleDialog.tsx:78 #: src/screens/Settings/components/ChangeHandleDialog.tsx:85 -#: src/screens/Settings/Settings.tsx:270 +#: src/screens/Settings/Settings.tsx:279 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 #: src/view/com/composer/Composer.tsx:960 @@ -1479,7 +1466,6 @@ msgstr "" #: src/view/com/modals/ChangePassword.tsx:282 #: src/view/com/modals/CreateOrEditList.tsx:333 #: src/view/com/modals/CropImage.web.tsx:97 -#: src/view/com/modals/EditProfile.tsx:269 #: src/view/com/modals/LinkWarning.tsx:105 #: src/view/com/modals/LinkWarning.tsx:107 #: src/view/shell/desktop/LeftNav.tsx:213 @@ -1502,10 +1488,6 @@ msgstr "" msgid "Cancel image crop" msgstr "" -#: src/view/com/modals/EditProfile.tsx:264 -msgid "Cancel profile editing" -msgstr "" - #: src/components/PostControls/RepostButton.tsx:203 msgid "Cancel quote post" msgstr "" @@ -1555,11 +1537,6 @@ msgstr "" msgid "Change app language" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:200 -#: src/components/dialogs/VerifyEmailDialog.tsx:225 -msgid "Change email address" -msgstr "" - #: src/screens/Settings/components/ChangeHandleDialog.tsx:94 #: src/screens/Settings/components/ChangeHandleDialog.tsx:98 msgid "Change Handle" @@ -1581,14 +1558,6 @@ msgstr "" msgid "Change report reason" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:53 -msgid "Change Your Email" -msgstr "" - -#: src/components/dialogs/VerifyEmailDialog.tsx:209 -msgid "Change your email address" -msgstr "" - #: src/screens/Settings/AppIconSettings/index.tsx:216 msgid "Changes app icon" msgstr "" @@ -1598,7 +1567,7 @@ msgstr "" msgid "Changes hosting provider" msgstr "" -#: src/Navigation.tsx:428 +#: src/Navigation.tsx:515 #: src/view/shell/bottom-bar/BottomBar.tsx:221 #: src/view/shell/desktop/LeftNav.tsx:553 #: src/view/shell/Drawer.tsx:455 @@ -1616,7 +1585,7 @@ msgctxt "toast" msgid "Chat muted" msgstr "" -#: src/Navigation.tsx:438 +#: src/Navigation.tsx:525 #: src/screens/Messages/components/InboxPreview.tsx:24 msgid "Chat request inbox" msgstr "" @@ -1627,7 +1596,7 @@ msgid "Chat requests" msgstr "" #: src/components/dms/ConvoMenu.tsx:75 -#: src/Navigation.tsx:433 +#: src/Navigation.tsx:520 #: src/screens/Messages/ChatList.tsx:341 msgid "Chat settings" msgstr "" @@ -1699,11 +1668,11 @@ msgstr "" msgid "Choose your username" msgstr "" -#: src/screens/Settings/Settings.tsx:423 +#: src/screens/Settings/Settings.tsx:432 msgid "Clear all storage data" msgstr "" -#: src/screens/Settings/Settings.tsx:425 +#: src/screens/Settings/Settings.tsx:434 msgid "Clear all storage data (restart after this)" msgstr "" @@ -1752,14 +1721,10 @@ msgstr "" msgid "Clip 🐴 clop 🐴" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:244 -#: src/components/dialogs/ChangeEmailDialog.tsx:250 #: src/components/dialogs/GifSelect.tsx:281 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 #: src/components/dialogs/SearchablePeopleList.tsx:295 -#: src/components/dialogs/VerifyEmailDialog.tsx:346 -#: src/components/dialogs/VerifyEmailDialog.tsx:352 #: src/components/dms/EmojiPopup.android.tsx:58 #: src/components/dms/ReportDialog.tsx:381 #: src/components/dms/ReportDialog.tsx:390 @@ -1872,7 +1837,7 @@ msgstr "" msgid "Comics" msgstr "" -#: src/Navigation.tsx:313 +#: src/Navigation.tsx:323 #: src/view/screens/CommunityGuidelines.tsx:34 msgid "Community Guidelines" msgstr "" @@ -1909,10 +1874,6 @@ msgstr "" msgid "Configured in <0>moderation settings." msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:203 -#: src/components/dialogs/ChangeEmailDialog.tsx:210 -#: src/components/dialogs/VerifyEmailDialog.tsx:316 -#: src/components/dialogs/VerifyEmailDialog.tsx:323 #: src/components/Prompt.tsx:186 #: src/components/Prompt.tsx:189 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:185 @@ -1936,10 +1897,7 @@ msgstr "" msgid "Confirm your birthdate" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:160 -#: src/components/dialogs/ChangeEmailDialog.tsx:164 #: src/components/dialogs/EmailDialog/components/TokenField.tsx:36 -#: src/components/dialogs/VerifyEmailDialog.tsx:252 #: src/screens/Login/LoginForm.tsx:274 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:144 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:150 @@ -1948,10 +1906,6 @@ msgstr "" msgid "Confirmation code" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:248 -msgid "Confirmation Code" -msgstr "" - #: src/screens/Login/LoginForm.tsx:337 msgid "Connecting..." msgstr "" @@ -1966,12 +1920,12 @@ msgid "Content & Media" msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:109 -#: src/screens/Settings/Settings.tsx:185 -#: src/screens/Settings/Settings.tsx:188 +#: src/screens/Settings/Settings.tsx:194 +#: src/screens/Settings/Settings.tsx:197 msgid "Content and media" msgstr "" -#: src/Navigation.tsx:387 +#: src/Navigation.tsx:474 msgid "Content and Media" msgstr "" @@ -2162,7 +2116,7 @@ msgstr "" msgid "Copy TXT record value" msgstr "" -#: src/Navigation.tsx:318 +#: src/Navigation.tsx:328 #: src/view/screens/CopyrightPolicy.tsx:31 msgid "Copyright Policy" msgstr "" @@ -2188,6 +2142,10 @@ msgstr "" msgid "Could not process your video" msgstr "" +#: src/state/queries/notifications/settings.ts:47 +msgid "Could not update notification settings" +msgstr "" + #: src/components/StarterPack/ProfileStarterPacks.tsx:300 msgid "Create" msgstr "" @@ -2198,7 +2156,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:178 #: src/components/StarterPack/ProfileStarterPacks.tsx:287 -#: src/Navigation.tsx:463 +#: src/Navigation.tsx:550 msgid "Create a starter pack" msgstr "" @@ -2309,7 +2267,7 @@ msgstr "" msgid "Deactivate account" msgstr "" -#: src/screens/Settings/Settings.tsx:397 +#: src/screens/Settings/Settings.tsx:406 msgid "Debug Moderation" msgstr "" @@ -2358,7 +2316,7 @@ msgstr "" msgid "Delete chat" msgstr "" -#: src/screens/Settings/Settings.tsx:404 +#: src/screens/Settings/Settings.tsx:413 msgid "Delete chat declaration record" msgstr "" @@ -2432,8 +2390,6 @@ msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:369 #: src/view/com/modals/CreateOrEditList.tsx:278 #: src/view/com/modals/CreateOrEditList.tsx:299 -#: src/view/com/modals/EditProfile.tsx:218 -#: src/view/com/modals/EditProfile.tsx:230 msgid "Description" msgstr "" @@ -2469,8 +2425,8 @@ msgctxt "toast" msgid "Developer mode enabled" msgstr "" -#: src/screens/Settings/Settings.tsx:252 -#: src/screens/Settings/Settings.tsx:255 +#: src/screens/Settings/Settings.tsx:261 +#: src/screens/Settings/Settings.tsx:264 msgid "Developer options" msgstr "" @@ -2577,14 +2533,9 @@ msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:320 #: src/screens/Profile/Header/EditProfileDialog.tsx:326 #: src/screens/Profile/Header/EditProfileDialog.tsx:376 -#: src/view/com/modals/EditProfile.tsx:194 msgid "Display name" msgstr "" -#: src/view/com/modals/EditProfile.tsx:182 -msgid "Display Name" -msgstr "" - #: src/screens/Profile/Header/EditProfileDialog.tsx:339 msgid "Display name is too long" msgstr "" @@ -2692,18 +2643,10 @@ msgstr "" msgid "e.g. Alice Lastname" msgstr "" -#: src/view/com/modals/EditProfile.tsx:187 -msgid "e.g. Alice Roberts" -msgstr "" - #: src/screens/Settings/components/ChangeHandleDialog.tsx:376 msgid "e.g. alice.com" msgstr "" -#: src/view/com/modals/EditProfile.tsx:223 -msgid "e.g. Artist, dog-lover, and avid reader." -msgstr "" - #: src/lib/moderation/useGlobalLabelStrings.ts:43 msgid "E.g. artistic nudes." msgstr "" @@ -2779,15 +2722,11 @@ msgstr "" msgid "Edit Moderation List" msgstr "" -#: src/Navigation.tsx:328 +#: src/Navigation.tsx:338 #: src/view/screens/Feeds.tsx:518 msgid "Edit My Feeds" msgstr "" -#: src/view/com/modals/EditProfile.tsx:154 -msgid "Edit my profile" -msgstr "" - #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:109 msgid "Edit People" msgstr "" @@ -2821,15 +2760,7 @@ msgstr "" msgid "Edit who can reply" msgstr "" -#: src/view/com/modals/EditProfile.tsx:195 -msgid "Edit your display name" -msgstr "" - -#: src/view/com/modals/EditProfile.tsx:231 -msgid "Edit your profile description" -msgstr "" - -#: src/Navigation.tsx:468 +#: src/Navigation.tsx:555 msgid "Edit your starter pack" msgstr "" @@ -2868,10 +2799,6 @@ msgstr "" msgid "Email sent!" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:63 -msgid "Email Updated!" -msgstr "" - #: src/components/dialogs/EmailDialog/screens/Verify.tsx:182 msgid "Email verification complete!" msgstr "" @@ -2926,9 +2853,9 @@ msgstr "" msgid "Enable media players for" msgstr "" -#: src/screens/Settings/NotificationSettings.tsx:74 -#: src/screens/Settings/NotificationSettings.tsx:77 -msgid "Enable priority notifications" +#: src/screens/Settings/NotificationSettings/index.tsx:102 +#: src/screens/Settings/NotificationSettings/index.tsx:106 +msgid "Enable push notifications" msgstr "" #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:387 @@ -2980,10 +2907,6 @@ msgstr "" msgid "Enter code" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:118 -msgid "Enter Code" -msgstr "" - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405 msgid "Enter fullscreen" msgstr "" @@ -3013,10 +2936,6 @@ msgstr "" msgid "Enter your email address" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:138 -msgid "Enter your new email address below." -msgstr "" - #: src/screens/Login/LoginForm.tsx:243 msgid "Enter your password" msgstr "" @@ -3072,9 +2991,16 @@ msgstr "" #: src/screens/Messages/Settings.tsx:83 #: src/screens/Messages/Settings.tsx:86 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:153 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:167 msgid "Everyone" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:236 +#: src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx:41 +msgid "Everything else" +msgstr "" + #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:73 #: src/lib/moderation/useReportOptions.ts:73 msgid "Excessive mentions or replies" @@ -3164,7 +3090,7 @@ msgstr "" msgid "Explicit sexual images." msgstr "" -#: src/Navigation.tsx:625 +#: src/Navigation.tsx:712 #: src/screens/Search/Shell.tsx:307 #: src/view/shell/desktop/LeftNav.tsx:635 #: src/view/shell/Drawer.tsx:403 @@ -3195,7 +3121,7 @@ msgstr "" msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." msgstr "" -#: src/Navigation.tsx:347 +#: src/Navigation.tsx:357 #: src/screens/Settings/ExternalMediaPreferences.tsx:31 msgid "External Media Preferences" msgstr "" @@ -3265,6 +3191,19 @@ msgstr "" msgid "Failed to load GIFs" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:115 +#: src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx:50 +#: src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx:52 +#: src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx:50 +#: src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx:53 +#: src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx:50 +#: src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx:50 +#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:52 +#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:50 +#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:53 +msgid "Failed to load notification settings." +msgstr "" + #: src/screens/Messages/components/MessageListError.tsx:23 msgid "Failed to load past messages" msgstr "" @@ -3302,15 +3241,11 @@ msgstr "" msgid "Failed to save image: {0}" msgstr "" -#: src/state/queries/notifications/settings.ts:39 -msgid "Failed to save notification preferences, please try again" -msgstr "" - #: src/screens/ModerationInteractionSettings/index.tsx:108 msgid "Failed to save settings. Please try again." msgstr "" -#: src/screens/Settings/SettingsInterests.tsx:133 +#: src/screens/Settings/InterestsSettings.tsx:136 msgctxt "toast" msgid "Failed to save your interests." msgstr "" @@ -3365,7 +3300,7 @@ msgstr "" msgid "Failed to verify handle. Please try again." msgstr "" -#: src/Navigation.tsx:263 +#: src/Navigation.tsx:273 msgid "Feed" msgstr "" @@ -3394,7 +3329,7 @@ msgctxt "toast" msgid "Feedback sent!" msgstr "" -#: src/Navigation.tsx:448 +#: src/Navigation.tsx:535 #: src/screens/Search/SearchResults.tsx:68 #: src/screens/StarterPack/StarterPackScreen.tsx:190 #: src/view/screens/Feeds.tsx:511 @@ -3555,7 +3490,7 @@ msgstr "" msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" msgstr "" -#: src/Navigation.tsx:217 +#: src/Navigation.tsx:227 msgid "Followers of @{0} that you know" msgstr "" @@ -3590,7 +3525,7 @@ msgstr "" msgid "Following feed preferences" msgstr "" -#: src/Navigation.tsx:334 +#: src/Navigation.tsx:344 #: src/screens/Settings/FollowingFeedPreferences.tsx:53 msgid "Following Feed Preferences" msgstr "" @@ -3675,10 +3610,40 @@ msgstr "" msgid "Get help" msgstr "" +#: src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx:43 +msgid "Get notifications when people follow you." +msgstr "" + +#: src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx:43 +msgid "Get notifications when people like posts that you've reposted." +msgstr "" + +#: src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx:43 +msgid "Get notifications when people like your posts." +msgstr "" + +#: src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx:43 +msgid "Get notifications when people mention you." +msgstr "" + +#: src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx:43 +msgid "Get notifications when people quote your posts." +msgstr "" + +#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:43 +msgid "Get notifications when people reply to your posts." +msgstr "" + +#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:43 +msgid "Get notifications when people repost posts that you've reposted." +msgstr "" + +#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:43 +msgid "Get notifications when people repost your posts." +msgstr "" + #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:76 #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:86 -#: src/components/dialogs/VerifyEmailDialog.tsx:263 -#: src/components/dialogs/VerifyEmailDialog.tsx:269 msgid "Get started" msgstr "" @@ -3817,7 +3782,7 @@ msgstr "" msgid "Harassment, trolling, or intolerance" msgstr "" -#: src/Navigation.tsx:418 +#: src/Navigation.tsx:505 msgid "Hashtag" msgstr "" @@ -3834,8 +3799,8 @@ msgstr "" msgid "Having trouble?" msgstr "" -#: src/screens/Settings/Settings.tsx:217 -#: src/screens/Settings/Settings.tsx:221 +#: src/screens/Settings/Settings.tsx:226 +#: src/screens/Settings/Settings.tsx:230 #: src/view/shell/desktop/RightNav.tsx:120 #: src/view/shell/desktop/RightNav.tsx:121 #: src/view/shell/Drawer.tsx:370 @@ -3974,8 +3939,8 @@ msgstr "" msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" -#: src/Navigation.tsx:620 -#: src/Navigation.tsx:640 +#: src/Navigation.tsx:707 +#: src/Navigation.tsx:727 #: src/view/shell/bottom-bar/BottomBar.tsx:178 #: src/view/shell/desktop/LeftNav.tsx:617 #: src/view/shell/Drawer.tsx:429 @@ -4007,10 +3972,6 @@ msgstr "" msgid "How should we open this link?" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:189 -#: src/components/dialogs/ChangeEmailDialog.tsx:196 -#: src/components/dialogs/VerifyEmailDialog.tsx:302 -#: src/components/dialogs/VerifyEmailDialog.tsx:309 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:133 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:136 msgid "I have a code" @@ -4102,6 +4063,34 @@ msgstr "" msgid "Impersonation, misinformation, or false claims" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:285 +msgid "In-app" +msgstr "" + +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:134 +msgid "In-app notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:268 +msgid "In-app, Everyone" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:276 +msgid "In-app, People you follow" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:283 +msgid "In-app, Push" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:266 +msgid "In-app, Push, Everyone" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:274 +msgid "In-app, Push, People you follow" +msgstr "" + #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:91 #: src/lib/moderation/useReportOptions.ts:91 msgid "Inappropriate messages or explicit links" @@ -4264,13 +4253,13 @@ msgstr "" msgid "Language selection" msgstr "" -#: src/Navigation.tsx:190 +#: src/Navigation.tsx:200 msgid "Language Settings" msgstr "" #: src/screens/Settings/LanguageSettings.tsx:78 -#: src/screens/Settings/Settings.tsx:209 -#: src/screens/Settings/Settings.tsx:212 +#: src/screens/Settings/Settings.tsx:218 +#: src/screens/Settings/Settings.tsx:221 msgid "Languages" msgstr "" @@ -4293,7 +4282,6 @@ msgstr "" #: src/screens/Moderation/VerificationSettings.tsx:47 #: src/screens/Profile/Header/EditProfileDialog.tsx:359 #: src/screens/Settings/components/ChangeHandleDialog.tsx:212 -#: src/view/com/modals/EditProfile.tsx:207 msgid "Learn more" msgstr "" @@ -4408,6 +4396,10 @@ msgstr "" msgid "Like 10 posts to train the Discover feed" msgstr "" +#: src/Navigation.tsx:426 +msgid "Like notifications" +msgstr "" + #: src/screens/Profile/components/ProfileFeedHeader.tsx:505 msgid "Like this feed" msgstr "" @@ -4416,8 +4408,8 @@ msgstr "" msgid "Like this labeler" msgstr "" -#: src/Navigation.tsx:268 -#: src/Navigation.tsx:273 +#: src/Navigation.tsx:278 +#: src/Navigation.tsx:283 msgid "Liked by" msgstr "" @@ -4439,10 +4431,21 @@ msgstr "" msgid "Liked by {likeCount, plural, one {# user} other {# users}}" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:159 +#: src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx:41 #: src/view/screens/Profile.tsx:229 msgid "Likes" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:208 +#: src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx:41 +msgid "Likes of your reposts" +msgstr "" + +#: src/Navigation.tsx:450 +msgid "Likes of your reposts notifications" +msgstr "" + #: src/screens/PostThread/components/ThreadItemAnchor.tsx:467 #: src/view/com/post-thread/PostThreadItem.tsx:243 msgid "Likes on this post" @@ -4455,7 +4458,7 @@ msgstr "" msgid "Linear" msgstr "" -#: src/Navigation.tsx:223 +#: src/Navigation.tsx:233 msgid "List" msgstr "" @@ -4513,7 +4516,7 @@ msgctxt "toast" msgid "List unmuted" msgstr "" -#: src/Navigation.tsx:144 +#: src/Navigation.tsx:154 #: src/view/screens/Lists.tsx:65 #: src/view/screens/Profile.tsx:224 #: src/view/screens/Profile.tsx:232 @@ -4566,7 +4569,7 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/Navigation.tsx:293 +#: src/Navigation.tsx:303 msgid "Log" msgstr "" @@ -4646,8 +4649,6 @@ msgstr "" #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:90 #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:97 -#: src/components/dialogs/VerifyEmailDialog.tsx:273 -#: src/components/dialogs/VerifyEmailDialog.tsx:281 msgid "Maybe later" msgstr "" @@ -4659,6 +4660,10 @@ msgstr "" msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" +#: src/Navigation.tsx:410 +msgid "Mention notifications" +msgstr "" + #: src/components/WhoCanReply.tsx:263 msgid "mentioned users" msgstr "" @@ -4667,6 +4672,8 @@ msgstr "" msgid "Mentioned users" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:137 +#: src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx:41 #: src/view/screens/Notifications.tsx:99 msgid "Mentions" msgstr "" @@ -4709,7 +4716,7 @@ msgstr "" msgid "Message options" msgstr "" -#: src/Navigation.tsx:635 +#: src/Navigation.tsx:722 msgid "Messages" msgstr "" @@ -4718,6 +4725,10 @@ msgctxt "Name of app icon variant" msgid "Midnight" msgstr "" +#: src/Navigation.tsx:466 +msgid "Miscellaneous notifications" +msgstr "" + #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:47 #: src/lib/moderation/useReportOptions.ts:47 msgid "Misleading Account" @@ -4728,10 +4739,10 @@ msgstr "" msgid "Misleading Post" msgstr "" -#: src/Navigation.tsx:149 +#: src/Navigation.tsx:159 #: src/screens/Moderation/index.tsx:93 -#: src/screens/Settings/Settings.tsx:177 -#: src/screens/Settings/Settings.tsx:180 +#: src/screens/Settings/Settings.tsx:178 +#: src/screens/Settings/Settings.tsx:181 msgid "Moderation" msgstr "" @@ -4767,7 +4778,7 @@ msgstr "" msgid "Moderation lists" msgstr "" -#: src/Navigation.tsx:154 +#: src/Navigation.tsx:164 #: src/view/screens/ModerationModlists.tsx:65 msgid "Moderation Lists" msgstr "" @@ -4776,7 +4787,7 @@ msgstr "" msgid "moderation settings" msgstr "" -#: src/Navigation.tsx:283 +#: src/Navigation.tsx:293 msgid "Moderation states" msgstr "" @@ -4899,7 +4910,7 @@ msgstr "" msgid "Muted accounts" msgstr "" -#: src/Navigation.tsx:159 +#: src/Navigation.tsx:169 #: src/view/screens/ModerationMutedAccounts.tsx:118 msgid "Muted Accounts" msgstr "" @@ -4972,10 +4983,6 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:234 -msgid "Need to change it?" -msgstr "" - #: src/components/moderation/ReportDialog/index.tsx:288 #: src/components/ReportDialog/SelectReportOptionView.tsx:128 msgid "Need to report a copyright violation?" @@ -5005,7 +5012,6 @@ msgstr "" msgid "New chat" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:142 #: src/components/dialogs/EmailDialog/screens/Update.tsx:222 msgid "New email address" msgstr "" @@ -5014,6 +5020,15 @@ msgstr "" msgid "New Feature" msgstr "" +#: src/Navigation.tsx:442 +msgid "New follower notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:181 +#: src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx:41 +msgid "New followers" +msgstr "" + #: src/screens/Settings/components/ChangeHandleDialog.tsx:221 #: src/screens/Settings/components/ChangeHandleDialog.tsx:229 #: src/screens/Settings/components/ChangeHandleDialog.tsx:375 @@ -5246,7 +5261,7 @@ msgstr "" msgid "Not followed by anyone you're following" msgstr "" -#: src/Navigation.tsx:139 +#: src/Navigation.tsx:149 #: src/view/screens/Profile.tsx:125 msgid "Not Found" msgstr "" @@ -5267,19 +5282,12 @@ msgstr "" msgid "Nothing here" msgstr "" -#: src/screens/Settings/NotificationSettings.tsx:64 -msgid "Notification filters" -msgstr "" - -#: src/Navigation.tsx:443 +#: src/Navigation.tsx:396 +#: src/Navigation.tsx:530 #: src/view/screens/Notifications.tsx:134 msgid "Notification settings" msgstr "" -#: src/screens/Settings/NotificationSettings.tsx:46 -msgid "Notification Settings" -msgstr "" - #: src/screens/Messages/Settings.tsx:123 msgid "Notification sounds" msgstr "" @@ -5288,7 +5296,19 @@ msgstr "" msgid "Notification Sounds" msgstr "" -#: src/Navigation.tsx:630 +#: src/Navigation.tsx:717 +#: src/screens/Settings/NotificationSettings/index.tsx:92 +#: src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30 +#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30 +#: src/screens/Settings/Settings.tsx:186 +#: src/screens/Settings/Settings.tsx:189 #: src/view/screens/Notifications.tsx:128 #: src/view/shell/bottom-bar/BottomBar.tsx:252 #: src/view/shell/desktop/LeftNav.tsx:654 @@ -5296,6 +5316,10 @@ msgstr "" msgid "Notifications" msgstr "" +#: src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx:43 +msgid "Notifications for everything else, such as when someone joins via one of your starter packs." +msgstr "" + #: src/lib/hooks/useTimeAgo.ts:135 msgid "now" msgstr "" @@ -5315,6 +5339,7 @@ msgid "Nudity or adult content not labeled as such" msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:11 +#: src/screens/Settings/NotificationSettings/index.tsx:292 msgid "Off" msgstr "" @@ -5356,7 +5381,7 @@ msgstr "" msgid "on<0><1/><2><3/>" msgstr "" -#: src/screens/Settings/Settings.tsx:358 +#: src/screens/Settings/Settings.tsx:367 msgid "Onboarding reset" msgstr "" @@ -5401,7 +5426,6 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:341 #: src/screens/Settings/AppPasswords.tsx:59 #: src/screens/Settings/components/ChangeHandleDialog.tsx:106 -#: src/screens/Settings/NotificationSettings.tsx:54 #: src/view/screens/Profile.tsx:125 msgid "Oops!" msgstr "" @@ -5449,7 +5473,7 @@ msgstr "" msgid "Open message options" msgstr "" -#: src/screens/Settings/Settings.tsx:395 +#: src/screens/Settings/Settings.tsx:404 msgid "Open moderation debug page" msgstr "" @@ -5478,12 +5502,12 @@ msgstr "" msgid "Open starter pack menu" msgstr "" -#: src/screens/Settings/Settings.tsx:388 -#: src/screens/Settings/Settings.tsx:402 +#: src/screens/Settings/Settings.tsx:397 +#: src/screens/Settings/Settings.tsx:411 msgid "Open storybook page" msgstr "" -#: src/screens/Settings/Settings.tsx:381 +#: src/screens/Settings/Settings.tsx:390 msgid "Open system log" msgstr "" @@ -5545,7 +5569,7 @@ msgstr "" msgid "Opens GIF select dialog" msgstr "" -#: src/screens/Settings/Settings.tsx:218 +#: src/screens/Settings/Settings.tsx:227 msgid "Opens helpdesk in browser" msgstr "" @@ -5683,14 +5707,19 @@ msgstr "" msgid "People" msgstr "" -#: src/Navigation.tsx:210 +#: src/Navigation.tsx:220 msgid "People followed by @{0}" msgstr "" -#: src/Navigation.tsx:203 +#: src/Navigation.tsx:213 msgid "People following @{0}" msgstr "" +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:171 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:185 +msgid "People I follow" +msgstr "" + #: src/lib/media/save-image.ts:51 msgid "Permission to access your photo library was denied. Please enable it in your system settings." msgstr "" @@ -5895,10 +5924,6 @@ msgstr "" msgid "Please verify your email" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:108 -msgid "Please Verify Your Email" -msgstr "" - #: src/screens/Onboarding/index.tsx:34 #: src/screens/Onboarding/state.ts:111 #: src/screens/Search/modules/ExploreTrendingTopics.tsx:235 @@ -5933,10 +5958,10 @@ msgstr "" msgid "Post by {0}" msgstr "" -#: src/Navigation.tsx:236 -#: src/Navigation.tsx:243 -#: src/Navigation.tsx:250 -#: src/Navigation.tsx:257 +#: src/Navigation.tsx:246 +#: src/Navigation.tsx:253 +#: src/Navigation.tsx:260 +#: src/Navigation.tsx:267 msgid "Post by @{0}" msgstr "" @@ -5974,7 +5999,7 @@ msgstr "" msgid "Post interaction settings" msgstr "" -#: src/Navigation.tsx:170 +#: src/Navigation.tsx:180 #: src/screens/ModerationInteractionSettings/index.tsx:34 msgid "Post Interaction Settings" msgstr "" @@ -6022,11 +6047,6 @@ msgstr "" msgid "Potentially Misleading Link" msgstr "" -#: src/state/queries/notifications/settings.ts:44 -msgctxt "toast" -msgid "Preference saved" -msgstr "" - #: src/screens/Messages/components/MessageListError.tsx:19 msgid "Press to attempt reconnection" msgstr "" @@ -6057,26 +6077,22 @@ msgstr "" msgid "Prioritize your Follows" msgstr "" -#: src/screens/Settings/NotificationSettings.tsx:67 -msgid "Priority notifications" -msgstr "" - #: src/view/shell/desktop/RightNav.tsx:110 #: src/view/shell/desktop/RightNav.tsx:111 msgid "Privacy" msgstr "" -#: src/screens/Settings/Settings.tsx:171 -#: src/screens/Settings/Settings.tsx:174 +#: src/screens/Settings/Settings.tsx:172 +#: src/screens/Settings/Settings.tsx:175 msgid "Privacy and security" msgstr "" -#: src/Navigation.tsx:379 +#: src/Navigation.tsx:389 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:36 msgid "Privacy and Security" msgstr "" -#: src/Navigation.tsx:303 +#: src/Navigation.tsx:313 #: src/screens/Settings/AboutSettings.tsx:92 #: src/screens/Settings/AboutSettings.tsx:95 #: src/view/screens/PrivacyPolicy.tsx:31 @@ -6107,7 +6123,6 @@ msgid "Profile" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:196 -#: src/view/com/modals/EditProfile.tsx:128 msgctxt "toast" msgid "Profile updated" msgstr "" @@ -6144,6 +6159,22 @@ msgstr "" msgid "Publish reply" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:287 +msgid "Push" +msgstr "" + +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:117 +msgid "Push notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:270 +msgid "Push, Everyone" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:278 +msgid "Push, People you follow" +msgstr "" + #: src/components/StarterPack/QrCodeDialog.tsx:134 msgid "QR code copied to your clipboard!" msgstr "" @@ -6156,6 +6187,10 @@ msgstr "" msgid "QR code saved to your camera roll!" msgstr "" +#: src/Navigation.tsx:418 +msgid "Quote notifications" +msgstr "" + #: src/components/PostControls/RepostButton.tsx:174 #: src/components/PostControls/RepostButton.tsx:197 #: src/components/PostControls/RepostButton.web.tsx:78 @@ -6183,6 +6218,8 @@ msgid "Quote settings" msgstr "" #: src/screens/Post/PostQuotes.tsx:38 +#: src/screens/Settings/NotificationSettings/index.tsx:148 +#: src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx:41 msgid "Quotes" msgstr "" @@ -6259,6 +6296,14 @@ msgstr "" msgid "Reason:" msgstr "" +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:123 +msgid "Receive in-app notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:106 +msgid "Receive push notifications" +msgstr "" + #: src/screens/Search/components/SearchHistory.tsx:49 msgid "Recent Searches" msgstr "" @@ -6290,7 +6335,7 @@ msgstr "" #: src/components/FeedCard.tsx:343 #: src/components/StarterPack/Wizard/WizardListCard.tsx:102 #: src/components/StarterPack/Wizard/WizardListCard.tsx:109 -#: src/screens/Settings/Settings.tsx:543 +#: src/screens/Settings/Settings.tsx:552 #: src/view/com/feeds/FeedSourceCard.tsx:322 #: src/view/com/modals/UserAddRemoveLists.tsx:235 #: src/view/com/posts/PostFeedErrorMessage.tsx:213 @@ -6305,8 +6350,8 @@ msgstr "" msgid "Remove {historyItem}" msgstr "" -#: src/screens/Settings/Settings.tsx:522 -#: src/screens/Settings/Settings.tsx:525 +#: src/screens/Settings/Settings.tsx:531 +#: src/screens/Settings/Settings.tsx:534 msgid "Remove account" msgstr "" @@ -6347,7 +6392,7 @@ msgstr "" msgid "Remove from my feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:535 +#: src/screens/Settings/Settings.tsx:544 msgid "Remove from quick access?" msgstr "" @@ -6442,6 +6487,8 @@ msgstr "" msgid "Replace with Discover" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:126 +#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:41 #: src/view/screens/Profile.tsx:226 msgid "Replies" msgstr "" @@ -6474,6 +6521,10 @@ msgstr "" msgid "Reply Hidden by You" msgstr "" +#: src/Navigation.tsx:402 +msgid "Reply notifications" +msgstr "" + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:385 msgid "Reply settings" msgstr "" @@ -6622,6 +6673,10 @@ msgstr "" msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgstr "" +#: src/Navigation.tsx:434 +msgid "Repost notifications" +msgstr "" + #: src/components/PostControls/RepostButton.tsx:144 #: src/components/PostControls/RepostButton.web.tsx:43 #: src/components/PostControls/RepostButton.web.tsx:97 @@ -6646,14 +6701,23 @@ msgstr "" msgid "Reposted by you" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:170 +#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:41 +msgid "Reposts" +msgstr "" + #: src/screens/PostThread/components/ThreadItemAnchor.tsx:433 #: src/view/com/post-thread/PostThreadItem.tsx:248 msgid "Reposts of this post" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:175 -#: src/components/dialogs/ChangeEmailDialog.tsx:182 -msgid "Request change" +#: src/screens/Settings/NotificationSettings/index.tsx:223 +#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:41 +msgid "Reposts of your reposts" +msgstr "" + +#: src/Navigation.tsx:458 +msgid "Reposts of your reposts notifications" msgstr "" #: src/view/com/modals/ChangePassword.tsx:253 @@ -6682,10 +6746,6 @@ msgstr "" msgid "Resend" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:217 -#: src/components/dialogs/ChangeEmailDialog.tsx:227 -#: src/components/dialogs/VerifyEmailDialog.tsx:330 -#: src/components/dialogs/VerifyEmailDialog.tsx:340 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:173 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:176 msgid "Resend email" @@ -6708,8 +6768,8 @@ msgstr "" msgid "Reset Code" msgstr "" -#: src/screens/Settings/Settings.tsx:409 -#: src/screens/Settings/Settings.tsx:411 +#: src/screens/Settings/Settings.tsx:418 +#: src/screens/Settings/Settings.tsx:420 msgid "Reset onboarding state" msgstr "" @@ -6790,7 +6850,6 @@ msgstr "" #: src/view/com/composer/photos/ImageAltTextDialog.tsx:153 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:163 #: src/view/com/modals/CreateOrEditList.tsx:315 -#: src/view/com/modals/EditProfile.tsx:244 #: src/view/screens/SavedFeeds.tsx:109 msgid "Save" msgstr "" @@ -6810,10 +6869,6 @@ msgstr "" msgid "Save changes" msgstr "" -#: src/view/com/modals/EditProfile.tsx:252 -msgid "Save Changes" -msgstr "" - #: src/components/StarterPack/ShareDialog.tsx:131 #: src/components/StarterPack/ShareDialog.tsx:138 msgid "Save image" @@ -6845,10 +6900,6 @@ msgstr "" msgid "Saved to your feeds" msgstr "" -#: src/view/com/modals/EditProfile.tsx:245 -msgid "Saves any changes to your profile" -msgstr "" - #: src/view/com/modals/CropImage.web.tsx:105 msgid "Saves image crop settings" msgstr "" @@ -6878,7 +6929,7 @@ msgstr "" msgid "Search" msgstr "" -#: src/Navigation.tsx:229 +#: src/Navigation.tsx:239 #: src/screens/Profile/ProfileSearch.tsx:37 msgid "Search @{0}'s posts" msgstr "" @@ -6955,10 +7006,6 @@ msgstr "" msgid "Security step required" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:57 -msgid "Security Step Required" -msgstr "" - #: src/components/RichTextTag.tsx:111 msgid "See {tag} posts" msgstr "" @@ -7093,7 +7140,7 @@ msgid "Select your date of birth" msgstr "" #: src/screens/Onboarding/StepInterests/index.tsx:192 -#: src/screens/Settings/SettingsInterests.tsx:162 +#: src/screens/Settings/InterestsSettings.tsx:165 msgid "Select your interests from the options below" msgstr "" @@ -7109,14 +7156,6 @@ msgstr "" msgid "Send a neat website!" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:295 -msgid "Send confirmation" -msgstr "" - -#: src/components/dialogs/VerifyEmailDialog.tsx:288 -msgid "Send confirmation email" -msgstr "" - #: src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx:174 #: src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx:181 #: src/components/dialogs/EmailDialog/screens/Verify.tsx:300 @@ -7202,13 +7241,49 @@ msgstr "" msgid "Sets email for password reset" msgstr "" -#: src/Navigation.tsx:185 -#: src/screens/Settings/Settings.tsx:90 +#: src/Navigation.tsx:195 +#: src/screens/Settings/Settings.tsx:91 #: src/view/shell/desktop/LeftNav.tsx:727 #: src/view/shell/Drawer.tsx:572 msgid "Settings" msgstr "" +#: src/screens/Settings/NotificationSettings/index.tsx:154 +msgid "Settings for like notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:132 +msgid "Settings for mention notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:176 +msgid "Settings for new follower notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:231 +msgid "Settings for notifications for everything else" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:202 +msgid "Settings for notifications for likes of your reposts" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:217 +msgid "Settings for notifications for reposts of your reposts" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:143 +msgid "Settings for quote notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:121 +msgid "Settings for reply notifications" +msgstr "" + +#: src/screens/Settings/NotificationSettings/index.tsx:165 +msgid "Settings for repost notifications" +msgstr "" + #: src/screens/ModerationInteractionSettings/index.tsx:102 msgctxt "toast" msgid "Settings saved" @@ -7301,7 +7376,7 @@ msgstr "" msgid "Share your favorite feed!" msgstr "" -#: src/Navigation.tsx:288 +#: src/Navigation.tsx:298 msgid "Shared Preferences Tester" msgstr "" @@ -7426,7 +7501,7 @@ msgstr "" msgid "Shows information about when this post was created" msgstr "" -#: src/screens/Settings/Settings.tsx:114 +#: src/screens/Settings/Settings.tsx:115 msgid "Shows other accounts you can switch to" msgstr "" @@ -7483,9 +7558,9 @@ msgstr "" msgid "Sign in to view post" msgstr "" -#: src/screens/Settings/Settings.tsx:235 -#: src/screens/Settings/Settings.tsx:237 -#: src/screens/Settings/Settings.tsx:269 +#: src/screens/Settings/Settings.tsx:244 +#: src/screens/Settings/Settings.tsx:246 +#: src/screens/Settings/Settings.tsx:278 #: src/screens/SignupQueued.tsx:93 #: src/screens/SignupQueued.tsx:96 #: src/screens/Takendown.tsx:85 @@ -7499,7 +7574,7 @@ msgstr "" msgid "Sign Out" msgstr "" -#: src/screens/Settings/Settings.tsx:266 +#: src/screens/Settings/Settings.tsx:275 #: src/view/shell/desktop/LeftNav.tsx:209 msgid "Sign out?" msgstr "" @@ -7532,7 +7607,6 @@ msgid "Smaller" msgstr "" #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:91 -#: src/components/dialogs/VerifyEmailDialog.tsx:274 msgid "Snoozes the reminder" msgstr "" @@ -7582,7 +7656,6 @@ msgid "Something went wrong, please try again." msgstr "" #: src/components/Lists.tsx:174 -#: src/screens/Settings/NotificationSettings.tsx:55 msgid "Something went wrong!" msgstr "" @@ -7663,8 +7736,8 @@ msgstr "" msgid "Start chat with {displayName}" msgstr "" -#: src/Navigation.tsx:453 -#: src/Navigation.tsx:458 +#: src/Navigation.tsx:540 +#: src/Navigation.tsx:545 #: src/screens/StarterPack/Wizard/index.tsx:186 msgid "Starter Pack" msgstr "" @@ -7704,12 +7777,12 @@ msgstr "" msgid "Step {0} of {1}" msgstr "" -#: src/screens/Settings/Settings.tsx:363 +#: src/screens/Settings/Settings.tsx:372 msgid "Storage cleared, you need to restart the app now." msgstr "" -#: src/Navigation.tsx:278 -#: src/screens/Settings/Settings.tsx:390 +#: src/Navigation.tsx:288 +#: src/screens/Settings/Settings.tsx:399 msgid "Storybook" msgstr "" @@ -7755,7 +7828,6 @@ msgid "Subscribe to this list" msgstr "" #: src/components/dialogs/EmailDialog/screens/Update.tsx:286 -#: src/components/dialogs/VerifyEmailDialog.tsx:124 msgid "Success!" msgstr "" @@ -7786,15 +7858,15 @@ msgctxt "Name of app icon variant" msgid "Sunset" msgstr "" -#: src/Navigation.tsx:298 +#: src/Navigation.tsx:308 #: src/view/screens/Support.tsx:31 #: src/view/screens/Support.tsx:34 msgid "Support" msgstr "" -#: src/screens/Settings/Settings.tsx:112 -#: src/screens/Settings/Settings.tsx:126 -#: src/screens/Settings/Settings.tsx:485 +#: src/screens/Settings/Settings.tsx:113 +#: src/screens/Settings/Settings.tsx:127 +#: src/screens/Settings/Settings.tsx:494 #: src/view/shell/desktop/LeftNav.tsx:246 msgid "Switch account" msgstr "" @@ -7821,7 +7893,7 @@ msgstr "" #: src/screens/Settings/AboutSettings.tsx:107 #: src/screens/Settings/AboutSettings.tsx:110 -#: src/screens/Settings/Settings.tsx:383 +#: src/screens/Settings/Settings.tsx:392 msgid "System log" msgstr "" @@ -7873,7 +7945,7 @@ msgstr "" msgid "Terms" msgstr "" -#: src/Navigation.tsx:308 +#: src/Navigation.tsx:318 #: src/screens/Settings/AboutSettings.tsx:84 #: src/screens/Settings/AboutSettings.tsx:87 #: src/view/screens/TermsOfService.tsx:31 @@ -7910,10 +7982,6 @@ msgstr "" msgid "Thank you for your feedback! It has been sent to the feed operator." msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:125 -msgid "Thank you! Your email has been successfully verified." -msgstr "" - #: src/components/ReportDialog/SubmitView.tsx:83 msgid "Thank you. Your report has been sent." msgstr "" @@ -7982,10 +8050,6 @@ msgstr "" msgid "The Discover feed now knows what you like" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:74 -msgid "The email address you entered is the same as your current email address." -msgstr "" - #: src/screens/StarterPack/StarterPackLandingScreen.tsx:324 msgid "The experience is better in the app. Download Bluesky now and we'll pick back up where you left off." msgstr "" @@ -8377,7 +8441,7 @@ msgstr "" msgid "This will delete \"{0}\" from your muted words. You can always add it back later." msgstr "" -#: src/screens/Settings/Settings.tsx:537 +#: src/screens/Settings/Settings.tsx:546 msgid "This will remove @{0} from the quick access list." msgstr "" @@ -8413,7 +8477,7 @@ msgstr "" msgid "Threaded mode" msgstr "" -#: src/Navigation.tsx:341 +#: src/Navigation.tsx:351 msgid "Threads Preferences" msgstr "" @@ -8467,7 +8531,7 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:423 +#: src/Navigation.tsx:510 msgid "Topic" msgstr "" @@ -8694,8 +8758,8 @@ msgstr "" msgid "Unpinned from your feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:416 -#: src/screens/Settings/Settings.tsx:418 +#: src/screens/Settings/Settings.tsx:425 +#: src/screens/Settings/Settings.tsx:427 msgid "Unsnooze email reminder" msgstr "" @@ -8930,7 +8994,7 @@ msgstr "" msgid "Verification settings" msgstr "" -#: src/Navigation.tsx:178 +#: src/Navigation.tsx:188 #: src/screens/Moderation/VerificationSettings.tsx:32 msgid "Verification Settings" msgstr "" @@ -8960,17 +9024,10 @@ msgstr "" msgid "Verify DNS Record" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:234 -#: src/components/dialogs/ChangeEmailDialog.tsx:240 -msgid "Verify email" -msgstr "" - #: src/components/dialogs/EmailDialog/screens/Verify.tsx:214 msgid "Verify email code" msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:122 -#: src/components/dialogs/VerifyEmailDialog.tsx:163 #: src/components/intents/VerifyEmailIntentDialog.tsx:67 msgid "Verify email dialog" msgstr "" @@ -8990,10 +9047,6 @@ msgstr "" msgid "Verify your email" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:114 -msgid "Verify Your Email" -msgstr "" - #: src/screens/Settings/AboutSettings.tsx:126 #: src/screens/Settings/AboutSettings.tsx:155 msgid "Version {appVersion}" @@ -9008,7 +9061,7 @@ msgstr "" msgid "Video failed to process" msgstr "" -#: src/Navigation.tsx:474 +#: src/Navigation.tsx:561 msgid "Video Feed" msgstr "" @@ -9228,7 +9281,7 @@ msgstr "" msgid "We ran out of posts from your follows. Here's the latest from <0/>." msgstr "" -#: src/screens/Settings/SettingsInterests.tsx:155 +#: src/screens/Settings/InterestsSettings.tsx:158 msgid "We recommend selecting at least two interests." msgstr "" @@ -9500,7 +9553,6 @@ msgid "You are verified" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:355 -#: src/view/com/modals/EditProfile.tsx:203 msgid "You are verified. You will lose your verification status if you change your display name. <0>Learn more." msgstr "" @@ -9701,7 +9753,7 @@ msgstr "" msgid "You previously deactivated @{0}." msgstr "" -#: src/screens/Settings/Settings.tsx:374 +#: src/screens/Settings/Settings.tsx:383 msgid "You probably want to restart the app now." msgstr "" @@ -9713,7 +9765,7 @@ msgstr "" msgid "You reacted {0} to {1}" msgstr "" -#: src/screens/Settings/Settings.tsx:267 +#: src/screens/Settings/Settings.tsx:276 #: src/view/shell/desktop/LeftNav.tsx:210 msgid "You will be signed out of all your accounts." msgstr "" @@ -9758,10 +9810,6 @@ msgstr "" msgid "You'll follow these people right away" msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:216 -msgid "You'll receive an email at <0>{0} to verify it's you." -msgstr "" - #: src/screens/StarterPack/StarterPackLandingScreen.tsx:274 msgid "You'll stay updated with these feeds" msgstr "" @@ -9856,10 +9904,6 @@ msgstr "" msgid "Your current handle <0>{0} will automatically remain reserved for you. You can switch back to it at any time from this account." msgstr "" -#: src/components/dialogs/ChangeEmailDialog.tsx:65 -msgid "Your email address has been updated but it is not yet verified. As a next step, please verify your new email." -msgstr "" - #: src/screens/Login/ForgotPasswordForm.tsx:51 #: src/screens/Signup/state.ts:270 #: src/screens/Signup/StepInfo/index.tsx:98 @@ -9871,10 +9915,6 @@ msgstr "" msgid "Your email has not yet been verified. Please verify your email in order to enjoy all the features of Bluesky." msgstr "" -#: src/components/dialogs/VerifyEmailDialog.tsx:110 -msgid "Your email has not yet been verified. This is an important security step which we recommend." -msgstr "" - #: src/state/shell/progress-guide.tsx:213 msgid "Your first like!" msgstr "" @@ -9895,15 +9935,15 @@ msgstr "" msgid "Your full username will be <0>@{0}" msgstr "" -#: src/Navigation.tsx:395 +#: src/Navigation.tsx:482 #: src/screens/Search/modules/ExploreInterestsCard.tsx:67 #: src/screens/Settings/ContentAndMediaSettings.tsx:92 #: src/screens/Settings/ContentAndMediaSettings.tsx:95 -#: src/screens/Settings/SettingsInterests.tsx:39 +#: src/screens/Settings/InterestsSettings.tsx:42 msgid "Your interests" msgstr "" -#: src/screens/Settings/SettingsInterests.tsx:124 +#: src/screens/Settings/InterestsSettings.tsx:127 msgctxt "toast" msgid "Your interests have been updated!" msgstr "" @@ -9952,7 +9992,7 @@ msgstr "" msgid "Your report will be sent to the Bluesky Moderation Service" msgstr "" -#: src/screens/Settings/SettingsInterests.tsx:53 +#: src/screens/Settings/InterestsSettings.tsx:56 msgid "Your selected interests help us serve you content you care about." msgstr "" From e672f43ec868f5d9d7aa4dabdfbcbc4f6bce69a2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 18 Jun 2025 14:08:44 +0300 Subject: [PATCH 34/49] fix other case of promise.all misuse (#8505) --- src/screens/Settings/InterestsSettings.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/screens/Settings/InterestsSettings.tsx b/src/screens/Settings/InterestsSettings.tsx index 746315f7b8..e3b5fcb084 100644 --- a/src/screens/Settings/InterestsSettings.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -113,13 +113,9 @@ function Inner({ }, ) await Promise.all([ - await qc.resetQueries({ - queryKey: createSuggestedStarterPacksQueryKey(), - }), - await qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}), - await qc.resetQueries({ - queryKey: createGetSuggestedUsersQueryKey({}), - }), + qc.resetQueries({queryKey: createSuggestedStarterPacksQueryKey()}), + qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}), + qc.resetQueries({queryKey: createGetSuggestedUsersQueryKey({})}), ]) Toast.show( From 13d21692bf28bbda502eb3694f94937dab851c41 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Wed, 18 Jun 2025 19:57:49 +0800 Subject: [PATCH 35/49] Mark translatable text in `PreferenceControls.tsx` (#8516) * Update PreferenceControls.tsx * Update PreferenceControls.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update PreferenceControls.tsx Co-authored-by: Minseo Lee * Update PreferenceControls.tsx --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Minseo Lee --- .../components/PreferenceControls.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx index 336e086950..3177bcadeb 100644 --- a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -98,7 +98,7 @@ export function Inner({ @@ -141,10 +141,12 @@ export function Inner({ {'filter' in preference && ( <> - From + + From + From dd86402763518ae94ced8274dda886f92ec7b51e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 18 Jun 2025 15:19:42 +0300 Subject: [PATCH 36/49] rearrange settings (#8519) --- .../Settings/NotificationSettings/index.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx index 7593635760..ec396d29a1 100644 --- a/src/screens/Settings/NotificationSettings/index.tsx +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -117,6 +117,28 @@ export function NotificationSettingsScreen({}: Props) { )} + + + Likes} + subtitleText={} + showSkeleton={!settings} + /> + + + + New followers} + subtitleText={} + showSkeleton={!settings} + /> + - - - Likes} - subtitleText={} - showSkeleton={!settings} - /> - - - - New followers} - subtitleText={} - showSkeleton={!settings} - /> - {/* Date: Wed, 18 Jun 2025 16:17:54 +0300 Subject: [PATCH 37/49] Modernise link warning dialog (#8243) * add link warning dialog * add copy for if sharing * delete old modal * get web working --- src/components/Link.tsx | 28 ++-- src/components/dialogs/Context.tsx | 12 ++ src/components/dialogs/LinkWarning.tsx | 161 ++++++++++++++++++++++ src/state/modals/index.tsx | 10 -- src/view/com/modals/LinkWarning.tsx | 180 ------------------------- src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/util/Link.tsx | 11 +- src/view/shell/index.tsx | 2 + src/view/shell/index.web.tsx | 2 + 10 files changed, 200 insertions(+), 213 deletions(-) create mode 100644 src/components/dialogs/LinkWarning.tsx delete mode 100644 src/view/com/modals/LinkWarning.tsx diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 49c9c52358..d0f8678ff1 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -24,6 +24,7 @@ import {Button, type ButtonProps} from '#/components/Button' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Text, type TextProps} from '#/components/Typography' import {router} from '#/routes' +import {useGlobalDialogsControlContext} from './dialogs/Context' /** * Only available within a `Link`, since that inherits from `Button`. @@ -111,7 +112,8 @@ export function useLink({ } const isExternal = isExternalUrl(href) - const {openModal, closeModal} = useModalControls() + const {closeModal} = useModalControls() + const {linkWarningDialogControl} = useGlobalDialogsControlContext() const openLink = useOpenLink() const onPress = React.useCallback( @@ -132,10 +134,9 @@ export function useLink({ } if (requiresWarning) { - openModal({ - name: 'link-warning', - text: displayText, - href: href, + linkWarningDialogControl.open({ + displayText, + href, }) } else { if (isExternal) { @@ -176,13 +177,13 @@ export function useLink({ displayText, isExternal, href, - openModal, openLink, closeModal, action, navigation, overridePresentation, shouldProxy, + linkWarningDialogControl, ], ) @@ -195,16 +196,21 @@ export function useLink({ ) if (requiresWarning) { - openModal({ - name: 'link-warning', - text: displayText, - href: href, + linkWarningDialogControl.open({ + displayText, + href, share: true, }) } else { shareUrl(href) } - }, [disableMismatchWarning, displayText, href, isExternal, openModal]) + }, [ + disableMismatchWarning, + displayText, + href, + isExternal, + linkWarningDialogControl, + ]) const onLongPress = React.useCallback( (e: GestureResponderEvent) => { diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx index 728044325b..1ee4d27398 100644 --- a/src/components/dialogs/Context.tsx +++ b/src/components/dialogs/Context.tsx @@ -17,6 +17,11 @@ type ControlsContext = { signinDialogControl: Control inAppBrowserConsentControl: StatefulControl emailDialogControl: StatefulControl + linkWarningDialogControl: StatefulControl<{ + href: string + displayText: string + share?: boolean + }> } const ControlsContext = createContext(null) @@ -36,6 +41,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const signinDialogControl = Dialog.useDialogControl() const inAppBrowserConsentControl = useStatefulDialogControl() const emailDialogControl = useStatefulDialogControl() + const linkWarningDialogControl = useStatefulDialogControl<{ + href: string + displayText: string + share?: boolean + }>() const ctx = useMemo( () => ({ @@ -43,12 +53,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { signinDialogControl, inAppBrowserConsentControl, emailDialogControl, + linkWarningDialogControl, }), [ mutedWordsDialogControl, signinDialogControl, inAppBrowserConsentControl, emailDialogControl, + linkWarningDialogControl, ], ) diff --git a/src/components/dialogs/LinkWarning.tsx b/src/components/dialogs/LinkWarning.tsx new file mode 100644 index 0000000000..9ae8718127 --- /dev/null +++ b/src/components/dialogs/LinkWarning.tsx @@ -0,0 +1,161 @@ +import {useCallback, useMemo} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useOpenLink} from '#/lib/hooks/useOpenLink' +import {shareUrl} from '#/lib/sharing' +import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {useGlobalDialogsControlContext} from './Context' + +export function LinkWarningDialog() { + const {linkWarningDialogControl} = useGlobalDialogsControlContext() + + return ( + + + + + ) +} + +function InAppBrowserConsentInner({ + link, +}: { + link?: {href: string; displayText: string; share?: boolean} +}) { + const control = Dialog.useDialogContext() + const {_} = useLingui() + const t = useTheme() + const openLink = useOpenLink() + const {gtMobile} = useBreakpoints() + + const potentiallyMisleading = useMemo( + () => link && isPossiblyAUrl(link.displayText), + [link], + ) + + const onPressVisit = useCallback(() => { + control.close(() => { + if (!link) return + if (link.share) { + shareUrl(link.href) + } else { + openLink(link.href, undefined, true) + } + }) + }, [control, link, openLink]) + + const onCancel = useCallback(() => { + control.close() + }, [control]) + + return ( + + + + + {potentiallyMisleading ? ( + Potentially misleading link + ) : ( + Leaving Bluesky + )} + + + This link is taking you to the following website: + + {link && } + {potentiallyMisleading && ( + + Make sure this is where you intend to go! + + )} + + + + + + + + + ) +} + +function LinkBox({href}: {href: string}) { + const t = useTheme() + const [scheme, hostname, rest] = useMemo(() => { + try { + const urlp = new URL(href) + const [subdomain, apexdomain] = splitApexDomain(urlp.hostname) + return [ + urlp.protocol + '//' + subdomain, + apexdomain, + urlp.pathname.replace(/\/$/, '') + urlp.search + urlp.hash, + ] + } catch { + return ['', href, ''] + } + }, [href]) + return ( + + + {scheme} + + {hostname} + + {rest} + + + ) +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 7ebcec4c79..a2cc637450 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -43,13 +43,6 @@ export interface ChangePasswordModal { name: 'change-password' } -export interface LinkWarningModal { - name: 'link-warning' - text: string - href: string - share?: boolean -} - export type Modal = // Account | DeleteAccountModal @@ -67,9 +60,6 @@ export type Modal = | WaitlistModal | InviteCodesModal - // Generic - | LinkWarningModal - const ModalContext = React.createContext<{ isModalActive: boolean activeModals: Modal[] diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx deleted file mode 100644 index b0bf76ede1..0000000000 --- a/src/view/com/modals/LinkWarning.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react' -import {SafeAreaView, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useOpenLink} from '#/lib/hooks/useOpenLink' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {shareUrl} from '#/lib/sharing' -import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers' -import {colors, s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from './util' - -export const snapPoints = ['50%'] - -export function Component({ - text, - href, - share, -}: { - text: string - href: string - share?: boolean -}) { - const pal = usePalette('default') - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - const {_} = useLingui() - const potentiallyMisleading = isPossiblyAUrl(text) - const openLink = useOpenLink() - - const onPressVisit = () => { - closeModal() - if (share) { - shareUrl(href) - } else { - openLink(href, false, true) - } - } - - return ( - - - - {potentiallyMisleading ? ( - <> - - - Potentially Misleading Link - - - ) : ( - - Leaving Bluesky - - )} - - - - - This link is taking you to the following website: - - - - - {potentiallyMisleading && ( - - Make sure this is where you intend to go! - - )} - - - -