From 887664b50d84a6eac023d1fb7aadf19cc75196b9 Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Tue, 7 Feb 2023 16:26:25 +0100 Subject: [PATCH] resilience Exposure --- .../main/java/com/nostros/classes/Event.java | 14 +- .../com/nostros/modules/DatabaseModule.java | 33 ++-- frontend/Constants/Relay/index.ts | 46 +----- frontend/Contexts/RelayPoolContext.tsx | 15 +- .../DatabaseFunctions/NotesRelays/index.ts | 50 ++++++ .../DatabaseFunctions/Relays/index.ts | 40 +++++ .../DatabaseFunctions/Users/index.ts | 19 +++ frontend/Functions/DatabaseFunctions/index.ts | 2 + frontend/Functions/NativeFunctions/index.ts | 37 ++++- frontend/Locales/de.json | 8 + frontend/Locales/en.json | 8 + frontend/Locales/es.json | 8 + frontend/Locales/fr.json | 8 + frontend/Locales/ru.json | 8 + frontend/Pages/ConversationPage/index.tsx | 3 +- frontend/Pages/RelaysPage/index.tsx | 156 ++++++++++++++++-- frontend/lib/nostr/RelayPool/intex.ts | 119 +++++++++++++ 17 files changed, 476 insertions(+), 98 deletions(-) create mode 100644 frontend/Functions/DatabaseFunctions/NotesRelays/index.ts diff --git a/android/app/src/main/java/com/nostros/classes/Event.java b/android/app/src/main/java/com/nostros/classes/Event.java index f4e7361..27003cd 100644 --- a/android/app/src/main/java/com/nostros/classes/Event.java +++ b/android/app/src/main/java/com/nostros/classes/Event.java @@ -44,6 +44,12 @@ public class Event { public void save(SQLiteDatabase database, String userPubKey, String relayUrl) { if (isValid()) { try { + ContentValues relayValues = new ContentValues(); + relayValues.put("note_id", id); + relayValues.put("pubkey", pubkey); + relayValues.put("relay_url", relayUrl); + database.replace("nostros_notes_relays", null, relayValues); + if (kind.equals("0")) { saveUserMeta(database); } else if (kind.equals("1") || kind.equals("2")) { @@ -180,12 +186,6 @@ public class Event { } protected void saveNote(SQLiteDatabase database, String userPubKey, String relayUrl) { - ContentValues relayValues = new ContentValues(); - relayValues.put("note_id", id); - relayValues.put("pubkey", pubkey); - relayValues.put("relay_url", relayUrl); - database.replace("nostros_notes_relays", null, relayValues); - ContentValues values = new ContentValues(); values.put("id", id); values.put("content", content.replace("'", "''")); @@ -286,6 +286,7 @@ public class Event { for (int i = 0; i < tags.length(); ++i) { JSONArray tag = tags.getJSONArray(i); String petId = tag.getString(1); + String relay = tag.getString(2); String name = ""; if (tag.length() >= 4) { name = tag.getString(3); @@ -298,6 +299,7 @@ public class Event { values.put("name", name); values.put("contact", true); values.put("blocked", 0); + values.put("main_relay", relay); values.put("pet_at", created_at); database.insert("nostros_users", null, values); } diff --git a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java index 533cf28..12dcf1a 100644 --- a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java +++ b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java @@ -40,8 +40,8 @@ public class DatabaseModule { " picture TEXT,\n" + " about TEXT,\n" + " main_relay TEXT,\n" + - " contact BOOLEAN DEFAULT 0,\n" + - " follower BOOLEAN DEFAULT 0\n" + + " contact INT DEFAULT 0,\n" + + " follower INT DEFAULT 0\n" + " );"); database.execSQL("CREATE TABLE IF NOT EXISTS nostros_relays(\n" + " url TEXT PRIMARY KEY NOT NULL,\n" + @@ -56,11 +56,11 @@ public class DatabaseModule { " sig TEXT NOT NULL,\n" + " tags TEXT NOT NULL,\n" + " conversation_id TEXT NOT NULL,\n" + - " read BOOLEAN DEFAULT 0\n" + + " read INT DEFAULT 0\n" + " );"); try { - database.execSQL("ALTER TABLE nostros_notes ADD COLUMN user_mentioned BOOLEAN DEFAULT 0;"); - database.execSQL("ALTER TABLE nostros_notes ADD COLUMN seen BOOLEAN DEFAULT 0;"); + database.execSQL("ALTER TABLE nostros_notes ADD COLUMN user_mentioned INT DEFAULT 0;"); + database.execSQL("ALTER TABLE nostros_notes ADD COLUMN seen INT DEFAULT 0;"); } catch (SQLException e) { } try { database.execSQL("ALTER TABLE nostros_users ADD COLUMN lnurl TEXT;"); @@ -77,7 +77,7 @@ public class DatabaseModule { " pubkey TEXT NOT NULL,\n" + " sig TEXT NOT NULL,\n" + " tags TEXT NOT NULL,\n" + - " positive BOOLEAN DEFAULT 1,\n" + + " positive INT DEFAULT 1,\n" + " reacted_event_id TEXT,\n" + " reacted_user_id TEXT\n" + " );"); @@ -98,15 +98,11 @@ public class DatabaseModule { } catch (SQLException e) { } try { database.execSQL("ALTER TABLE nostros_users ADD COLUMN nip05 TEXT;"); - } catch (SQLException e) { } - try { - database.execSQL("ALTER TABLE nostros_users ADD COLUMN valid_nip05 BOOLEAN DEFAULT 0;"); + database.execSQL("ALTER TABLE nostros_users ADD COLUMN valid_nip05 INT DEFAULT 0;"); } catch (SQLException e) { } try { database.execSQL("ALTER TABLE nostros_notes ADD COLUMN repost_id TEXT;"); - } catch (SQLException e) { } - try { - database.execSQL("ALTER TABLE nostros_relays ADD COLUMN active BOOLEAN DEFAULT 1;"); + database.execSQL("ALTER TABLE nostros_relays ADD COLUMN active INT DEFAULT 1;"); } catch (SQLException e) { } try { database.execSQL("CREATE INDEX nostros_notes_repost_id_created_at_index ON nostros_notes(repost_id, pubkey, created_at); "); @@ -122,12 +118,9 @@ public class DatabaseModule { database.execSQL("CREATE INDEX nostros_users_contact_follower_index ON nostros_users(contact, follower); "); database.execSQL("CREATE INDEX nostros_users_id_name_index ON nostros_users(id, name); "); - } catch (SQLException e) { } - try { + database.execSQL("DROP TABLE IF EXISTS nostros_config;"); - } catch (SQLException e) { } - try { - database.execSQL("ALTER TABLE nostros_users ADD COLUMN blocked BOOLEAN DEFAULT 0;"); + database.execSQL("ALTER TABLE nostros_users ADD COLUMN blocked INT DEFAULT 0;"); } catch (SQLException e) { } try { database.execSQL("CREATE TABLE IF NOT EXISTS nostros_notes_relays(\n" + @@ -140,9 +133,13 @@ public class DatabaseModule { database.execSQL("CREATE INDEX nostros_notes_relays_pubkey_index ON nostros_notes_relays(pubkey);"); } catch (SQLException e) { } try { - database.execSQL("ALTER TABLE nostros_relays ADD COLUMN global_feed BOOLEAN DEFAULT 1;"); database.execSQL("ALTER TABLE nostros_users ADD COLUMN pet_at INT;"); database.execSQL("ALTER TABLE nostros_users ADD COLUMN follower_at INT;"); + database.execSQL("ALTER TABLE nostros_relays ADD COLUMN global_feed INT DEFAULT 1;"); + } catch (SQLException e) { } + try { + database.execSQL("ALTER TABLE nostros_relays ADD COLUMN resilient INT DEFAULT 0;"); + database.execSQL("ALTER TABLE nostros_relays ADD COLUMN manual INT DEFAULT 1;"); } catch (SQLException e) { } } diff --git a/frontend/Constants/Relay/index.ts b/frontend/Constants/Relay/index.ts index a71bbd1..c17c7bb 100644 --- a/frontend/Constants/Relay/index.ts +++ b/frontend/Constants/Relay/index.ts @@ -1,45 +1 @@ -export const defaultRelays = [ - 'wss://brb.io', - 'wss://damus.io', - 'wss://nostr-pub.wellorder.net', - 'wss://nostr.swiss-enigma.ch', - 'wss://nostr.onsats.org', - 'wss://nostr-pub.semisol.dev', - 'wss://nostr.openchain.fr', - 'wss://relay.nostr.info', - 'wss://nostr.oxtr.dev', - 'wss://nostr.ono.re', - 'wss://relay.grunch.dev', - 'wss://nostr.developer.li', -] - -export const REGEX_SOCKET_LINK = /^wss:\/\/.*\..*$/i - -export const relayColors = [ - '#3016dd', - '#43e1ef', - '#ef3b50', - '#d3690c', - '#43e23b', - '#3a729e', - '#805de2', - '#b2ff5b', - '#eaa123', - '#ba7a3b', - '#90c900', - '#26e08c', - '#090660', - '#9edb62', - '#db48f2', - '#5d14aa', - '#f2d859', - '#0b8458', - '#cdea10', - '#6473e0', - '#6721a5', - '#f76f8c', - '#ce2780', - '#403ba0', - '#a9f41d', - '#BBF067', -] +export const REGEX_SOCKET_LINK = /^(ws|wss):\/\/.*\..*$/i diff --git a/frontend/Contexts/RelayPoolContext.tsx b/frontend/Contexts/RelayPoolContext.tsx index 478b153..fb4edb9 100644 --- a/frontend/Contexts/RelayPoolContext.tsx +++ b/frontend/Contexts/RelayPoolContext.tsx @@ -3,7 +3,7 @@ import RelayPool from '../lib/nostr/RelayPool/intex' import { AppContext } from './AppContext' import { DeviceEventEmitter } from 'react-native' import debounce from 'lodash.debounce' -import { createRelay, getRelays, Relay } from '../Functions/DatabaseFunctions/Relays' +import { getRelays, Relay } from '../Functions/DatabaseFunctions/Relays' import { UserContext } from './UserContext' export interface RelayPoolContextProps { @@ -74,6 +74,7 @@ export const RelayPoolContextProvider = ({ DeviceEventEmitter.addListener('WebsocketEvent', debouncedEventIdHandler) DeviceEventEmitter.addListener('WebsocketConfirmation', debouncedConfirmationHandler) const initRelayPool = new RelayPool(privateKey) + await initRelayPool.resilientMode(database, publicKey) initRelayPool.connect(publicKey, () => setRelayPoolReady(true)) setRelayPool(initRelayPool) loadRelays() @@ -140,18 +141,6 @@ export const RelayPoolContextProvider = ({ } }, [publicKey]) - useEffect(() => { - if (database) { - getRelays(database).then((results) => { - if (results.length === 0) { - createRelay(database, 'wss://brb.io') - createRelay(database, 'wss://damus.io') - createRelay(database, 'wss://nostr-pub.wellorder.net') - } - }) - } - }, [database]) - return ( Promise> = async (db) => { + const result: Record = {} + const query = ` + SELECT relay_url, COUNT(*) as count FROM nostros_notes_relays + LEFT JOIN + nostros_users ON nostros_users.id = nostros_notes_relays.pubkey + WHERE nostros_users.contact > 0 + GROUP BY relay_url + ` + const resultSet = db.execute(query) + if (resultSet?.rows?.length) { + for (let index = 0; index < resultSet?.rows?.length; index++) { + const row = resultSet?.rows?.item(index) + result[row.relay_url] = row.count + } + } + return result +} + +export const getNoteRelaysPresence: ( + db: QuickSQLiteConnection, +) => Promise> = async (db) => { + const result: Record = {} + const query = ` + SELECT relay_url, pubkey FROM nostros_notes_relays + LEFT JOIN + nostros_users ON nostros_users.id = nostros_notes_relays.pubkey + WHERE nostros_users.contact > 0 + GROUP BY relay_url, pubkey + ORDER BY random() + ` + const resultSet = db.execute(query) + if (resultSet?.rows?.length) { + for (let index = 0; index < resultSet?.rows?.length; index++) { + const row = resultSet?.rows?.item(index) + result[row.relay_url] = [...(result[row.relay_url] ?? []), row.pubkey] + } + } + return result +} diff --git a/frontend/Functions/DatabaseFunctions/Relays/index.ts b/frontend/Functions/DatabaseFunctions/Relays/index.ts index 5405415..68c735a 100644 --- a/frontend/Functions/DatabaseFunctions/Relays/index.ts +++ b/frontend/Functions/DatabaseFunctions/Relays/index.ts @@ -1,11 +1,15 @@ import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite' import { getItems } from '..' +import { median } from '../../NativeFunctions' +import { getNoteRelaysUsage } from '../NotesRelays' export interface Relay { url: string name?: string active?: number global_feed?: number + resilient?: number + manual?: number } export interface RelayInfo { @@ -61,3 +65,39 @@ export const createRelay: (db: QuickSQLiteConnection, url: string) => Promise Promise = async (db, url) => { + const query = ` + INSERT OR IGNORE INTO nostros_relays (url, resilient, active) VALUES (?, ?, ?) + ` + return db.execute(query, [url, 1, 0]) +} + +export const activateResilientRelays: ( + db: QuickSQLiteConnection, + relayUrls: string[], +) => Promise = async (db, relayUrls) => { + const userQuery = `UPDATE nostros_relays SET resilient = 1 WHERE url IN ('${relayUrls.join( + "', '", + )}');` + return db.execute(userQuery) +} + +export const desactivateResilientRelays: ( + db: QuickSQLiteConnection, +) => Promise = async (db) => { + const userQuery = `UPDATE nostros_relays SET resilient = 0 WHERE resilient = 1;` + return db.execute(userQuery) +} + +export const getResilientRelays: (db: QuickSQLiteConnection) => Promise = async (db) => { + const relayUsage = await getNoteRelaysUsage(db) + const medianUsage = median(Object.values(relayUsage)) + const resilientRelays = Object.keys(relayUsage).sort((n1: string, n2: string) => { + return Math.abs(relayUsage[n1] - medianUsage) - Math.abs(relayUsage[n2] - medianUsage) + }) + return resilientRelays +} diff --git a/frontend/Functions/DatabaseFunctions/Users/index.ts b/frontend/Functions/DatabaseFunctions/Users/index.ts index 04eb4a2..5dfc64b 100644 --- a/frontend/Functions/DatabaseFunctions/Users/index.ts +++ b/frontend/Functions/DatabaseFunctions/Users/index.ts @@ -68,6 +68,25 @@ export const addUser: (pubKey: string, db: QuickSQLiteConnection) => Promise Promise> = async (db) => { + const result: Record = {} + const query = ` + SELECT main_relay, COUNT(*) as count FROM nostros_users + WHERE nostros_users.contact > 0 + GROUP BY main_relay + ` + const resultSet = db.execute(query) + if (resultSet?.rows?.length) { + for (let index = 0; index < resultSet?.rows?.length; index++) { + const row = resultSet?.rows?.item(index) + console.log(row) + } + } + return result +} + export const getContactsCount: (db: QuickSQLiteConnection) => Promise = async (db) => { const countQuery = 'SELECT COUNT(*) FROM nostros_users WHERE contact = 1' const resultSet = db.execute(countQuery) diff --git a/frontend/Functions/DatabaseFunctions/index.ts b/frontend/Functions/DatabaseFunctions/index.ts index b9e40ab..1b46094 100644 --- a/frontend/Functions/DatabaseFunctions/index.ts +++ b/frontend/Functions/DatabaseFunctions/index.ts @@ -32,6 +32,8 @@ export const dropTables: (db: QuickSQLiteConnection) => Promise = [ ['DELETE FROM nostros_users;', [[]]], ['DELETE FROM nostros_notes;', [[]]], + ['DELETE FROM nostros_reactions;', [[]]], + ['DELETE FROM nostros_notes_relays;', [[]]], ['DELETE FROM nostros_direct_messages;', [[]]], ] return db.executeBatch(dropQueries) diff --git a/frontend/Functions/NativeFunctions/index.ts b/frontend/Functions/NativeFunctions/index.ts index ee97205..01f46a8 100644 --- a/frontend/Functions/NativeFunctions/index.ts +++ b/frontend/Functions/NativeFunctions/index.ts @@ -1,5 +1,3 @@ -import { relayColors } from '../../Constants/Relay' - export const handleInfinityScroll: (event: any) => boolean = (event) => { const mHeight = event.nativeEvent.layoutMeasurement.height const cSize = event.nativeEvent.contentSize.height @@ -9,6 +7,35 @@ export const handleInfinityScroll: (event: any) => boolean = (event) => { return false } +export const relayColors = [ + '#3016dd', + '#43e1ef', + '#ef3b50', + '#d3690c', + '#43e23b', + '#3a729e', + '#805de2', + '#b2ff5b', + '#eaa123', + '#ba7a3b', + '#90c900', + '#26e08c', + '#090660', + '#9edb62', + '#db48f2', + '#5d14aa', + '#f2d859', + '#0b8458', + '#cdea10', + '#6473e0', + '#6721a5', + '#f76f8c', + '#ce2780', + '#403ba0', + '#a9f41d', + '#BBF067', +] + export const relayToColor: (string: string) => string = (string) => { let hash = 0 for (let i = 0; i < string.length; i++) { @@ -61,3 +88,9 @@ export const validNip21: (string: string | undefined) => boolean = (string) => { export const randomInt: (min: number, max: number) => number = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min + +export const median: (arr: number[]) => number = (arr) => { + const mid = Math.floor(arr.length / 2) + const nums = [...arr].sort((a, b) => a - b) + return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2 +} diff --git a/frontend/Locales/de.json b/frontend/Locales/de.json index cd9321c..66a06f2 100644 --- a/frontend/Locales/de.json +++ b/frontend/Locales/de.json @@ -183,6 +183,14 @@ } }, "relaysPage": { + "resilienceTitle": "Resilience (experimental)", + "resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.", + "resilienceCategories": "Resilience exposure", + "resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.", + "centralized": "Centralized", + "small": "Small", + "contacts": "# Contacts", + "resilienceMode": "Resilience (experimental)", "relayName": "Addresse", "globalFeed": "Globaler Feed", "active": "Aktiv", diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index 2cd6377..a9ca167 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -166,6 +166,14 @@ "newMessages": "{{newNotesCount}} new notes. Pull to refresh." }, "relaysPage": { + "resilienceTitle": "Resilience (experimental)", + "resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.", + "resilienceCategories": "Resilience exposure", + "resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.", + "centralized": "Centralized", + "small": "Small", + "contacts": "# Contacts", + "resilienceMode": "Resilience (experimental)", "relayName": "Address", "globalFeed": "Global feed", "active": "Active", diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index 5ccd4be..ad5d560 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -179,6 +179,14 @@ } }, "relaysPage": { + "resilienceTitle": "Resilience (experimental)", + "resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.", + "resilienceCategories": "Resilience exposure", + "resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.", + "centralized": "Centralizado", + "small": "Pequeño", + "contacts": "Nº contactos", + "resilienceMode": "Resiliencia (experimental)", "relayName": "Dirección", "globalFeed": "Feed global", "active": "Activo", diff --git a/frontend/Locales/fr.json b/frontend/Locales/fr.json index 4296105..bc2f201 100644 --- a/frontend/Locales/fr.json +++ b/frontend/Locales/fr.json @@ -178,6 +178,14 @@ } }, "relaysPage": { + "resilienceTitle": "Resilience (experimental)", + "resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.", + "resilienceCategories": "Resilience exposure", + "resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.", + "centralized": "Centralized", + "small": "Small", + "contacts": "# Contacts", + "resilienceMode": "Resilience (experimental)", "relayName": "Adresse", "globalFeed": "Flux global", "active": "Actif", diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index a190861..819e3ce 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -178,6 +178,14 @@ } }, "relaysPage": { + "resilienceTitle": "Resilience (experimental)", + "resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.", + "resilienceCategories": "Resilience exposure", + "resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.", + "centralized": "Centralized", + "small": "Small", + "contacts": "# Contacts", + "resilienceMode": "Resilience (experimental)", "relayName": "Address", "globalFeed": "Общая лента", "active": "Active", diff --git a/frontend/Pages/ConversationPage/index.tsx b/frontend/Pages/ConversationPage/index.tsx index 85b2095..8f0aeef 100644 --- a/frontend/Pages/ConversationPage/index.tsx +++ b/frontend/Pages/ConversationPage/index.tsx @@ -36,7 +36,7 @@ interface ConversationPageProps { export const ConversationPage: React.FC = ({ route }) => { const theme = useTheme() const scrollViewRef = useRef() - const { database } = useContext(AppContext) + const { database, setRefreshBottomBarAt } = useContext(AppContext) const { relayPool, lastEventId } = useContext(RelayPoolContext) const { publicKey, privateKey, name } = useContext(UserContext) const otherPubKey = useMemo(() => route.params.pubKey, []) @@ -64,6 +64,7 @@ export const ConversationPage: React.FC = ({ route }) => if (database && publicKey) { const conversationId = route.params?.conversationId updateConversationRead(conversationId, database) + setRefreshBottomBarAt(getUnixTime(new Date())) getUser(otherPubKey, database).then((user) => { if (user) setOtherUser(user) }) diff --git a/frontend/Pages/RelaysPage/index.tsx b/frontend/Pages/RelaysPage/index.tsx index 5de0bd8..b00056f 100644 --- a/frontend/Pages/RelaysPage/index.tsx +++ b/frontend/Pages/RelaysPage/index.tsx @@ -4,7 +4,7 @@ import Clipboard from '@react-native-clipboard/clipboard' import { useTranslation } from 'react-i18next' import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { Relay } from '../../Functions/DatabaseFunctions/Relays' -import { defaultRelays, REGEX_SOCKET_LINK } from '../../Constants/Relay' +import { REGEX_SOCKET_LINK } from '../../Constants/Relay' import { List, Switch, @@ -22,6 +22,21 @@ import { relayToColor } from '../../Functions/NativeFunctions' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import { useFocusEffect } from '@react-navigation/native' +export const defaultRelays = [ + 'wss://brb.io', + 'wss://damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.swiss-enigma.ch', + 'wss://nostr.onsats.org', + 'wss://nostr-pub.semisol.dev', + 'wss://nostr.openchain.fr', + 'wss://relay.nostr.info', + 'wss://nostr.oxtr.dev', + 'wss://nostr.ono.re', + 'wss://relay.grunch.dev', + 'wss://nostr.developer.li', +] + export const RelaysPage: React.FC = () => { const defaultRelayInput = React.useMemo(() => 'wss://', []) const { updateRelayItem, addRelayItem, removeRelayItem, relays, relayPool } = @@ -30,6 +45,7 @@ export const RelaysPage: React.FC = () => { const theme = useTheme() const bottomSheetAddRef = React.useRef(null) const bottomSheetEditRef = React.useRef(null) + const bottomSheetResilenseRef = React.useRef(null) const [selectedRelay, setSelectedRelay] = useState() const [addRelayInput, setAddRelayInput] = useState(defaultRelayInput) const [showNotification, setShowNotification] = useState() @@ -119,13 +135,13 @@ export const RelaysPage: React.FC = () => { } }, []) - const myRelays = relays - .filter((relay) => !defaultRelays.includes(relay.url)) - .sort((a, b) => { - if (a.url > b.url) return 1 - if (a.url < b.url) return -1 - return 0 - }) + const relayList = relays.sort((a, b) => { + if (a.url > b.url) return 1 + if (a.url < b.url) return -1 + return 0 + }) + + const myRelays = relayList.filter((relay) => !defaultRelays.includes(relay.url)) const renderItem: ListRenderItem = ({ item, index }) => { return ( @@ -162,18 +178,86 @@ export const RelaysPage: React.FC = () => { ) } - return ( - + const renderResilienceItem: (item: string, index: number, type?: string) => JSX.Element = ( + item, + index, + type, + ) => { + return ( ( + + )} right={() => ( <> - {t('relaysPage.globalFeed')} - {t('relaysPage.active')} + {type === 'centralized' && ( + + {relayPool?.resilientAssignation.centralizedRelays[item] && + t('relaysPage.centralized')} + + )} + {type === 'small' && ( + + {relayPool?.resilientAssignation.smallRelays[item] && t('relaysPage.small')} + + )} + + {type === undefined && relayPool?.resilientAssignation.resilientRelays[item]?.length} + {type === 'small' && relayPool?.resilientAssignation.smallRelays[item]?.length} + {type === 'centralized' && + relayPool?.resilientAssignation.centralizedRelays[item]?.length} + )} /> + ) + } + + return ( + + + + + {t('relaysPage.resilienceMode')} + + bottomSheetResilenseRef.current?.open()} + /> + + + + {t('relaysPage.contacts')}} + /> + renderResilienceItem(data.item, data.index)} + /> + + + + renderResilienceItem(data.item, data.index, 'centralized')} + /> + renderResilienceItem(data.item, data.index, 'small')} + /> {myRelays.length > 0 && ( <> @@ -182,6 +266,15 @@ export const RelaysPage: React.FC = () => { + ( + <> + {t('relaysPage.globalFeed')} + {t('relaysPage.active')} + + )} + /> { + ( + <> + {t('relaysPage.globalFeed')} + {t('relaysPage.active')} + + )} + /> { {t(`relaysPage.notifications.${showNotification}`)} )} + + + {t('relaysPage.resilienceTitle')} + {t('relaysPage.resilienceDescription')} + {t('relaysPage.resilienceCategories')} + {t('relaysPage.resilienceCategoriesDescription')} + + @@ -294,6 +408,12 @@ const styles = StyleSheet.create({ }, title: { marginBottom: 8, + flexDirection: 'row', + justifyContent: 'space-between', + }, + titleAction: { + marginTop: -5, + marginBottom: -10, }, bottomDrawerButton: { paddingBottom: 16, @@ -341,6 +461,16 @@ const styles = StyleSheet.create({ marginBottom: 26, marginTop: 26, }, + centralizedRelay: { + paddingRight: 10, + }, + smallRelay: { + paddingRight: 10, + }, + resilienceDrawer: { + height: 600, + justifyContent: 'space-between', + }, }) export default RelaysPage diff --git a/frontend/lib/nostr/RelayPool/intex.ts b/frontend/lib/nostr/RelayPool/intex.ts index b8854a2..8d980ba 100644 --- a/frontend/lib/nostr/RelayPool/intex.ts +++ b/frontend/lib/nostr/RelayPool/intex.ts @@ -1,6 +1,14 @@ // import { spawnThread } from 'react-native-multithreading' import { signEvent, validateEvent, Event } from '../Events' import RelayPoolModule from '../../Native/WebsocketModule' +import { QuickSQLiteConnection } from 'react-native-quick-sqlite' +import { median, randomInt } from '../../../Functions/NativeFunctions' +import { + activateResilientRelays, + createResilientRelay, + desactivateResilientRelays, +} from '../../../Functions/DatabaseFunctions/Relays' +import { getNoteRelaysPresence } from '../../../Functions/DatabaseFunctions/NotesRelays' export interface RelayFilters { ids?: string[] @@ -17,14 +25,42 @@ export interface RelayMessage { data: string } +export interface ResilientAssignation { + resilientRelays: Record + smallRelays: Record + centralizedRelays: Record +} + +export const fallbackRelays = [ + 'wss://brb.io', + 'wss://damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.swiss-enigma.ch', + 'wss://nostr.onsats.org', + 'wss://nostr-pub.semisol.dev', + 'wss://nostr.openchain.fr', + 'wss://relay.nostr.info', + 'wss://nostr.oxtr.dev', + 'wss://nostr.ono.re', + 'wss://relay.grunch.dev', + 'wss://nostr.developer.li', +] + class RelayPool { constructor(privateKey?: string) { this.privateKey = privateKey this.subscriptions = {} + this.resilientAssignation = { + resilientRelays: {}, + smallRelays: {}, + centralizedRelays: {}, + fallback: {}, + } } private readonly privateKey?: string private subscriptions: Record + public resilientAssignation: ResilientAssignation private readonly send: (message: object, globalFeed?: boolean) => void = async ( message, @@ -39,6 +75,89 @@ class RelayPool { RelayPoolModule.connect(publicKey, onEventId) } + public readonly resilientMode: (db: QuickSQLiteConnection, publicKey: string) => void = async ( + db, + ) => { + await desactivateResilientRelays(db) + // Get relays with contacts' pubkeys with at least one event found, randomly sorted + const relaysPresence: Record = await getNoteRelaysPresence(db) + // Median of users per relay + const medianUsage = median( + Object.keys(relaysPresence).map((relay) => relaysPresence[relay].length), + ) + + // Sort relays by abs distance from the mediam + const relaysByPresence = Object.keys(relaysPresence).sort((n1: string, n2: string) => { + return ( + Math.abs(relaysPresence[n1].length - medianUsage) - + Math.abs(relaysPresence[n2].length - medianUsage) + ) + }) + // Get top5 relays closer to the mediam + const medianRelays = relaysByPresence.slice(0, 5) + + // Set helpers + let biggestRelayLenght = 0 + this.resilientAssignation.resilientRelays = {} + const allocatedUsers: string[] = [] + medianRelays.forEach((relayUrl) => { + this.resilientAssignation.resilientRelays[relayUrl] = [] + const length = relaysPresence[relayUrl].length + if (length > biggestRelayLenght) biggestRelayLenght = length + }) + + // Iterate over the N index of top5 relay list removing identical pubkey from others + for (let index = 0; index < biggestRelayLenght - 1; index++) { + medianRelays.forEach((relayUrl) => { + const pubKey = relaysPresence[relayUrl][index] + if (pubKey && !allocatedUsers.includes(pubKey)) { + allocatedUsers.push(pubKey) + this.resilientAssignation.resilientRelays[relayUrl].push(pubKey) + } + }) + } + + // Iterate over remaining relays and assigns as much remaining users as possible + relaysByPresence.slice(5, relaysByPresence.length).forEach((relayUrl) => { + relaysPresence[relayUrl].forEach((pubKey) => { + if (!allocatedUsers.includes(pubKey)) { + allocatedUsers.push(pubKey) + if (relaysPresence[relayUrl].length > medianUsage) { + if (!this.resilientAssignation.centralizedRelays[relayUrl]) + this.resilientAssignation.centralizedRelays[relayUrl] = [] + this.resilientAssignation.centralizedRelays[relayUrl].push(pubKey) + } else { + if (!this.resilientAssignation.smallRelays[relayUrl]) + this.resilientAssignation.smallRelays[relayUrl] = [] + this.resilientAssignation.smallRelays[relayUrl].push(pubKey) + } + } + }) + }) + + // Target list size is 5, adds random relays from a fallback list + const resilientUrls = [ + ...Object.keys(this.resilientAssignation.resilientRelays), + ...Object.keys(this.resilientAssignation.centralizedRelays), + ...Object.keys(this.resilientAssignation.smallRelays), + ] + while (resilientUrls.length < 5) { + let fallbackRelay = '' + while (fallbackRelay === '') { + const randomRelayIndex = randomInt(0, fallbackRelays.length - 1) + if (!resilientUrls.includes(fallbackRelays[randomRelayIndex])) { + fallbackRelay = fallbackRelays[randomRelayIndex] + } + } + resilientUrls.push(fallbackRelay) + this.resilientAssignation.centralizedRelays[fallbackRelay] = [] + } + + // Stores in DB + resilientUrls.forEach(async (relayUrl) => await createResilientRelay(db, relayUrl)) + activateResilientRelays(db, resilientUrls) + } + public readonly add: (relayUrl: string, callback?: () => void) => void = async ( relayUrl, callback = () => {},