From 3ed7ae3bfea161545e7eda901859a0dc65899104 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 14 Oct 2024 17:30:36 +0200 Subject: [PATCH 01/14] draft --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index ece2c27..aa7529b 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,38 @@ It would be great to at some point connect with BW and Couchers over Nostr. If you see "nostr token", run away, it is a scam. There's no nostr token. There was no nostr ICO, nostr is not a DAO, there is no blockchain. Nostr makes it easy to integrate bitcoin lightning, which may at some point be helpful to for example keep out spammers. But this is not something we are interested in for the foreseeable future. + +## Roadmap + +Goal: 70% of active Trustroots users are on Nostroots by middle of 2026 +- active trustroots users: around 5K active within last month, 70% is around 3.5K ([Trustroots statistics](https://www.trustroots.org/statistics)) +- "are on Nostroots": Have had a Nostroots experience means have some feature use that went well and is associated with Nostroots. The users don't need to recognize Nostr as the protocol, just that something is possible that wasn't before. This could be logging into a different site, transporting some of their network, or interacting with content from a different platform. + +First step: Trial this in Berlin. Largest userbase, close to some of the developers. + +200 users in Berlin, likely around 30 active within the last 6 months. Probably around 5 people requesting hosting every week. + + +The technical side of things are manageable as long as we just care about Trustroots functionality. There are two big challenges for migrating our users. +- telling the story +- finding partners in the ecosystem. + +### Telling the story +Trustroots users skew hippie, alternative, vanguard, experimental, left, gifting. The Nostr userbase is generally cryptocurrency and privacy focused. + +As far as our users are concerned, Trustroots is fine and nothing is broken. So a degradation of their experience will likely only lead to frustration. At best, we can justify inconvenience through appealing to the values of the community. The community also won't care that much about the admins' wish to make Trustroots more maintainable. + +Trustroots users interact with the app when they're looking for something in a new city. That is the moment they're engaged and ready to be excited and we should find a story that works for them. + +Story: +- Trustroots was never meant to be just for hosting. It's meant to enable gifting and sharing based on trust and shared values. +- In a world of companies owning your identity online, Trustroots wants to empower you to own your own identity. +- There's more cool stuff like Trustroots in the world. + + +### Partners in the ecosystem +There are no good partner organisations in the ecosystem. + +"Log in with trustroots" – forward. + +What's the simplest login-with functionality we can implement? \ No newline at end of file From a5f33cfc5875d3a3a348c5bb7e23397733bbe63f Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Oct 2024 11:53:00 +0200 Subject: [PATCH 02/14] update numbers --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa7529b..a34a0b5 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Goal: 70% of active Trustroots users are on Nostroots by middle of 2026 First step: Trial this in Berlin. Largest userbase, close to some of the developers. -200 users in Berlin, likely around 30 active within the last 6 months. Probably around 5 people requesting hosting every week. +200 hosts/maybe hosts in Berlin. Probably around 5 people requesting hosting every week. The technical side of things are manageable as long as we just care about Trustroots functionality. There are two big challenges for migrating our users. @@ -116,8 +116,18 @@ Story: - There's more cool stuff like Trustroots in the world. -### Partners in the ecosystem -There are no good partner organisations in the ecosystem. +### Partners in the ecosystem +We need space-focused organisations in Berlin we can work with: +- Bike Surf Berlin +- Geocaching? +- Party groups? + +There are no good partner organisations in the current Nostr ecosystem. Our best bet will be supportive interested other groups that we build the tech for. So we need to build a good DX for adding logging in. + + +Who will the first 5 users be? + +Log "Log in with trustroots" – forward. From b821f5db6afa2eed38d4e667dea26dfc2ecafecc Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Oct 2024 13:49:50 +0200 Subject: [PATCH 03/14] finished draft --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a34a0b5..c73526e 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Goal: 70% of active Trustroots users are on Nostroots by middle of 2026 First step: Trial this in Berlin. Largest userbase, close to some of the developers. -200 hosts/maybe hosts in Berlin. Probably around 5 people requesting hosting every week. +200 yes/maybe hosts in Berlin. Around 1000 users. Estimate 5-10 people requesting hosting every week. The technical side of things are manageable as long as we just care about Trustroots functionality. There are two big challenges for migrating our users. @@ -108,27 +108,56 @@ Trustroots users skew hippie, alternative, vanguard, experimental, left, gifting As far as our users are concerned, Trustroots is fine and nothing is broken. So a degradation of their experience will likely only lead to frustration. At best, we can justify inconvenience through appealing to the values of the community. The community also won't care that much about the admins' wish to make Trustroots more maintainable. -Trustroots users interact with the app when they're looking for something in a new city. That is the moment they're engaged and ready to be excited and we should find a story that works for them. - -Story: +Trustroots users interact with the app when they're looking for something in a new city. That is the moment they're engaged and ready to be excited and we should find a story that works for them. The core elements of this story should be: - Trustroots was never meant to be just for hosting. It's meant to enable gifting and sharing based on trust and shared values. - In a world of companies owning your identity online, Trustroots wants to empower you to own your own identity. - There's more cool stuff like Trustroots in the world. ### Partners in the ecosystem -We need space-focused organisations in Berlin we can work with: -- Bike Surf Berlin -- Geocaching? -- Party groups? - -There are no good partner organisations in the current Nostr ecosystem. Our best bet will be supportive interested other groups that we build the tech for. So we need to build a good DX for adding logging in. - +We need platforms and communities that work in Berlin, are not money-focused, are valuable to travellers, and encourage personal connection and sharing. There are no good partner organisations in the current Nostr ecosystem. Our best bet will be supportive interested other groups that we build the tech for. So we need to build a good DX for adding logging in. -Who will the first 5 users be? - -Log - -"Log in with trustroots" – forward. - -What's the simplest login-with functionality we can implement? \ No newline at end of file +Possible groups and communities: +- [Bike Surf Berlin](bikesurf.org) +- Geocaching? +- Semi-legal rave groups +- [Couchers](couchers.org) and other hospex platforms + +### Timeline +**Q4 2024:** +- Add functionality on main trustroots site to display and link recommended organisations in Berlin + - at most 3, possibly rotating + - maybe also based on Circles? + - track what gets clicked on + - solicit experience reports and recs for other groups to display +- Build out Trustroots app + - full notes functionality + - "login-with-trustroots" functionality + +**Q1 2025:** +- Add login-with functionality to most promising one partner org +- Add more recommended orgs +- Solicit for some Berlin community management role? + + + + + +### Log in with Nostr/Trustroots +#### User flow +- People search for something in Berlin +- A little sidebar informs them of other services in Berlin they might be interested in + - it includes a mention of the app and ease of using them via the app. +- User downloads app. +- They're onboarded onto Nostr + - private key generated and saved + - public key NIP-5 verified + - profile information published on the Nostr ecosystem (do we need extra consent here?) +- In the app, they can click on a link to an app and get taken straight to the service onto the "edit account" page to fill in missing information. + +#### Technical Flow: +- user clicks a "login with trustroots" button +- it redirects to a trustroots-controlled domain with the redirect url as query param that gets handled by the trustroots app +- client-side, we find the relevant profile event, the NIP-5 verification URL, and any other events we might need for vouching +- all are sent together to a mynewservice.org/nostr-callback URL with the stringified events as request params or data body +- service verfies the events are appropriately signed and that trustroots verified the user, checks if the corresponding public key is already associated with an account, and then signs up/logs in the user. \ No newline at end of file From 92b7d90c20f87f1cc4f5a3fc5559f9bdba46c4f0 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Oct 2024 17:39:20 +0200 Subject: [PATCH 04/14] more updates --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c73526e..2c0c658 100644 --- a/README.md +++ b/README.md @@ -132,15 +132,18 @@ Possible groups and communities: - solicit experience reports and recs for other groups to display - Build out Trustroots app - full notes functionality - - "login-with-trustroots" functionality + - ["login-with-trustroots" functionality](https://nips.nostr.com/46) + - putting more profile data onto Nostr with opt-in **Q1 2025:** - Add login-with functionality to most promising one partner org - Add more recommended orgs - Solicit for some Berlin community management role? +- feed more data into the map - - +**Q2 2025:** +- add nip-46 login to Trustroots app and begin encouraging users to store their nsec outside of the Trustroots app +- add login-with functionality to another partner org ### Log in with Nostr/Trustroots @@ -156,8 +159,9 @@ Possible groups and communities: - In the app, they can click on a link to an app and get taken straight to the service onto the "edit account" page to fill in missing information. #### Technical Flow: -- user clicks a "login with trustroots" button -- it redirects to a trustroots-controlled domain with the redirect url as query param that gets handled by the trustroots app -- client-side, we find the relevant profile event, the NIP-5 verification URL, and any other events we might need for vouching -- all are sent together to a mynewservice.org/nostr-callback URL with the stringified events as request params or data body -- service verfies the events are appropriately signed and that trustroots verified the user, checks if the corresponding public key is already associated with an account, and then signs up/logs in the user. \ No newline at end of file +- partner site embeds javascript we provide on their website +- partner site adds `login-with-nostr` endpoint to their API +- user clicks a "login with trustroots" button on +- nip-46 flow is initiated +- browser sends a signed-by-user event with all user data to `login-with-nostr` api endpoint +- endpoint verifies event is properly signed and logs in the user and updates their user fields, creating their account first if necessary \ No newline at end of file From 963b13af8be5dc05a0c40abcc171e624b41ab47d Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 15 Oct 2024 17:47:57 +0200 Subject: [PATCH 05/14] more draft --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2c0c658..94f6d87 100644 --- a/README.md +++ b/README.md @@ -133,13 +133,13 @@ Possible groups and communities: - Build out Trustroots app - full notes functionality - ["login-with-trustroots" functionality](https://nips.nostr.com/46) - - putting more profile data onto Nostr with opt-in + - putting more profile data onto Nostr with opt-in, starting with Circles **Q1 2025:** - Add login-with functionality to most promising one partner org - Add more recommended orgs - Solicit for some Berlin community management role? -- feed more data into the map +- feed more data into the map and filter by Circles **Q2 2025:** - add nip-46 login to Trustroots app and begin encouraging users to store their nsec outside of the Trustroots app @@ -155,7 +155,7 @@ Possible groups and communities: - They're onboarded onto Nostr - private key generated and saved - public key NIP-5 verified - - profile information published on the Nostr ecosystem (do we need extra consent here?) + - profile information published on the Nostr ecosystem (do we need extra consent here?). - In the app, they can click on a link to an app and get taken straight to the service onto the "edit account" page to fill in missing information. #### Technical Flow: From 193d5f23371fd0fcb146a684bcaa88de175bcdcf Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 13:21:21 +0200 Subject: [PATCH 06/14] Store events with metadata. --- nr-app/app/(tabs)/explore.tsx | 12 +++-- nr-app/src/redux/eventsSlice.ts | 77 +++++++++++++++++++++++++++------ 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/nr-app/app/(tabs)/explore.tsx b/nr-app/app/(tabs)/explore.tsx index d04f04b..0c7bc60 100644 --- a/nr-app/app/(tabs)/explore.tsx +++ b/nr-app/app/(tabs)/explore.tsx @@ -53,7 +53,13 @@ export default function TabTwoScreen() { const relay = new Relay("wss://nos.lol"); await relay.connect(); const sub = relay.subscribe([{ kinds: [0], limit: 10 }], { - onevent: (event) => void dispatch(addEvent(event)), + onevent: (event) => + void dispatch( + addEvent({ + event, + fromRelay: "wss://nos.lol", + }), + ), oneose: () => { sub.close(); }, @@ -64,8 +70,8 @@ export default function TabTwoScreen() { We have a total of {events.length} events. {events.map((event) => ( - - {event.id} + + {event.event.id} {JSON.stringify(event)} ))} diff --git a/nr-app/src/redux/eventsSlice.ts b/nr-app/src/redux/eventsSlice.ts index 411c528..9bd7763 100644 --- a/nr-app/src/redux/eventsSlice.ts +++ b/nr-app/src/redux/eventsSlice.ts @@ -9,8 +9,15 @@ import { RootState } from "./store"; export const SLICE_NAME = "events" as const; -function getStorageId(profileEvent: Event) { - const { id, kind, pubkey } = profileEvent; +type NostrEventWithMetadata = { + event: Event; + metadata: { + seenOnRelays: string[]; + }; +}; + +function getStorageId(nostrEvent: Event) { + const { id, kind, pubkey } = nostrEvent; // Replaceable events if (kind === 0 || (kind >= 10e3 && kind < 20e3)) { @@ -19,7 +26,7 @@ function getStorageId(profileEvent: Event) { // Parameterized replaceable events if (kind >= 30e3 && kind < 40e3) { - const dTag = profileEvent.tags.find(([tagName]) => tagName === "d"); + const dTag = nostrEvent.tags.find(([tagName]) => tagName === "d"); if (typeof dTag !== "undefined") { const [, tagValue] = dTag; const storageId = [pubkey, kind, tagValue].join(ID_SEPARATOR); @@ -30,8 +37,11 @@ function getStorageId(profileEvent: Event) { return id; } -export const eventsAdapter = createEntityAdapter({ - selectId: getStorageId, +export const eventsAdapter = createEntityAdapter< + NostrEventWithMetadata, + string +>({ + selectId: (model) => getStorageId(model.event), }); const localSelectors = eventsAdapter.getSelectors(); @@ -40,25 +50,66 @@ export const eventsSlice = createSlice({ name: SLICE_NAME, initialState: eventsAdapter.getInitialState(), reducers: { - setAllEvents: (state, action: PayloadAction) => - eventsAdapter.setAll(state, action.payload), - addEvent: (state, action: PayloadAction) => { - const event = action.payload; + setAllEvents: (state, action: PayloadAction) => { + const eventsWithMetadata = action.payload.map( + (event): NostrEventWithMetadata => ({ + event, + metadata: { seenOnRelays: [] }, + }), + ); + eventsAdapter.setAll(state, eventsWithMetadata); + }, + addEvent: ( + state, + action: PayloadAction<{ event: Event; fromRelay: string }>, + ) => { + const { event, fromRelay } = action.payload; const id = getStorageId(event); const isExistingEvent = state.ids.includes(id); if (!isExistingEvent) { - return eventsAdapter.setOne(state, event); + const eventWithMetadata = { + event, + metadata: { + seenOnRelays: [fromRelay], + }, + }; + eventsAdapter.setOne(state, eventWithMetadata); + return; } const existingEvent = localSelectors.selectById(state, id); - if (event.created_at > existingEvent.created_at) { - return eventsAdapter.setOne(state, event); + if (existingEvent.event.id === event.id) { + if (existingEvent.metadata.seenOnRelays.includes(fromRelay)) { + return; + } + const metadata = { + ...existingEvent.metadata, + seenOnRelays: existingEvent.metadata.seenOnRelays.includes(fromRelay) + ? existingEvent.metadata.seenOnRelays + : existingEvent.metadata.seenOnRelays.concat(fromRelay), + }; + eventsAdapter.updateOne(state, { + id, + changes: { + metadata, + }, + }); + return; } - return state; + if (event.created_at > existingEvent.event.created_at) { + const eventWithMetadata = { + event, + metadata: { + seenOnRelays: [fromRelay], + }, + }; + eventsAdapter.setOne(state, eventWithMetadata); + return; + } }, }, }); From 948c6b38b7c638c814f423a2c50a4048cf1ef04a Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 13:32:16 +0200 Subject: [PATCH 07/14] Clarity. --- nr-app/src/redux/eventsSlice.ts | 95 ++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/nr-app/src/redux/eventsSlice.ts b/nr-app/src/redux/eventsSlice.ts index 9bd7763..7592c77 100644 --- a/nr-app/src/redux/eventsSlice.ts +++ b/nr-app/src/redux/eventsSlice.ts @@ -9,11 +9,12 @@ import { RootState } from "./store"; export const SLICE_NAME = "events" as const; -type NostrEventWithMetadata = { +type EventMetadata = { + seenOnRelays: string[]; +}; +type EventWithMetadata = { event: Event; - metadata: { - seenOnRelays: string[]; - }; + metadata: EventMetadata; }; function getStorageId(nostrEvent: Event) { @@ -37,76 +38,86 @@ function getStorageId(nostrEvent: Event) { return id; } -export const eventsAdapter = createEntityAdapter< - NostrEventWithMetadata, - string ->({ +export const eventsAdapter = createEntityAdapter({ selectId: (model) => getStorageId(model.event), }); const localSelectors = eventsAdapter.getSelectors(); +export const _hasEventBeenSeenOnRelay = ( + eventWithMetadata: EventWithMetadata, + relayUrl: string, +): boolean => { + return eventWithMetadata.metadata.seenOnRelays.includes(relayUrl); +}; + +export const _addSeenOnRelayToMetadata = ( + metadata: EventMetadata, + seenOnRelay: string, +): EventMetadata => { + if (metadata.seenOnRelays.includes(seenOnRelay)) { + return metadata; + } + const updatedSeenOnRelays = metadata.seenOnRelays.concat(seenOnRelay); + const updatedMetadata = { ...metadata, seenOnRelay: updatedSeenOnRelays }; + return updatedMetadata; +}; + export const eventsSlice = createSlice({ name: SLICE_NAME, initialState: eventsAdapter.getInitialState(), reducers: { - setAllEvents: (state, action: PayloadAction) => { - const eventsWithMetadata = action.payload.map( - (event): NostrEventWithMetadata => ({ - event, - metadata: { seenOnRelays: [] }, - }), - ); - eventsAdapter.setAll(state, eventsWithMetadata); + setAllEventsWithMetadata: ( + state, + action: PayloadAction, + ) => { + eventsAdapter.setAll(state, action.payload); }, addEvent: ( state, action: PayloadAction<{ event: Event; fromRelay: string }>, ) => { - const { event, fromRelay } = action.payload; - const id = getStorageId(event); + const { event, fromRelay: seenOnRelay } = action.payload; + const storageId = getStorageId(event); + + const eventWithMetadata = { + event, + metadata: { + seenOnRelays: [seenOnRelay], + }, + }; - const isExistingEvent = state.ids.includes(id); + const isExistingEvent = state.ids.includes(storageId); if (!isExistingEvent) { - const eventWithMetadata = { - event, - metadata: { - seenOnRelays: [fromRelay], - }, - }; eventsAdapter.setOne(state, eventWithMetadata); return; } - const existingEvent = localSelectors.selectById(state, id); + const existingEvent = localSelectors.selectById(state, storageId); if (existingEvent.event.id === event.id) { - if (existingEvent.metadata.seenOnRelays.includes(fromRelay)) { + if (_hasEventBeenSeenOnRelay(existingEvent, seenOnRelay)) { return; } - const metadata = { - ...existingEvent.metadata, - seenOnRelays: existingEvent.metadata.seenOnRelays.includes(fromRelay) - ? existingEvent.metadata.seenOnRelays - : existingEvent.metadata.seenOnRelays.concat(fromRelay), - }; + + const updatedMetadata = _addSeenOnRelayToMetadata( + existingEvent.metadata, + seenOnRelay, + ); eventsAdapter.updateOne(state, { - id, + id: storageId, changes: { - metadata, + metadata: updatedMetadata, }, }); return; } - if (event.created_at > existingEvent.event.created_at) { - const eventWithMetadata = { - event, - metadata: { - seenOnRelays: [fromRelay], - }, - }; + const isUpdatedVersionOfEvent = + event.created_at > existingEvent.event.created_at; + + if (isUpdatedVersionOfEvent) { eventsAdapter.setOne(state, eventWithMetadata); return; } From dc79be978f3422c6c3c88c57b87ccaa238bea572 Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 13:33:38 +0200 Subject: [PATCH 08/14] Bugfix. Correct export. --- nr-app/src/redux/eventsSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nr-app/src/redux/eventsSlice.ts b/nr-app/src/redux/eventsSlice.ts index 7592c77..21c6862 100644 --- a/nr-app/src/redux/eventsSlice.ts +++ b/nr-app/src/redux/eventsSlice.ts @@ -127,7 +127,7 @@ export const eventsSlice = createSlice({ export default eventsSlice.reducer; -export const { addEvent, setAllEvents } = eventsSlice.actions; +export const { addEvent, setAllEventsWithMetadata } = eventsSlice.actions; export const eventsSelectors = eventsAdapter.getSelectors( (state: RootState) => state[SLICE_NAME], From 57d88cf87b21ebe81faee817b984f69dced7b19b Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 13:39:57 +0200 Subject: [PATCH 09/14] Log map movements. --- nr-app/src/components/Map.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nr-app/src/components/Map.tsx b/nr-app/src/components/Map.tsx index 59b1c84..4bbeb3d 100644 --- a/nr-app/src/components/Map.tsx +++ b/nr-app/src/components/Map.tsx @@ -5,7 +5,12 @@ import MapView, { Marker } from "react-native-maps"; export default function Map() { return ( - + { + console.log("#rIMmxg Map move completed", region, details); + }} + > From 51b9707effe55b6da4b1ffc4b611244e2c1a5363 Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 13:59:24 +0200 Subject: [PATCH 10/14] Folder for slices. --- nr-app/app/(tabs)/explore.tsx | 2 +- nr-app/src/redux/actions/map.actions.ts | 3 +++ .../src/redux/{eventsSlice.ts => slices/events.slice.ts} | 2 +- nr-app/src/redux/{mapSlice.ts => slices/map.slice.ts} | 2 +- .../src/redux/{relaysSlice.ts => slices/relays.slice.ts} | 0 nr-app/src/redux/store.ts | 7 +++++-- 6 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 nr-app/src/redux/actions/map.actions.ts rename nr-app/src/redux/{eventsSlice.ts => slices/events.slice.ts} (98%) rename nr-app/src/redux/{mapSlice.ts => slices/map.slice.ts} (94%) rename nr-app/src/redux/{relaysSlice.ts => slices/relays.slice.ts} (100%) diff --git a/nr-app/app/(tabs)/explore.tsx b/nr-app/app/(tabs)/explore.tsx index 0c7bc60..9a20490 100644 --- a/nr-app/app/(tabs)/explore.tsx +++ b/nr-app/app/(tabs)/explore.tsx @@ -4,7 +4,7 @@ import { Button, StyleSheet, Text, View } from "react-native"; import ParallaxScrollView from "@/components/ParallaxScrollView"; import { ThemedText } from "@/components/ThemedText"; import { ThemedView } from "@/components/ThemedView"; -import { addEvent, eventsSelectors } from "@/redux/eventsSlice"; +import { addEvent, eventsSelectors } from "@/redux/slices/events.slice"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { Relay, finalizeEvent, verifyEvent } from "nostr-tools"; diff --git a/nr-app/src/redux/actions/map.actions.ts b/nr-app/src/redux/actions/map.actions.ts new file mode 100644 index 0000000..f1628c4 --- /dev/null +++ b/nr-app/src/redux/actions/map.actions.ts @@ -0,0 +1,3 @@ +import { createAction } from "@reduxjs/toolkit"; + +export const setVisiblePlusCodes = createAction("SET_VISIBLE_PLUS_CODES"); diff --git a/nr-app/src/redux/eventsSlice.ts b/nr-app/src/redux/slices/events.slice.ts similarity index 98% rename from nr-app/src/redux/eventsSlice.ts rename to nr-app/src/redux/slices/events.slice.ts index 21c6862..05b70ce 100644 --- a/nr-app/src/redux/eventsSlice.ts +++ b/nr-app/src/redux/slices/events.slice.ts @@ -5,7 +5,7 @@ import { createSlice, PayloadAction, } from "@reduxjs/toolkit"; -import { RootState } from "./store"; +import { RootState } from "../store"; export const SLICE_NAME = "events" as const; diff --git a/nr-app/src/redux/mapSlice.ts b/nr-app/src/redux/slices/map.slice.ts similarity index 94% rename from nr-app/src/redux/mapSlice.ts rename to nr-app/src/redux/slices/map.slice.ts index 5447509..7fce84d 100644 --- a/nr-app/src/redux/mapSlice.ts +++ b/nr-app/src/redux/slices/map.slice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { RootState } from "./store"; +import { RootState } from "../store"; export const SLICE_NAME = "map"; diff --git a/nr-app/src/redux/relaysSlice.ts b/nr-app/src/redux/slices/relays.slice.ts similarity index 100% rename from nr-app/src/redux/relaysSlice.ts rename to nr-app/src/redux/slices/relays.slice.ts diff --git a/nr-app/src/redux/store.ts b/nr-app/src/redux/store.ts index 850734e..efc73a0 100644 --- a/nr-app/src/redux/store.ts +++ b/nr-app/src/redux/store.ts @@ -4,8 +4,11 @@ import createSagaMiddleware from "redux-saga"; import { SLICE_NAME as eventsName, default as eventsReducer, -} from "./eventsSlice"; -import { SLICE_NAME as mapName, default as mapReducer } from "./mapSlice"; +} from "./slices/events.slice"; +import { + SLICE_NAME as mapName, + default as mapReducer, +} from "./slices/map.slice"; import rootSaga from "./sagas/root.saga"; const sagaMiddleware = createSagaMiddleware(); From 5aac4d1576e92c0fbeaf81f9bfb0af945bad8c25 Mon Sep 17 00:00:00 2001 From: guaka Date: Wed, 16 Oct 2024 13:23:18 +0000 Subject: [PATCH 11/14] Update README.md: roadmap edits links to tr circles, thought about tool to help communities (that are now often on telegram, facebook, whatsapp) --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf3c435..e09f5ec 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The technical side of things are manageable as long as we just care about Trustr - finding partners in the ecosystem. ### Telling the story -Trustroots users skew hippie, alternative, vanguard, experimental, left, gifting. The Nostr userbase is generally cryptocurrency and privacy focused. +Trustroots [circles](https://www.trustroots.org/circles) around hippie, alternative, vanguard, experimental, left, gifting. The 2024 Nostr userbase is generally cryptocurrency and privacy focused. As far as our users are concerned, Trustroots is fine and nothing is broken. So a degradation of their experience will likely only lead to frustration. At best, we can justify inconvenience through appealing to the values of the community. The community also won't care that much about the admins' wish to make Trustroots more maintainable. @@ -70,6 +70,8 @@ Trustroots users interact with the app when they're looking for something in a n - In a world of companies owning your identity online, Trustroots wants to empower you to own your own identity. - There's more cool stuff like Trustroots in the world. +A lot of coordination around events and groups occurs on telegram, whatsapp and facebook, we think a nostr geo tool can do better. + ### Partners in the ecosystem We need platforms and communities that work in Berlin, are not money-focused, are valuable to travellers, and encourage personal connection and sharing. There are no good partner organisations in the current Nostr ecosystem. Our best bet will be supportive interested other groups that we build the tech for. So we need to build a good DX for adding logging in. @@ -77,8 +79,13 @@ We need platforms and communities that work in Berlin, are not money-focused, ar Possible groups and communities: - [Bike Surf Berlin](bikesurf.org) - Geocaching? -- Semi-legal rave groups - [Couchers](couchers.org) and other hospex platforms +- related to [circles](https://www.trustroots.org/circles): + - Semi-legal rave groups [circle](https://www.trustroots.org/circles/ravers) + - foodsharing.de, [circle](https://www.trustroots.org/circles/foodsharing) + - [acroyoga circle](https://www.trustroots.org/circles/acroyoga) + - [lindyhop circle](https://www.trustroots.org/circles/lindyhoppers) + ### Timeline **Q4 2024:** From c9dd15898cbb880e392100979957a8e398c80991 Mon Sep 17 00:00:00 2001 From: guaka Date: Wed, 16 Oct 2024 13:27:10 +0000 Subject: [PATCH 12/14] Update README.md burners circle --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e09f5ec..6281df4 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Possible groups and communities: - [Couchers](couchers.org) and other hospex platforms - related to [circles](https://www.trustroots.org/circles): - Semi-legal rave groups [circle](https://www.trustroots.org/circles/ravers) + - [burners circle](https://www.trustroots.org/circles/burners) - foodsharing.de, [circle](https://www.trustroots.org/circles/foodsharing) - [acroyoga circle](https://www.trustroots.org/circles/acroyoga) - [lindyhop circle](https://www.trustroots.org/circles/lindyhoppers) From 07be9ef2e241d46a37ed1abcae70ef99a220ddae Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 18:32:30 +0200 Subject: [PATCH 13/14] Install open-location-code-typescript. --- nr-app/package.json | 1 + nr-app/pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/nr-app/package.json b/nr-app/package.json index 2e64a29..833c760 100644 --- a/nr-app/package.json +++ b/nr-app/package.json @@ -35,6 +35,7 @@ "fast-text-encoding": "^1.0.6", "nip06": "^1.2.0", "nostr-tools": "^2.7.2", + "open-location-code-typescript": "^1.5.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.5", diff --git a/nr-app/pnpm-lock.yaml b/nr-app/pnpm-lock.yaml index ac8c056..eef9446 100644 --- a/nr-app/pnpm-lock.yaml +++ b/nr-app/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: nostr-tools: specifier: ^2.7.2 version: 2.7.2(typescript@5.3.3) + open-location-code-typescript: + specifier: ^1.5.0 + version: 1.5.0 react: specifier: 18.2.0 version: 18.2.0 @@ -4132,6 +4135,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open-location-code-typescript@1.5.0: + resolution: {integrity: sha512-nGlOwLrmexCqCxEx7Vy6hsHGA9NmrUf/1bZNj0XzP5WrXYj+Oz1MJwUWFv3N012P/Y15bwC+ViVuCV6inoI1KQ==} + open@6.4.0: resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} engines: {node: '>=8'} @@ -10961,6 +10967,8 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open-location-code-typescript@1.5.0: {} + open@6.4.0: dependencies: is-wsl: 1.1.0 From 984d99af0d8f16d34f0a6d96688f98889dd3bcef Mon Sep 17 00:00:00 2001 From: Callum Macdonald Date: Wed, 16 Oct 2024 18:33:03 +0200 Subject: [PATCH 14/14] Working towards data on the map. --- nr-app/src/components/Map.tsx | 21 ++++ nr-app/src/constants.ts | 3 + nr-app/src/redux/actions/map.actions.ts | 4 +- nr-app/src/redux/sagas/map.saga.ts | 9 +- nr-app/src/redux/slices/map.slice.ts | 16 ++- nr-app/src/utils/map.utils.ts | 146 ++++++++++++++++++++++++ 6 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 nr-app/src/utils/map.utils.ts diff --git a/nr-app/src/components/Map.tsx b/nr-app/src/components/Map.tsx index 4bbeb3d..c370034 100644 --- a/nr-app/src/components/Map.tsx +++ b/nr-app/src/components/Map.tsx @@ -1,3 +1,7 @@ +import { + allPlusCodesForRegion, + coordinatesToPlusCode, +} from "@/utils/map.utils"; import { StyleSheet, View } from "react-native"; import MapView, { Marker } from "react-native-maps"; @@ -7,8 +11,25 @@ export default function Map() { { console.log("#rIMmxg Map move completed", region, details); + const topRightCoordinates = { + latitude: region.latitude + region.latitudeDelta, + longitude: region.longitude + region.longitudeDelta, + }; + const bottomLeftCoordinates = { + latitude: region.latitude - region.latitudeDelta, + longitude: region.longitude - region.longitudeDelta, + }; + const topRightCode = coordinatesToPlusCode(topRightCoordinates); + const bottomLeftCode = coordinatesToPlusCode(bottomLeftCoordinates); + console.log( + `#bu2PoU Bottom left is ${bottomLeftCode}, top right is ${topRightCode}`, + ); + const parts = allPlusCodesForRegion(region); + console.log("#fWrvAt Got parts", parts); }} > diff --git a/nr-app/src/constants.ts b/nr-app/src/constants.ts index bce134f..742bf82 100644 --- a/nr-app/src/constants.ts +++ b/nr-app/src/constants.ts @@ -1 +1,4 @@ export const ID_SEPARATOR = ":" as const; + +export const DEFAULT_PLUS_CODE_LENGTH = 4; +export const NOSTR_EVENT_INDEX_MAXIMUM_PLUS_CODE_LENGTH = 6; diff --git a/nr-app/src/redux/actions/map.actions.ts b/nr-app/src/redux/actions/map.actions.ts index f1628c4..bb47c59 100644 --- a/nr-app/src/redux/actions/map.actions.ts +++ b/nr-app/src/redux/actions/map.actions.ts @@ -1,3 +1,5 @@ import { createAction } from "@reduxjs/toolkit"; -export const setVisiblePlusCodes = createAction("SET_VISIBLE_PLUS_CODES"); +export const setVisiblePlusCodes = createAction( + "map/setVisiblePlusCodes", +); diff --git a/nr-app/src/redux/sagas/map.saga.ts b/nr-app/src/redux/sagas/map.saga.ts index 5ef6f61..bc2f086 100644 --- a/nr-app/src/redux/sagas/map.saga.ts +++ b/nr-app/src/redux/sagas/map.saga.ts @@ -1,5 +1,9 @@ import { PayloadAction } from "@reduxjs/toolkit"; import { put, takeEvery } from "redux-saga/effects"; +import { + setMapSubscriptionIsUpdating, + setVisiblePlusCodes, +} from "../slices/map.slice"; function* updateDataForMap(action: PayloadAction) { try { @@ -7,7 +11,8 @@ function* updateDataForMap(action: PayloadAction) { const visiblePlusCodes = action.payload; console.log("#tJ7hyp Got visible plus codes", visiblePlusCodes); // Write the state to redux - put({ type: "success" }); + put(setMapSubscriptionIsUpdating(true)); + // Call a subscription } catch (error) { const message = error instanceof Error ? error.message : ""; yield put({ type: "fail", action: message }); @@ -15,7 +20,7 @@ function* updateDataForMap(action: PayloadAction) { } export function* mapSaga() { - yield takeEvery("some_action", updateDataForMap); + yield takeEvery(setVisiblePlusCodes, updateDataForMap); } /** diff --git a/nr-app/src/redux/slices/map.slice.ts b/nr-app/src/redux/slices/map.slice.ts index 7fce84d..cfe48f6 100644 --- a/nr-app/src/redux/slices/map.slice.ts +++ b/nr-app/src/redux/slices/map.slice.ts @@ -1,13 +1,16 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "../store"; +import { setVisiblePlusCodes as REAL_setVisiblePlusCodes } from "../actions/map.actions"; export const SLICE_NAME = "map"; interface MapState { + mapSubscriptionIsUpdating: boolean; visiblePlusCodes: string[]; } const initialState: MapState = { + mapSubscriptionIsUpdating: false, visiblePlusCodes: [], }; @@ -15,15 +18,26 @@ const mapSlice = createSlice({ name: SLICE_NAME, initialState, reducers: { + setMapSubscriptionIsUpdating: (state, action: PayloadAction) => { + if (state.mapSubscriptionIsUpdating !== action.payload) { + state.mapSubscriptionIsUpdating = action.payload; + } + }, setVisiblePlusCodes: (state, action: PayloadAction) => { state.visiblePlusCodes = action.payload; }, }, + extraReducers: (builder) => { + builder.addCase(REAL_setVisiblePlusCodes, (state, action) => { + state.visiblePlusCodes = action.payload; + }); + }, }); export default mapSlice.reducer; -export const { setVisiblePlusCodes } = mapSlice.actions; +export const { setMapSubscriptionIsUpdating, setVisiblePlusCodes } = + mapSlice.actions; export const mapSelectors = mapSlice.getSelectors( (state: RootState) => state[SLICE_NAME], diff --git a/nr-app/src/utils/map.utils.ts b/nr-app/src/utils/map.utils.ts new file mode 100644 index 0000000..4b6662b --- /dev/null +++ b/nr-app/src/utils/map.utils.ts @@ -0,0 +1,146 @@ +import { + DEFAULT_PLUS_CODE_LENGTH, + NOSTR_EVENT_INDEX_MAXIMUM_PLUS_CODE_LENGTH, +} from "@/constants"; +import OpenLocationCode from "open-location-code-typescript"; + +type PlusCodeShortLength = 2 | 4 | 6 | 8; + +const plusCodeCharacters = "23456789CFGHJMPQRVWX" as const; + +export function coordinatesToPlusCode({ + latitude, + longitude, + length: codeLength = DEFAULT_PLUS_CODE_LENGTH, +}: { + latitude: number; + longitude: number; + length?: PlusCodeShortLength; +}): string { + const plusCode = OpenLocationCode.encode(latitude, longitude, codeLength); + return plusCode; +} + +export function plusCodeToArrayPairs(plusCode: string): [string, string][] { + const [beforePlus, afterPlus] = plusCode.split("+"); + const [ + firstRow, + firstColumn, + secondRow, + secondColumn, + thirdRow, + thirdColumn, + fourthRow, + fourthColumn, + ] = beforePlus; + if (afterPlus !== "") { + throw new Error( + "Cannot split plus codes with values after the plus. #GKPQHB", + ); + } + + const allPairs: [string, string][] = [ + [firstRow, firstColumn], + [secondRow, secondColumn], + [thirdRow, thirdColumn], + [fourthRow, fourthColumn], + ]; + + const pairs = allPairs.filter(([row]) => row !== "0"); + + return pairs; +} + +export function allPlusCodesForRegion({ + latitude, + latitudeDelta, + longitude, + longitudeDelta, + codeLength = NOSTR_EVENT_INDEX_MAXIMUM_PLUS_CODE_LENGTH, +}: { + latitude: number; + latitudeDelta: number; + longitude: number; + longitudeDelta: number; + codeLength?: PlusCodeShortLength; +}) { + // - Code for bottom left + // - Code for top right + const bottomLeftCoordinates = { + latitude: latitude - latitudeDelta, + longitude: longitude - longitudeDelta, + }; + const topRightCoordinates = { + latitude: latitude + latitudeDelta, + longitude: longitude + longitudeDelta, + }; + + const bottomLeftCode = OpenLocationCode.encode( + bottomLeftCoordinates.latitude, + bottomLeftCoordinates.longitude, + codeLength, + ); + const topRightCode = OpenLocationCode.encode( + topRightCoordinates.latitude, + topRightCoordinates.longitude, + codeLength, + ); + + const bottomLeftPairs = plusCodeToArrayPairs(bottomLeftCode); + const topRightPairs = plusCodeToArrayPairs(topRightCode); + + // Find the first digit that changes for row and column + const firstIndexWithDifference = bottomLeftPairs.findIndex( + ([bottomRow, leftColumn], index) => { + const [topRow, rightColumn] = topRightPairs[index]; + return topRow !== bottomRow || leftColumn !== rightColumn; + }, + ); + + const outputCodeLength = (firstIndexWithDifference + 1) * 2; + console.log( + "#x6E7CR Got output code length", + firstIndexWithDifference, + outputCodeLength, + ); + + const bottomLeftLastPair = bottomLeftPairs.at(firstIndexWithDifference)!; + const topRightLastPair = topRightPairs.at(firstIndexWithDifference)!; + + const [bottomRow, leftColumn] = bottomLeftLastPair; + const [topRow, rightColumn] = topRightLastPair; + + const bottomRowIndex = plusCodeCharacters.indexOf(bottomRow); + const topRowIndex = plusCodeCharacters.indexOf(topRow); + const leftColumnIndex = plusCodeCharacters.indexOf(leftColumn); + const rightColumnIndex = plusCodeCharacters.indexOf(rightColumn); + + const rows = topRowIndex - bottomRowIndex + 1; + const columns = rightColumnIndex - leftColumnIndex + 1; + + // Nested iteration + const parts = Array.from({ length: rows }) + .map((empty, rowIndex) => { + return Array.from({ length: columns }).map((empty, columnIndex) => { + const outputRowIndex = bottomRowIndex + rowIndex; + const outputColumnIndex = leftColumnIndex + columnIndex; + + const rowCode = plusCodeCharacters[outputRowIndex]; + const columnCode = plusCodeCharacters[outputColumnIndex]; + + return [rowCode, columnCode]; + }); + }) + .flat(); + + const codePrefixLength = outputCodeLength > 2 ? outputCodeLength - 2 : 0; + const codePrefix = bottomLeftCode.slice(0, codePrefixLength); + + const codes = parts.map(([row, column]) => { + const partialCode = `${codePrefix}${row}${column}`; + const codeToPlus = partialCode.padEnd(8, "0"); + const code = `${codeToPlus}+`; + return code; + }); + return codes; +}