Skip to content

Commit

Permalink
Merge branch '40-redux'
Browse files Browse the repository at this point in the history
  • Loading branch information
chmac committed Oct 17, 2024
2 parents 3a51901 + ab1d676 commit a19c729
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 54 deletions.
50 changes: 9 additions & 41 deletions nr-app/app/(tabs)/list.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// import Ionicons from "@expo/vector-icons/Ionicons";
import {
Button,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
ScrollView,
SafeAreaView,
View,
} from "react-native";

import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { addEvent, eventsSelectors } from "@/redux/slices/events.slice";
import { startSubscription } from "@/redux/actions/subscription.actions";
import { useAppDispatch, useAppSelector } from "@/redux/hooks";

import { Relay, finalizeEvent, verifyEvent } from "nostr-tools";
import { hexToBytes } from "@noble/hashes/utils";
import { generateSeedWords, accountFromSeedWords } from "nip06";
import { eventsSelectors } from "@/redux/slices/events.slice";

export default function TabTwoScreen() {
const events = useAppSelector(eventsSelectors.selectAll);
Expand All @@ -31,41 +28,12 @@ export default function TabTwoScreen() {
<Button
title="Load 10 notes"
onPress={async () => {
const { mnemonic } = generateSeedWords();
const account = accountFromSeedWords({ mnemonic });
console.log("#0GAjcE Generated seed and private key", {
mnemonic,
account,
});
const eventTemplate = {
kind: 0,
created_at: Math.round(Date.now() / 1e3),
content: JSON.stringify({ name: "Aarhus" }),
tags: [],
};
const event = finalizeEvent(
eventTemplate,
hexToBytes(account.privateKey.hex),
dispatch(
startSubscription({
filter: { kinds: [397], limit: 10 },
relayUrls: ["wss://relay.damus.io"],
}),
);
console.log("#cBiwGN Signed event", event);
const verificationResult = verifyEvent(event);
console.log("#QPwp7w Verification result", verificationResult);

const relay_uri = "wss://relay.damus.io";
const relay = new Relay(relay_uri);
await relay.connect();
const sub = relay.subscribe([{ kinds: [397], limit: 10 }], {
onevent: (event) =>
void dispatch(
addEvent({
event,
fromRelay: relay_uri,
}),
),
oneose: () => {
sub.close();
},
});
}}
/>
</View>
Expand Down
4 changes: 4 additions & 0 deletions nr-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ globalThis.crypto = {
getRandomValues,
} as any;

import { store } from "@/redux/store";
import { injectStore } from "./redux/sagas/subscriptions.saga";
injectStore(store);

import "expo-router/entry";
20 changes: 20 additions & 0 deletions nr-app/src/nostr/relays.nostr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Relay } from "nostr-tools";

const relayMap = new Map<string, Relay>();

export async function getRelay(url: string): Promise<Relay> {
if (relayMap.has(url)) {
const relay = relayMap.get(url)!;
await relay.connect();
return relay;
}
const relay = new Relay(url);
relayMap.set(url, relay);
await relay.connect();
return relay;
}

export function getAllRelays(): Relay[] {
const relays = Array.from(relayMap.values());
return relays;
}
69 changes: 69 additions & 0 deletions nr-app/src/nostr/subscriptions.nostr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { addEvent } from "@/redux/slices/events.slice";
import { setSubscriptionHasSeenEOSE } from "@/redux/slices/relays.slice";
import { Event, Filter } from "nostr-tools";
import { Subscription } from "nostr-tools/lib/types/abstract-relay";
import { getRelay } from "./relays.nostr";

const subscriptions = new Map<string, Subscription>();

function generateId() {
return Math.random().toString().slice(2);
}

/**
* - The relays are not linked to redux currently
* - We should set it up to log relay state into redux
* - Then we can push subscription state
* - Then we can update subscription state
*/

export async function subscribeToFilter({
filter,
relayUrl,
subscriptionId,
store,
}: {
filter: Filter;
relayUrl: string;
subscriptionId?: string;
store: any;
}) {
const relay = await getRelay(relayUrl);

const id =
typeof subscriptionId === "string" && subscriptionId.length > 3
? subscriptionId
: generateId();

const subscription = relay.subscribe([filter], {
id,
onevent: (event: Event) => {
store.dispatch(addEvent({ event, fromRelay: relayUrl }));
},
oneose: () => {
store.dispatch(setSubscriptionHasSeenEOSE({ id, relayUrl }));
},
// onevent,
// oneose,
// onevent: (event: Event) => {
// store.dispatch(addEvent({ event, fromRelay: relayUrl }));
// },
// oneose: () => {
// store.dispatch(setSubscriptionHasSeenEOSE({ id, relayUrl }));
// },
// NOTE: Type casting here because `id` is not available on `.subscribe()`
// https://github.com/nbd-wtf/nostr-tools/issues/439
} as {});

subscriptions.set(id, subscription);

return id;
}

export function getSubscription(id: string) {
const subscription = subscriptions.get(id);
if (typeof subscription === "undefined") {
throw new Error("Tried to get invalid subscription by ID #MITKA7");
}
return subscription;
}
12 changes: 12 additions & 0 deletions nr-app/src/redux/actions/subscription.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createAction } from "@reduxjs/toolkit";
import { Filter } from "nostr-tools";

export const startSubscription = createAction<{
filter: Filter;
id?: string;
relayUrls?: string[];
}>("subscriptions/startSubscription");

export const stopSubscription = createAction<string>(
"subscriptions/stopSubscription",
);
12 changes: 8 additions & 4 deletions nr-app/src/redux/sagas/map.saga.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PayloadAction } from "@reduxjs/toolkit";
import { put, takeEvery } from "redux-saga/effects";
import { all, put, takeEvery } from "redux-saga/effects";
import {
setMapSubscriptionIsUpdating,
setVisiblePlusCodes,
} from "../slices/map.slice";

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

export function* mapSaga() {
yield takeEvery(setVisiblePlusCodes, updateDataForMap);
export function* updateDataForMapSaga() {
yield takeEvery(setVisiblePlusCodes, updateDataForMapSagaEffect);
}

export default function* mapSaga() {
yield all([updateDataForMapSaga()]);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion nr-app/src/redux/sagas/root.saga.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { all } from "redux-saga/effects";
import mapSaga from "./map.saga";
import subscriptionSaga from "./subscriptions.saga";

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

export default function* rootSaga() {
yield all([helloSaga()]);
yield all([helloSaga(), mapSaga(), subscriptionSaga()]);
}
62 changes: 58 additions & 4 deletions nr-app/src/redux/sagas/subscriptions.saga.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,63 @@
import { takeEvery } from "redux-saga/effects";
import {
getSubscription,
subscribeToFilter,
} from "@/nostr/subscriptions.nostr";
import { Subscription } from "nostr-tools/lib/types/abstract-relay";
import { all, call, fork, StrictEffect, takeEvery } from "redux-saga/effects";
import {
startSubscription,
stopSubscription,
} from "../actions/subscription.actions";
import { AppStore } from "../store";

function* subscriptionSagaWorker() {
// Do something
// NOTE: This pattern is required to avoid a circular import dependency
let store: AppStore;
export function injectStore(_store: AppStore) {
store = _store;
}

function getRelayUrlsOrDefaults(relayUrls?: string[]) {
if (typeof relayUrls === "undefined" || relayUrls.length === 0) {
// TODO: Get defaults from redux
const defaultRelayUrls = ["wss://nos.lol"];
return defaultRelayUrls;
}

return relayUrls;
}

function* startSubscriptionSagaEffect(
action: ReturnType<typeof startSubscription>,
) {
const { filter, id, relayUrls } = action.payload;

const actualRelayUrls = getRelayUrlsOrDefaults(relayUrls);

for (const relayUrl of actualRelayUrls) {
yield fork(subscribeToFilter, {
filter,
relayUrl,
subscriptionId: id,
store,
});
}
}

export function* startSubscriptionSaga() {
yield takeEvery(startSubscription, startSubscriptionSagaEffect);
}

function* stopSubscriptionSagaEffect(
action: ReturnType<typeof stopSubscription>,
): Generator<StrictEffect, void, Subscription> {
const subscription = yield call(getSubscription, action.payload);
yield call(subscription.close);
}

function* stopSubscriptionSaga() {
yield takeEvery(stopSubscription, stopSubscriptionSagaEffect);
}

export default function* subscriptionSaga() {
yield takeEvery("some_action", subscriptionSagaWorker);
yield all([startSubscriptionSaga(), stopSubscriptionSaga()]);
}
34 changes: 31 additions & 3 deletions nr-app/src/redux/slices/relays.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type Subscription = {
query: Filter[];
relaysStatus: {
[relayUrl: string]: {
haveSeenEOSE: boolean;
hasSeenEOSE: boolean;
isOpen: boolean;
serverCloseMessage?: string;
};
Expand All @@ -41,7 +41,7 @@ const initialState: RelaysState = {
subscriptions: {},
};

const profileSlice = createSlice({
const relaysSlice = createSlice({
name: SLICE_NAME,
initialState,
reducers: {
Expand Down Expand Up @@ -72,6 +72,25 @@ const profileSlice = createSlice({
const subscription = action.payload;
state.subscriptions[subscription.id] = subscription;
},
setSubscriptionHasSeenEOSE: (
state,
action: PayloadAction<{ id: string; relayUrl: string }>,
) => {
const { id, relayUrl } = action.payload;
const subscription = state.subscriptions[id];
if (typeof subscription === "undefined") {
throw new Error(
"Unable to set hasSeenEOSE on invalid subscription ID #AQ4WZB",
);
}
const relayStatus = subscription.relaysStatus[relayUrl];
if (typeof relayStatus === "undefined") {
throw new Error(
"Unable to set hasSeenEOSE on invalid relay URL #WFAGJN",
);
}
relayStatus.hasSeenEOSE = true;
},
setServerClosedMessage: (
state,
action: PayloadAction<{
Expand All @@ -90,4 +109,13 @@ const profileSlice = createSlice({
},
});

export default profileSlice.reducer;
export default relaysSlice.reducer;

export const {
setRelays,
setRelayConnected,
addRelayNotice,
setSubscription,
setSubscriptionHasSeenEOSE,
setServerClosedMessage,
} = relaysSlice.actions;
9 changes: 8 additions & 1 deletion nr-app/src/redux/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";

import rootSaga from "./sagas/root.saga";
import {
SLICE_NAME as eventsName,
default as eventsReducer,
Expand All @@ -9,19 +10,25 @@ import {
SLICE_NAME as mapName,
default as mapReducer,
} from "./slices/map.slice";
import rootSaga from "./sagas/root.saga";
import {
SLICE_NAME as relayName,
default as relayReducer,
} from "./slices/relays.slice";

const sagaMiddleware = createSagaMiddleware();

export const store = configureStore({
reducer: {
[eventsName]: eventsReducer,
[mapName]: mapReducer,
[relayName]: relayReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware),
});

export type AppStore = typeof store;

sagaMiddleware.run(rootSaga);

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

0 comments on commit a19c729

Please sign in to comment.