Merge remote-tracking branch 'origin/main' into next/embeds

* origin/main: (241 commits)
  Nightly source-language update
  Update react-navigation (#5967)
  Update SetNewPasswordForm.tsx (#8349)
  Nightly source-language update
  bskyweb: bump indigo (several codegen updates) (#8442)
  Replace "Note about sharing" prompt with an inline hint (#8452)
  Bump version to v1.104 (#8447)
  show misclass button by the same logic as feedContext (#8445)
  change fontSize: 15 to a.text_sm (#7896)
  Nightly source-language update
  use method that's actually available on android (#8448)
  Release 1.103.0 prep (#8434)
  Add language: European Portuguese (`pt-PT`) (#8375)
  Small test fix
  tweak ordering of explore components (#8432)
  add metrics (#8426)
  invert flag for sending to statsig (#8431)
  Fix link to like-via-repost  (#8428)
  Nightly source-language update
  Make via-repost notifs groupable (#8429)
  ...
This commit is contained in:
Eric Bailey
2025-06-10 10:27:43 -05:00
556 changed files with 149398 additions and 82913 deletions
+10 -11
View File
@@ -5,7 +5,7 @@ import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
AppBskyFeedDefs,
AppBskyFeedThreadgate,
type AppBskyFeedThreadgate,
moderatePost,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
@@ -14,6 +14,7 @@ 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'
@@ -25,19 +26,18 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
fillThreadModerationCache,
sortThread,
ThreadBlocked,
ThreadModerationCache,
ThreadNode,
ThreadNotFound,
ThreadPost,
type ThreadBlocked,
type ThreadModerationCache,
type ThreadNode,
type ThreadNotFound,
type ThreadPost,
usePostThreadQuery,
} from '#/state/queries/post-thread'
import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {List, ListMethods} from '#/view/com/util/List'
import {List, type ListMethods} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
@@ -122,7 +122,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
const serverTreeViewEnabled = serverPrefs?.lab_treeViewEnabled ?? false
const serverSortReplies = serverPrefs?.sort ?? 'hotness'
// However, we also need these to work locally for PWI (without persistance).
// However, we also need these to work locally for PWI (without persistence).
// So we're mirroring them locally.
const prioritizeFollowedUsers = serverPrioritizeFollowedUsers
const [treeViewEnabled, setTreeViewEnabled] = useState(serverTreeViewEnabled)
@@ -394,7 +394,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
[refetch],
)
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const onPressReply = React.useCallback(() => {
if (thread?.type !== 'post') {
return
@@ -566,7 +566,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
? MAINTAIN_VISIBLE_CONTENT_POSITION
: undefined
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
ListFooterComponent={
+149 -49
View File
@@ -1,6 +1,6 @@
import React, {memo, useMemo} from 'react'
import {memo, useCallback, useMemo, useState} from 'react'
import {
GestureResponderEvent,
type GestureResponderEvent,
StyleSheet,
Text as RNText,
View,
@@ -8,15 +8,17 @@ import {
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedThreadgate,
type AppBskyFeedThreadgate,
AtUri,
ModerationDecision,
type ModerationDecision,
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 {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
@@ -26,17 +28,23 @@ import {countLines} from '#/lib/strings/helpers'
import {niceDate} from '#/lib/strings/time'
import {s} from '#/lib/styles'
import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
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 {ThreadPost} from '#/state/queries/post-thread'
import {type ThreadPost} from '#/state/queries/post-thread'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useUnstablePostSource} 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 {formatCount} from '#/view/com/util/numeric/format'
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
import {PostMeta} from '#/view/com/util/PostMeta'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
@@ -51,12 +59,14 @@ import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {PostHider} from '#/components/moderation/PostHider'
import {AppModerationCause} from '#/components/Pills'
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'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {WhoCanReply} from '#/components/WhoCanReply'
import * as bsky from '#/types/bsky'
@@ -193,17 +203,21 @@ let PostThreadItemLoaded = ({
hideTopBorder?: boolean
threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => {
const {currentAccount, hasSession} = useSession()
const source = useUnstablePostSource(post.uri)
const feedFeedback = useFeedFeedback(source?.feed, hasSession)
const t = useTheme()
const pal = usePalette('default')
const {_, i18n} = useLingui()
const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls()
const [limitLines, setLimitLines] = React.useState(
const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = useState(
() => countLines(richText?.text) >= MAX_POST_LINES,
)
const {currentAccount} = useSession()
const shadowedPostAuthor = useProfileShadow(post.author)
const rootUri = 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])
@@ -211,12 +225,12 @@ let PostThreadItemLoaded = ({
const authorHref = makeProfileLink(post.author)
const authorTitle = post.author.handle
const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
const likesHref = React.useMemo(() => {
const likesHref = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
}, [post.uri, post.author])
const likesTitle = _(msg`Likes on this post`)
const repostsHref = React.useMemo(() => {
const repostsHref = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author])
@@ -224,7 +238,7 @@ let PostThreadItemLoaded = ({
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
threadgateRecord,
})
const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
return isControlledByViewer && isPostHiddenByThreadgate
@@ -237,7 +251,7 @@ let PostThreadItemLoaded = ({
]
: []
}, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
const quotesHref = React.useMemo(() => {
const quotesHref = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
}, [post.uri, post.author])
@@ -261,7 +275,15 @@ let PostThreadItemLoaded = ({
[post, langPrefs.primaryLanguage],
)
const onPressReply = React.useCallback(() => {
const onPressReply = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionReply',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
openComposer({
replyTo: {
uri: post.uri,
@@ -273,12 +295,46 @@ let PostThreadItemLoaded = ({
},
onPost: onPostReply,
})
}, [openComposer, post, record, onPostReply, moderation])
}
const onPressShowMore = React.useCallback(() => {
const onOpenAuthor = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#clickthroughAuthor',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
}
const onOpenEmbed = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#clickthroughEmbed',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
}
const onPressShowMore = useCallback(() => {
setLimitLines(false)
}, [setLimitLines])
const {isActive: live} = useActorStatus(post.author)
const reason = source?.post.reason
const viaRepost = useMemo(() => {
if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
return {
uri: reason.uri,
cid: reason.cid,
}
}
}, [reason])
if (!record) {
return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
}
@@ -298,10 +354,8 @@ let PostThreadItemLoaded = ({
<View
style={[
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
},
a.flex_grow,
{backgroundColor: pal.colors.replyLine},
]}
/>
</View>
@@ -322,20 +376,40 @@ let PostThreadItemLoaded = ({
profile={post.author}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
live={live}
onBeforePress={onOpenAuthor}
/>
<View style={[a.flex_1]}>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<Text
emoji
style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]}
numberOfLines={1}>
{sanitizeDisplayName(
post.author.displayName ||
sanitizeHandle(post.author.handle),
moderation.ui('displayName'),
)}
</Text>
</Link>
<View style={[a.flex_row, a.align_center]}>
<Link
style={[a.flex_shrink]}
href={authorHref}
title={authorTitle}
onBeforePress={onOpenAuthor}>
<Text
emoji
style={[
a.text_lg,
a.font_bold,
a.leading_snug,
a.self_start,
]}
numberOfLines={1}>
{sanitizeDisplayName(
post.author.displayName ||
sanitizeHandle(post.author.handle),
moderation.ui('displayName'),
)}
</Text>
</Link>
<View style={[{paddingLeft: 3, top: -1}]}>
<VerificationCheckButton
profile={shadowedPostAuthor}
size="md"
/>
</View>
</View>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<Text
emoji
@@ -384,6 +458,7 @@ let PostThreadItemLoaded = ({
embed={post.embed}
moderation={moderation}
viewContext={PostEmbedViewContext.ThreadHighlighted}
onOpen={onOpenEmbed}
/>
</View>
)}
@@ -465,16 +540,21 @@ let PostThreadItemLoaded = ({
marginLeft: -5,
},
]}>
<PostCtrls
big
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
onPostReply={onPostReply}
logContext="PostThreadItem"
threadgateRecord={threadgateRecord}
/>
<FeedFeedbackProvider value={feedFeedback}>
<PostControls
big
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
onPostReply={onPostReply}
logContext="PostThreadItem"
threadgateRecord={threadgateRecord}
feedContext={source?.post?.feedContext}
reqId={source?.post?.reqId}
viaRepost={viaRepost}
/>
</FeedFeedbackProvider>
</View>
</View>
</View>
@@ -550,6 +630,7 @@ let PostThreadItemLoaded = ({
profile={post.author}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
live={live}
/>
{showChildReplyLine && (
@@ -612,7 +693,7 @@ let PostThreadItemLoaded = ({
/>
</View>
)}
<PostCtrls
<PostControls
post={post}
record={record}
richText={richText}
@@ -747,14 +828,33 @@ function ExpandedPostDetails({
const {_, i18n} = useLingui()
const openLink = useOpenLink()
const isRootPost = !('reply' in post.record)
const langPrefs = useLanguagePrefs()
const onTranslatePress = React.useCallback(
const onTranslatePress = useCallback(
(e: GestureResponderEvent) => {
e.preventDefault()
openLink(translatorUrl, true)
if (
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
) {
logger.metric(
'translate',
{
sourceLanguages: post.record.langs ?? [],
targetLanguage: langPrefs.primaryLanguage,
textLength: post.record.text.length,
},
{statsig: false},
)
}
return false
},
[openLink, translatorUrl],
[openLink, translatorUrl, langPrefs, post],
)
return (
@@ -51,7 +51,7 @@ export function PostThreadShowHiddenReplies({
<EyeSlash size="sm" fill={t.atoms.text_contrast_medium.color} />
</View>
<Text
style={[t.atoms.text_contrast_medium, a.flex_1]}
style={[t.atoms.text_contrast_medium, a.flex_1, a.leading_snug]}
numberOfLines={1}>
{label}
</Text>