Skip to content

Commit 15b84cf

Browse files
committed
Improve the UX around Post replies when on a post page
1 parent 1363c4d commit 15b84cf

4 files changed

Lines changed: 122 additions & 39 deletions

File tree

apps/polycentric/src/features/composer/ComposerInput.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import { Atoms, useTheme, withHexOpacity } from '@/src/common/theme';
66
import { Ionicons } from '@expo/vector-icons';
77
import { Pressable, StyleSheet, View } from 'react-native';
88

9+
interface ComposerInputProps {
10+
/** When set, opens the composer pre-filled to reply to this post. */
11+
replyTo?: { identityId: string; sequence: string };
12+
}
13+
914
/**
1015
* A non-interactive placeholder that looks like a composer input. Tapping
1116
* anywhere on it opens the full compose modal.
1217
*/
13-
export function ComposerInput() {
18+
export function ComposerInput({ replyTo }: ComposerInputProps = {}) {
1419
const { identity: currentIdentity } = useCurrentIdentity();
1520
const { theme } = useTheme();
1621
const { hovered, onHoverIn, onHoverOut } = useWebHover();
@@ -30,7 +35,7 @@ export function ComposerInput() {
3035
return (
3136
<Pressable
3237
accessibilityLabel="New post"
33-
onPress={() => openCompose()}
38+
onPress={() => openCompose({ replyTo })}
3439
onHoverIn={onHoverIn}
3540
onHoverOut={onHoverOut}
3641
style={[
@@ -49,14 +54,14 @@ export function ComposerInput() {
4954
>
5055
<ProfileAvatar identityKey={currentIdentity.identityKey} size="md" />
5156
<Text variant="body" color="neutral_500" style={Atoms.flex_1}>
52-
What&apos;s on your mind?
57+
{replyTo ? 'Post your reply' : "What's on your mind?"}
5358
</Text>
5459
<Pressable
5560
accessibilityRole="button"
5661
accessibilityLabel="Attach image"
5762
onPress={(e) => {
5863
e.stopPropagation?.();
59-
openCompose({ attachImage: true });
64+
openCompose({ attachImage: true, replyTo });
6065
}}
6166
onHoverIn={onAttachHoverIn}
6267
onHoverOut={onAttachHoverOut}

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

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { useCallback, useMemo, useState } from 'react';
2-
import { RefreshControl, StyleSheet, View } from 'react-native';
2+
import {
3+
RefreshControl,
4+
StyleSheet,
5+
useWindowDimensions,
6+
View,
7+
} from 'react-native';
38
import { FlashList } from '@shopify/flash-list';
4-
import { type PostData } from '@/src/common/lib/polycentric-hooks';
9+
import {
10+
postIdToSequence,
11+
type PostData,
12+
} from '@/src/common/lib/polycentric-hooks';
513
import { Post } from './Post';
614
import { usePostById } from './hooks/usePostById';
15+
import { Atoms } from '@/src/common/theme';
16+
import { ComposerInput } from '../composer';
717

818
interface ConversationViewProps {
919
post: PostData;
@@ -14,6 +24,7 @@ interface ConversationViewProps {
1424
// supported.
1525
export function ConversationView({ post }: ConversationViewProps) {
1626
const [refreshing, setRefreshing] = useState(false);
27+
const { height: windowHeight } = useWindowDimensions();
1728

1829
const { post: rootPost } = usePostById(
1930
post.reply?.root?.identity,
@@ -38,28 +49,34 @@ export function ConversationView({ post }: ConversationViewProps) {
3849
setTimeout(() => setRefreshing(false), 0);
3950
}, []);
4051

52+
const replyTo = useMemo(() => {
53+
if (!post.identity) return undefined;
54+
const sequence = postIdToSequence(post.id);
55+
if (!sequence) return undefined;
56+
return { identityId: post.identity, sequence };
57+
}, [post.id, post.identity]);
58+
4159
return (
4260
<FlashList
4361
data={items}
4462
keyExtractor={(p) => p.id}
45-
renderItem={({ item }) => (
46-
<View style={styles.row}>
63+
renderItem={({ item, index }) => (
64+
<View style={[Atoms.w_full]}>
4765
<Post
4866
post={item}
4967
hideReplyingTo={false}
5068
disablePress={item.id === post.id}
69+
showThreadLineAbove={!!item.reply?.parent}
70+
showThreadLineBelow={index < items.length - 1}
71+
hideBottomBorder={item.id !== post.id}
5172
/>
73+
{item.id === post.id ? <ComposerInput replyTo={replyTo} /> : null}
5274
</View>
5375
)}
76+
ListFooterComponent={<View style={{ height: windowHeight }} />}
5477
refreshControl={
5578
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
5679
}
5780
/>
5881
);
5982
}
60-
61-
const styles = StyleSheet.create({
62-
row: {
63-
width: '100%',
64-
},
65-
});

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

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ interface PostProps {
2727
hideReplyingTo?: boolean;
2828
/** When true, tapping the card root is a no-op (e.g. the focused post in a conversation). */
2929
disablePress?: boolean;
30+
/** Draw a vertical line above the avatar — connects up to the previous post. */
31+
showThreadLineAbove?: boolean;
32+
/** Draw a vertical line below the avatar — connects down to the next post. */
33+
showThreadLineBelow?: boolean;
34+
/** Hide the bottom hairline (used inside conversation views where the thread line is the visual seam instead). */
35+
hideBottomBorder?: boolean;
3036
}
3137

3238
export const Post = memo(function Post({
3339
post,
3440
hideReplyingTo: _hideReplyingTo = false,
3541
disablePress = false,
42+
showThreadLineAbove = false,
43+
showThreadLineBelow = false,
44+
hideBottomBorder = false,
3645
}: PostProps) {
3746
const { theme } = useTheme();
3847
const postId = post.id;
@@ -108,12 +117,11 @@ export const Post = memo(function Post({
108117

109118
return (
110119
<Pressable
120+
role="article"
111121
style={({ pressed }) => [
112122
Atoms.w_full,
113123
Atoms.px_lg,
114-
Atoms.pt_md,
115-
{ paddingBottom: 6 },
116-
{
124+
!hideBottomBorder && {
117125
borderBottomWidth: 1,
118126
borderBottomColor: withHexOpacity(theme.palette.neutral_500, '20'),
119127
},
@@ -124,16 +132,68 @@ export const Post = memo(function Post({
124132
onPress={handlePress}
125133
disabled={disablePress}
126134
>
135+
{/* Top padding bar */}
127136
<View style={[Atoms.flex_row, Atoms.gap_lg]}>
128-
{authorIdentity ? (
129-
<ProfileAvatar
130-
identityKey={authorIdentity}
131-
size="md"
132-
onPress={handleAuthorPress}
133-
/>
134-
) : null}
137+
<View
138+
style={[
139+
Atoms.align_center,
140+
!showThreadLineAbove && Atoms.pt_md,
141+
showThreadLineAbove && Atoms.mb_xs,
142+
{
143+
flexBasis: 40,
144+
},
145+
]}
146+
>
147+
{showThreadLineAbove ? (
148+
<View
149+
style={[
150+
Atoms.flex_1,
151+
{
152+
width: 2,
153+
backgroundColor: withHexOpacity(
154+
theme.palette.neutral_500,
155+
'30',
156+
),
157+
},
158+
]}
159+
/>
160+
) : null}
161+
</View>
162+
{/* Empty */}
163+
<View style={[Atoms.flex_1, showThreadLineAbove && Atoms.pt_md]}></View>
164+
</View>
165+
166+
{/* Main post body */}
167+
<View style={[Atoms.flex_row, Atoms.gap_lg]}>
168+
{/* Left side (avatar and thread line) */}
169+
<View style={[Atoms.align_center]}>
170+
{authorIdentity ? (
171+
<ProfileAvatar
172+
identityKey={authorIdentity}
173+
size="md"
174+
onPress={handleAuthorPress}
175+
/>
176+
) : null}
177+
{showThreadLineBelow ? (
178+
<View
179+
style={[
180+
Atoms.flex_1,
181+
Atoms.mt_xs,
182+
{
183+
width: 2,
184+
backgroundColor: withHexOpacity(
185+
theme.palette.neutral_500,
186+
'30',
187+
),
188+
},
189+
]}
190+
/>
191+
) : null}
192+
</View>
135193

136-
<View style={Atoms.flex_1}>
194+
{/* Main post content */}
195+
<View style={[Atoms.flex_1, Atoms.pb_md]}>
196+
{/* Author name and other topbar items */}
137197
<View
138198
style={[
139199
Atoms.flex_row,
@@ -172,10 +232,12 @@ export const Post = memo(function Post({
172232
) : null}
173233
</View>
174234

175-
<Text variant="secondary" style={{ marginTop: 4, lineHeight: 20 }}>
176-
{displayContent}
177-
{isTruncatedPreview ? '...' : ''}
178-
</Text>
235+
{displayContent ? (
236+
<Text variant="secondary" style={[Atoms.mt_xs]}>
237+
{displayContent}
238+
{isTruncatedPreview ? '...' : ''}
239+
</Text>
240+
) : null}
179241
{post.images?.length > 0 && <PostImages images={post.images} />}
180242
{showContentExpandToggle && (
181243
<Pressable
@@ -197,17 +259,16 @@ export const Post = memo(function Post({
197259
</Text>
198260
</Pressable>
199261
)}
262+
<PostToolbar
263+
onReply={handleReply}
264+
onLike={handleLike}
265+
onDislike={handleDislike}
266+
liked={liked}
267+
disliked={disliked}
268+
style={{ marginTop: 8 }}
269+
/>
200270
</View>
201271
</View>
202-
203-
<PostToolbar
204-
onReply={handleReply}
205-
onLike={handleLike}
206-
onDislike={handleDislike}
207-
liked={liked}
208-
disliked={disliked}
209-
style={{ marginTop: 8, paddingLeft: 50 }}
210-
/>
211272
</Pressable>
212273
);
213274
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function ActionButton({
8585
: color;
8686

8787
const iconSurface: StyleProp<ViewStyle> = [
88-
Atoms.p_xs,
88+
// Atoms.p_xs,
8989
Atoms.rounded_md,
9090
{
9191
backgroundColor: hovered

0 commit comments

Comments
 (0)