Skip to content

Commit 6ee85a5

Browse files
committed
(chore): improve the topbar logic when on a thread post
1 parent f1b142b commit 6ee85a5

8 files changed

Lines changed: 69 additions & 40 deletions

File tree

apps/polycentric/src/common/components/List/List.tsx

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import type { PostData } from '@/src/common/lib/polycentric-hooks';
22
import { isWeb } from '@/src/common/util/platform';
33
import {
44
FlashList,
5+
FlashListRef,
56
type FlashListProps,
67
type ListRenderItem,
78
type ListRenderItemInfo,
89
} from '@shopify/flash-list';
9-
import {
10+
import React, {
1011
cloneElement,
1112
isValidElement,
1213
useEffect,
@@ -19,6 +20,7 @@ import Animated, {
1920
useAnimatedStyle,
2021
useSharedValue,
2122
} from 'react-native-reanimated';
23+
import { Atoms } from '../../theme';
2224

2325
// A reanimated-compatible FlashList.
2426
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
@@ -32,7 +34,14 @@ const HEADER_HIDE_THRESHOLD = 50;
3234

3335
export type { FlashListProps, ListRenderItem, ListRenderItemInfo };
3436

35-
export type ListProps<T> = FlashListProps<T>;
37+
export type ListProps<T> = FlashListProps<T> & {
38+
HeaderComponent?:
39+
| React.ComponentType<any>
40+
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
41+
| React.ExoticComponent<any>
42+
| null
43+
| undefined;
44+
};
3645

3746
export function List<T extends PostData = PostData>(props: ListProps<T>) {
3847
if (isWeb) {
@@ -43,36 +52,55 @@ export function List<T extends PostData = PostData>(props: ListProps<T>) {
4352
}
4453

4554
function NativeList<T>({
46-
ListHeaderComponent,
55+
HeaderComponent,
4756
contentContainerStyle,
4857
refreshControl,
4958
onScroll: _ignoredOnScroll,
5059
...rest
51-
}: FlashListProps<T>) {
60+
}: ListProps<T>) {
61+
const ref = useRef<FlashListRef<T>>(null);
5262
const lastScrollY = useSharedValue(0);
5363
const headerTranslate = useSharedValue(0);
5464
const headerHeightShared = useSharedValue(0);
65+
const isDragging = useSharedValue(false);
66+
const isMomentum = useSharedValue(false);
5567
const [headerHeight, setHeaderHeight] = useState(0);
5668

57-
const onScroll = useAnimatedScrollHandler((event) => {
58-
const currentY = event.contentOffset.y;
59-
const h = headerHeightShared.value;
60-
61-
if (currentY <= HEADER_HIDE_THRESHOLD) {
62-
headerTranslate.value = 0;
63-
} else {
64-
// Compute delta relative to the threshold so movement past it
65-
// starts the hide from translate 0 instead of snapping.
66-
const lastEffective = Math.max(
67-
0,
68-
lastScrollY.value - HEADER_HIDE_THRESHOLD,
69-
);
70-
const currentEffective = currentY - HEADER_HIDE_THRESHOLD;
71-
const delta = currentEffective - lastEffective;
72-
const next = headerTranslate.value - delta;
73-
headerTranslate.value = Math.min(0, Math.max(-h, next));
74-
}
75-
lastScrollY.value = currentY;
69+
const onScroll = useAnimatedScrollHandler({
70+
onBeginDrag: () => {
71+
isDragging.value = true;
72+
},
73+
onEndDrag: () => {
74+
isDragging.value = false;
75+
},
76+
onMomentumBegin: () => {
77+
isMomentum.value = true;
78+
},
79+
onMomentumEnd: () => {
80+
isMomentum.value = false;
81+
},
82+
onScroll: (event) => {
83+
const currentY = event.contentOffset.y;
84+
const h = headerHeightShared.value;
85+
86+
if (currentY <= HEADER_HIDE_THRESHOLD) {
87+
headerTranslate.value = 0;
88+
} else if (isDragging.value || isMomentum.value) {
89+
// Compute delta relative to the threshold so movement past it
90+
// starts the hide from translate 0 instead of snapping.
91+
const lastEffective = Math.max(
92+
0,
93+
lastScrollY.value - HEADER_HIDE_THRESHOLD,
94+
);
95+
const currentEffective = currentY - HEADER_HIDE_THRESHOLD;
96+
const delta = currentEffective - lastEffective;
97+
const next = headerTranslate.value - delta;
98+
headerTranslate.value = Math.min(0, Math.max(-h, next));
99+
}
100+
// Always update so the next user-driven event computes the
101+
// correct delta — even if we skipped animating this tick.
102+
lastScrollY.value = currentY;
103+
},
76104
});
77105

78106
const headerAnimatedStyle = useAnimatedStyle(() => ({
@@ -85,7 +113,7 @@ function NativeList<T>({
85113
if (next !== headerHeight) setHeaderHeight(next);
86114
};
87115

88-
const renderedHeader = renderNode(ListHeaderComponent);
116+
const renderedHeader = renderNode(HeaderComponent);
89117

90118
// Show below the sticky header
91119
const adjustedRefreshControl = (
@@ -100,15 +128,16 @@ function NativeList<T>({
100128
) as FlashListProps<T>['refreshControl'];
101129

102130
return (
103-
<View style={styles.container}>
131+
<View style={[Atoms.flex_1]}>
104132
{renderedHeader ? (
105133
<Animated.View
106134
onLayout={onHeaderLayout}
107-
style={[styles.stickyHeader, headerAnimatedStyle]}
135+
style={[Atoms.absolute, styles.stickyHeader, headerAnimatedStyle]}
108136
>
109137
{renderedHeader}
110138
</Animated.View>
111139
) : null}
140+
112141
<AnimatedFlashList
113142
{...(rest as FlashListProps<unknown>)}
114143
refreshControl={adjustedRefreshControl}
@@ -213,11 +242,7 @@ function renderNode(node: ReactNodeOrComponent) {
213242
}
214243

215244
const styles = StyleSheet.create({
216-
container: {
217-
flex: 1,
218-
},
219245
stickyHeader: {
220-
position: 'absolute',
221246
top: 0,
222247
left: 0,
223248
right: 0,

apps/polycentric/src/features/activity/ActivityScreen.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import Topbar from '@/src/common/components/layout/Topbar';
44
import { Atoms } from '@/src/common/theme';
55
import { FlashList } from '@shopify/flash-list';
66
import { View } from 'react-native';
7+
import { List } from '@/src/common/components/List/List';
78

89
export default function ActivityScreen() {
910
return (
1011
<Screen>
1112
<Screen.PrimaryColumn>
12-
<FlashList
13+
<List
1314
data={[]}
1415
renderItem={() => <></>}
15-
ListHeaderComponent={<Topbar title="Activity" />}
16+
HeaderComponent={<Topbar title="Activity" />}
1617
ListEmptyComponent={() => (
1718
<View
1819
style={[

apps/polycentric/src/features/feed/ExploreScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export default function ExploreScreen() {
100100
return (
101101
<Screen>
102102
<Screen.PrimaryColumn>
103-
<FeedList feed={feed} ListHeaderComponent={ListHeader} />
103+
<FeedList feed={feed} HeaderComponent={ListHeader} />
104104
{showComposeFab ? (
105105
<Fab
106106
onPress={openCompose}

apps/polycentric/src/features/feed/FeedScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default function FeedScreen() {
5959
return (
6060
<Screen>
6161
<Screen.PrimaryColumn>
62-
<FeedList feed={feed} ListHeaderComponent={ListHeader} />
62+
<FeedList feed={feed} HeaderComponent={ListHeader} />
6363
{showComposeFab ? (
6464
<Fab
6565
onPress={openCompose}

apps/polycentric/src/features/post/PostScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function FeedPostScreen() {
2828
router.back();
2929
}, []);
3030

31-
if (isLoading) {
31+
if (isLoading && !post) {
3232
return (
3333
<Screen>
3434
<Screen.PrimaryColumn>
@@ -64,7 +64,7 @@ export default function FeedPostScreen() {
6464
<View style={[Atoms.flex_1, Atoms.mt_md]}>
6565
<ThreadList
6666
post={post}
67-
ListHeaderComponent={<Screen.Topbar title="Post" />}
67+
HeaderComponent={<Screen.Topbar title="Post" />}
6868
/>
6969
</View>
7070
</Screen.PrimaryColumn>

apps/polycentric/src/features/post/ThreadList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export function ThreadList({ post, ...rest }: ThreadListProps) {
2121
// Fall back to rendering just the subject until the server response lands.
2222
const items = thread.length > 0 ? thread : [post];
2323

24+
const subjectIndex = items.findIndex((p) => p.id === post.id);
25+
2426
const handleRefresh = useCallback(() => {
2527
setRefreshing(true);
2628
setTimeout(() => setRefreshing(false), 0);
@@ -30,6 +32,7 @@ export function ThreadList({ post, ...rest }: ThreadListProps) {
3032
<List
3133
{...rest}
3234
data={items}
35+
initialScrollIndex={subjectIndex > 0 ? subjectIndex : undefined}
3336
keyExtractor={(p) => p.id}
3437
renderItem={({ item, index }) => {
3538
const above = index > 0 ? items[index - 1] : null;

apps/polycentric/src/features/profile/ProfileFeedSwitcher.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ type ListHeader = FlashListProps<unknown>['ListHeaderComponent'];
2121
export function ProfileFeedSwitcher({
2222
tabs,
2323
activeKey,
24-
ListHeaderComponent,
24+
HeaderComponent: HeaderComponent,
2525
}: {
2626
tabs: ProfileFeedTab[];
2727
activeKey: string;
28-
ListHeaderComponent?: ListHeader;
28+
HeaderComponent?: ListHeader;
2929
}) {
3030
const active = tabs.find((t) => t.key === activeKey) ?? tabs[0];
3131
if (!active) return null;
@@ -51,7 +51,7 @@ export function ProfileFeedSwitcher({
5151
<FeedList
5252
key={active.key}
5353
feed={feed}
54-
ListHeaderComponent={ListHeaderComponent}
54+
HeaderComponent={HeaderComponent}
5555
contentContainerStyle={
5656
bottomPadding ? { paddingBottom: bottomPadding } : undefined
5757
}

apps/polycentric/src/features/profile/ProfileScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function ProfileScreenContent() {
9090
<ProfileFeedSwitcher
9191
tabs={tabs}
9292
activeKey={activeFeed}
93-
ListHeaderComponent={profileHeader}
93+
HeaderComponent={profileHeader}
9494
/>
9595
</Screen.PrimaryColumn>
9696
</Screen>

0 commit comments

Comments
 (0)