Skip to content

Commit a19c729

Browse files
committed
Merge branch '40-redux'
2 parents 3a51901 + ab1d676 commit a19c729

File tree

10 files changed

+222
-54
lines changed

10 files changed

+222
-54
lines changed

nr-app/app/(tabs)/list.tsx

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
// import Ionicons from "@expo/vector-icons/Ionicons";
22
import {
33
Button,
4+
SafeAreaView,
5+
ScrollView,
46
StyleSheet,
57
Text,
6-
ScrollView,
7-
SafeAreaView,
88
View,
99
} from "react-native";
1010

1111
import { ThemedText } from "@/components/ThemedText";
1212
import { ThemedView } from "@/components/ThemedView";
13-
import { addEvent, eventsSelectors } from "@/redux/slices/events.slice";
13+
import { startSubscription } from "@/redux/actions/subscription.actions";
1414
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
15-
16-
import { Relay, finalizeEvent, verifyEvent } from "nostr-tools";
17-
import { hexToBytes } from "@noble/hashes/utils";
18-
import { generateSeedWords, accountFromSeedWords } from "nip06";
15+
import { eventsSelectors } from "@/redux/slices/events.slice";
1916

2017
export default function TabTwoScreen() {
2118
const events = useAppSelector(eventsSelectors.selectAll);
@@ -31,41 +28,12 @@ export default function TabTwoScreen() {
3128
<Button
3229
title="Load 10 notes"
3330
onPress={async () => {
34-
const { mnemonic } = generateSeedWords();
35-
const account = accountFromSeedWords({ mnemonic });
36-
console.log("#0GAjcE Generated seed and private key", {
37-
mnemonic,
38-
account,
39-
});
40-
const eventTemplate = {
41-
kind: 0,
42-
created_at: Math.round(Date.now() / 1e3),
43-
content: JSON.stringify({ name: "Aarhus" }),
44-
tags: [],
45-
};
46-
const event = finalizeEvent(
47-
eventTemplate,
48-
hexToBytes(account.privateKey.hex),
31+
dispatch(
32+
startSubscription({
33+
filter: { kinds: [397], limit: 10 },
34+
relayUrls: ["wss://relay.damus.io"],
35+
}),
4936
);
50-
console.log("#cBiwGN Signed event", event);
51-
const verificationResult = verifyEvent(event);
52-
console.log("#QPwp7w Verification result", verificationResult);
53-
54-
const relay_uri = "wss://relay.damus.io";
55-
const relay = new Relay(relay_uri);
56-
await relay.connect();
57-
const sub = relay.subscribe([{ kinds: [397], limit: 10 }], {
58-
onevent: (event) =>
59-
void dispatch(
60-
addEvent({
61-
event,
62-
fromRelay: relay_uri,
63-
}),
64-
),
65-
oneose: () => {
66-
sub.close();
67-
},
68-
});
6937
}}
7038
/>
7139
</View>

nr-app/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ globalThis.crypto = {
1313
getRandomValues,
1414
} as any;
1515

16+
import { store } from "@/redux/store";
17+
import { injectStore } from "./redux/sagas/subscriptions.saga";
18+
injectStore(store);
19+
1620
import "expo-router/entry";

nr-app/src/nostr/relays.nostr.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Relay } from "nostr-tools";
2+
3+
const relayMap = new Map<string, Relay>();
4+
5+
export async function getRelay(url: string): Promise<Relay> {
6+
if (relayMap.has(url)) {
7+
const relay = relayMap.get(url)!;
8+
await relay.connect();
9+
return relay;
10+
}
11+
const relay = new Relay(url);
12+
relayMap.set(url, relay);
13+
await relay.connect();
14+
return relay;
15+
}
16+
17+
export function getAllRelays(): Relay[] {
18+
const relays = Array.from(relayMap.values());
19+
return relays;
20+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { addEvent } from "@/redux/slices/events.slice";
2+
import { setSubscriptionHasSeenEOSE } from "@/redux/slices/relays.slice";
3+
import { Event, Filter } from "nostr-tools";
4+
import { Subscription } from "nostr-tools/lib/types/abstract-relay";
5+
import { getRelay } from "./relays.nostr";
6+
7+
const subscriptions = new Map<string, Subscription>();
8+
9+
function generateId() {
10+
return Math.random().toString().slice(2);
11+
}
12+
13+
/**
14+
* - The relays are not linked to redux currently
15+
* - We should set it up to log relay state into redux
16+
* - Then we can push subscription state
17+
* - Then we can update subscription state
18+
*/
19+
20+
export async function subscribeToFilter({
21+
filter,
22+
relayUrl,
23+
subscriptionId,
24+
store,
25+
}: {
26+
filter: Filter;
27+
relayUrl: string;
28+
subscriptionId?: string;
29+
store: any;
30+
}) {
31+
const relay = await getRelay(relayUrl);
32+
33+
const id =
34+
typeof subscriptionId === "string" && subscriptionId.length > 3
35+
? subscriptionId
36+
: generateId();
37+
38+
const subscription = relay.subscribe([filter], {
39+
id,
40+
onevent: (event: Event) => {
41+
store.dispatch(addEvent({ event, fromRelay: relayUrl }));
42+
},
43+
oneose: () => {
44+
store.dispatch(setSubscriptionHasSeenEOSE({ id, relayUrl }));
45+
},
46+
// onevent,
47+
// oneose,
48+
// onevent: (event: Event) => {
49+
// store.dispatch(addEvent({ event, fromRelay: relayUrl }));
50+
// },
51+
// oneose: () => {
52+
// store.dispatch(setSubscriptionHasSeenEOSE({ id, relayUrl }));
53+
// },
54+
// NOTE: Type casting here because `id` is not available on `.subscribe()`
55+
// https://github.com/nbd-wtf/nostr-tools/issues/439
56+
} as {});
57+
58+
subscriptions.set(id, subscription);
59+
60+
return id;
61+
}
62+
63+
export function getSubscription(id: string) {
64+
const subscription = subscriptions.get(id);
65+
if (typeof subscription === "undefined") {
66+
throw new Error("Tried to get invalid subscription by ID #MITKA7");
67+
}
68+
return subscription;
69+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createAction } from "@reduxjs/toolkit";
2+
import { Filter } from "nostr-tools";
3+
4+
export const startSubscription = createAction<{
5+
filter: Filter;
6+
id?: string;
7+
relayUrls?: string[];
8+
}>("subscriptions/startSubscription");
9+
10+
export const stopSubscription = createAction<string>(
11+
"subscriptions/stopSubscription",
12+
);

nr-app/src/redux/sagas/map.saga.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { PayloadAction } from "@reduxjs/toolkit";
2-
import { put, takeEvery } from "redux-saga/effects";
2+
import { all, put, takeEvery } from "redux-saga/effects";
33
import {
44
setMapSubscriptionIsUpdating,
55
setVisiblePlusCodes,
66
} from "../slices/map.slice";
77

8-
function* updateDataForMap(action: PayloadAction<string[]>) {
8+
function* updateDataForMapSagaEffect(action: PayloadAction<string[]>) {
99
try {
1010
// Setup a subscription
1111
const visiblePlusCodes = action.payload;
@@ -19,8 +19,12 @@ function* updateDataForMap(action: PayloadAction<string[]>) {
1919
}
2020
}
2121

22-
export function* mapSaga() {
23-
yield takeEvery(setVisiblePlusCodes, updateDataForMap);
22+
export function* updateDataForMapSaga() {
23+
yield takeEvery(setVisiblePlusCodes, updateDataForMapSagaEffect);
24+
}
25+
26+
export default function* mapSaga() {
27+
yield all([updateDataForMapSaga()]);
2428
}
2529

2630
/**

nr-app/src/redux/sagas/root.saga.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { all } from "redux-saga/effects";
2+
import mapSaga from "./map.saga";
3+
import subscriptionSaga from "./subscriptions.saga";
24

35
function* helloSaga() {
46
console.log("#W0W1gS Hello from the hello saga");
57
}
68

79
export default function* rootSaga() {
8-
yield all([helloSaga()]);
10+
yield all([helloSaga(), mapSaga(), subscriptionSaga()]);
911
}
Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,63 @@
1-
import { takeEvery } from "redux-saga/effects";
1+
import {
2+
getSubscription,
3+
subscribeToFilter,
4+
} from "@/nostr/subscriptions.nostr";
5+
import { Subscription } from "nostr-tools/lib/types/abstract-relay";
6+
import { all, call, fork, StrictEffect, takeEvery } from "redux-saga/effects";
7+
import {
8+
startSubscription,
9+
stopSubscription,
10+
} from "../actions/subscription.actions";
11+
import { AppStore } from "../store";
212

3-
function* subscriptionSagaWorker() {
4-
// Do something
13+
// NOTE: This pattern is required to avoid a circular import dependency
14+
let store: AppStore;
15+
export function injectStore(_store: AppStore) {
16+
store = _store;
17+
}
18+
19+
function getRelayUrlsOrDefaults(relayUrls?: string[]) {
20+
if (typeof relayUrls === "undefined" || relayUrls.length === 0) {
21+
// TODO: Get defaults from redux
22+
const defaultRelayUrls = ["wss://nos.lol"];
23+
return defaultRelayUrls;
24+
}
25+
26+
return relayUrls;
27+
}
28+
29+
function* startSubscriptionSagaEffect(
30+
action: ReturnType<typeof startSubscription>,
31+
) {
32+
const { filter, id, relayUrls } = action.payload;
33+
34+
const actualRelayUrls = getRelayUrlsOrDefaults(relayUrls);
35+
36+
for (const relayUrl of actualRelayUrls) {
37+
yield fork(subscribeToFilter, {
38+
filter,
39+
relayUrl,
40+
subscriptionId: id,
41+
store,
42+
});
43+
}
44+
}
45+
46+
export function* startSubscriptionSaga() {
47+
yield takeEvery(startSubscription, startSubscriptionSagaEffect);
48+
}
49+
50+
function* stopSubscriptionSagaEffect(
51+
action: ReturnType<typeof stopSubscription>,
52+
): Generator<StrictEffect, void, Subscription> {
53+
const subscription = yield call(getSubscription, action.payload);
54+
yield call(subscription.close);
55+
}
56+
57+
function* stopSubscriptionSaga() {
58+
yield takeEvery(stopSubscription, stopSubscriptionSagaEffect);
559
}
660

761
export default function* subscriptionSaga() {
8-
yield takeEvery("some_action", subscriptionSagaWorker);
62+
yield all([startSubscriptionSaga(), stopSubscriptionSaga()]);
963
}

nr-app/src/redux/slices/relays.slice.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type Subscription = {
2020
query: Filter[];
2121
relaysStatus: {
2222
[relayUrl: string]: {
23-
haveSeenEOSE: boolean;
23+
hasSeenEOSE: boolean;
2424
isOpen: boolean;
2525
serverCloseMessage?: string;
2626
};
@@ -41,7 +41,7 @@ const initialState: RelaysState = {
4141
subscriptions: {},
4242
};
4343

44-
const profileSlice = createSlice({
44+
const relaysSlice = createSlice({
4545
name: SLICE_NAME,
4646
initialState,
4747
reducers: {
@@ -72,6 +72,25 @@ const profileSlice = createSlice({
7272
const subscription = action.payload;
7373
state.subscriptions[subscription.id] = subscription;
7474
},
75+
setSubscriptionHasSeenEOSE: (
76+
state,
77+
action: PayloadAction<{ id: string; relayUrl: string }>,
78+
) => {
79+
const { id, relayUrl } = action.payload;
80+
const subscription = state.subscriptions[id];
81+
if (typeof subscription === "undefined") {
82+
throw new Error(
83+
"Unable to set hasSeenEOSE on invalid subscription ID #AQ4WZB",
84+
);
85+
}
86+
const relayStatus = subscription.relaysStatus[relayUrl];
87+
if (typeof relayStatus === "undefined") {
88+
throw new Error(
89+
"Unable to set hasSeenEOSE on invalid relay URL #WFAGJN",
90+
);
91+
}
92+
relayStatus.hasSeenEOSE = true;
93+
},
7594
setServerClosedMessage: (
7695
state,
7796
action: PayloadAction<{
@@ -90,4 +109,13 @@ const profileSlice = createSlice({
90109
},
91110
});
92111

93-
export default profileSlice.reducer;
112+
export default relaysSlice.reducer;
113+
114+
export const {
115+
setRelays,
116+
setRelayConnected,
117+
addRelayNotice,
118+
setSubscription,
119+
setSubscriptionHasSeenEOSE,
120+
setServerClosedMessage,
121+
} = relaysSlice.actions;

nr-app/src/redux/store.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { configureStore } from "@reduxjs/toolkit";
22
import createSagaMiddleware from "redux-saga";
33

4+
import rootSaga from "./sagas/root.saga";
45
import {
56
SLICE_NAME as eventsName,
67
default as eventsReducer,
@@ -9,19 +10,25 @@ import {
910
SLICE_NAME as mapName,
1011
default as mapReducer,
1112
} from "./slices/map.slice";
12-
import rootSaga from "./sagas/root.saga";
13+
import {
14+
SLICE_NAME as relayName,
15+
default as relayReducer,
16+
} from "./slices/relays.slice";
1317

1418
const sagaMiddleware = createSagaMiddleware();
1519

1620
export const store = configureStore({
1721
reducer: {
1822
[eventsName]: eventsReducer,
1923
[mapName]: mapReducer,
24+
[relayName]: relayReducer,
2025
},
2126
middleware: (getDefaultMiddleware) =>
2227
getDefaultMiddleware().concat(sagaMiddleware),
2328
});
2429

30+
export type AppStore = typeof store;
31+
2532
sagaMiddleware.run(rootSaga);
2633

2734
export type RootState = ReturnType<typeof store.getState>;

0 commit comments

Comments
 (0)