Skip to content

Commit e81388c

Browse files
committed
Introduce follow feed
1 parent e36e2c3 commit e81388c

26 files changed

Lines changed: 618 additions & 128 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import FeedTabScreen from '@/src/features/feed/FeedScreen';
2+
3+
export default function FeedIndexRoute() {
4+
return <FeedTabScreen />;
5+
}

apps/polycentric/src/common/components/layout/Layout.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Ionicons } from '@expo/vector-icons';
2828
import { FUTO_URL, openCompose } from '../../constants';
2929
import { Button } from '../primitives';
3030
import { useCurrentIdentity } from '../../lib/polycentric-hooks';
31+
import { IdentityFooter } from '@/src/features/core/identity/IdentityFooter';
3132

3233
type MainProps = {
3334
children: ReactElement | ReactElement[];
@@ -177,7 +178,13 @@ export const LeftSidebar = memo(function LeftSidebar({
177178
]}
178179
>
179180
{/* 1st section (top) */}
180-
<View>
181+
<View
182+
style={[
183+
Atoms.w_full,
184+
narrowSidebar && Atoms.align_center,
185+
Atoms.flex_col,
186+
]}
187+
>
181188
<Link
182189
href="/"
183190
style={[
@@ -196,6 +203,27 @@ export const LeftSidebar = memo(function LeftSidebar({
196203
</Link>
197204

198205
<VerticalNav />
206+
207+
<View
208+
style={[
209+
Atoms.py_md,
210+
Atoms.self_stretch,
211+
narrowSidebar && Atoms.align_center,
212+
]}
213+
>
214+
{identity && (
215+
<Button
216+
title={narrowSidebar ? '' : 'Post'}
217+
variant="primary"
218+
size="md"
219+
fullWidth={!narrowSidebar}
220+
icon={({ size, color }) => (
221+
<Ionicons name="add-circle" size={size} color={color} />
222+
)}
223+
onPress={() => openCompose()}
224+
/>
225+
)}
226+
</View>
199227
</View>
200228
{/* 2nd Section (bottom) */}
201229
<View
@@ -205,18 +233,7 @@ export const LeftSidebar = memo(function LeftSidebar({
205233
narrowSidebar && Atoms.align_center,
206234
]}
207235
>
208-
{identity && (
209-
<Button
210-
title={narrowSidebar ? '' : 'New Post'}
211-
variant="primary"
212-
size="md"
213-
fullWidth={!narrowSidebar}
214-
icon={({ size, color }) => (
215-
<Ionicons name="add-circle" size={size} color={color} />
216-
)}
217-
onPress={() => openCompose()}
218-
/>
219-
)}
236+
{identity && <IdentityFooter compact={narrowSidebar} />}
220237
</View>
221238
</View>
222239
</View>

apps/polycentric/src/common/components/layout/nav/VerticalNav.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export function VerticalNav({ style }: VerticalNavProps) {
2323
icon={<Ionicons name="home-outline" size={24} />}
2424
href="/feed"
2525
/>
26+
<NavItem
27+
label="Explore"
28+
icon={<Ionicons name="search-outline" size={24} />}
29+
href="/explore"
30+
/>
2631
{identity?.identityKey && (
2732
<NavItem
2833
label="Profile"

apps/polycentric/src/common/constants/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export const Routes = {
1717
feed: {
1818
index: '/feed',
1919
compose: '/feed/compose',
20-
identity: '/feed/identity',
2120
},
2221
search: '/search',
2322
claims: '/claims',
23+
identitySwitch: '/identity/switch',
2424
profile: (identityId: string) => `/${identityId}` as const,
2525
editProfile: (identityId: string) => `/${identityId}/edit` as const,
2626
post: (identityId: string, postId: string) =>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Avatar, Text } from '@/src/common/components/primitives';
2+
import { openCompose } from '@/src/common/constants';
3+
import {
4+
identiconUrl,
5+
useCurrentIdentity,
6+
} from '@/src/common/lib/polycentric-hooks';
7+
import { useWebHover } from '@/src/common/lib/useWebHover';
8+
import { Atoms, useTheme, withHexOpacity } from '@/src/common/theme';
9+
import { Pressable, StyleSheet, View } from 'react-native';
10+
11+
/**
12+
* A non-interactive placeholder that looks like a composer input. Tapping
13+
* anywhere on it opens the full compose modal.
14+
*/
15+
export function ComposerInput() {
16+
const { identity: currentIdentity } = useCurrentIdentity();
17+
const { theme } = useTheme();
18+
const { hovered, onHoverIn, onHoverOut } = useWebHover();
19+
20+
if (!currentIdentity?.identityKey) return null;
21+
22+
const avatarUrl = identiconUrl(currentIdentity.identityKey);
23+
24+
const hoverOverlay =
25+
theme.scheme === 'light'
26+
? withHexOpacity(theme.palette.neutral_500, '14')
27+
: withHexOpacity(theme.palette.black, '28');
28+
29+
return (
30+
<Pressable
31+
accessibilityRole="button"
32+
accessibilityLabel="New post"
33+
onPress={() => openCompose()}
34+
onHoverIn={onHoverIn}
35+
onHoverOut={onHoverOut}
36+
style={[
37+
Atoms.flex_row,
38+
Atoms.items_center,
39+
Atoms.gap_lg,
40+
Atoms.px_lg,
41+
Atoms.py_md,
42+
{
43+
borderBottomWidth: 1,
44+
borderBottomColor: withHexOpacity(theme.palette.neutral_500, '40'),
45+
backgroundColor: theme.palette.background_primary,
46+
overflow: 'hidden',
47+
},
48+
]}
49+
>
50+
<Avatar source={{ uri: avatarUrl }} size="md" />
51+
<Text variant="body" color="neutral_500" style={Atoms.flex_1}>
52+
What&apos;s on your mind?
53+
</Text>
54+
{hovered ? (
55+
<View
56+
pointerEvents="none"
57+
style={[StyleSheet.absoluteFill, { backgroundColor: hoverOverlay }]}
58+
/>
59+
) : null}
60+
</Pressable>
61+
);
62+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './ComposeSheetInner';
2+
export * from './ComposerInput';

apps/polycentric/src/features/core/identity/IdentityHeader.tsx renamed to apps/polycentric/src/features/core/identity/IdentityFooter.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import { Ionicons } from '@expo/vector-icons';
1616
import { router } from 'expo-router';
1717
import { Pressable, StyleSheet, View } from 'react-native';
1818

19-
export function IdentityHeader() {
19+
type IdentityFooterProps = {
20+
/** Render only the avatar — used in the narrow sidebar. */
21+
compact?: boolean;
22+
};
23+
24+
export function IdentityFooter({ compact = false }: IdentityFooterProps) {
2025
const { identity: currentIdentity } = useCurrentIdentity();
2126
const { theme } = useTheme();
2227
const { hovered, onHoverIn, onHoverOut } = useWebHover();
@@ -37,53 +42,54 @@ export function IdentityHeader() {
3742
? withHexOpacity(theme.palette.neutral_500, '14')
3843
: withHexOpacity(theme.palette.black, '28');
3944

45+
const avatar = (
46+
<Pressable
47+
onPress={() => {
48+
if (!currentIdentity.identityKey) return;
49+
router.push(Routes.tabs.profile(currentIdentity.identityKey));
50+
}}
51+
>
52+
<Avatar source={avatarUrl ? { uri: avatarUrl } : undefined} />
53+
</Pressable>
54+
);
55+
56+
if (compact) {
57+
return avatar;
58+
}
59+
4060
return (
4161
<View
4262
style={[
4363
Atoms.flex_row,
4464
Atoms.justify_between,
4565
Atoms.items_center,
46-
Atoms.gap_md,
66+
Atoms.gap_sm,
4767
]}
4868
>
69+
{avatar}
4970
<Pressable
50-
onPress={() => router.push(Routes.tabs.feed.identity)}
71+
onPress={() => router.push(Routes.tabs.identitySwitch)}
5172
onHoverIn={onHoverIn}
5273
onHoverOut={onHoverOut}
5374
hitSlop={10}
5475
style={[
76+
Atoms.flex_1,
77+
Atoms.flex_row,
78+
Atoms.align_center,
5579
Atoms.px_sm,
5680
Atoms.py_sm,
57-
{
58-
flexShrink: 1,
59-
borderRadius: BorderRadius.sm,
60-
overflow: 'hidden',
61-
position: 'relative',
62-
},
6381
]}
6482
>
65-
<View
66-
style={[
67-
Atoms.flex_row,
68-
Atoms.gap_sm,
69-
{ flex: 1, alignItems: 'baseline' },
70-
]}
71-
>
83+
<View style={[Atoms.flex_col, Atoms.flex_1]}>
7284
<Text
73-
variant="title"
85+
fontSize="md"
7486
fontWeight="bold"
7587
color="neutral_1000"
7688
numberOfLines={1}
77-
style={{ flexShrink: 1 }}
7889
>
7990
{username}
8091
</Text>
8192
<IdentityTag identity={currentIdentity.identityKey} />
82-
<Ionicons
83-
name="chevron-down"
84-
size={22}
85-
color={theme.palette.neutral_1000}
86-
/>
8793
</View>
8894
{hovered ? (
8995
<View
@@ -97,14 +103,11 @@ export function IdentityHeader() {
97103
]}
98104
/>
99105
) : null}
100-
</Pressable>
101-
<Pressable
102-
onPress={() => {
103-
if (!currentIdentity.identityKey) return;
104-
router.push(Routes.tabs.profile(currentIdentity.identityKey));
105-
}}
106-
>
107-
<Avatar source={avatarUrl ? { uri: avatarUrl } : undefined} />
106+
<Ionicons
107+
name="chevron-down"
108+
size={18}
109+
color={theme.palette.neutral_1000}
110+
/>
108111
</Pressable>
109112
</View>
110113
);

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

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import { useState } from 'react';
21
import { Ionicons } from '@expo/vector-icons';
32
import { Screen } from '@/src/common/components/layout';
43
import { View } from 'react-native';
5-
import { Fab, HorizontalScrollGroup } from '@/src/common/components';
6-
import {
7-
IdentityHeader,
8-
FeedChip,
9-
FeedViewer,
10-
type FeedType,
11-
} from '@/src/features/post';
4+
import { Fab } from '@/src/common/components';
5+
import { FeedViewer, type FeedType } from '@/src/features/post';
6+
import { ComposerInput } from '@/src/features/composer';
127
import { useExploreFeed } from './hooks/useExploreFeed';
138
import { useFollowingFeed } from './hooks/useFollowingFeed';
149
import { openCompose } from '@/src/common/constants';
1510
import { Atoms } from '@/src/common/theme';
1611
import { isWeb } from '@/src/common/util/platform';
12+
import { usePathname } from 'expo-router';
13+
14+
function feedTypeFromPath(pathname: string): FeedType {
15+
// The /feed route shows the following feed; /explore is the public feed.
16+
// Default to explore so any unrelated caller gets a sensible view.
17+
return pathname.startsWith('/feed') ? 'following' : 'explore';
18+
}
1719

1820
export default function FeedScreen() {
1921
const showComposeFab = !isWeb;
2022

21-
const [selectedFeed, setSelectedFeed] = useState<FeedType>('explore');
23+
const pathname = usePathname();
24+
const selectedFeed = feedTypeFromPath(pathname);
2225

2326
const exploreFeed = useExploreFeed({
2427
enabled: selectedFeed === 'explore',
@@ -37,26 +40,8 @@ export default function FeedScreen() {
3740
return (
3841
<Screen>
3942
<Screen.PrimaryColumn>
40-
<View style={[Atoms.mx_lg, Atoms.mt_lg]}>
41-
<IdentityHeader />
42-
<View style={Atoms.mt_lg}>
43-
<HorizontalScrollGroup>
44-
<FeedChip
45-
type="explore"
46-
title="Explore"
47-
isSelected={selectedFeed === 'explore'}
48-
onPress={() => setSelectedFeed('explore')}
49-
/>
50-
<FeedChip
51-
type="following"
52-
title="Following"
53-
isSelected={selectedFeed === 'following'}
54-
onPress={() => setSelectedFeed('following')}
55-
/>
56-
</HorizontalScrollGroup>
57-
</View>
58-
</View>
59-
<View style={[Atoms.flex_1, Atoms.mt_md]}>
43+
{selectedFeed === 'following' ? <ComposerInput /> : null}
44+
<View style={[Atoms.flex_1]}>
6045
<FeedViewer
6146
key={selectedFeed}
6247
items={currentFeed.items}

0 commit comments

Comments
 (0)