Skip to content

Commit 2cd4c8d

Browse files
Merge upstream develop
2 parents 1cb06cc + e9d321a commit 2cd4c8d

40 files changed

Lines changed: 723 additions & 288 deletions

File tree

.gitlab/ci/build_js_sdks.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
when: always
66
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
77
changes:
8-
- packages/js-*/*
8+
- packages/js-*/**/*
99
- .gitlab/ci/build_js_sdks.yml
1010
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
1111
when: never
1212
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
1313
changes:
14-
- packages/js-*/*
14+
- packages/js-*/**/*
1515
- .gitlab/ci/build_js_sdks.yml
1616

1717
.js-sdks-base:

.gitlab/ci/build_rs_core.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
when: always
66
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
77
changes:
8-
- packages/rs-core/*
8+
- packages/rs-core/**/*
9+
- packages/rs-common/**/*
910
- .gitlab/ci/build_rs_core.yml
1011
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
1112
when: never
1213
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
1314
changes:
14-
- packages/rs-core/*
15+
- packages/rs-core/**/*
16+
- packages/rs-common/**/*
1517
- .gitlab/ci/build_rs_core.yml
1618
variables:
1719
RUNNER_SCRIPT_TIMEOUT: 179m

.gitlab/ci/build_server.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
when: always
66
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
77
changes:
8-
- services/server/*
8+
- services/server/**/*
99
- .gitlab/ci/build_server.yml
1010
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
1111
when: never
1212
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
1313
changes:
14-
- services/server/*
14+
- services/server/**/*
1515
- .gitlab/ci/build_server.yml
1616
variables:
1717
RUNNER_SCRIPT_TIMEOUT: 179m

apps/polycentric/src/common/components/composites/IdentitySwitcherSheetInner.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { SheetHeaderBlock, useSheetContext } from '@/src/common/lib/sheet';
1919
import { Atoms, useTheme } from '@/src/common/theme';
2020
import { Ionicons } from '@expo/vector-icons';
2121
import {
22-
createIdentityWithDefaultServer,
22+
createIdentity,
23+
KEY_TYPE,
2324
type KeyPair,
2425
types,
2526
} from '@polycentric/react-native';
@@ -73,7 +74,14 @@ export function IdentitySwitcherSheetInner() {
7374
const [isEditing, setIsEditing] = useState(false);
7475

7576
const handleCreateIdentity = useCallback(async () => {
76-
await createIdentityWithDefaultServer(client, DEFAULT_SERVER);
77+
// Adding a second identity on this device means generating a fresh
78+
// keypair for it; the initial device-wide keypair stays paired with
79+
// whatever identity it currently owns.
80+
await client.keyPairManager.createKeyPair({
81+
keyType: KEY_TYPE.ED25519,
82+
setAsCurrent: true,
83+
});
84+
await createIdentity(client, DEFAULT_SERVER);
7785
await client.sync().catch(() => {});
7886
}, [client]);
7987

apps/polycentric/src/common/components/primitives/PillChip.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { type ComponentProps } from 'react';
22
import { Text } from './Text';
3-
import { getIdentityIdShort } from '@/src/common/lib/polycentric-hooks';
3+
import {
4+
getIdentityIdShort,
5+
shortenIdentityId,
6+
} from '@/src/common/lib/polycentric-hooks';
47
import { types } from '@polycentric/react-native';
58

69
interface PubkeyTagProps {
710
publicKey: types.PublicKey;
11+
/**
12+
* v2 identity id (hex sha256 of the initial Identity content). When
13+
* provided, the tag renders a short form of this identity instead of the
14+
* signer's public key — this is what users should actually see.
15+
*/
16+
identity?: string;
817
style?: ComponentProps<typeof Text>['style'];
918
}
1019

11-
export function PubkeyTag({ publicKey, style }: PubkeyTagProps) {
12-
const label = getIdentityIdShort(publicKey);
20+
export function PubkeyTag({ publicKey, identity, style }: PubkeyTagProps) {
21+
const label = identity
22+
? shortenIdentityId(identity)
23+
: getIdentityIdShort(publicKey);
1324

1425
return (
1526
<Text

apps/polycentric/src/common/lib/polycentric-hooks/PolycentricProvider.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { DEFAULT_IDENTITY_NAME } from '@/src/common/constants';
22
import {
33
PolycentricClient,
4-
createIdentityWithDefaultServer,
54
createPolycentricClient,
65
types,
76
v2,
7+
type IdentityState,
88
} from '@polycentric/react-native';
9-
10-
type Identity = v2.Identity;
119
import {
1210
createContext,
1311
useCallback,
@@ -31,7 +29,7 @@ export interface PolycentricContextValue {
3129
isLoading: boolean;
3230
isReady: boolean;
3331
error: Error | null;
34-
currentIdentity: Identity | null;
32+
currentIdentity: IdentityState | null;
3533
switchIdentity: (publicKey: types.PublicKey) => Promise<void>;
3634
}
3735

@@ -107,9 +105,10 @@ function DefaultErrorComponent({ error }: { error: Error }) {
107105

108106
async function resolveIdentity(
109107
client: PolycentricClient,
110-
): Promise<Identity | null> {
108+
): Promise<IdentityState | null> {
109+
if (!client.activeIdentityKey) return null;
111110
const state = await client.identityManager.getCurrent();
112-
return state ?? null;
111+
return state.identityKey ? state : null;
113112
}
114113

115114
export function PolycentricProvider({
@@ -119,7 +118,9 @@ export function PolycentricProvider({
119118
}: PolycentricProviderProps) {
120119
const [client, setClient] = useState<PolycentricClient | null>(null);
121120
const [store, setStore] = useState<PolycentricStoreApi | null>(null);
122-
const [currentIdentity, setCurrentIdentity] = useState<Identity | null>(null);
121+
const [currentIdentity, setCurrentIdentity] = useState<IdentityState | null>(
122+
null,
123+
);
123124
const [isLoading, setIsLoading] = useState(true);
124125
const [error, setError] = useState<Error | null>(null);
125126

@@ -134,18 +135,15 @@ export function PolycentricProvider({
134135

135136
(async () => {
136137
try {
138+
// PolycentricClient.initialize() guarantees a keypair exists on
139+
// every device. Identity (the published Identity doc) is a separate
140+
// concept — the onboarding gate handles creating or pairing one.
137141
const c = await createPolycentricClient({
138142
databaseName: 'polycentric.db',
139143
});
140144

141145
if (cancelled) return;
142146

143-
if ((await c.keyPairManager.getKeys()).length === 0) {
144-
await createIdentityWithDefaultServer(c, DEFAULT_SERVER);
145-
}
146-
147-
if (cancelled) return;
148-
149147
const s = createPolycentricStore(c);
150148
await s.getState().refreshIdentities();
151149

@@ -154,19 +152,26 @@ export function PolycentricProvider({
154152
setCurrentIdentity(await resolveIdentity(c));
155153
setIsLoading(false);
156154

157-
void c.sync().catch((syncError) => {
158-
console.warn('Initial Polycentric sync failed:', syncError);
159-
});
155+
// Only sync when we already have an identity to sync for.
156+
if (c.activeIdentityKey) {
157+
void c.sync().catch((syncError) => {
158+
console.warn('Initial Polycentric sync failed:', syncError);
159+
});
160+
}
160161

161162
c.events.onKeyPairChanged(async () => {
162163
if (cancelled) return;
163-
if ((await c.keyPairManager.getKeys()).length === 0) {
164-
await createIdentityWithDefaultServer(c, DEFAULT_SERVER);
165-
await c.sync().catch(() => {});
166-
}
167164
setCurrentIdentity(await resolveIdentity(c));
168165
await s.getState().refreshIdentities();
169166
});
167+
168+
// Identity onboarding (create / claim) publishes an Identity event,
169+
// which flows through onContentCreated. Re-resolve so the gate
170+
// flips from onboarding → app once the user completes signup.
171+
c.events.onContentCreated(async () => {
172+
if (cancelled) return;
173+
setCurrentIdentity(await resolveIdentity(c));
174+
});
170175
} catch (err) {
171176
if (!cancelled) {
172177
console.error('Failed to initialize PolycentricProvider:', err);

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export type PostData = {
1818
id: string;
1919
content: string;
2020
authorPublicKey: types.PublicKey;
21+
/**
22+
* v2 identity key (hex sha256 of the initial Identity content) that the
23+
* author signed this post on behalf of. Undefined for legacy v1 posts,
24+
* which pre-date identity separation.
25+
*/
26+
authorIdentity?: string;
2127
timestamp: number;
2228
parentAuthorPublicKey?: types.PublicKey;
2329
parentProcess?: types.Process;
@@ -144,6 +150,7 @@ export function decodeV2PostBundle(bundle: v2.EventBundle): PostData | null {
144150
keyType: key.signedBy.keyType,
145151
key: authorKey,
146152
}),
153+
authorIdentity: key.identity,
147154
timestamp: Number(event.createdAt),
148155
// v1 compat shim — store expects types.SignedEvent
149156
signedEvent: types.SignedEvent.create({
@@ -260,6 +267,11 @@ export function stringURLSafeToPublicKey(str: string): types.PublicKey {
260267
return stringToPublicKey(decodeURIComponent(str));
261268
}
262269

270+
/**
271+
* @deprecated misnamed — this returns a short base64 form of the signer's
272+
* public key, not the identity id. Use {@link shortenIdentityId} or render
273+
* the v2 `key.identity` string directly.
274+
*/
263275
export function getIdentityId(publicKey: types.PublicKey): string {
264276
const bytes = publicKey.key ?? new Uint8Array();
265277
if (bytes.length === 0) return '...';
@@ -270,6 +282,18 @@ export function getIdentityIdShort(publicKey: types.PublicKey): string {
270282
return getIdentityId(publicKey).slice(0, 4);
271283
}
272284

285+
/**
286+
* Short display form of a v2 identity id (hex sha256 of the initial
287+
* Identity content). Returns a placeholder if the id is empty.
288+
*/
289+
export function shortenIdentityId(
290+
identity: string | undefined,
291+
len = 10,
292+
): string {
293+
if (!identity) return '...';
294+
return identity.slice(0, len);
295+
}
296+
273297
export function pointerToURLString(pointer: types.Pointer): string {
274298
const systemStr = publicKeyToString(
275299
pointer.system ?? types.PublicKey.create(),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export {
5555
stringURLSafeToPublicKey,
5656
getIdentityId,
5757
getIdentityIdShort,
58+
shortenIdentityId,
5859
pointerToURLString,
5960
urlStringToPointer,
6061
signedEventToHex,

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMemo, useState, useEffect } from 'react';
22
import { types } from '@polycentric/react-native';
33
import {
4+
usePolycentricContext,
45
useCurrentIdentity,
56
useUsername,
67
useProfile,
@@ -11,17 +12,22 @@ import {
1112
import {
1213
getIdentityId,
1314
identiconUrl,
15+
shortenIdentityId,
1416
stringURLSafeToPublicKey,
1517
} from './helpers';
18+
import { useStore } from './store';
1619

1720
export type ProfileScreenData = {
1821
publicKey: types.PublicKey;
22+
/** v2 identity id (hex) for this profile, or null if none can be resolved. */
23+
identityKey: string | null;
1924
isSelf: boolean;
2025
username: string;
2126
profile: ReturnType<typeof useProfile>;
2227
authorFeed: ReturnType<typeof useAuthorFeed>;
2328
likesFeed: ReturnType<typeof useLikesFeed>;
2429
followStatus: ReturnType<typeof useFollowStatus>;
30+
/** Short display string — identity id when known, pubkey short otherwise. */
2531
short: string;
2632
avatarUrl: string;
2733
activeFeed: 'posts' | 'likes';
@@ -40,7 +46,7 @@ export function useProfileScreenData(
4046
[publicKeyParam],
4147
);
4248

43-
const { isCurrentIdentity } = useCurrentIdentity();
49+
const { isCurrentIdentity, identity: selfIdentity } = useCurrentIdentity();
4450
const isSelf = isCurrentIdentity(publicKey);
4551
const getIsAborted = options?.getIsAborted;
4652

@@ -50,7 +56,27 @@ export function useProfileScreenData(
5056
const likesFeed = useLikesFeed({ enabled: isSelf, getIsAborted });
5157
const followStatus = useFollowStatus(publicKey);
5258

53-
const short = getIdentityId(publicKey);
59+
// Resolve an identity id for this profile. For self we already know it
60+
// from the client; for other users we scan locally-known posts for the
61+
// first one signed by this public key.
62+
const { store } = usePolycentricContext();
63+
const postIdentity = useStore(store, (state) => {
64+
for (const post of Object.values(state.posts)) {
65+
const key = post.decoded.authorPublicKey.key;
66+
if (key && bytesEqual(key, publicKey.key ?? new Uint8Array())) {
67+
return post.decoded.authorIdentity ?? null;
68+
}
69+
}
70+
return null;
71+
});
72+
73+
const identityKey = isSelf
74+
? (selfIdentity?.identityKey ?? null)
75+
: postIdentity;
76+
77+
const short = identityKey
78+
? shortenIdentityId(identityKey)
79+
: getIdentityId(publicKey);
5480
const avatarUrl = identiconUrl(publicKey);
5581

5682
const [activeFeed, setActiveFeed] = useState<'posts' | 'likes'>('posts');
@@ -60,6 +86,7 @@ export function useProfileScreenData(
6086

6187
return {
6288
publicKey,
89+
identityKey,
6390
isSelf,
6491
username,
6592
profile,
@@ -72,3 +99,11 @@ export function useProfileScreenData(
7299
setActiveFeed,
73100
};
74101
}
102+
103+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
104+
if (a.length !== b.length) return false;
105+
for (let i = 0; i < a.length; i++) {
106+
if (a[i] !== b[i]) return false;
107+
}
108+
return true;
109+
}

apps/polycentric/src/common/navigation/IndexScreen.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { View, ActivityIndicator } from 'react-native';
33
import { usePolycentricContext } from '@/src/common/lib/polycentric-hooks';
44

55
export default function IndexScreen() {
6-
const { client, isLoading, isReady } = usePolycentricContext();
6+
const { client, currentIdentity, isLoading, isReady } =
7+
usePolycentricContext();
78

89
if (isLoading || !isReady || !client) {
910
return (
@@ -13,11 +14,13 @@ export default function IndexScreen() {
1314
);
1415
}
1516

16-
// TODO: re-enable onboarding flow when ready
17-
// const hasIdentity = client.currentIdentity !== null;
18-
// if (!hasIdentity) {
19-
// return <Redirect href="/(onboarding)" />;
20-
// }
17+
// Gate on the resolved identity, not just the localStorage hint
18+
// (`activeIdentityKey`). If the hint points at an identity we don't have
19+
// content for, `currentIdentity` is null and we still send the user to
20+
// onboarding — self-heals stale state from earlier builds.
21+
if (!currentIdentity) {
22+
return <Redirect href="/(onboarding)" />;
23+
}
2124

2225
return <Redirect href="/(tabs)/feed" />;
2326
}

0 commit comments

Comments
 (0)