[APP-1934] replace image grid layout with carousel (#10157)

Co-authored-by: RetroSunstar <57507616+RetroSunstar@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Spence Pope
2026-04-15 18:53:01 -04:00
committed by GitHub
parent d3f5093817
commit e804546809
20 changed files with 1697 additions and 511 deletions
+158 -139
View File
@@ -34,6 +34,10 @@ import {Link} from '#/view/com/util/Link'
import {PostMeta} from '#/view/com/util/PostMeta'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a} from '#/alf'
import {
GalleryBleed,
maybeApplyGalleryOffsetStyles,
} from '#/components/images/Gallery'
import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
@@ -163,6 +167,7 @@ let FeedItemInner = ({
const queryClient = useQueryClient()
const {openComposer} = useOpenComposer()
const pal = usePalette('default')
const {currentAccount} = useSession()
const [hover, setHover] = useState(false)
@@ -293,140 +298,6 @@ let FeedItemInner = ({
}
}, [reason])
return (
<Link
testID={`feedItem-by-${post.author.handle}`}
style={outerStyles}
href={href}
noFeedback
accessible={false}
onBeforePress={onBeforePress}
dataSet={{feedContext}}
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}>
<SubtleHover hover={hover} />
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
<View style={{width: 42}}>
{isThreadChild && (
<View
style={[
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
marginBottom: 4,
},
]}
/>
)}
</View>
<View style={[a.pt_sm, a.flex_shrink]}>
{reason && (
<PostFeedReason
reason={reason}
moderation={moderation}
onOpenReposter={onOpenReposter}
/>
)}
</View>
</View>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<PreviewableUserAvatar
size={42}
profile={post.author}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
onBeforePress={onOpenAuthor}
live={live}
/>
{isThreadParent && (
<View
style={[
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
marginTop: live ? 8 : 4,
},
]}
/>
)}
</View>
<View style={styles.layoutContent}>
<PostMeta
author={post.author}
moderation={moderation}
timestamp={post.indexedAt}
postHref={href}
onOpenAuthor={onOpenAuthor}
/>
{showReplyTo &&
(parentAuthor || isParentBlocked || isParentNotFound) && (
<PostRepliedTo
parentAuthor={parentAuthor}
isParentBlocked={isParentBlocked}
isParentNotFound={isParentNotFound}
/>
)}
<LabelsOnMyPost post={post} />
<PostContent
moderation={moderation}
richText={richText}
postEmbed={post.embed}
postAuthor={post.author}
onOpenEmbed={onOpenEmbed}
post={post}
threadgateRecord={threadgateRecord}
/>
<PostControls
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
logContext="FeedItem"
feedContext={feedContext}
reqId={reqId}
threadgateRecord={threadgateRecord}
onShowLess={onShowLess}
viaRepost={viaRepost}
/>
</View>
<DiscoverDebug feedContext={feedContext} />
</View>
</Link>
)
}
FeedItemInner = memo(FeedItemInner)
let PostContent = ({
post,
moderation,
richText,
postEmbed,
postAuthor,
onOpenEmbed,
threadgateRecord,
}: {
moderation: ModerationDecision
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
onOpenEmbed: () => void
post: AppBskyFeedDefs.PostView
threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => {
const {currentAccount} = useSession()
const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES,
)
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
threadgateRecord,
})
@@ -451,6 +322,150 @@ let PostContent = ({
: []
}, [post, currentAccount?.did, threadgateHiddenReplies])
return (
<GalleryBleed>
<Link
testID={`feedItem-by-${post.author.handle}`}
style={outerStyles}
href={href}
noFeedback
accessible={false}
onBeforePress={onBeforePress}
dataSet={{feedContext}}
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}>
<SubtleHover hover={hover} />
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
<View style={{width: 42}}>
{isThreadChild && (
<View
style={[
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
marginBottom: 4,
},
]}
/>
)}
</View>
<View style={[a.pt_sm, a.flex_shrink]}>
{reason && (
<PostFeedReason
reason={reason}
moderation={moderation}
onOpenReposter={onOpenReposter}
/>
)}
</View>
</View>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<PreviewableUserAvatar
size={42}
profile={post.author}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
onBeforePress={onOpenAuthor}
live={live}
/>
{isThreadParent && (
<View
style={[
styles.replyLine,
{
flexGrow: 1,
backgroundColor: pal.colors.replyLine,
marginTop: live ? 8 : 4,
},
]}
/>
)}
</View>
<View
style={[
styles.layoutContent,
maybeApplyGalleryOffsetStyles('meta', {
post,
modui: moderation.ui('contentList'),
additionalCauses: additionalPostAlerts,
}),
]}>
<PostMeta
author={post.author}
moderation={moderation}
timestamp={post.indexedAt}
postHref={href}
onOpenAuthor={onOpenAuthor}
/>
{showReplyTo &&
(parentAuthor || isParentBlocked || isParentNotFound) && (
<PostRepliedTo
parentAuthor={parentAuthor}
isParentBlocked={isParentBlocked}
isParentNotFound={isParentNotFound}
/>
)}
<LabelsOnMyPost post={post} />
<PostContent
moderation={moderation}
richText={richText}
postEmbed={post.embed}
postAuthor={post.author}
onOpenEmbed={onOpenEmbed}
post={post}
additionalPostAlerts={additionalPostAlerts}
/>
<PostControls
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
logContext="FeedItem"
feedContext={feedContext}
reqId={reqId}
threadgateRecord={threadgateRecord}
onShowLess={onShowLess}
viaRepost={viaRepost}
/>
</View>
<DiscoverDebug feedContext={feedContext} />
</View>
</Link>
</GalleryBleed>
)
}
FeedItemInner = memo(FeedItemInner)
let PostContent = ({
post,
moderation,
richText,
postEmbed,
postAuthor,
onOpenEmbed,
additionalPostAlerts,
}: {
moderation: ModerationDecision
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
onOpenEmbed: () => void
post: AppBskyFeedDefs.PostView
additionalPostAlerts?: AppModerationCause[]
}): React.ReactNode => {
const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES,
)
const record = useMemo<AppBskyFeedPost.Record | undefined>(
() =>
bsky.validate(post.record, AppBskyFeedPost.validateRecord)
@@ -492,7 +507,15 @@ let PostContent = ({
) : undefined}
{record && <TranslatedPost hideTranslateLink post={post} />}
{postEmbed ? (
<View style={[a.pb_xs]}>
<View
style={[
a.pb_xs,
maybeApplyGalleryOffsetStyles('embed', {
post,
modui: moderation.ui('contentList'),
additionalCauses: additionalPostAlerts,
}),
]}>
<Embed
embed={postEmbed}
moderation={moderation}
@@ -524,13 +547,9 @@ const styles = StyleSheet.create({
layoutAvi: {
paddingLeft: 8,
paddingRight: 10,
position: 'relative',
zIndex: 999,
},
layoutContent: {
position: 'relative',
flex: 1,
zIndex: 0,
},
alert: {
marginTop: 6,