Skip to content

Commit c6d4926

Browse files
Merge upstream develop
2 parents 421334b + 105d1d7 commit c6d4926

31 files changed

Lines changed: 724 additions & 234 deletions

.gitlab/issue_templates/Bug.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!---
2+
Before opening this issue, please search existing issues with the "bug" label
3+
to make sure it isn't already reported.
4+
--->
5+
6+
### Summary
7+
8+
<!---
9+
Summarize the bug in a sentence or two.
10+
--->
11+
12+
### Steps to reproduce
13+
14+
<!---
15+
List the exact steps to trigger the bug, as a numbered list. Mention whether it
16+
happens every time or only sometimes.
17+
--->
18+
19+
### Platform information
20+
21+
<!---
22+
Tell us where you saw this: web or native app, browser and version, device and OS,
23+
app version, and screen size if it looks layout-related.
24+
--->
25+
26+
### What is the current *bug* behavior?
27+
28+
<!---
29+
Describe what actually happens.
30+
--->
31+
32+
### What is the expected *correct* behavior?
33+
34+
<!---
35+
Describe what you expected to happen instead.
36+
--->
37+
38+
### Relevant logs and/or screenshots
39+
40+
<!---
41+
Add screenshots and paste any console or server logs. Wrap logs in code blocks (```)
42+
so they stay readable.
43+
--->
44+
45+
### Possible fixes
46+
47+
<!---
48+
Optional: if you have a hunch, link to the line of code that might be responsible.
49+
--->
50+
51+
/label ~"bug"

.gitlab/issue_templates/Default.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Goal
2+
3+
<!---
4+
State what we want to achieve and why it matters.
5+
--->
6+
7+
8+
## What needs to be done
9+
10+
<!---
11+
Outline the work required to reach the goal above. Break it into tasks if it helps.
12+
--->
13+
14+
15+
## Acceptance Criteria
16+
17+
<!---
18+
List the conditions that must be true for this to be considered done. Make them
19+
concrete enough that anyone can verify them.
20+
--->
21+
22+
- [ ] ...
23+
24+
25+
## Definition of Ready Checklist
26+
27+
- [ ] Acceptance criteria
28+
- [ ] Weighted
29+

apps/polycentric/app/_layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Atoms, ThemeProvider, useTheme } from '@/src/common/theme';
33
import { isWeb } from '@/src/common/util/platform';
44
import '@/src/common/util/react-native-screens-feature-flags';
55
import { TrueSheetProvider } from '@lodev09/react-native-true-sheet';
6+
import { PortalHost } from '@rn-primitives/portal';
67
import { Stack } from 'expo-router';
78
import * as SplashScreen from 'expo-splash-screen';
89
import { StatusBar } from 'expo-status-bar';
@@ -79,6 +80,7 @@ export default function RootLayout() {
7980
<PolycentricProvider onInitialized={onInitialized}>
8081
<TrueSheetProvider>
8182
<RootStack />
83+
<PortalHost />
8284
</TrueSheetProvider>
8385
</PolycentricProvider>
8486
</ThemeProvider>

apps/polycentric/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@react-navigation/elements": "^2.8.0",
2828
"@react-navigation/native": "^7.1.32",
2929
"@rn-primitives/dropdown-menu": "^1.4.0",
30+
"@rn-primitives/portal": "^1.4.0",
3031
"@shopify/flash-list": "^2.3.1",
3132
"@types/react-native-web": "^0.19.2",
3233
"better-sqlite3": "^12.6.2",

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

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as DropdownMenuPrimitive from '@rn-primitives/dropdown-menu';
2-
import React, { ReactNode, useState } from 'react';
3-
import { View } from 'react-native';
2+
import React, { ReactNode } from 'react';
3+
import { StyleSheet, View } from 'react-native';
44

55
import { Atoms, useTheme } from '../theme';
66

@@ -13,17 +13,17 @@ function DropdownMenuContent({
1313

1414
return (
1515
<DropdownMenuPrimitive.Portal>
16-
<DropdownMenuPrimitive.Overlay>
16+
<DropdownMenuPrimitive.Overlay style={StyleSheet.absoluteFill}>
1717
<DropdownMenuPrimitive.Content
18-
style={(state) => [
18+
style={StyleSheet.flatten([
1919
{ borderWidth: 1, borderColor: theme.palette.neutral_50 },
2020
Atoms.rounded_lg,
21-
{ minWidth: 256 },
22-
//Atoms.outline_none,
21+
{ minWidth: 225 },
22+
Atoms.outline_none,
2323
{ backgroundColor: theme.palette.neutral_25 },
2424
Atoms.overflow_hidden,
25-
typeof style === 'function' ? style(state) : style,
26-
]}
25+
typeof style === 'function' ? undefined : style,
26+
])}
2727
{...props}
2828
/>
2929
</DropdownMenuPrimitive.Overlay>
@@ -37,27 +37,27 @@ function DropdownMenuItem({
3737
...props
3838
}: DropdownMenuPrimitive.ItemProps & { children: ReactNode }) {
3939
const { theme } = useTheme();
40-
const [hovered, setHovering] = useState(false);
4140

4241
return (
43-
<DropdownMenuPrimitive.Item
44-
style={{ outline: 'none' }}
45-
onHoverIn={() => setHovering(true)}
46-
onHoverOut={() => setHovering(false)}
47-
{...props}
48-
>
49-
<View
50-
style={[
51-
Atoms.py_md,
52-
Atoms.px_lg,
53-
hovered && { backgroundColor: theme.palette.neutral_50 },
54-
Atoms.flex_row,
55-
Atoms.align_center,
56-
Atoms.gap_lg,
57-
]}
58-
>
59-
{children}
60-
</View>
42+
<DropdownMenuPrimitive.Item style={Atoms.outline_none} {...props}>
43+
{({ pressed, hovered }) => (
44+
<View
45+
style={[
46+
Atoms.py_md,
47+
Atoms.px_lg,
48+
// Press on native mirrors hover on web — same highlight, so items
49+
// give feedback on touch devices that have no hover state.
50+
(hovered || pressed) && {
51+
backgroundColor: theme.palette.neutral_50,
52+
},
53+
Atoms.flex_row,
54+
Atoms.align_center,
55+
Atoms.gap_lg,
56+
]}
57+
>
58+
{children}
59+
</View>
60+
)}
6161
</DropdownMenuPrimitive.Item>
6262
);
6363
}

apps/polycentric/src/common/lib/polycentric-hooks/helpers.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ export type PostData = {
3838
/** Hex of the quoted post's EventKey — same encoding as `PostData.id`. */
3939
quoteId?: string;
4040

41+
/** Identity that reposted this post, when this item represents a
42+
* repost. The rest of the fields are the *reposted* post's data. */
43+
repostedBy?: string;
44+
/** Hex of the repost event's own EventKey — used as the feed list
45+
* key so a repost is distinct from the original post. */
46+
repostId?: string;
47+
4148
signedEvent: v2.SignedEvent;
4249
};
4350

@@ -102,6 +109,71 @@ export function decodePostBundle(bundle: v2.EventBundle): PostData | null {
102109
}
103110
}
104111

112+
/** A repost event decoded into who reposted, the target post's id, and
113+
* the repost event's own id. `null` if the bundle isn't a repost. */
114+
function decodeRepostBundle(bundle: v2.EventBundle): {
115+
repostedBy: string;
116+
targetId: string;
117+
repostId: string;
118+
} | null {
119+
try {
120+
if (!bundle.signedEvent) return null;
121+
const event = v2.Event.fromBinary(bundle.signedEvent.eventBytes);
122+
const key = event.key;
123+
if (!key?.signedBy?.key) return null;
124+
if (!bundle.serializedContent?.contentBytes) return null;
125+
const content = v2.Content.fromBinary(
126+
bundle.serializedContent.contentBytes,
127+
);
128+
if (content.contentBody.oneofKind !== 'repost') return null;
129+
const target = content.contentBody.repost.post;
130+
if (!target) return null;
131+
return {
132+
repostedBy: key.identity,
133+
targetId: bytesToHex(v2.EventKey.toBinary(target)),
134+
repostId: bytesToHex(v2.EventKey.toBinary(key)),
135+
};
136+
} catch {
137+
return null;
138+
}
139+
}
140+
141+
/**
142+
* Decode a `GetFeedResponse` into renderable posts. Plain posts decode
143+
* directly; reposts resolve their target post from `event_hints` (the
144+
* server ships the reposted post alongside) and surface it tagged with
145+
* `repostedBy`. A repost whose target isn't in the hints is dropped.
146+
*/
147+
export function decodeFeedItems(response: v2.GetFeedResponse): PostData[] {
148+
const hintPosts = new Map<string, PostData>();
149+
for (const hint of response.eventHints) {
150+
if (!hint.eventBundle) continue;
151+
const post = decodePostBundle(hint.eventBundle);
152+
if (post) hintPosts.set(post.id, post);
153+
}
154+
155+
const items: PostData[] = [];
156+
for (const bundle of response.eventBundles) {
157+
const post = decodePostBundle(bundle);
158+
if (post) {
159+
items.push(post);
160+
continue;
161+
}
162+
const repost = decodeRepostBundle(bundle);
163+
if (repost) {
164+
const target = hintPosts.get(repost.targetId);
165+
if (target) {
166+
items.push({
167+
...target,
168+
repostedBy: repost.repostedBy,
169+
repostId: repost.repostId,
170+
});
171+
}
172+
}
173+
}
174+
return items;
175+
}
176+
105177
/**
106178
* Dicebear identicon URL for a public key.
107179
*/

apps/polycentric/src/common/lib/polycentric-hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
// Helpers
2828
export {
2929
decodePostBundle as decodeV2PostBundle,
30+
decodeFeedItems,
3031
pubkeyStr,
3132
identiconUrl,
3233
pickImageVariant,

apps/polycentric/src/common/theme/tokens.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StyleSheet, TextStyle } from 'react-native';
1+
import { StyleSheet, TextStyle, ViewStyle } from 'react-native';
22

33
export const Spacing = {
44
'0': 0,
@@ -210,6 +210,10 @@ export const Atoms = StyleSheet.create({
210210
textAlign: 'right',
211211
},
212212

213+
text_underline: {
214+
textDecorationLine: 'underline',
215+
},
216+
213217
font_normal: {
214218
fontWeight: 400,
215219
},
@@ -647,4 +651,15 @@ export const Atoms = StyleSheet.create({
647651
rounded_full: {
648652
borderRadius: BorderRadius.full,
649653
},
654+
655+
/**
656+
* Outline
657+
*/
658+
// `outlineStyle: 'none'` is web-only and not in RN's ViewStyle union, so cast
659+
// (as TextInput does). On web this fully removes the focus ring that
660+
// `outlineWidth: 0` alone leaves behind; on native both props are ignored.
661+
outline_none: {
662+
outlineStyle: 'none',
663+
outlineWidth: 0,
664+
} as unknown as ViewStyle,
650665
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type FeedListProps = Omit<ListProps<PostData>, 'data' | 'renderItem'> & {
1818
renderItem?: ListProps<PostData>['renderItem'];
1919
};
2020

21-
const defaultKeyExtractor = (item: PostData) => item.id;
21+
const defaultKeyExtractor = (item: PostData) => item.repostId ?? item.id;
2222

2323
const defaultRenderItem: ListRenderItem<PostData> = ({ item }) => (
2424
<Post post={item} />

apps/polycentric/src/features/feed/hooks/useExploreFeed.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { useMemo } from 'react';
22
import { Query, QueryStatus, v2 } from '@polycentric/react-native';
33
import {
4-
decodeV2PostBundle,
5-
PostData,
4+
decodeFeedItems,
65
usePolycentricContext,
76
} from '@/src/common/lib/polycentric-hooks';
87
import { type FeedHookResult, NOOP } from './types';
@@ -31,13 +30,7 @@ export function useExploreFeed(options?: {
3130
return [];
3231
}
3332
const response = v2.GetFeedResponse.fromBinary(new Uint8Array(query.data));
34-
const items: PostData[] = [];
35-
for (const bundle of response.eventBundles) {
36-
const decoded = decodeV2PostBundle(bundle);
37-
if (decoded) items.push(decoded);
38-
}
39-
40-
return items;
33+
return decodeFeedItems(response);
4134
}, [query.data]);
4235

4336
return {

0 commit comments

Comments
 (0)