Android Performance Imrpovement

This commit is contained in:
KoalaSat 2022-11-15 15:07:17 +01:00
parent 151d215fd3
commit e28437084a
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
21 changed files with 364 additions and 492 deletions

View File

@ -4,7 +4,7 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManager;
import com.nostros.modules.WebsocketModule; import com.nostros.modules.RelayPoolModule;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -21,7 +21,7 @@ public class NostrosPackage implements ReactPackage {
ReactApplicationContext reactContext) { ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>(); List<NativeModule> modules = new ArrayList<>();
modules.add(new WebsocketModule(reactContext)); modules.add(new RelayPoolModule(reactContext));
return modules; return modules;
} }

View File

@ -0,0 +1,45 @@
package com.nostros.classes;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.nostros.modules.DatabaseModule;
import java.io.IOException;
public class Relay {
private Websocket webSocket;
public String url;
public Relay(String serverUrl, DatabaseModule database) throws IOException {
webSocket = new Websocket(serverUrl, database);
url = serverUrl;
}
public void send(String message) {
webSocket.send(message);
}
public void disconnect() {
webSocket.disconnect();
}
public void connect(String userPubKey) throws IOException {
webSocket.connect(userPubKey);
}
public void save(SQLiteDatabase database) {
ContentValues values = new ContentValues();
values.put("url", url);
database.replace("nostros_relays", null, values);
}
public void destroy(SQLiteDatabase database) {
String whereClause = "url = ?";
String[] whereArgs = new String[] {
url
};
database.delete ("nostros_relays", whereClause, whereArgs);
}
}

View File

@ -1,58 +1,45 @@
package com.nostros.modules; package com.nostros.classes;
import android.util.Log; import android.util.Log;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.neovisionaries.ws.client.HostnameUnverifiedException; import com.neovisionaries.ws.client.HostnameUnverifiedException;
import com.neovisionaries.ws.client.OpeningHandshakeException; import com.neovisionaries.ws.client.OpeningHandshakeException;
import com.neovisionaries.ws.client.WebSocket; import com.neovisionaries.ws.client.WebSocket;
import com.neovisionaries.ws.client.WebSocketAdapter; import com.neovisionaries.ws.client.WebSocketAdapter;
import com.neovisionaries.ws.client.WebSocketException; import com.neovisionaries.ws.client.WebSocketException;
import com.neovisionaries.ws.client.WebSocketFactory; import com.neovisionaries.ws.client.WebSocketFactory;
import com.nostros.modules.DatabaseModule;
import org.json.JSONArray; import org.json.JSONArray;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map;
public class WebsocketModule extends ReactContextBaseJavaModule { public class Websocket {
private WebSocket webSocket; private WebSocket webSocket;
private DatabaseModule database; private DatabaseModule database;
private String userPubKey; private String url;
public WebsocketModule(ReactApplicationContext reactContext) { public Websocket(String serverUrl, DatabaseModule databaseModule) {
super(reactContext); database = databaseModule;
database = new DatabaseModule(reactContext); url = serverUrl;
} }
@Override
public String getName() {
return "WebsocketModule";
}
@ReactMethod
public void send(String message) { public void send(String message) {
Log.d("Websocket", "SEND URL:" + url + " __ " + message);
webSocket.sendText(message); webSocket.sendText(message);
} }
@ReactMethod public void disconnect() {
public void connectWebsocket(Callback callBack) throws IOException { webSocket.disconnect();
WebSocketFactory factory = new WebSocketFactory();
webSocket = factory.createSocket("wss://relay.damus.io");
webSocket.addListener(new WebSocketAdapter() {
@Override
public void onConnected(WebSocket ws, Map<String, List<String>> headers) throws Exception
{
callBack.invoke("connected");
} }
public void connect(String userPubKey) throws IOException {
WebSocketFactory factory = new WebSocketFactory();
webSocket = factory.createSocket(url);
webSocket.addListener(new WebSocketAdapter() {
@Override @Override
public void onTextMessage(WebSocket websocket, String message) throws Exception { public void onTextMessage(WebSocket websocket, String message) throws Exception {
Log.d("Websocket", message); Log.d("Websocket", "RECEIVE URL:" + url + " __ " + message);
JSONArray jsonArray = new JSONArray(message); JSONArray jsonArray = new JSONArray(message);
if (jsonArray.get(0).toString().equals("EVENT")) { if (jsonArray.get(0).toString().equals("EVENT")) {
database.saveEvent(jsonArray.getJSONObject(2), userPubKey); database.saveEvent(jsonArray.getJSONObject(2), userPubKey);
@ -77,9 +64,4 @@ public class WebsocketModule extends ReactContextBaseJavaModule {
Log.d("WebSocket", "Failed to establish a WebSocket connection."); Log.d("WebSocket", "Failed to establish a WebSocket connection.");
} }
} }
@ReactMethod
public void setUserPubKey(String userPubKey) {
this.userPubKey = userPubKey;
}
} }

View File

@ -1,33 +1,57 @@
package com.nostros.modules; package com.nostros.modules;
import android.annotation.SuppressLint;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.util.Log; import android.util.Log;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.nostros.classes.Event; import com.nostros.classes.Event;
import com.nostros.classes.Relay;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
public class DatabaseModule extends ReactContextBaseJavaModule { import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DatabaseModule {
private SQLiteDatabase database; private SQLiteDatabase database;
DatabaseModule(ReactApplicationContext reactContext) { DatabaseModule(String absoluteFilesPath) {
super(reactContext); database = SQLiteDatabase.openDatabase( absoluteFilesPath + "/nostros.sqlite", null, SQLiteDatabase.OPEN_READWRITE);
String dbPath = reactContext.getFilesDir().getAbsolutePath();
database = SQLiteDatabase.openDatabase( dbPath + "/nostros.sqlite", null, SQLiteDatabase.OPEN_READWRITE);
} }
@Override
public String getName() {
return "DatabaseModule";
}
@ReactMethod
public void saveEvent(JSONObject data, String userPubKey) throws JSONException { public void saveEvent(JSONObject data, String userPubKey) throws JSONException {
Event event = new Event(data); Event event = new Event(data);
event.save(database, userPubKey); event.save(database, userPubKey);
} }
public void saveRelay(Relay relay) {
relay.save(database);
}
public void destroyRelay(Relay relay) {
relay.destroy(database);
}
public List<Relay> getRelays() {
List<Relay> relayList = new ArrayList<>();
String query = "SELECT url FROM nostros_relays;";
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {});
if (cursor.getCount() > 0) {
Log.d("WebSocket", String.valueOf(cursor.getCount()));
for (int i = 1; i < cursor.getCount(); i++) {
Log.d("WebSocket", String.valueOf(i));
try {
String relayUrl = cursor.getString(i);
Relay relay = new Relay(relayUrl, this);
relayList.add(relay);
} catch (IOException e) {
Log.d("WebSocket", e.toString());
}
}
}
return relayList;
}
} }

View File

@ -0,0 +1,82 @@
package com.nostros.modules;
import android.util.Log;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.nostros.classes.Relay;
import java.io.IOException;
import java.util.List;
public class RelayPoolModule extends ReactContextBaseJavaModule {
protected List<Relay> relays;
private String userPubKey;
private DatabaseModule database;
public RelayPoolModule(ReactApplicationContext reactContext) {
database = new DatabaseModule(reactContext.getFilesDir().getAbsolutePath());
List<Relay> relayList = database.getRelays();
if (relayList.isEmpty()) {
try {
relayList.add(new Relay("wss://relay.damus.io", database));
} catch (IOException e) {
Log.d("WebSocket", e.toString());
}
}
relays = relayList;
}
@Override
public String getName() {
return "RelayPoolModule";
}
@ReactMethod
public void add(String url, Callback callback) {
try {
Relay relay = new Relay(url, database);
relay.connect(userPubKey);
relays.add(relay);
database.saveRelay(relay);
} catch (IOException e) {
Log.d("WebSocket", e.toString());
}
callback.invoke();
}
@ReactMethod
public void remove(String url, Callback callback) {
for (Relay relay : relays) {
if (relay.url.equals(url)) {
relay.disconnect();
relays.remove(relay);
database.destroyRelay(relay);
}
}
callback.invoke();
}
@ReactMethod
public void connect(String pubKey, Callback callback) {
userPubKey = pubKey;
for (Relay relay : relays) {
try {
relay.connect(pubKey);
} catch (IOException e) {
Log.d("WebSocket", e.toString());
}
}
callback.invoke();
}
@ReactMethod
public void send(String message) {
for (Relay relay : relays) {
relay.send(message);
}
}
}

View File

@ -1,12 +1,4 @@
import { import { Button, Divider, Input, Layout, TopNavigation, useTheme } from '@ui-kitten/components'
Button,
Divider,
Input,
Layout,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components'
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react'
import { Clipboard, StyleSheet } from 'react-native' import { Clipboard, StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
@ -49,9 +41,10 @@ export const ConfigPage: React.FC = () => {
} }
const renderBackAction = (): JSX.Element => ( const renderBackAction = (): JSX.Element => (
<TopNavigationAction <Button
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack} onPress={onPressBack}
appearance='ghost'
/> />
) )

View File

@ -12,7 +12,7 @@ import { populatePets } from '../../Functions/RelayFunctions/Users'
export const ContactsPage: React.FC = () => { export const ContactsPage: React.FC = () => {
const { database } = useContext(AppContext) const { database } = useContext(AppContext)
const { relayPool, publicKey, privateKey } = useContext(RelayPoolContext) const { relayPool, publicKey, privateKey, lastEventId } = useContext(RelayPoolContext)
const theme = useTheme() const theme = useTheme()
const [users, setUsers] = useState<User[]>() const [users, setUsers] = useState<User[]>()
const [refreshing, setRefreshing] = useState(true) const [refreshing, setRefreshing] = useState(true)
@ -22,6 +22,10 @@ export const ContactsPage: React.FC = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
useEffect(() => {
loadUsers()
}, [lastEventId])
useEffect(() => { useEffect(() => {
setUsers([]) setUsers([])
loadUsers() loadUsers()
@ -36,13 +40,16 @@ export const ContactsPage: React.FC = () => {
} }
getUsers(database, filters).then((results) => { getUsers(database, filters).then((results) => {
if (results) { if (results && results.length > 0) {
setUsers(results) setUsers(results)
const missingDataUsers = results.filter((user) => !user.picture).map((user) => user.id)
if (missingDataUsers.length > 0) {
relayPool?.subscribe('main-channel', { relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta], kinds: [EventKind.meta],
authors: results.map((user) => user.id), authors: missingDataUsers,
}) })
} }
}
setRefreshing(false) setRefreshing(false)
}) })
} }

View File

@ -15,11 +15,11 @@ import NoteCard from '../NoteCard'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events' import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/Relay'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events' import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users' import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { handleInfinityScroll } from '../../Functions/NativeFunctions' import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import Loading from '../Loading' import Loading from '../Loading'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
export const HomePage: React.FC = () => { export const HomePage: React.FC = () => {
const { database, goToPage } = useContext(AppContext) const { database, goToPage } = useContext(AppContext)
@ -33,7 +33,7 @@ export const HomePage: React.FC = () => {
const calculateInitialNotes: () => Promise<void> = async () => { const calculateInitialNotes: () => Promise<void> = async () => {
if (database && publicKey) { if (database && publicKey) {
setTimeout(() => setRefreshing(false), 3000) setTimeout(() => setRefreshing(false), 2000)
const users = await getUsers(database, { contacts: true, includeIds: [publicKey] }) const users = await getUsers(database, { contacts: true, includeIds: [publicKey] })
setAuthors(users) setAuthors(users)
subscribeNotes(users) subscribeNotes(users)
@ -43,21 +43,12 @@ export const HomePage: React.FC = () => {
const subscribeNotes: (users: User[], past?: boolean) => void = (users, past) => { const subscribeNotes: (users: User[], past?: boolean) => void = (users, past) => {
if (!database || !publicKey || users.length === 0) return if (!database || !publicKey || users.length === 0) return
const limit = past ? pageSize : initialPageSize
getNotes(database, { contacts: true, includeIds: [publicKey], limit }).then((results) => {
const message: RelayFilters = { const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer], kinds: [EventKind.textNote, EventKind.recommendServer],
authors: users.map((user) => user.id), authors: users.map((user) => user.id),
limit: initialPageSize, limit: pageSize,
}
if (past) {
message.until = results[results.length - 1]?.created_at
} else if (results.length >= pageSize) {
message.since = results[0]?.created_at
} }
relayPool?.subscribe('main-channel', message) relayPool?.subscribe('main-channel', message)
})
} }
const loadNotes: () => void = () => { const loadNotes: () => void = () => {
@ -65,6 +56,15 @@ export const HomePage: React.FC = () => {
getNotes(database, { contacts: true, includeIds: [publicKey], limit: pageSize }).then( getNotes(database, { contacts: true, includeIds: [publicKey], limit: pageSize }).then(
(notes) => { (notes) => {
setNotes(notes) setNotes(notes)
const missingDataNotes = notes
.filter((note) => !note.picture || note.picture === '')
.map((note) => note.pubkey)
if (missingDataNotes.length > 0) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors: missingDataNotes,
})
}
}, },
) )
} }

View File

@ -44,7 +44,7 @@ export const Logger: React.FC = () => {
kinds: [EventKind.petNames, EventKind.meta], kinds: [EventKind.petNames, EventKind.meta],
authors: [publicKey], authors: [publicKey],
}) })
setTimeout(() => goToPage('home', true), 3000) setTimeout(() => goToPage('home', true), 5000)
} }
}, [loadingRelayPool, publicKey, loadingDb]) }, [loadingRelayPool, publicKey, loadingDb])

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { Button, Layout, Text, useTheme } from '@ui-kitten/components' import { Button, Layout, Text, useTheme } from '@ui-kitten/components'
import { Note } from '../../Functions/DatabaseFunctions/Notes' import { Note } from '../../Functions/DatabaseFunctions/Notes'
import { StyleSheet, TouchableOpacity } from 'react-native' import { StyleSheet, TouchableOpacity } from 'react-native'
@ -6,7 +6,6 @@ import Markdown from 'react-native-markdown-display'
import { EventKind } from '../../lib/nostr/Events' import { EventKind } from '../../lib/nostr/Events'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { addRelay } from '../../Functions/DatabaseFunctions/Relays'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import { showMessage } from 'react-native-flash-message' import { showMessage } from 'react-native-flash-message'
import { t } from 'i18next' import { t } from 'i18next'
@ -15,6 +14,7 @@ import moment from 'moment'
import { populateRelay } from '../../Functions/RelayFunctions' import { populateRelay } from '../../Functions/RelayFunctions'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { markdownIt, markdownStyle } from '../../Constants/AppConstants' import { markdownIt, markdownStyle } from '../../Constants/AppConstants'
import { searchRelays } from '../../Functions/DatabaseFunctions/Relays'
interface NoteCardProps { interface NoteCardProps {
note: Note note: Note
@ -22,11 +22,17 @@ interface NoteCardProps {
export const NoteCard: React.FC<NoteCardProps> = ({ note }) => { export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
const theme = useTheme() const theme = useTheme()
const { relayPool, setRelayPool, publicKey } = useContext(RelayPoolContext) const { relayPool, publicKey } = useContext(RelayPoolContext)
const { database, goToPage } = useContext(AppContext) const { database, goToPage } = useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>( const [relayAdded, setRelayAdded] = useState<boolean>(false)
Object.keys(relayPool?.relays ?? {}).includes(note.content),
) useEffect(() => {
if (database) {
searchRelays(note.content, database).then((result) => {
setRelayAdded(result.length > 0)
})
}
}, [database])
const textNote: () => JSX.Element = () => { const textNote: () => JSX.Element = () => {
return ( return (
@ -73,9 +79,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
const addRelayItem: () => void = () => { const addRelayItem: () => void = () => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
relayPool.add(note.content) relayPool.add(note.content, () => {
setRelayPool(relayPool)
addRelay({ url: note.content }, database)
populateRelay(relayPool, database, publicKey) populateRelay(relayPool, database, publicKey)
showMessage({ showMessage({
message: t('alerts.relayAdded'), message: t('alerts.relayAdded'),
@ -83,6 +87,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
type: 'success', type: 'success',
}) })
setRelayAdded(true) setRelayAdded(true)
})
} }
} }

View File

@ -1,11 +1,4 @@
import { import { Button, Card, Layout, Spinner, TopNavigation, useTheme } from '@ui-kitten/components'
Card,
Layout,
Spinner,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react' import React, { useCallback, useContext, useEffect, useState } from 'react'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes' import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
@ -13,7 +6,6 @@ import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import NoteCard from '../NoteCard' import NoteCard from '../NoteCard'
import { EventKind } from '../../lib/nostr/Events' import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/Relay'
import { import {
Clipboard, Clipboard,
RefreshControl, RefreshControl,
@ -25,6 +17,7 @@ import {
} from 'react-native' } from 'react-native'
import Loading from '../Loading' import Loading from '../Loading'
import { getDirectReplies, getReplyEventId } from '../../Functions/RelayFunctions/Events' import { getDirectReplies, getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
export const NotePage: React.FC = () => { export const NotePage: React.FC = () => {
const { page, goBack, goToPage, database, getActualPage } = useContext(AppContext) const { page, goBack, goToPage, database, getActualPage } = useContext(AppContext)
@ -113,18 +106,20 @@ export const NotePage: React.FC = () => {
const renderBackAction = (): JSX.Element => { const renderBackAction = (): JSX.Element => {
return ( return (
<TopNavigationAction <Button
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack} onPress={onPressBack}
appearance='ghost'
/> />
) )
} }
const renderNoteActions = (): JSX.Element => { const renderNoteActions = (): JSX.Element => {
return note && getReplyEventId(note) ? ( return note && getReplyEventId(note) ? (
<TopNavigationAction <Button
icon={<Icon name='arrow-up' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-up' size={16} color={theme['text-basic-color']} />}
onPress={onPressGoParent} onPress={onPressGoParent}
appearance='ghost'
/> />
) : ( ) : (
<></> <></>

View File

@ -1,12 +1,4 @@
import { import { Button, Card, Layout, Spinner, Text, TopNavigation, useTheme } from '@ui-kitten/components'
Card,
Layout,
Spinner,
Text,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react' import React, { useCallback, useContext, useEffect, useState } from 'react'
import { import {
Clipboard, Clipboard,
@ -24,13 +16,13 @@ import NoteCard from '../NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getUser, User, updateUserContact } from '../../Functions/DatabaseFunctions/Users' import { getUser, User, updateUserContact } from '../../Functions/DatabaseFunctions/Users'
import { EventKind } from '../../lib/nostr/Events' import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/Relay'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import { populatePets } from '../../Functions/RelayFunctions/Users' import { populatePets } from '../../Functions/RelayFunctions/Users'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events' import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import Loading from '../Loading' import Loading from '../Loading'
import { handleInfinityScroll } from '../../Functions/NativeFunctions' import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
export const ProfilePage: React.FC = () => { export const ProfilePage: React.FC = () => {
const { database, page, goToPage, goBack } = useContext(AppContext) const { database, page, goToPage, goBack } = useContext(AppContext)
@ -80,20 +72,14 @@ export const ProfilePage: React.FC = () => {
const subscribeNotes: (past?: boolean) => void = (past) => { const subscribeNotes: (past?: boolean) => void = (past) => {
if (!database) return if (!database) return
const limit = past ? pageSize : initialPageSize
getNotes(database, { filters: { pubkey: userId }, limit }).then((results) => {
const message: RelayFilters = { const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer], kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId], authors: [userId],
limit: initialPageSize, limit: pageSize,
}
if (past) {
message.until = results[results.length - 1]?.created_at
} else if (results.length >= pageSize) {
message.since = results[0]?.created_at
} }
relayPool?.subscribe('main-channel', message) relayPool?.subscribe('main-channel', message)
})
} }
const subscribeProfile: () => Promise<void> = async () => { const subscribeProfile: () => Promise<void> = async () => {
@ -148,25 +134,32 @@ export const ProfilePage: React.FC = () => {
const renderOptions: () => JSX.Element = () => { const renderOptions: () => JSX.Element = () => {
if (publicKey === userId) { if (publicKey === userId) {
return ( return (
<TopNavigationAction <Button
icon={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />} accessoryRight={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
onPress={() => goToPage('config')} onPress={() => goToPage('config')}
appearance='ghost'
/> />
) )
} else { } else {
if (user) { if (user) {
if (isContact) { if (isContact) {
return ( return (
<TopNavigationAction <Button
icon={<Icon name='user-minus' size={16} color={theme['color-danger-500']} solid />} accessoryRight={
<Icon name='user-minus' size={16} color={theme['color-danger-500']} solid />
}
onPress={removeAuthor} onPress={removeAuthor}
appearance='ghost'
/> />
) )
} else { } else {
return ( return (
<TopNavigationAction <Button
icon={<Icon name='user-plus' size={16} color={theme['color-success-500']} solid />} accessoryRight={
<Icon name='user-plus' size={16} color={theme['color-success-500']} solid />
}
onPress={addAuthor} onPress={addAuthor}
appearance='ghost'
/> />
) )
} }
@ -177,7 +170,6 @@ export const ProfilePage: React.FC = () => {
} }
const onPressBack: () => void = () => { const onPressBack: () => void = () => {
relayPool?.removeOn('event', 'profile')
relayPool?.unsubscribeAll() relayPool?.unsubscribeAll()
goBack() goBack()
} }
@ -187,9 +179,10 @@ export const ProfilePage: React.FC = () => {
return <></> return <></>
} else { } else {
return ( return (
<TopNavigationAction <Button
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack} onPress={onPressBack}
appearance='ghost'
/> />
) )
} }
@ -273,8 +266,7 @@ export const ProfilePage: React.FC = () => {
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => { const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) { if (handleInfinityScroll(event)) {
const newSize: number = notes?.length === pageSize ? pageSize + initialPageSize : pageSize setPageSize(pageSize + initialPageSize)
setPageSize(newSize)
} }
} }

View File

@ -1,26 +1,18 @@
import { import { Button, Layout, TopNavigation, useTheme, Text } from '@ui-kitten/components'
Button,
Layout,
TopNavigation,
TopNavigationAction,
useTheme,
Text,
} from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { RefreshControl, ScrollView, StyleSheet } from 'react-native' import { RefreshControl, ScrollView, StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getRelays, Relay, addRelay, removeRelay } from '../../Functions/DatabaseFunctions/Relays' import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays'
import { defaultRelays } from '../../Constants/RelayConstants' import { defaultRelays } from '../../Constants/RelayConstants'
import { populateRelay } from '../../Functions/RelayFunctions'
import { showMessage } from 'react-native-flash-message' import { showMessage } from 'react-native-flash-message'
export const RelaysPage: React.FC = () => { export const RelaysPage: React.FC = () => {
const theme = useTheme() const theme = useTheme()
const { goBack, database } = useContext(AppContext) const { goBack, database } = useContext(AppContext)
const { relayPool, publicKey, setRelayPool } = useContext(RelayPoolContext) const { relayPool, publicKey } = useContext(RelayPoolContext)
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [relays, setRelays] = useState<Relay[]>([]) const [relays, setRelays] = useState<Relay[]>([])
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@ -43,40 +35,38 @@ export const RelaysPage: React.FC = () => {
} }
const renderBackAction = (): JSX.Element => ( const renderBackAction = (): JSX.Element => (
<TopNavigationAction <Button
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack} onPress={onPressBack}
appearance='ghost'
/> />
) )
const addRelayItem: (url: string) => void = async (url) => { const addRelayItem: (url: string) => void = async (url) => {
if (relayPool && database && publicKey && (relays?.length ?? 0) < 2) { if (relayPool && database && publicKey && (relays?.length ?? 0) < 2) {
setLoading(true) setLoading(true)
relayPool.add(url) relayPool.add(url, () => {
setRelayPool(relayPool)
await addRelay({ url }, database)
populateRelay(relayPool, database, publicKey)
showMessage({ showMessage({
message: t('alerts.relayAdded'), message: t('alerts.relayAdded'),
description: url, description: url,
type: 'success', type: 'success',
}) })
loadRelays() loadRelays()
})
} }
} }
const removeRelayItem: (url: string) => void = async (url) => { const removeRelayItem: (url: string) => void = async (url) => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
setLoading(true) setLoading(true)
relayPool.remove(url) relayPool.remove(url, () => {
setRelayPool(relayPool)
await removeRelay({ url }, database)
showMessage({ showMessage({
message: t('alerts.relayRemoved'), message: t('alerts.relayRemoved'),
description: url, description: url,
type: 'danger', type: 'danger',
}) })
loadRelays() loadRelays()
})
} }
} }

View File

@ -1,12 +1,4 @@
import { import { Button, Input, Layout, Spinner, TopNavigation, useTheme } from '@ui-kitten/components'
Button,
Input,
Layout,
Spinner,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
@ -87,9 +79,10 @@ export const SendPage: React.FC = () => {
} }
const renderBackAction = (): JSX.Element => ( const renderBackAction = (): JSX.Element => (
<TopNavigationAction <Button
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />} accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack} onPress={onPressBack}
appearance='ghost'
/> />
) )

View File

@ -1,14 +1,8 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import Relay from '../lib/nostr/Relay'
import { Event } from '../lib/nostr/Events'
import RelayPool from '../lib/nostr/RelayPool/intex' import RelayPool from '../lib/nostr/RelayPool/intex'
import { AppContext } from './AppContext' import { AppContext } from './AppContext'
import { getRelays, addRelay } from '../Functions/DatabaseFunctions/Relays'
import { showMessage } from 'react-native-flash-message'
import SInfo from 'react-native-sensitive-info' import SInfo from 'react-native-sensitive-info'
import { getPublickey } from '../lib/nostr/Bip' import { getPublickey } from '../lib/nostr/Bip'
import { defaultRelays } from '../Constants/RelayConstants'
import WebsocketModule from '../lib/nostr/Native/WebsocketModule'
import moment from 'moment' import moment from 'moment'
export interface RelayPoolContextProps { export interface RelayPoolContextProps {
@ -50,31 +44,9 @@ export const RelayPoolContextProvider = ({
const [lastPage, setLastPage] = useState<string>(page) const [lastPage, setLastPage] = useState<string>(page)
const loadRelayPool: () => void = async () => { const loadRelayPool: () => void = async () => {
if (database) { if (database && publicKey) {
const relays = await getRelays(database)
const initRelayPool = new RelayPool([], privateKey) const initRelayPool = new RelayPool([], privateKey)
if (relays && relays.length > 0) { initRelayPool.connect(publicKey)
relays.forEach((relay) => {
initRelayPool.add(relay.url)
})
} else {
// pickRandomItems(defaultRelays, 1).forEach((relayUrl) => {
initRelayPool.add(defaultRelays[4])
addRelay({ url: defaultRelays[4] }, database)
// })
}
initRelayPool?.on(
'notice',
'RelayPoolContextProvider',
async (relay: Relay, _subId?: string, event?: Event) => {
showMessage({
message: relay.url,
description: event?.content ?? '',
type: 'info',
})
},
)
setRelayPool(initRelayPool) setRelayPool(initRelayPool)
setLoadingRelayPool(false) setLoadingRelayPool(false)
} }
@ -86,22 +58,17 @@ export const RelayPoolContextProvider = ({
} }
useEffect(() => { useEffect(() => {
WebsocketModule.connectWebsocket((message) => {
console.log('WEBSOCKET', message)
setClock() setClock()
})
}, []) }, [])
useEffect(() => { useEffect(() => {
if (relayPool && lastPage !== page) { if (relayPool && lastPage !== page) {
relayPool.removeOn('event', lastPage)
setLastPage(page) setLastPage(page)
} }
}, [page]) }, [page])
useEffect(() => { useEffect(() => {
if (publicKey && publicKey !== '') { if (publicKey && publicKey !== '') {
WebsocketModule.setUserPubKey(publicKey)
if (!loadingRelayPool && page !== 'landing') { if (!loadingRelayPool && page !== 'landing') {
goToPage('home', true) goToPage('home', true)
} else { } else {

View File

@ -1,4 +1,4 @@
import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite' import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..' import { getItems } from '..'
export interface Relay { export interface Relay {
@ -10,38 +10,6 @@ const databaseToEntity: (object: any) => Relay = (object) => {
return object as Relay return object as Relay
} }
export const addRelay: (
relay: Relay,
db: QuickSQLiteConnection,
) => Promise<QueryResult | undefined> = async (relay, db) => {
if (relay.url) {
const relays: Relay[] = await searchRelays(relay.url, db)
if (relays.length === 0) {
const query = `
INSERT OR IGNORE INTO nostros_relays
(url)
VALUES
(?);
`
const queryValues = [relay.url.split("'").join("''")]
return db.execute(query, queryValues)
}
}
}
export const removeRelay: (
relay: Relay,
db: QuickSQLiteConnection,
) => Promise<QueryResult | undefined> = async (relay, db) => {
if (relay.url) {
const query = `
DELETE FROM nostros_relays WHERE url=?;
`
return db.execute(query, [relay.url])
}
}
export const searchRelays: ( export const searchRelays: (
relayUrl: string, relayUrl: string,
db: QuickSQLiteConnection, db: QuickSQLiteConnection,

View File

@ -22,7 +22,7 @@ export const isDirectReply: (mainEvent: Event, reply: Event) => boolean = (mainE
const taggedReplyEventsIds: string[] = getTaggedEventIds(reply) const taggedReplyEventsIds: string[] = getTaggedEventIds(reply)
const difference = taggedReplyEventsIds.filter((item) => !taggedMainEventsIds.includes(item)) const difference = taggedReplyEventsIds.filter((item) => !taggedMainEventsIds.includes(item))
return difference.length === 1 && difference[0] === mainEvent.id return difference.length === 1 && difference[0] === mainEvent?.id
} }
export const getTaggedEventIds: (event: Event) => string[] = (event) => { export const getTaggedEventIds: (event: Event) => string[] = (event) => {
@ -31,5 +31,5 @@ export const getTaggedEventIds: (event: Event) => string[] = (event) => {
} }
export const getETags: (event: Event) => string[][] = (event) => { export const getETags: (event: Event) => string[][] = (event) => {
return event.tags.filter((tag) => tag[0] === 'e') return event?.tags.filter((tag) => tag[0] === 'e') || []
} }

View File

@ -0,0 +1,11 @@
import { NativeModules } from 'react-native'
const { RelayPoolModule } = NativeModules
interface RelayPoolInterface {
send: (message: string) => void
connect: (pubKey: string, callback: () => void) => void
add: (url: string, callback: () => void) => void
remove: (url: string, callback: () => void) => void
}
export default RelayPoolModule as RelayPoolInterface

View File

@ -1,10 +0,0 @@
import { NativeModules } from 'react-native'
const { WebsocketModule } = NativeModules
interface WebsocketInterface {
connectWebsocket: (callback: (message: string) => void) => void
send: (message: string) => void
setUserPubKey: (pubKey: string) => void
}
export default WebsocketModule as WebsocketInterface

View File

@ -1,131 +0,0 @@
import { Event } from '../Events'
import { v5 as uuidv5 } from 'uuid'
import WebsocketModule from '../Native/WebsocketModule'
export interface RelayFilters {
ids?: string[]
authors?: string[]
kinds?: number[]
'#e'?: string[]
'#p'?: string[]
since?: number
limit?: number
until?: number
}
export interface RelayMessage {
data: string
}
export interface RelayOptions {
reconnect?: boolean
}
class Relay {
constructor(relayUrl: string, options: RelayOptions = { reconnect: true }) {
this.url = relayUrl
this.options = options
this.manualClose = false
this.socket = new WebSocket(this.url)
this.subscriptions = {}
this.onOpen = () => {}
this.onEvent = () => {}
this.onEsoe = () => {}
this.onNotice = () => {}
this.initWebsocket()
}
private readonly options: RelayOptions
private socket: WebSocket
private manualClose: boolean
private subscriptions: { [subId: string]: string[] }
private readonly initWebsocket: () => void = async () => {
this.socket.onmessage = (message) => {
this.handleNostrMessage(message as RelayMessage)
}
this.socket.onclose = this.onClose
this.socket.onerror = this.onError
this.socket.onopen = () => this.onOpen(this)
}
private readonly onClose: () => void = async () => {
if (!this.manualClose && this.options.reconnect) this.initWebsocket()
}
private readonly onError: () => void = async () => {
if (this.options.reconnect) this.initWebsocket()
}
private readonly handleNostrMessage: (message: RelayMessage) => void = async (message) => {
const data: any[] = JSON.parse(message.data)
if (data.length >= 2) {
const id: string = data[1]
if (data[0] === 'EVENT') {
if (data.length < 3) return
const message: Event = data[2]
return this.onEvent(this, id, message)
} else if (data[0] === 'EOSE') {
return this.onEsoe(this, id)
} else if (data[0] === 'NOTICE') {
return this.onNotice(this, [...data.slice(1)])
}
}
}
private readonly send: (message: object) => void = async (message) => {
const tosend = JSON.stringify(message)
console.log('SEND =====>', tosend)
WebsocketModule.send(tosend)
}
public url: string
public onOpen: (relay: Relay) => void
public onEvent: (relay: Relay, subId: string, event: Event) => void
public onEsoe: (relay: Relay, subId: string) => void
public onNotice: (relay: Relay, events: Event[]) => void
public readonly close: () => void = async () => {
if (this.socket) {
this.manualClose = true
this.socket.close()
}
}
public readonly sendEvent: (event: Event) => void = async (event) => {
this.send(['EVENT', event])
}
public readonly subscribe: (subId: string, filters?: RelayFilters) => void = async (
subId,
filters = {},
) => {
const uuid = uuidv5(
`${subId}${JSON.stringify(filters)}`,
'57003344-b2cb-4b6f-a579-fae9e82c370a',
)
if (this.subscriptions[subId]?.includes(uuid)) {
console.log('Subscription already done!')
} else {
this.send(['REQ', subId, filters])
const newSubscriptions = [...(this.subscriptions[subId] ?? []), uuid]
this.subscriptions[subId] = newSubscriptions
}
}
public readonly unsubscribe: (subId: string) => void = async (subId) => {
this.send(['CLOSE', subId])
delete this.subscriptions[subId]
}
public readonly unsubscribeAll: () => void = async () => {
Object.keys(this.subscriptions).forEach((subId: string) => {
this.unsubscribe(subId)
})
}
}
export default Relay

View File

@ -1,105 +1,60 @@
// import { spawnThread } from 'react-native-multithreading' // import { spawnThread } from 'react-native-multithreading'
import { signEvent, validateEvent, Event } from '../Events' import { signEvent, validateEvent, Event } from '../Events'
import Relay, { RelayFilters, RelayOptions } from '../Relay' import { v5 as uuidv5 } from 'uuid'
import RelayPoolModule from '../../Native/WebsocketModule'
export interface OnFunctions { export interface RelayFilters {
open: { [id: string]: (relay: Relay) => void } ids?: string[]
event: { [id: string]: (relay: Relay, subId: string, event: Event) => void } authors?: string[]
esoe: { [id: string]: (relay: Relay, subId: string) => void } kinds?: number[]
notice: { [id: string]: (relay: Relay, events: Event[]) => void } '#e'?: string[]
'#p'?: string[]
since?: number
limit?: number
until?: number
}
export interface RelayMessage {
data: string
} }
class RelayPool { class RelayPool {
constructor(relaysUrls: string[], privateKey?: string, options: RelayOptions = {}) { constructor(relaysUrls: string[], privateKey?: string) {
this.relays = {}
this.privateKey = privateKey this.privateKey = privateKey
this.options = options this.subscriptions = {}
this.onFunctions = {
open: {},
event: {},
esoe: {},
notice: {},
}
relaysUrls.forEach((relayUrl) => { relaysUrls.forEach((relayUrl) => {
this.add(relayUrl) this.add(relayUrl)
}) })
this.setupHandlers()
} }
private readonly privateKey?: string private readonly privateKey?: string
private readonly options: RelayOptions private subscriptions: { [subId: string]: string[] }
private readonly onFunctions: OnFunctions
public relays: { [url: string]: Relay }
private readonly setupHandlers: () => void = () => { private readonly send: (message: object) => void = async (message) => {
Object.keys(this.relays).forEach((relayUrl: string) => { const tosend = JSON.stringify(message)
const relay: Relay = this.relays[relayUrl] RelayPoolModule.send(tosend)
relay.onOpen = (openRelay) => {
Object.keys(this.onFunctions.open).forEach((id) => this.onFunctions.open[id](openRelay))
}
relay.onEvent = (eventRelay, subId, event) => {
Object.keys(this.onFunctions.event).forEach((id) => {
this.onFunctions.event[id](eventRelay, subId, event)
})
}
relay.onEsoe = (eventRelay, subId) => {
Object.keys(this.onFunctions.esoe).forEach((id) =>
this.onFunctions.esoe[id](eventRelay, subId),
)
}
relay.onNotice = (eventRelay, events) => {
Object.keys(this.onFunctions.notice).forEach((id) =>
this.onFunctions.notice[id](eventRelay, events),
)
}
})
} }
public on: ( public readonly connect: (publicKey: string, callback?: () => void) => void = async (
method: 'open' | 'event' | 'esoe' | 'notice', publicKey,
id: string, callback = () => {},
fn: (relay: Relay, subId?: string, event?: Event) => void,
) => void = async (method, id, fn) => {
this.onFunctions[method][id] = fn
}
public removeOn: (method: 'open' | 'event' | 'esoe' | 'notice', id: string) => void = async (
method,
id,
) => { ) => {
delete this.onFunctions[method][id] RelayPoolModule.connect(publicKey, callback)
} }
public readonly add: (relayUrl: string) => Promise<boolean> = async (relayUrl) => { public readonly add: (relayUrl: string, callback?: () => void) => void = async (
if (this.relays[relayUrl]) return false relayUrl,
callback = () => {},
this.relays[relayUrl] = new Relay(relayUrl, this.options) ) => {
this.setupHandlers() RelayPoolModule.add(relayUrl, callback)
return true
} }
public readonly close: () => void = async () => { public readonly remove: (relayUrl: string, callback?: () => void) => void = async (
Object.keys(this.relays).forEach((relayUrl: string) => { relayUrl,
const relay: Relay = this.relays[relayUrl] callback = () => {},
relay.close() ) => {
}) RelayPoolModule.remove(relayUrl, callback)
}
public readonly remove: (relayUrl: string) => Promise<boolean> = async (relayUrl) => {
const relay: Relay | undefined = this.relays[relayUrl]
if (relay) {
relay.close()
delete this.relays[relayUrl]
return true
} else {
return false
}
} }
public readonly sendEvent: (event: Event) => Promise<Event | null> = async (event) => { public readonly sendEvent: (event: Event) => Promise<Event | null> = async (event) => {
@ -107,10 +62,7 @@ class RelayPool {
const signedEvent: Event = await signEvent(event, this.privateKey) const signedEvent: Event = await signEvent(event, this.privateKey)
if (validateEvent(signedEvent)) { if (validateEvent(signedEvent)) {
Object.keys(this.relays).forEach((relayUrl: string) => { this.send(['EVENT', event])
const relay: Relay = this.relays[relayUrl]
relay.sendEvent(signedEvent)
})
return signedEvent return signedEvent
} else { } else {
@ -126,20 +78,27 @@ class RelayPool {
subId, subId,
filters, filters,
) => { ) => {
Object.keys(this.relays).forEach((relayUrl: string) => { const uuid = uuidv5(
this.relays[relayUrl].subscribe(subId, filters) `${subId}${JSON.stringify(filters)}`,
}) '57003344-b2cb-4b6f-a579-fae9e82c370a',
)
if (this.subscriptions[subId]?.includes(uuid)) {
console.log('Subscription already done!', filters)
} else {
this.send(['REQ', subId, filters])
const newSubscriptions = [...(this.subscriptions[subId] ?? []), uuid]
this.subscriptions[subId] = newSubscriptions
}
} }
public readonly unsubscribe: (subId: string) => void = async (subId) => { public readonly unsubscribe: (subId: string) => void = async (subId) => {
Object.keys(this.relays).forEach((relayUrl: string) => { this.send(['CLOSE', subId])
this.relays[relayUrl].unsubscribe(subId) delete this.subscriptions[subId]
})
} }
public readonly unsubscribeAll: () => void = async () => { public readonly unsubscribeAll: () => void = async () => {
Object.keys(this.relays).forEach((relayUrl: string) => { Object.keys(this.subscriptions).forEach((subId: string) => {
this.relays[relayUrl].unsubscribeAll() this.unsubscribe(subId)
}) })
} }
} }