This commit is contained in:
KoalaSat 2022-10-31 01:37:42 +01:00
parent f56bf837a7
commit 0466e45da5
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
34 changed files with 1207 additions and 1222 deletions

View File

@ -4,8 +4,7 @@ module.exports = {
'eslint:recommended',
'plugin:react/recommended',
'standard-with-typescript',
'prettier',
'@react-native-community',
'prettier'
],
parserOptions: {
ecmaVersion: 8,

View File

@ -1,6 +1,6 @@
import App from './frontend';
import { Buffer as SafeBuffer } from 'safe-buffer';
import App from './frontend'
import { Buffer as SafeBuffer } from 'safe-buffer'
global.Buffer = SafeBuffer;
global.Buffer = SafeBuffer
export default App;
export default App

View File

@ -1,14 +0,0 @@
/**
* @format
*/
import 'react-native';
import React from 'react';
import App from '../App';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
renderer.create(<App />);
});

View File

@ -3,66 +3,66 @@ import {
Layout,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components';
import React, { useContext, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { useTranslation } from 'react-i18next';
useTheme
} from '@ui-kitten/components'
import React, { useContext, useEffect } from 'react'
import { StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { useTranslation } from 'react-i18next'
// import EncryptedStorage from 'react-native-encrypted-storage';
import { dropTables } from '../../Functions/DatabaseFunctions';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import { dropTables } from '../../Functions/DatabaseFunctions'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
export const ConfigPage: React.FC = () => {
const theme = useTheme();
const { goToPage, goBack, database, init } = useContext(AppContext);
const { setPrivateKey, setPublicKey, relayPool } = useContext(RelayPoolContext);
const { t } = useTranslation('common');
const theme = useTheme()
const { goToPage, goBack, database, init } = useContext(AppContext)
const { setPrivateKey, setPublicKey, relayPool } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
useEffect(() => {
relayPool?.unsubscribeAll();
}, []);
relayPool?.unsubscribeAll()
}, [])
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll();
goBack();
};
relayPool?.unsubscribeAll()
goBack()
}
const onPressLogout: () => void = () => {
if (database) {
relayPool?.unsubscribeAll();
setPrivateKey(undefined);
setPublicKey(undefined);
relayPool?.unsubscribeAll()
setPrivateKey(undefined)
setPublicKey(undefined)
dropTables(database).then(() => {
// EncryptedStorage.removeItem('privateKey').then(() => {
init();
goToPage('landing', true);
init()
goToPage('landing', true)
// });
});
})
}
};
}
const renderBackAction = (): JSX.Element => (
<TopNavigationAction
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
/>
);
)
const styles = StyleSheet.create({
container: {
flex: 1,
flex: 1
},
actionContainer: {
marginTop: 30,
paddingLeft: 32,
paddingRight: 32,
paddingRight: 32
},
button: {
marginTop: 30,
},
});
marginTop: 30
}
})
return (
<>
@ -87,7 +87,7 @@ export const ConfigPage: React.FC = () => {
</Layout>
</Layout>
</>
);
};
)
}
export default ConfigPage;
export default ConfigPage

View File

@ -1,109 +1,109 @@
import { Button, Card, Input, Layout, Modal, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import ActionButton from 'react-native-action-button';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { Event, EventKind } from '../../lib/nostr/Events';
import { useTranslation } from 'react-i18next';
import { Button, Card, Input, Layout, Modal, useTheme } from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { RefreshControl, ScrollView, StyleSheet } from 'react-native'
import ActionButton from 'react-native-action-button'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { Event, EventKind } from '../../lib/nostr/Events'
import { useTranslation } from 'react-i18next'
import {
addContact,
getUsers,
insertUserContact,
User,
} from '../../Functions/DatabaseFunctions/Users';
import UserCard from '../UserCard';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import Relay from '../../lib/nostr/Relay';
import { populatePets, tagToUser } from '../../Functions/RelayFunctions/Users';
User
} from '../../Functions/DatabaseFunctions/Users'
import UserCard from '../UserCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import Relay from '../../lib/nostr/Relay'
import { populatePets, tagToUser } from '../../Functions/RelayFunctions/Users'
export const ContactsPage: React.FC = () => {
const { database } = useContext(AppContext);
const { relayPool, publicKey, lastEventId, setLastEventId } = useContext(RelayPoolContext);
const theme = useTheme();
const [users, setUsers] = useState<User[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [showAddContact, setShowAddContant] = useState<boolean>(false);
const [contactInput, setContactInput] = useState<string>();
const { t } = useTranslation('common');
const { database } = useContext(AppContext)
const { relayPool, publicKey, lastEventId, setLastEventId } = useContext(RelayPoolContext)
const theme = useTheme()
const [users, setUsers] = useState<User[]>([])
const [refreshing, setRefreshing] = useState(false)
const [showAddContact, setShowAddContant] = useState<boolean>(false)
const [contactInput, setContactInput] = useState<string>()
const { t } = useTranslation('common')
useEffect(() => {
if (database && publicKey) {
getUsers(database, { contacts: true }).then((results) => {
if (results) setUsers(results);
});
if (results) setUsers(results)
})
}
}, [lastEventId]);
}, [lastEventId])
useEffect(() => {
subscribeContacts();
}, []);
subscribeContacts()
}, [])
const subscribeContacts: () => Promise<void> = async () => {
return await new Promise<void>((resolve, _reject) => {
relayPool?.unsubscribeAll();
relayPool?.unsubscribeAll()
relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => {
console.log('CONTACTS PAGE EVENT =======>', relay.url, event);
console.log('CONTACTS PAGE EVENT =======>', relay.url, event)
if (database && event?.id && event.kind === EventKind.petNames) {
insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? ''));
insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? ''))
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors: event.tags.map((tag) => tagToUser(tag).id),
});
relayPool?.removeOn('event', 'contacts');
authors: event.tags.map((tag) => tagToUser(tag).id)
})
relayPool?.removeOn('event', 'contacts')
}
});
})
if (publicKey) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.petNames],
authors: [publicKey],
});
authors: [publicKey]
})
}
resolve();
});
};
resolve()
})
}
const onPressAddContact: () => void = () => {
if (contactInput && relayPool && database && publicKey) {
addContact(contactInput, database).then(() => {
populatePets(relayPool, database, publicKey);
setShowAddContant(false);
});
populatePets(relayPool, database, publicKey)
setShowAddContant(false)
})
}
};
}
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeContacts().finally(() => setRefreshing(false));
}, []);
setRefreshing(true)
relayPool?.unsubscribeAll()
subscribeContacts().finally(() => setRefreshing(false))
}, [])
const styles = StyleSheet.create({
container: {
flex: 1,
flex: 1
},
actionContainer: {
marginTop: 30,
marginBottom: 30,
paddingLeft: 12,
paddingRight: 12,
paddingRight: 12
},
button: {
marginTop: 30,
marginTop: 30
},
icon: {
width: 32,
height: 32,
height: 32
},
modal: {
paddingLeft: 32,
paddingRight: 32,
width: '100%',
width: '100%'
},
backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
});
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}
})
return (
<>
@ -152,7 +152,7 @@ export const ContactsPage: React.FC = () => {
</ActionButton.Item>
</ActionButton> */}
</>
);
};
)
}
export default ContactsPage;
export default ContactsPage

View File

@ -1,111 +1,111 @@
import { Card, Layout, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
import NoteCard from '../NoteCard';
import ActionButton from 'react-native-action-button';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import { EventKind } from '../../lib/nostr/Events';
import { RelayFilters } from '../../lib/nostr/Relay';
import { getReplyEventId } from '../../Functions/RelayFunctions/Events';
import { getUsers } from '../../Functions/DatabaseFunctions/Users';
import Loading from '../Loading';
import { Card, Layout, useTheme } from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RefreshControl, ScrollView, StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../NoteCard'
import ActionButton from 'react-native-action-button'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/Relay'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { getUsers } from '../../Functions/DatabaseFunctions/Users'
import Loading from '../Loading'
export const HomePage: React.FC = () => {
const { database, goToPage, page } = useContext(AppContext);
const { lastEventId, relayPool, publicKey } = useContext(RelayPoolContext);
const theme = useTheme();
const [notes, setNotes] = useState<Note[]>([]);
const [totalContacts, setTotalContacts] = useState<number>(-1);
const [refreshing, setRefreshing] = useState(false);
const { t } = useTranslation('common');
const { database, goToPage, page } = useContext(AppContext)
const { lastEventId, relayPool, publicKey } = useContext(RelayPoolContext)
const theme = useTheme()
const [notes, setNotes] = useState<Note[]>([])
const [totalContacts, setTotalContacts] = useState<number>(-1)
const [refreshing, setRefreshing] = useState(false)
const { t } = useTranslation('common')
const loadNotes: () => void = () => {
if (database && publicKey) {
getNotes(database, { contacts: true, includeIds: [publicKey], limit: 15 }).then((notes) => {
setNotes(notes);
});
setNotes(notes)
})
}
};
}
const subscribeNotes: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
if (database && publicKey && relayPool) {
getNotes(database, { limit: 1 }).then((notes) => {
getUsers(database, { contacts: true, includeIds: [publicKey] }).then((users) => {
setTotalContacts(users.length);
setTotalContacts(users.length)
let message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: users.map((user) => user.id),
limit: 15,
};
limit: 15
}
if (notes.length !== 0) {
message = {
...message,
since: notes[0].created_at,
};
since: notes[0].created_at
}
}
relayPool?.subscribe('main-channel', message);
resolve();
});
});
relayPool?.subscribe('main-channel', message)
resolve()
})
})
} else {
reject(new Error('Not Ready'));
reject(new Error('Not Ready'))
}
});
};
})
}
useEffect(() => {
relayPool?.unsubscribeAll();
}, []);
relayPool?.unsubscribeAll()
}, [])
useEffect(() => {
loadNotes();
}, [lastEventId]);
loadNotes()
}, [lastEventId])
useEffect(() => {
loadNotes();
subscribeNotes();
}, [database, publicKey, relayPool]);
loadNotes()
subscribeNotes()
}, [database, publicKey, relayPool])
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeNotes().finally(() => setRefreshing(false));
}, []);
setRefreshing(true)
relayPool?.unsubscribeAll()
subscribeNotes().finally(() => setRefreshing(false))
}, [])
const onPress: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const replyEventId = getReplyEventId(note);
const replyEventId = getReplyEventId(note)
if (replyEventId) {
goToPage(`note#${replyEventId}`);
goToPage(`note#${replyEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`);
goToPage(`note#${note.id}`)
}
}
};
}
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPress(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
};
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
flex: 1
},
icon: {
width: 32,
height: 32,
},
});
height: 32
}
})
return (
<>
@ -135,7 +135,7 @@ export const HomePage: React.FC = () => {
</ActionButton.Item>
</ActionButton> */}
</>
);
};
)
}
export default HomePage;
export default HomePage

View File

@ -1,112 +1,112 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, Input } from '@ui-kitten/components';
import { StyleSheet } from 'react-native';
import { RelayPoolContext } from '../../../Contexts/RelayPoolContext';
import { useTranslation } from 'react-i18next';
import { tagToUser } from '../../../Functions/RelayFunctions/Users';
import Relay from '../../../lib/nostr/Relay';
import { Event, EventKind } from '../../../lib/nostr/Events';
import { AppContext } from '../../../Contexts/AppContext';
import { insertUserContact } from '../../../Functions/DatabaseFunctions/Users';
import React, { useContext, useEffect, useState } from 'react'
import { Button, Input } from '@ui-kitten/components'
import { StyleSheet } from 'react-native'
import { RelayPoolContext } from '../../../Contexts/RelayPoolContext'
import { useTranslation } from 'react-i18next'
import { tagToUser } from '../../../Functions/RelayFunctions/Users'
import Relay from '../../../lib/nostr/Relay'
import { Event, EventKind } from '../../../lib/nostr/Events'
import { AppContext } from '../../../Contexts/AppContext'
import { insertUserContact } from '../../../Functions/DatabaseFunctions/Users'
// import EncryptedStorage from 'react-native-encrypted-storage';
export const Logger: React.FC = () => {
const { database, goToPage } = useContext(AppContext);
const { privateKey, publicKey, relayPool, setPrivateKey } = useContext(RelayPoolContext);
const { t } = useTranslation('common');
const [loading, setLoading] = useState<boolean>(false);
const [status, setStatus] = useState<number>(0);
const [totalPets, setTotalPets] = useState<number>();
const [inputValue, setInputValue] = useState<string>('');
const [loadedUsers, setLoadedUsers] = useState<number>();
const { database, goToPage } = useContext(AppContext)
const { privateKey, publicKey, relayPool, setPrivateKey } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
const [loading, setLoading] = useState<boolean>(false)
const [status, setStatus] = useState<number>(0)
const [totalPets, setTotalPets] = useState<number>()
const [inputValue, setInputValue] = useState<string>('')
const [loadedUsers, setLoadedUsers] = useState<number>()
const styles = StyleSheet.create({
input: {
marginVertical: 2,
padding: 32,
},
});
padding: 32
}
})
useEffect(() => {
if (relayPool && publicKey) {
relayPool?.unsubscribeAll();
setStatus(1);
initEvents();
relayPool?.unsubscribeAll()
setStatus(1)
initEvents()
relayPool?.subscribe('main-channel', {
kinds: [EventKind.petNames],
authors: [publicKey],
});
authors: [publicKey]
})
}
}, [relayPool, publicKey]);
}, [relayPool, publicKey])
useEffect(() => {
if (status > 2) {
relayPool?.removeOn('event', 'landing');
goToPage('home', true);
relayPool?.removeOn('event', 'landing')
goToPage('home', true)
}
}, [status]);
}, [status])
useEffect(() => {
if (loadedUsers) {
const timer = setTimeout(() => setStatus(3), 4000);
const timer = setTimeout(() => setStatus(3), 4000)
return () => {
clearTimeout(timer);
};
clearTimeout(timer)
}
}
}, [loadedUsers]);
}, [loadedUsers])
const initEvents: () => void = () => {
relayPool?.on('event', 'landing', (_relay: Relay, _subId?: string, event?: Event) => {
console.log('LANDING EVENT =======>', event);
console.log('LANDING EVENT =======>', event)
if (event && database) {
if (event.kind === EventKind.petNames) {
loadPets(event);
loadPets(event)
} else if (event.kind === EventKind.meta) {
setLoadedUsers((prev) => (prev ? prev + 1 : 1));
if (loadedUsers && loadedUsers - 1 === totalPets) setStatus(3);
setLoadedUsers((prev) => (prev ? prev + 1 : 1))
if (loadedUsers && loadedUsers - 1 === totalPets) setStatus(3)
}
}
});
};
})
}
const loadPets: (event: Event) => void = (event) => {
if (database) {
setTotalPets(event.tags.length);
setTotalPets(event.tags.length)
if (event.tags.length > 0) {
setStatus(2);
setStatus(2)
insertUserContact(event, database).then(() => {
requestUserData(event);
});
requestUserData(event)
})
} else {
setStatus(3);
setStatus(3)
}
}
};
}
const requestUserData: (event: Event) => void = (event) => {
if (publicKey) {
const authors: string[] = [publicKey, ...event.tags.map((tag) => tagToUser(tag).id)];
const authors: string[] = [publicKey, ...event.tags.map((tag) => tagToUser(tag).id)]
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors,
});
authors
})
}
};
}
const onPress: () => void = () => {
if (inputValue && inputValue !== '') {
setLoading(true);
setPrivateKey(inputValue);
setStatus(1);
setLoading(true)
setPrivateKey(inputValue)
setStatus(1)
// EncryptedStorage.setItem('privateKey', inputValue);
}
};
}
const statusName: { [status: number]: string } = {
0: t('landing.connect'),
1: t('landing.connecting'),
2: t('landing.loadingContacts'),
3: t('landing.ready'),
};
3: t('landing.ready')
}
return !privateKey || status !== 0 ? (
<>
@ -125,7 +125,7 @@ export const Logger: React.FC = () => {
</>
) : (
<></>
);
};
)
}
export default Logger;
export default Logger

View File

@ -1,27 +1,27 @@
import React from 'react';
import { Layout, Text } from '@ui-kitten/components';
import { StyleSheet } from 'react-native';
import Loading from '../Loading';
import Logger from './Logger';
import React from 'react'
import { Layout, Text } from '@ui-kitten/components'
import { StyleSheet } from 'react-native'
import Loading from '../Loading'
import Logger from './Logger'
export const LandingPage: React.FC = () => {
const styles = StyleSheet.create({
tab: {
height: '100%',
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'center'
},
svg: {
height: 340,
width: 340,
width: 340
},
title: {
marginTop: -40,
marginBottom: -20,
fontFamily: 'SpaceGrotesk-Bold',
fontSize: 45,
},
});
fontSize: 45
}
})
return (
<Layout style={styles.tab}>
@ -33,7 +33,7 @@ export const LandingPage: React.FC = () => {
</Text>
<Logger />
</Layout>
);
};
)
}
export default LandingPage;
export default LandingPage

View File

@ -1,31 +1,31 @@
import React from 'react';
import { Animated, Easing } from 'react-native';
import { SvgXml } from 'react-native-svg';
import React from 'react'
import { Animated, Easing } from 'react-native'
import { SvgXml } from 'react-native-svg'
interface LoadingProps {
style?: object;
style?: object
}
export const Loading: React.FC<LoadingProps> = ({ style = {} }) => {
const logo = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 800"><g stroke="hsl(0, 0%, 100%)" fill="none" stroke-linecap="round" style="--darkreader-inline-stroke: #e8e6e3;" data-darkreader-inline-stroke=""><circle r="256" cx="400" cy="400" stroke-width="24" stroke-dasharray="68 51" stroke-dashoffset="25" transform="rotate(309, 400, 400)" opacity="1.00"></circle><circle r="224" cx="400" cy="400" stroke-width="23" stroke-dasharray="76 63" stroke-dashoffset="25" transform="rotate(258, 400, 400)" opacity="0.86"></circle><circle r="192" cx="400" cy="400" stroke-width="22" stroke-dasharray="63 31" stroke-dashoffset="25" transform="rotate(74, 400, 400)" opacity="0.73"></circle><circle r="160" cx="400" cy="400" stroke-width="21" stroke-dasharray="75 71" stroke-dashoffset="25" transform="rotate(62, 400, 400)" opacity="0.59"></circle><circle r="128" cx="400" cy="400" stroke-width="21" stroke-dasharray="43 37" stroke-dashoffset="25" transform="rotate(333, 400, 400)" opacity="0.46"></circle><circle r="96" cx="400" cy="400" stroke-width="20" stroke-dasharray="65 73" stroke-dashoffset="25" transform="rotate(17, 400, 400)" opacity="0.32"></circle><circle r="64" cx="400" cy="400" stroke-width="19" stroke-dasharray="33 58" stroke-dashoffset="25" transform="rotate(269, 400, 400)" opacity="0.19"></circle><circle r="32" cx="400" cy="400" stroke-width="18" stroke-dasharray="66 52" stroke-dashoffset="25" transform="rotate(281, 400, 400)" opacity="0.05"></circle></g></svg>`;
const logo = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 800"><g stroke="hsl(0, 0%, 100%)" fill="none" stroke-linecap="round" style="--darkreader-inline-stroke: #e8e6e3;" data-darkreader-inline-stroke=""><circle r="256" cx="400" cy="400" stroke-width="24" stroke-dasharray="68 51" stroke-dashoffset="25" transform="rotate(309, 400, 400)" opacity="1.00"></circle><circle r="224" cx="400" cy="400" stroke-width="23" stroke-dasharray="76 63" stroke-dashoffset="25" transform="rotate(258, 400, 400)" opacity="0.86"></circle><circle r="192" cx="400" cy="400" stroke-width="22" stroke-dasharray="63 31" stroke-dashoffset="25" transform="rotate(74, 400, 400)" opacity="0.73"></circle><circle r="160" cx="400" cy="400" stroke-width="21" stroke-dasharray="75 71" stroke-dashoffset="25" transform="rotate(62, 400, 400)" opacity="0.59"></circle><circle r="128" cx="400" cy="400" stroke-width="21" stroke-dasharray="43 37" stroke-dashoffset="25" transform="rotate(333, 400, 400)" opacity="0.46"></circle><circle r="96" cx="400" cy="400" stroke-width="20" stroke-dasharray="65 73" stroke-dashoffset="25" transform="rotate(17, 400, 400)" opacity="0.32"></circle><circle r="64" cx="400" cy="400" stroke-width="19" stroke-dasharray="33 58" stroke-dashoffset="25" transform="rotate(269, 400, 400)" opacity="0.19"></circle><circle r="32" cx="400" cy="400" stroke-width="18" stroke-dasharray="66 52" stroke-dashoffset="25" transform="rotate(281, 400, 400)" opacity="0.05"></circle></g></svg>'
const spinValue = new Animated.Value(0);
const spinValue = new Animated.Value(0)
Animated.timing(spinValue, {
toValue: 1,
duration: 100000,
easing: Easing.linear, // Easing is an additional import from react-native
useNativeDriver: true, // To make use of native driver for performance
}).start();
useNativeDriver: true // To make use of native driver for performance
}).start()
const spin = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '2016deg'],
});
outputRange: ['0deg', '2016deg']
})
return (
<Animated.View style={{ ...style, ...{ transform: [{ rotate: spin }] } }}>
<SvgXml xml={logo} />
</Animated.View>
);
};
)
}
export default Loading;
export default Loading

View File

@ -1,24 +1,24 @@
import React, { useContext } from 'react';
import { Layout } from '@ui-kitten/components';
import { StyleSheet } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import HomePage from '../HomePage';
import ProfilePage from '../ProfilePage';
import NavigationBar from '../NavigationBar';
import SendPage from '../SendPage';
import ContactsPage from '../ContactsPage';
import NotePage from '../NotePage';
import LandingPage from '../LandingPage';
import ConfigPage from '../ConfigPage';
import React, { useContext } from 'react'
import { Layout } from '@ui-kitten/components'
import { StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import HomePage from '../HomePage'
import ProfilePage from '../ProfilePage'
import NavigationBar from '../NavigationBar'
import SendPage from '../SendPage'
import ContactsPage from '../ContactsPage'
import NotePage from '../NotePage'
import LandingPage from '../LandingPage'
import ConfigPage from '../ConfigPage'
export const MainLayout: React.FC = () => {
const { page } = useContext(AppContext);
const { page } = useContext(AppContext)
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
flex: 1
}
})
const pagination: { [pageName: string]: JSX.Element } = {
landing: <LandingPage />,
@ -27,21 +27,21 @@ export const MainLayout: React.FC = () => {
profile: <ProfilePage />,
contacts: <ContactsPage />,
note: <NotePage />,
config: <ConfigPage />,
};
config: <ConfigPage />
}
const breadcrump: string[] = page.split('%');
const pageToDisplay: string = breadcrump[breadcrump.length - 1].split('#')[0];
const breadcrump: string[] = page.split('%')
const pageToDisplay: string = breadcrump[breadcrump.length - 1].split('#')[0]
const view: () => JSX.Element = () => {
if (page === '') {
return <Layout style={styles.container} level='4' />;
return <Layout style={styles.container} level='4' />
} else if (page === 'landing') {
return (
<Layout style={styles.container} level='4'>
<LandingPage />
</Layout>
);
)
} else {
return (
<>
@ -50,11 +50,11 @@ export const MainLayout: React.FC = () => {
</Layout>
<NavigationBar />
</>
);
)
}
};
}
return <>{view()}</>;
};
return <>{view()}</>
}
export default MainLayout;
export default MainLayout

View File

@ -1,26 +1,26 @@
import React, { useContext } from 'react';
import { BottomNavigation, BottomNavigationTab, useTheme } from '@ui-kitten/components';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import React, { useContext } from 'react'
import { BottomNavigation, BottomNavigationTab, useTheme } from '@ui-kitten/components'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
export const NavigationBar: React.FC = () => {
const { goToPage, page } = useContext(AppContext);
const { publicKey } = useContext(RelayPoolContext);
const theme = useTheme();
const profilePage = `profile#${publicKey ?? ''}`;
const { goToPage, page } = useContext(AppContext)
const { publicKey } = useContext(RelayPoolContext)
const theme = useTheme()
const profilePage = `profile#${publicKey ?? ''}`
const pageIndex: string[] = ['home', 'contacts', profilePage];
const pageIndex: string[] = ['home', 'contacts', profilePage]
const getIndex: () => number = () => {
if (page.includes('profile')) {
return page === profilePage ? 2 : 1;
return page === profilePage ? 2 : 1
} else if (page.includes('note#')) {
return 0;
return 0
} else {
return pageIndex.indexOf(page);
return pageIndex.indexOf(page)
}
};
}
return page ? (
<BottomNavigation
@ -39,7 +39,7 @@ export const NavigationBar: React.FC = () => {
</BottomNavigation>
) : (
<></>
);
};
)
}
export default NavigationBar;
export default NavigationBar

View File

@ -1,31 +1,31 @@
import React, { useContext, useState } from 'react';
import { Button, Layout, Text, useTheme } from '@ui-kitten/components';
import { Note } from '../../Functions/DatabaseFunctions/Notes';
import { StyleSheet, TouchableOpacity } from 'react-native';
import UserAvatar from 'react-native-user-avatar';
import Markdown from 'react-native-markdown-display';
import { EventKind } from '../../lib/nostr/Events';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import { storeRelay } from '../../Functions/DatabaseFunctions/Relays';
import { AppContext } from '../../Contexts/AppContext';
import { showMessage } from 'react-native-flash-message';
import { t } from 'i18next';
import { getReplyEventId } from '../../Functions/RelayFunctions/Events';
import moment from 'moment';
import { populateRelay } from '../../Functions/RelayFunctions';
import React, { useContext, useState } from 'react'
import { Button, Layout, Text, useTheme } from '@ui-kitten/components'
import { Note } from '../../Functions/DatabaseFunctions/Notes'
import { StyleSheet, TouchableOpacity } from 'react-native'
import UserAvatar from 'react-native-user-avatar'
import Markdown from 'react-native-markdown-display'
import { EventKind } from '../../lib/nostr/Events'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { storeRelay } from '../../Functions/DatabaseFunctions/Relays'
import { AppContext } from '../../Contexts/AppContext'
import { showMessage } from 'react-native-flash-message'
import { t } from 'i18next'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import moment from 'moment'
import { populateRelay } from '../../Functions/RelayFunctions'
interface NoteCardProps {
note: Note;
note: Note
}
export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
const theme = useTheme();
const { relayPool, setRelayPool, publicKey } = useContext(RelayPoolContext);
const { database, goToPage } = useContext(AppContext);
const theme = useTheme()
const { relayPool, setRelayPool, publicKey } = useContext(RelayPoolContext)
const { database, goToPage } = useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>(
Object.keys(relayPool?.relays ?? {}).includes(note.content),
);
Object.keys(relayPool?.relays ?? {}).includes(note.content)
)
const textNote: () => JSX.Element = () => {
return (
@ -65,26 +65,26 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
</Layout>
</Layout>
</>
);
};
)
}
const relayNote: () => JSX.Element = () => {
const relayName = note.content.split('wss://')[1].split('/')[0];
const relayName = note.content.split('wss://')[1].split('/')[0]
const addRelay: () => void = () => {
if (relayPool && database && publicKey) {
relayPool.add(note.content);
setRelayPool(relayPool);
storeRelay({ url: note.content }, database);
populateRelay(relayPool, database, publicKey);
relayPool.add(note.content)
setRelayPool(relayPool)
storeRelay({ url: note.content }, database)
populateRelay(relayPool, database, publicKey)
showMessage({
message: t('alerts.relayAdded'),
description: note.content,
type: 'success',
});
setRelayAdded(true);
type: 'success'
})
setRelayAdded(true)
}
};
}
return (
<>
@ -107,97 +107,97 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
)}
</Layout>
</>
);
};
)
}
const onPressUser: () => void = () => {
goToPage(`profile#${note.pubkey}`);
};
goToPage(`profile#${note.pubkey}`)
}
const styles = StyleSheet.create({
layout: {
flexDirection: 'row',
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
profile: {
flex: 1,
width: 38,
alignItems: 'center',
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
content: {
flex: 4,
backgroundColor: 'transparent',
paddingLeft: 16,
paddingRight: 16,
paddingRight: 16
},
contentNoAction: {
flex: 5,
backgroundColor: 'transparent',
paddingLeft: 16,
paddingRight: 16,
paddingRight: 16
},
actions: {
flex: 1,
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
pubkey: {
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
footer: {
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
tags: {
backgroundColor: 'transparent',
marginLeft: 12,
marginLeft: 12
},
titleText: {
backgroundColor: 'transparent',
flexDirection: 'row',
flexDirection: 'row'
},
text: {
backgroundColor: 'transparent',
paddingRight: 10,
},
});
paddingRight: 10
}
})
const markdownStyle = {
text: {
color: theme['text-basic-color'],
color: theme['text-basic-color']
},
tr: {
borderColor: theme['border-primary-color-5'],
borderColor: theme['border-primary-color-5']
},
table: {
borderColor: theme['border-primary-color-5'],
borderColor: theme['border-primary-color-5']
},
blocklink: {
borderColor: theme['border-primary-color-5'],
borderColor: theme['border-primary-color-5']
},
hr: {
backgroundColor: theme['background-basic-color-3'],
backgroundColor: theme['background-basic-color-3']
},
blockquote: {
backgroundColor: theme['background-basic-color-3'],
borderColor: theme['border-primary-color-5'],
color: theme['text-basic-color'],
color: theme['text-basic-color']
},
code_inline: {
borderColor: theme['border-primary-color-5'],
backgroundColor: theme['background-basic-color-3'],
color: theme['text-basic-color'],
color: theme['text-basic-color']
},
code_block: {
borderColor: theme['border-primary-color-5'],
backgroundColor: theme['background-basic-color-3'],
color: theme['text-basic-color'],
color: theme['text-basic-color']
},
fence: {
borderColor: theme['border-primary-color-5'],
backgroundColor: theme['background-basic-color-3'],
color: theme['text-basic-color'],
},
};
color: theme['text-basic-color']
}
}
return (
note && (
@ -205,7 +205,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note }) => {
{note.kind === EventKind.recommendServer ? relayNote() : textNote()}
</Layout>
)
);
};
)
}
export default NoteCard;
export default NoteCard

View File

@ -1,59 +1,59 @@
import { Card, Layout, TopNavigation, TopNavigationAction, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { AppContext } from '../../Contexts/AppContext';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import NoteCard from '../NoteCard';
import { EventKind } from '../../lib/nostr/Events';
import { RelayFilters } from '../../lib/nostr/Relay';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import Loading from '../Loading';
import ActionButton from 'react-native-action-button';
import { useTranslation } from 'react-i18next';
import { getDirectReplies, getReplyEventId } from '../../Functions/RelayFunctions/Events';
import { Card, Layout, TopNavigation, TopNavigationAction, useTheme } from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import NoteCard from '../NoteCard'
import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/Relay'
import { RefreshControl, ScrollView, StyleSheet } from 'react-native'
import Loading from '../Loading'
import ActionButton from 'react-native-action-button'
import { useTranslation } from 'react-i18next'
import { getDirectReplies, getReplyEventId } from '../../Functions/RelayFunctions/Events'
export const NotePage: React.FC = () => {
const { page, goBack, goToPage, database } = useContext(AppContext);
const { lastEventId, relayPool } = useContext(RelayPoolContext);
const [note, setNote] = useState<Note>();
const [replies, setReplies] = useState<Note[]>();
const [refreshing, setRefreshing] = useState(false);
const theme = useTheme();
const { t } = useTranslation('common');
const breadcrump = page.split('%');
const eventId = breadcrump[breadcrump.length - 1].split('#')[1];
const { page, goBack, goToPage, database } = useContext(AppContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const [note, setNote] = useState<Note>()
const [replies, setReplies] = useState<Note[]>()
const [refreshing, setRefreshing] = useState(false)
const theme = useTheme()
const { t } = useTranslation('common')
const breadcrump = page.split('%')
const eventId = breadcrump[breadcrump.length - 1].split('#')[1]
const reload: (newEventId?: string) => void = (newEventId) => {
setNote(undefined);
setReplies(undefined);
relayPool?.unsubscribeAll();
setNote(undefined)
setReplies(undefined)
relayPool?.unsubscribeAll()
relayPool?.subscribe('main-channel', {
kinds: [EventKind.textNote],
ids: [newEventId ?? eventId],
});
};
ids: [newEventId ?? eventId]
})
}
useEffect(reload, []);
useEffect(reload, [])
useEffect(() => {
subscribeNotes();
}, [lastEventId, page]);
subscribeNotes()
}, [lastEventId, page])
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll();
goBack();
};
relayPool?.unsubscribeAll()
goBack()
}
const onPressGoParent: () => void = () => {
if (note) {
const replyId = getReplyEventId(note);
const replyId = getReplyEventId(note)
if (replyId) {
goToPage(`note#${replyId}`);
reload(replyId);
goToPage(`note#${replyId}`)
reload(replyId)
}
}
};
}
const renderBackAction = (): JSX.Element => {
return (
@ -61,8 +61,8 @@ export const NotePage: React.FC = () => {
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
/>
);
};
)
}
const renderNoteActions = (): JSX.Element => {
return note && getReplyEventId(note) ? (
@ -72,20 +72,20 @@ export const NotePage: React.FC = () => {
/>
) : (
<></>
);
};
)
}
const onPressNote: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const replyEventId = getReplyEventId(note);
const replyEventId = getReplyEventId(note)
if (replyEventId && replyEventId !== eventId) {
goToPage(`note#${replyEventId}`);
goToPage(`note#${replyEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`);
goToPage(`note#${note.id}`)
}
reload();
reload()
}
};
}
const itemCard: (note?: Note) => JSX.Element = (note) => {
if (note?.id === eventId) {
@ -93,72 +93,72 @@ export const NotePage: React.FC = () => {
<Layout style={styles.main} level='2' key={note.id ?? ''}>
<NoteCard note={note} />
</Layout>
);
)
} else if (note) {
return (
<Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
)
} else {
return <></>;
return <></>
}
};
}
const subscribeNotes: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
if (database) {
getNotes(database, { filters: { id: eventId } }).then((events) => {
if (events.length > 0) {
const event = events[0];
setNote(event);
const event = events[0]
setNote(event)
if (!replies) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.textNote],
'#e': [eventId],
});
'#e': [eventId]
})
}
getNotes(database, { filters: { reply_event_id: eventId } }).then((notes) => {
const rootReplies = getDirectReplies(event, notes);
const rootReplies = getDirectReplies(event, notes)
if (rootReplies.length > 0) {
setReplies(rootReplies as Note[]);
setReplies(rootReplies as Note[])
const message: RelayFilters = {
kinds: [EventKind.meta],
authors: [...rootReplies.map((note) => note.pubkey), event.pubkey],
};
relayPool?.subscribe('main-channel', message);
authors: [...rootReplies.map((note) => note.pubkey), event.pubkey]
}
relayPool?.subscribe('main-channel', message)
} else {
setReplies([]);
setReplies([])
}
resolve();
});
resolve()
})
} else {
resolve();
resolve()
}
});
})
} else {
reject(new Error('Not Ready'));
reject(new Error('Not Ready'))
}
});
};
})
}
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeNotes().finally(() => setRefreshing(false));
}, []);
setRefreshing(true)
relayPool?.unsubscribeAll()
subscribeNotes().finally(() => setRefreshing(false))
}, [])
const styles = StyleSheet.create({
main: {
paddingBottom: 32,
paddingTop: 26,
paddingLeft: 26,
paddingRight: 26,
paddingRight: 26
},
loading: {
maxHeight: 160,
},
});
maxHeight: 160
}
})
return (
<>
@ -193,7 +193,7 @@ export const NotePage: React.FC = () => {
</ActionButton.Item>
</ActionButton> */}
</>
);
};
)
}
export default NotePage;
export default NotePage

View File

@ -5,89 +5,89 @@ import {
Text,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import UserAvatar from 'react-native-user-avatar';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
import NoteCard from '../NoteCard';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
useTheme
} from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import UserAvatar from 'react-native-user-avatar'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import {
getUser,
removeContact,
addContact,
User,
getUsers,
} from '../../Functions/DatabaseFunctions/Users';
import { EventKind, Event } from '../../lib/nostr/Events';
import Relay, { RelayFilters } from '../../lib/nostr/Relay';
import Icon from 'react-native-vector-icons/FontAwesome5';
import ActionButton from 'react-native-action-button';
import { useTranslation } from 'react-i18next';
import { populatePets, tagToUser } from '../../Functions/RelayFunctions/Users';
import { getReplyEventId } from '../../Functions/RelayFunctions/Events';
import Loading from '../Loading';
import { storeEvent } from '../../Functions/DatabaseFunctions/Events';
getUsers
} from '../../Functions/DatabaseFunctions/Users'
import { EventKind, Event } from '../../lib/nostr/Events'
import Relay, { RelayFilters } from '../../lib/nostr/Relay'
import Icon from 'react-native-vector-icons/FontAwesome5'
import ActionButton from 'react-native-action-button'
import { useTranslation } from 'react-i18next'
import { populatePets, tagToUser } from '../../Functions/RelayFunctions/Users'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import Loading from '../Loading'
import { storeEvent } from '../../Functions/DatabaseFunctions/Events'
export const ProfilePage: React.FC = () => {
const { database, page, goToPage, goBack } = useContext(AppContext);
const { publicKey, lastEventId, relayPool, setLastEventId } = useContext(RelayPoolContext);
const theme = useTheme();
const [notes, setNotes] = useState<Note[]>();
const { t } = useTranslation('common');
const [user, setUser] = useState<User>();
const [contactsIds, setContactsIds] = useState<string[]>();
const [isContact, setIsContact] = useState<boolean>();
const [refreshing, setRefreshing] = useState(false);
const breadcrump = page.split('%');
const userId = breadcrump[breadcrump.length - 1].split('#')[1] ?? publicKey;
const username = user?.name === '' ? user?.id : user?.name;
const { database, page, goToPage, goBack } = useContext(AppContext)
const { publicKey, lastEventId, relayPool, setLastEventId } = useContext(RelayPoolContext)
const theme = useTheme()
const [notes, setNotes] = useState<Note[]>()
const { t } = useTranslation('common')
const [user, setUser] = useState<User>()
const [contactsIds, setContactsIds] = useState<string[]>()
const [isContact, setIsContact] = useState<boolean>()
const [refreshing, setRefreshing] = useState(false)
const breadcrump = page.split('%')
const userId = breadcrump[breadcrump.length - 1].split('#')[1] ?? publicKey
const username = user?.name === '' ? user?.id : user?.name
useEffect(() => {
setContactsIds(undefined);
setNotes(undefined);
setUser(undefined);
loadProfile();
}, [page]);
setContactsIds(undefined)
setNotes(undefined)
setUser(undefined)
loadProfile()
}, [page])
useEffect(() => {
if (database) {
getUser(userId, database).then((result) => {
if (result) {
setUser(result);
setIsContact(result?.contact);
setUser(result)
setIsContact(result?.contact)
}
});
})
if (userId === publicKey) {
getUsers(database, { contacts: true }).then((users) => {
setContactsIds(users.map((user) => user.id));
});
setContactsIds(users.map((user) => user.id))
})
}
getNotes(database, { filters: { pubkey: userId }, limit: 10 }).then((results) => {
if (results.length > 0) setNotes(results);
});
if (results.length > 0) setNotes(results)
})
}
}, [lastEventId]);
}, [lastEventId])
const removeAuthor: () => void = () => {
if (relayPool && database && publicKey) {
removeContact(userId, database).then(() => {
populatePets(relayPool, database, publicKey);
setIsContact(false);
});
populatePets(relayPool, database, publicKey)
setIsContact(false)
})
}
};
}
const addAuthor: () => void = () => {
if (relayPool && database && publicKey) {
addContact(userId, database).then(() => {
populatePets(relayPool, database, publicKey);
setIsContact(true);
});
populatePets(relayPool, database, publicKey)
setIsContact(true)
})
}
};
}
const renderOptions: () => JSX.Element = () => {
if (publicKey === userId) {
@ -96,7 +96,7 @@ export const ProfilePage: React.FC = () => {
icon={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
onPress={() => goToPage('config')}
/>
);
)
} else {
if (user) {
if (isContact) {
@ -105,45 +105,45 @@ export const ProfilePage: React.FC = () => {
icon={<Icon name='user-minus' size={16} color={theme['color-danger-500']} solid />}
onPress={removeAuthor}
/>
);
)
} else {
return (
<TopNavigationAction
icon={<Icon name='user-plus' size={16} color={theme['color-success-500']} solid />}
onPress={addAuthor}
/>
);
)
}
} else {
return <Spinner size='tiny' />;
return <Spinner size='tiny' />
}
}
};
}
const onPressBack: () => void = () => {
relayPool?.removeOn('event', 'profile');
relayPool?.unsubscribeAll();
goBack();
};
relayPool?.removeOn('event', 'profile')
relayPool?.unsubscribeAll()
goBack()
}
const renderBackAction = (): JSX.Element => {
if (publicKey === userId) {
return <></>;
return <></>
} else {
return (
<TopNavigationAction
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
/>
);
)
}
};
}
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
loadProfile().finally(() => setRefreshing(false));
}, []);
setRefreshing(true)
relayPool?.unsubscribeAll()
loadProfile().finally(() => setRefreshing(false))
}, [])
const subscribeNotes: () => void = () => {
if (database) {
@ -152,62 +152,62 @@ export const ProfilePage: React.FC = () => {
const notesEvent: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId],
limit: 10,
};
if (results.length >= 10) {
notesEvent.since = results[0]?.created_at;
limit: 10
}
relayPool?.subscribe('main-channel', notesEvent);
if (results.length >= 10) {
notesEvent.since = results[0]?.created_at
}
relayPool?.subscribe('main-channel', notesEvent)
}
});
})
}
};
}
const loadProfile: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta, EventKind.petNames],
authors: [userId],
});
authors: [userId]
})
relayPool?.on('event', 'profile', (_relay: Relay, _subId?: string, event?: Event) => {
console.log('PROFILE EVENT =======>', event);
console.log('PROFILE EVENT =======>', event)
if (database) {
if (event?.id && event.pubkey === userId) {
if (event.kind === EventKind.petNames) {
const ids = event.tags.map((tag) => tagToUser(tag).id);
setContactsIds(ids);
const ids = event.tags.map((tag) => tagToUser(tag).id)
setContactsIds(ids)
} else if (event.kind === EventKind.meta) {
storeEvent(event, database).finally(() => {
if (event?.id) setLastEventId(event.id);
});
if (event?.id) setLastEventId(event.id)
})
}
subscribeNotes();
subscribeNotes()
}
} else {
reject(new Error('Not Ready'));
reject(new Error('Not Ready'))
}
});
resolve();
});
};
})
resolve()
})
}
const styles = StyleSheet.create({
list: {
flex: 1,
flex: 1
},
icon: {
width: 32,
height: 32,
height: 32
},
settingsIcon: {
width: 48,
height: 48,
height: 48
},
avatar: {
width: 130,
marginBottom: 16,
marginBottom: 16
},
profile: {
flex: 1,
@ -215,59 +215,59 @@ export const ProfilePage: React.FC = () => {
alignItems: 'center',
marginBottom: 2,
paddingLeft: 32,
paddingRight: 32,
paddingRight: 32
},
loading: {
maxHeight: 160,
maxHeight: 160
},
about: {
flex: 4,
maxHeight: 200,
maxHeight: 200
},
stats: {
flex: 1,
flex: 1
},
statsItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5,
marginBottom: 5
},
description: {
marginTop: 16,
flexDirection: 'row',
},
});
flexDirection: 'row'
}
})
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
};
)
}
const onPressNote: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const mainEventId = getReplyEventId(note);
const mainEventId = getReplyEventId(note)
if (mainEventId) {
goToPage(`note#${mainEventId}`);
goToPage(`note#${mainEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`);
goToPage(`note#${note.id}`)
}
}
};
}
const onPressId: () => void = () => {
// FIXME
// Clipboard.setString(user?.id ?? '');
};
}
const isFollowingUser: () => boolean = () => {
if (contactsIds !== undefined && publicKey) {
return contactsIds?.includes(publicKey);
return contactsIds?.includes(publicKey)
}
return false;
};
return false
}
const stats: () => JSX.Element = () => {
if (contactsIds === undefined) {
@ -275,7 +275,7 @@ export const ProfilePage: React.FC = () => {
<Layout style={styles.stats} level='3'>
<Spinner size='tiny' />
</Layout>
);
)
}
return (
@ -297,8 +297,8 @@ export const ProfilePage: React.FC = () => {
</Layout>
)}
</Layout>
);
};
)
}
const profile: JSX.Element = (
<Layout style={styles.profile} level='3'>
@ -330,7 +330,7 @@ export const ProfilePage: React.FC = () => {
)}
</Layout>
</Layout>
);
)
return (
<>
@ -369,7 +369,7 @@ export const ProfilePage: React.FC = () => {
<></>
)}
</>
);
};
)
}
export default ProfilePage;
export default ProfilePage

View File

@ -5,70 +5,70 @@ import {
Spinner,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components';
import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { Event, EventKind } from '../../lib/nostr/Events';
import { useTranslation } from 'react-i18next';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
import moment from 'moment';
import { getNotes } from '../../Functions/DatabaseFunctions/Notes';
import { getETags } from '../../Functions/RelayFunctions/Events';
useTheme
} from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react'
import { StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { Event, EventKind } from '../../lib/nostr/Events'
import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import moment from 'moment'
import { getNotes } from '../../Functions/DatabaseFunctions/Notes'
import { getETags } from '../../Functions/RelayFunctions/Events'
export const SendPage: React.FC = () => {
const theme = useTheme();
const { goBack, page, database } = useContext(AppContext);
const { relayPool, publicKey, lastEventId } = useContext(RelayPoolContext);
const { t } = useTranslation('common');
const [content, setContent] = useState<string>('');
const [sending, setSending] = useState<boolean>(false);
const [noteId, setNoteId] = useState<string>();
const breadcrump = page.split('%');
const eventId = breadcrump[breadcrump.length - 1].split('#')[1];
const theme = useTheme()
const { goBack, page, database } = useContext(AppContext)
const { relayPool, publicKey, lastEventId } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
const [content, setContent] = useState<string>('')
const [sending, setSending] = useState<boolean>(false)
const [noteId, setNoteId] = useState<string>()
const breadcrump = page.split('%')
const eventId = breadcrump[breadcrump.length - 1].split('#')[1]
useEffect(() => {
relayPool?.unsubscribeAll();
}, []);
relayPool?.unsubscribeAll()
}, [])
useEffect(() => {
if (sending && noteId === lastEventId) {
goBack();
goBack()
}
}, [lastEventId]);
}, [lastEventId])
const styles = StyleSheet.create({
container: {
flex: 1,
flex: 1
},
actionContainer: {
marginTop: 30,
paddingLeft: 32,
paddingRight: 32,
paddingRight: 32
},
button: {
marginTop: 30,
},
});
marginTop: 30
}
})
const onPressBack: () => void = () => {
goBack();
};
goBack()
}
const onPressSend: () => void = () => {
if (database && publicKey) {
getNotes(database, { filters: { id: eventId } }).then((notes) => {
let tags: string[][] = [];
const note = notes[0];
let tags: string[][] = []
const note = notes[0]
if (note) {
tags = note.tags;
tags = note.tags
if (getETags(note).length === 0) {
tags.push(['e', eventId, '', 'root']);
tags.push(['e', eventId, '', 'root'])
} else {
tags.push(['e', eventId, '', 'reply']);
tags.push(['e', eventId, '', 'reply'])
}
}
@ -77,28 +77,28 @@ export const SendPage: React.FC = () => {
created_at: moment().unix(),
kind: EventKind.textNote,
pubkey: publicKey,
tags,
};
tags
}
relayPool?.sendEvent(event).then((sentNote) => {
if (sentNote?.id) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.textNote],
ids: [sentNote.id],
});
setNoteId(sentNote.id);
ids: [sentNote.id]
})
setNoteId(sentNote.id)
}
});
setSending(true);
});
})
setSending(true)
})
}
};
}
const renderBackAction = (): JSX.Element => (
<TopNavigationAction
icon={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
/>
);
)
return (
<>
@ -133,7 +133,7 @@ export const SendPage: React.FC = () => {
</Layout>
</Layout>
</>
);
};
)
}
export default SendPage;
export default SendPage

View File

@ -1,40 +1,40 @@
import React, { useContext } from 'react';
import { Card, Layout, Text, useTheme } from '@ui-kitten/components';
import { User } from '../../Functions/DatabaseFunctions/Users';
import { StyleSheet } from 'react-native';
import UserAvatar from 'react-native-user-avatar';
import { AppContext } from '../../Contexts/AppContext';
import React, { useContext } from 'react'
import { Card, Layout, Text, useTheme } from '@ui-kitten/components'
import { User } from '../../Functions/DatabaseFunctions/Users'
import { StyleSheet } from 'react-native'
import UserAvatar from 'react-native-user-avatar'
import { AppContext } from '../../Contexts/AppContext'
interface NoteCardProps {
user: User;
user: User
}
export const NoteCard: React.FC<NoteCardProps> = ({ user }) => {
const { goToPage } = useContext(AppContext);
const theme = useTheme();
const { goToPage } = useContext(AppContext)
const theme = useTheme()
const styles = StyleSheet.create({
layout: {
flex: 1,
flexDirection: 'row',
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
profile: {
flex: 1,
width: 38,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
content: {
flex: 5,
backgroundColor: 'transparent',
backgroundColor: 'transparent'
},
actions: {
flex: 1,
backgroundColor: 'transparent',
},
});
backgroundColor: 'transparent'
}
})
return (
user && (
@ -55,7 +55,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({ user }) => {
</Layout>
</Card>
)
);
};
)
}
export default NoteCard;
export default NoteCard

View File

@ -1,21 +1,21 @@
import React, { useEffect, useState } from 'react';
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { initDatabase } from '../Functions/DatabaseFunctions';
import { createInitDatabase } from '../Functions/DatabaseFunctions/Migrations';
import FlashMessage from 'react-native-flash-message';
import React, { useEffect, useState } from 'react'
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { initDatabase } from '../Functions/DatabaseFunctions'
import { createInitDatabase } from '../Functions/DatabaseFunctions/Migrations'
import FlashMessage from 'react-native-flash-message'
// import EncryptedStorage from 'react-native-encrypted-storage';
export interface AppContextProps {
page: string;
goToPage: (path: string, root?: boolean) => void;
goBack: () => void;
init: () => void;
loadingDb: boolean;
database: SQLiteDatabase | null;
page: string
goToPage: (path: string, root?: boolean) => void
goBack: () => void
init: () => void
loadingDb: boolean
database: SQLiteDatabase | null
}
export interface AppContextProviderProps {
children: React.ReactNode;
children: React.ReactNode
}
export const initialAppContext: AppContextProps = {
@ -24,43 +24,43 @@ export const initialAppContext: AppContextProps = {
goToPage: () => {},
goBack: () => {},
loadingDb: true,
database: null,
};
database: null
}
export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => {
const [page, setPage] = useState<string>(initialAppContext.page);
const [database, setDatabase] = useState<SQLiteDatabase | null>(null);
const [loadingDb, setLoadingDb] = useState<boolean>(initialAppContext.loadingDb);
const [page, setPage] = useState<string>(initialAppContext.page)
const [database, setDatabase] = useState<SQLiteDatabase | null>(null)
const [loadingDb, setLoadingDb] = useState<boolean>(initialAppContext.loadingDb)
const init: () => void = () => {
const result = '';
const result = ''
// EncryptedStorage.getItem('privateKey').then((result) => {
const db = initDatabase();
setDatabase(db);
if (!result || result === '') {
createInitDatabase(db).then(() => {
setLoadingDb(false);
});
} else {
setLoadingDb(false);
}
const db = initDatabase()
setDatabase(db)
if (!result || result === '') {
createInitDatabase(db).then(() => {
setLoadingDb(false)
})
} else {
setLoadingDb(false)
}
// });
};
}
useEffect(init, []);
useEffect(init, [])
const goToPage: (path: string, root?: boolean) => void = (path, root) => {
if (page !== '' && !root) {
setPage(`${page}%${path}`);
setPage(`${page}%${path}`)
} else {
setPage(path);
setPage(path)
}
};
}
const goBack: () => void = () => {
const breadcrump = page.split('%');
setPage(breadcrump.slice(0, -1).join('%') || 'home');
};
const breadcrump = page.split('%')
setPage(breadcrump.slice(0, -1).join('%') || 'home')
}
return (
<AppContext.Provider
@ -70,13 +70,13 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
goToPage,
goBack,
loadingDb,
database,
database
}}
>
{children}
<FlashMessage position='top' />
</AppContext.Provider>
);
};
)
}
export const AppContext = React.createContext(initialAppContext);
export const AppContext = React.createContext(initialAppContext)

View File

@ -1,60 +1,60 @@
import React, { useContext, useEffect, useState } from 'react';
import Relay from '../lib/nostr/Relay';
import { Event, EventKind } from '../lib/nostr/Events';
import RelayPool from '../lib/nostr/RelayPool/intex';
import { AppContext } from './AppContext';
import { storeEvent } from '../Functions/DatabaseFunctions/Events';
import { getRelays, Relay as RelayEntity, storeRelay } from '../Functions/DatabaseFunctions/Relays';
import { showMessage } from 'react-native-flash-message';
import React, { useContext, useEffect, useState } from 'react'
import Relay from '../lib/nostr/Relay'
import { Event, EventKind } from '../lib/nostr/Events'
import RelayPool from '../lib/nostr/RelayPool/intex'
import { AppContext } from './AppContext'
import { storeEvent } from '../Functions/DatabaseFunctions/Events'
import { getRelays, Relay as RelayEntity, storeRelay } from '../Functions/DatabaseFunctions/Relays'
import { showMessage } from 'react-native-flash-message'
// import EncryptedStorage from 'react-native-encrypted-storage';
import { getPublickey } from '../lib/nostr/Bip';
import { getPublickey } from '../lib/nostr/Bip'
export interface RelayPoolContextProps {
relayPool?: RelayPool;
setRelayPool: (relayPool: RelayPool) => void;
publicKey?: string;
setPublicKey: (privateKey: string | undefined) => void;
privateKey?: string;
setPrivateKey: (privateKey: string | undefined) => void;
lastEventId?: string;
setLastEventId: (lastEventId: string) => void;
relayPool?: RelayPool
setRelayPool: (relayPool: RelayPool) => void
publicKey?: string
setPublicKey: (privateKey: string | undefined) => void
privateKey?: string
setPrivateKey: (privateKey: string | undefined) => void
lastEventId?: string
setLastEventId: (lastEventId: string) => void
}
export interface RelayPoolContextProviderProps {
children: React.ReactNode;
children: React.ReactNode
}
export const initialRelayPoolContext: RelayPoolContextProps = {
setPublicKey: () => {},
setPrivateKey: () => {},
setRelayPool: () => {},
setLastEventId: () => {},
};
setLastEventId: () => {}
}
export const RelayPoolContextProvider = ({
children,
children
}: RelayPoolContextProviderProps): JSX.Element => {
const { database, loadingDb, goToPage, page } = useContext(AppContext);
const { database, loadingDb, goToPage, page } = useContext(AppContext)
const [publicKey, setPublicKey] = useState<string>();
const [privateKey, setPrivateKey] = useState<string>();
const [relayPool, setRelayPool] = useState<RelayPool>();
const [lastEventId, setLastEventId] = useState<string>();
const [lastPage, setLastPage] = useState<string>(page);
const [publicKey, setPublicKey] = useState<string>()
const [privateKey, setPrivateKey] = useState<string>()
const [relayPool, setRelayPool] = useState<RelayPool>()
const [lastEventId, setLastEventId] = useState<string>()
const [lastPage, setLastPage] = useState<string>(page)
const loadRelayPool: () => void = () => {
if (database && privateKey) {
getRelays(database).then((relays: RelayEntity[]) => {
const initRelayPool = new RelayPool([], privateKey);
const initRelayPool = new RelayPool([], privateKey)
if (relays.length > 0) {
relays.forEach((relay) => {
initRelayPool.add(relay.url);
});
initRelayPool.add(relay.url)
})
} else {
['wss://relay.damus.io'].forEach((relayUrl) => {
initRelayPool.add(relayUrl);
storeRelay({ url: relayUrl }, database);
});
initRelayPool.add(relayUrl)
storeRelay({ url: relayUrl }, database)
})
}
initRelayPool?.on(
@ -64,59 +64,59 @@ export const RelayPoolContextProvider = ({
showMessage({
message: relay.url,
description: event?.content ?? '',
type: 'info',
});
},
);
type: 'info'
})
}
)
initRelayPool?.on(
'event',
'RelayPoolContextProvider',
(relay: Relay, _subId?: string, event?: Event) => {
console.log('RELAYPOOL EVENT =======>', relay.url, event);
console.log('RELAYPOOL EVENT =======>', relay.url, event)
if (database && event?.id && event.kind !== EventKind.petNames) {
storeEvent(event, database)
.then(() => setLastEventId(event.id))
.catch(() => setLastEventId(event.id));
.catch(() => setLastEventId(event.id))
}
},
);
setRelayPool(initRelayPool);
});
}
)
setRelayPool(initRelayPool)
})
}
};
}
useEffect(() => {
if (privateKey && privateKey !== '') {
setPublicKey(getPublickey(privateKey));
setPublicKey(getPublickey(privateKey))
}
}, [privateKey]);
}, [privateKey])
useEffect(() => {
if (privateKey !== '' && !loadingDb && !relayPool) {
loadRelayPool();
loadRelayPool()
}
}, [privateKey, loadingDb]);
}, [privateKey, loadingDb])
useEffect(() => {
if (relayPool && lastPage !== page) {
relayPool.removeOn('event', lastPage);
setLastPage(page);
relayPool.removeOn('event', lastPage)
setLastPage(page)
}
}, [page]);
}, [page])
useEffect(() => {
const result = ''
// EncryptedStorage.getItem('privateKey').then((result) => {
if (result && result !== '') {
loadRelayPool();
goToPage('home', true);
setPrivateKey(result);
setPublicKey(getPublickey(result));
} else {
goToPage('landing', true);
}
if (result && result !== '') {
loadRelayPool()
goToPage('home', true)
setPrivateKey(result)
setPublicKey(getPublickey(result))
} else {
goToPage('landing', true)
}
// });
}, []);
}, [])
return (
<RelayPoolContext.Provider
@ -128,12 +128,12 @@ export const RelayPoolContextProvider = ({
privateKey,
setPrivateKey,
lastEventId,
setLastEventId,
setLastEventId
}}
>
{children}
</RelayPoolContext.Provider>
);
};
)
}
export const RelayPoolContext = React.createContext(initialRelayPoolContext);
export const RelayPoolContext = React.createContext(initialRelayPoolContext)

View File

@ -1,13 +1,13 @@
import { SQLError, Transaction } from 'react-native-sqlite-storage';
import { SQLError, Transaction } from 'react-native-sqlite-storage'
export const errorCallback: (
query: string,
reject?: (reason?: any) => void,
) => (transaction: Transaction, error: SQLError) => void = (query, reject) => {
const callback: (transaction: Transaction, error: SQLError) => void = (_transaction, error) => {
console.log('SQL ERROR =========>', query, error);
if (reject) reject(error);
};
console.log('SQL ERROR =========>', query, error)
if (reject) reject(error)
}
return callback;
};
return callback
}

View File

@ -1,21 +1,21 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { Event, EventKind } from '../../../lib/nostr/Events';
import { insertNote } from '../Notes';
import { insertUserMeta } from '../Users';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { Event, EventKind } from '../../../lib/nostr/Events'
import { insertNote } from '../Notes'
import { insertUserMeta } from '../Users'
export const storeEvent: (event: Event, db: SQLiteDatabase) => Promise<void> = async (
event,
db,
db
) => {
return await new Promise<void>((resolve, reject) => {
try {
if (event.kind === EventKind.meta) {
insertUserMeta(event, db).then(resolve);
insertUserMeta(event, db).then(resolve)
} else if (event.kind === EventKind.textNote || event.kind === EventKind.recommendServer) {
insertNote(event, db).then(resolve);
insertNote(event, db).then(resolve)
}
} catch (e) {
reject(e);
reject(e)
}
});
};
})
}

View File

@ -1,5 +1,5 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { simpleExecute } from '..';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { simpleExecute } from '..'
export const createInitDatabase: (db: SQLiteDatabase) => Promise<void> = async (db) => {
return await new Promise<void>((resolve) => {
@ -17,7 +17,7 @@ export const createInitDatabase: (db: SQLiteDatabase) => Promise<void> = async (
reply_event_id TEXT
);
`,
db,
db
).then(() => {
simpleExecute(
`
@ -30,7 +30,7 @@ export const createInitDatabase: (db: SQLiteDatabase) => Promise<void> = async (
contact BOOLEAN DEFAULT FALSE
);
`,
db,
db
).then(() => {
simpleExecute(
`
@ -39,9 +39,9 @@ export const createInitDatabase: (db: SQLiteDatabase) => Promise<void> = async (
pet INTEGER
);
`,
db,
).then(() => resolve());
});
});
});
};
db
).then(() => resolve())
})
})
})
}

View File

@ -1,34 +1,34 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { getItems } from '..';
import { Event, EventKind, verifySignature } from '../../../lib/nostr/Events';
import { getMainEventId, getReplyEventId } from '../../RelayFunctions/Events';
import { errorCallback } from '../Errors';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { getItems } from '..'
import { Event, EventKind, verifySignature } from '../../../lib/nostr/Events'
import { getMainEventId, getReplyEventId } from '../../RelayFunctions/Events'
import { errorCallback } from '../Errors'
export interface Note extends Event {
name: string;
picture: string;
name: string
picture: string
}
const databaseToEntity: (object: any) => Note = (object) => {
object.tags = JSON.parse(object.tags);
return object as Note;
};
object.tags = JSON.parse(object.tags)
return object as Note
}
export const insertNote: (event: Event, db: SQLiteDatabase) => Promise<void> = async (
event,
db,
db
) => {
return await new Promise<void>((resolve, reject) => {
if (!verifySignature(event) || !event.id) return reject(new Error('Bad event'));
if (!verifySignature(event) || !event.id) return reject(new Error('Bad event'))
if (![EventKind.textNote, EventKind.recommendServer].includes(event.kind))
return reject(new Error('Bad Kind'));
return reject(new Error('Bad Kind'))
getNotes(db, { filters: { id: event.id } }).then((notes) => {
if (notes.length === 0 && event.id && event.sig) {
const mainEventId = getMainEventId(event) ?? '';
const replyEventId = getReplyEventId(event) ?? '';
const content = event.content.split("'").join("''");
const tags = JSON.stringify(event.tags).split("'").join("''");
const mainEventId = getMainEventId(event) ?? ''
const replyEventId = getReplyEventId(event) ?? ''
const content = event.content.split("'").join("''")
const tags = JSON.stringify(event.tags).split("'").join("''")
const eventQuery = `INSERT INTO nostros_notes
(id,content,created_at,kind,pubkey,sig,tags,main_event_id,reply_event_id)
VALUES
@ -42,67 +42,67 @@ export const insertNote: (event: Event, db: SQLiteDatabase) => Promise<void> = a
'${tags}',
'${mainEventId}',
'${replyEventId}'
);`;
);`
db.transaction((transaction) => {
transaction.executeSql(
eventQuery,
[],
() => resolve(),
errorCallback(eventQuery, reject),
);
});
errorCallback(eventQuery, reject)
)
})
} else {
reject(new Error('Note already exists'));
reject(new Error('Note already exists'))
}
});
});
};
})
})
}
export const getNotes: (
db: SQLiteDatabase,
options: {
filters?: { [column: string]: string };
limit?: number;
contacts?: boolean;
includeIds?: string[];
filters?: { [column: string]: string }
limit?: number
contacts?: boolean
includeIds?: string[]
},
) => Promise<Note[]> = async (db, { filters = {}, limit, contacts, includeIds }) => {
let notesQuery = `
SELECT nostros_notes.*, nostros_users.name, nostros_users.picture, nostros_users.contact FROM nostros_notes
LEFT JOIN nostros_users ON nostros_users.id = nostros_notes.pubkey
`;
`
if (filters) {
const keys = Object.keys(filters);
const keys = Object.keys(filters)
if (Object.keys(filters).length > 0) {
notesQuery += 'WHERE ';
notesQuery += 'WHERE '
keys.forEach((column, index) => {
notesQuery += `nostros_notes.${column} = '${filters[column]}' `;
if (index < keys.length - 1) notesQuery += 'AND ';
});
notesQuery += `nostros_notes.${column} = '${filters[column]}' `
if (index < keys.length - 1) notesQuery += 'AND '
})
}
}
if (contacts) {
if (Object.keys(filters).length > 0) {
notesQuery += 'AND ';
notesQuery += 'AND '
} else {
notesQuery += 'WHERE ';
notesQuery += 'WHERE '
}
notesQuery += 'nostros_users.contact = TRUE ';
notesQuery += 'nostros_users.contact = TRUE '
}
if (includeIds) {
if (Object.keys(filters).length > 0 || contacts) {
notesQuery += 'OR ';
notesQuery += 'OR '
} else {
notesQuery += 'WHERE ';
notesQuery += 'WHERE '
}
notesQuery += `nostros_users.id IN ('${includeIds.join("', '")}') `;
notesQuery += `nostros_users.id IN ('${includeIds.join("', '")}') `
}
notesQuery += `ORDER BY created_at DESC `;
notesQuery += 'ORDER BY created_at DESC '
if (limit) {
notesQuery += `LIMIT ${limit}`;
notesQuery += `LIMIT ${limit}`
}
return await new Promise<Note[]>((resolve, reject) => {
@ -111,12 +111,12 @@ export const getNotes: (
notesQuery,
[],
(_transaction, resultSet) => {
const items: object[] = getItems(resultSet);
const notes: Note[] = items.map((object) => databaseToEntity(object));
resolve(notes);
const items: object[] = getItems(resultSet)
const notes: Note[] = items.map((object) => databaseToEntity(object))
resolve(notes)
},
errorCallback(notesQuery, reject),
);
});
});
};
errorCallback(notesQuery, reject)
)
})
})
}

View File

@ -1,19 +1,19 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { getItems } from '..';
import { errorCallback } from '../Errors';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { getItems } from '..'
import { errorCallback } from '../Errors'
export interface Relay {
url: string;
name?: string;
url: string
name?: string
}
const databaseToEntity: (object: any) => Relay = (object) => {
return object as Relay;
};
return object as Relay
}
export const storeRelay: (relay: Relay, db: SQLiteDatabase) => void = async (relay, db) => {
if (relay.url) {
const relays: Relay[] = await searchRelays(relay.url, db);
const relays: Relay[] = await searchRelays(relay.url, db)
if (relays.length === 0) {
const eventQuery = `
INSERT INTO nostros_relays
@ -22,7 +22,7 @@ export const storeRelay: (relay: Relay, db: SQLiteDatabase) => void = async (rel
(
'${relay.url.split("'").join("''")}'
);
`;
`
await new Promise<void>((resolve, reject) => {
db.transaction((transaction) => {
@ -30,21 +30,21 @@ export const storeRelay: (relay: Relay, db: SQLiteDatabase) => void = async (rel
eventQuery,
[],
() => resolve(),
errorCallback(eventQuery, reject),
);
});
});
errorCallback(eventQuery, reject)
)
})
})
}
}
};
}
export const searchRelays: (relayUrl: string, db: SQLiteDatabase) => Promise<Relay[]> = async (
relayUrl,
db,
db
) => {
const searchQuery = `
SELECT * FROM nostros_relays WHERE url = '${relayUrl}';
`;
`
return await new Promise<Relay[]>((resolve, reject) => {
db.transaction((transaction) => {
@ -52,18 +52,18 @@ export const searchRelays: (relayUrl: string, db: SQLiteDatabase) => Promise<Rel
searchQuery,
[],
(_transaction, resultSet) => {
const items: object[] = getItems(resultSet);
const notes: Relay[] = items.map((object) => databaseToEntity(object));
resolve(notes);
const items: object[] = getItems(resultSet)
const notes: Relay[] = items.map((object) => databaseToEntity(object))
resolve(notes)
},
errorCallback(searchQuery, reject),
);
});
});
};
errorCallback(searchQuery, reject)
)
})
})
}
export const getRelays: (db: SQLiteDatabase) => Promise<Relay[]> = async (db) => {
const notesQuery = `SELECT * FROM nostros_relays;`;
const notesQuery = 'SELECT * FROM nostros_relays;'
return await new Promise<Relay[]>((resolve, reject) => {
db.readTransaction((transaction) => {
@ -71,12 +71,12 @@ export const getRelays: (db: SQLiteDatabase) => Promise<Relay[]> = async (db) =>
notesQuery,
[],
(_transaction, resultSet) => {
const items: object[] = getItems(resultSet);
const relays: Relay[] = items.map((object) => databaseToEntity(object));
resolve(relays);
const items: object[] = getItems(resultSet)
const relays: Relay[] = items.map((object) => databaseToEntity(object))
resolve(relays)
},
errorCallback(notesQuery, reject),
);
});
});
};
errorCallback(notesQuery, reject)
)
})
})
}

View File

@ -1,39 +1,39 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import { getItems } from '..';
import { Event, EventKind, verifySignature } from '../../../lib/nostr/Events';
import { tagToUser } from '../../RelayFunctions/Users';
import { errorCallback } from '../Errors';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import { getItems } from '..'
import { Event, EventKind, verifySignature } from '../../../lib/nostr/Events'
import { tagToUser } from '../../RelayFunctions/Users'
import { errorCallback } from '../Errors'
export interface User {
id: string;
main_relay?: string;
name?: string;
picture?: string;
about?: string;
contact?: boolean;
id: string
main_relay?: string
name?: string
picture?: string
about?: string
contact?: boolean
}
const databaseToEntity: (object: object) => User = (object) => {
return object as User;
};
return object as User
}
export const insertUserMeta: (event: Event, db: SQLiteDatabase) => Promise<void> = async (
event,
db,
db
) => {
return await new Promise<void>((resolve, reject) => {
if (!verifySignature(event)) return reject(new Error('Bad signature'));
if (event.kind !== EventKind.meta) return reject(new Error('Bad Kind'));
if (!verifySignature(event)) return reject(new Error('Bad signature'))
if (event.kind !== EventKind.meta) return reject(new Error('Bad Kind'))
const user: User = JSON.parse(event.content);
user.id = event.pubkey;
const user: User = JSON.parse(event.content)
user.id = event.pubkey
getUser(user.id, db).then((userDb) => {
let userQuery = '';
const id = user.id?.replace("\\'", "'") ?? '';
const name = user.name?.replace("\\'", "'") ?? '';
const mainRelay = user.main_relay?.replace("\\'", "'") ?? '';
const about = user.about?.replace("\\'", "'") ?? '';
const picture = user.picture?.replace("\\'", "'") ?? '';
let userQuery = ''
const id = user.id?.replace("\\'", "'") ?? ''
const name = user.name?.replace("\\'", "'") ?? ''
const mainRelay = user.main_relay?.replace("\\'", "'") ?? ''
const about = user.about?.replace("\\'", "'") ?? ''
const picture = user.picture?.replace("\\'", "'") ?? ''
if (userDb) {
userQuery = `
@ -43,7 +43,7 @@ export const insertUserMeta: (event: Event, db: SQLiteDatabase) => Promise<void>
about = '${about.split("'").join("''")}',
picture = '${picture.split("'").join("''")}'
WHERE id = '${user.id}';
`;
`
} else {
userQuery = `
INSERT INTO nostros_users
@ -52,35 +52,35 @@ export const insertUserMeta: (event: Event, db: SQLiteDatabase) => Promise<void>
('${id}', '${name.split("'").join("''")}', '${picture.split("'").join("''")}', '${about
.split("'")
.join("''")}', '');
`;
`
}
db.transaction((transaction) => {
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject));
});
});
});
};
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject))
})
})
})
}
export const insertUserContact: (event: Event, db: SQLiteDatabase) => Promise<void> = async (
event,
db,
db
) => {
return await new Promise<void>((resolve, reject) => {
if (!verifySignature(event)) return reject(new Error('Bad signature'));
if (event.kind !== EventKind.petNames) return reject(new Error('Bad Kind'));
const userTags: string[] = event.tags.map((tag) => tagToUser(tag).id);
if (!verifySignature(event)) return reject(new Error('Bad signature'))
if (event.kind !== EventKind.petNames) return reject(new Error('Bad Kind'))
const userTags: string[] = event.tags.map((tag) => tagToUser(tag).id)
userTags.forEach((userId) => {
addContact(userId, db);
});
resolve();
});
};
addContact(userId, db)
})
resolve()
})
}
export const getUser: (pubkey: string, db: SQLiteDatabase) => Promise<User | null> = async (
pubkey,
db,
db
) => {
const userQuery = `SELECT * FROM nostros_users WHERE id = '${pubkey}';`;
const userQuery = `SELECT * FROM nostros_users WHERE id = '${pubkey}';`
return await new Promise<User | null>((resolve, reject) => {
db.readTransaction((transaction) => {
transaction.executeSql(
@ -88,83 +88,83 @@ export const getUser: (pubkey: string, db: SQLiteDatabase) => Promise<User | nul
[],
(_transaction, resultSet) => {
if (resultSet.rows.length > 0) {
const items: object[] = getItems(resultSet);
const user: User = databaseToEntity(items[0]);
resolve(user);
const items: object[] = getItems(resultSet)
const user: User = databaseToEntity(items[0])
resolve(user)
} else {
resolve(null);
resolve(null)
}
},
errorCallback(userQuery, reject),
);
});
});
};
errorCallback(userQuery, reject)
)
})
})
}
export const removeContact: (pubkey: string, db: SQLiteDatabase) => Promise<void> = async (
pubkey,
db,
db
) => {
const userQuery = `UPDATE nostros_users SET contact = FALSE WHERE id = '${pubkey}'`;
const userQuery = `UPDATE nostros_users SET contact = FALSE WHERE id = '${pubkey}'`
return await new Promise<void>((resolve, reject) => {
db.transaction((transaction) => {
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject));
});
});
};
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject))
})
})
}
export const addContact: (pubkey: string, db: SQLiteDatabase) => Promise<void> = async (
pubkey,
db,
db
) => {
return await new Promise<void>((resolve, reject) => {
getUser(pubkey, db).then((userDb) => {
let userQuery = '';
let userQuery = ''
if (userDb) {
userQuery = `
UPDATE nostros_users SET contact = TRUE WHERE id = '${pubkey}';
`;
`
} else {
userQuery = `
INSERT INTO nostros_users (id, contact) VALUES ('${pubkey}', TRUE);
`;
`
}
db.transaction((transaction) => {
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject));
});
});
});
};
transaction.executeSql(userQuery, [], () => resolve(), errorCallback(userQuery, reject))
})
})
})
}
export const getUsers: (
db: SQLiteDatabase,
options: { exludeIds?: string[]; contacts?: boolean; includeIds?: string[] },
options: { exludeIds?: string[], contacts?: boolean, includeIds?: string[] },
) => Promise<User[]> = async (db, { exludeIds, contacts, includeIds }) => {
let userQuery = `SELECT * FROM nostros_users `;
let userQuery = 'SELECT * FROM nostros_users '
if (contacts) {
userQuery += `WHERE contact = TRUE `;
userQuery += 'WHERE contact = TRUE '
}
if (exludeIds && exludeIds.length > 0) {
if (!contacts) {
userQuery += `WHERE `;
userQuery += 'WHERE '
} else {
userQuery += `AND `;
userQuery += 'AND '
}
userQuery += `id NOT IN ('${exludeIds.join("', '")}') `;
userQuery += `id NOT IN ('${exludeIds.join("', '")}') `
}
if (includeIds && includeIds.length > 0) {
if (!contacts && !exludeIds) {
userQuery += `WHERE `;
userQuery += 'WHERE '
} else {
userQuery += `OR `;
userQuery += 'OR '
}
userQuery += `id IN ('${includeIds.join("', '")}') `;
userQuery += `id IN ('${includeIds.join("', '")}') `
}
userQuery += `ORDER BY name,id`;
userQuery += 'ORDER BY name,id'
return await new Promise<User[]>((resolve, reject) => {
db.readTransaction((transaction) => {
@ -173,15 +173,15 @@ export const getUsers: (
[],
(_transaction, resultSet) => {
if (resultSet.rows.length > 0) {
const items: object[] = getItems(resultSet);
const users: User[] = items.map((object) => databaseToEntity(object));
resolve(users);
const items: object[] = getItems(resultSet)
const users: User[] = items.map((object) => databaseToEntity(object))
resolve(users)
} else {
resolve([]);
resolve([])
}
},
errorCallback(userQuery, reject),
);
});
});
};
errorCallback(userQuery, reject)
)
})
})
}

View File

@ -1,42 +1,42 @@
import SQLite, { ResultSet, SQLiteDatabase, Transaction } from 'react-native-sqlite-storage';
import { errorCallback } from './Errors';
import SQLite, { ResultSet, SQLiteDatabase, Transaction } from 'react-native-sqlite-storage'
import { errorCallback } from './Errors'
export const initDatabase: () => SQLiteDatabase = () => {
return SQLite.openDatabase(
{ name: 'nostros.db', location: 'default' },
() => {},
() => {},
);
};
() => {}
)
}
export const getItems: (resultSet: ResultSet) => object[] = (resultSet) => {
const result: object[] = [];
const result: object[] = []
for (let i = 0; i < resultSet.rows.length; i++) {
result.push(resultSet.rows.item(i));
result.push(resultSet.rows.item(i))
}
return result;
};
return result
}
export const simpleExecute: (query: string, db: SQLiteDatabase) => Promise<Transaction> = async (
query,
db,
db
) => {
return await db.transaction((transaction) => {
transaction.executeSql(query, [], () => {}, errorCallback(query));
});
};
transaction.executeSql(query, [], () => {}, errorCallback(query))
})
}
export const dropTables: (db: SQLiteDatabase) => Promise<Transaction> = async (db) => {
const dropQueries = [
'DROP TABLE IF EXISTS nostros_notes;',
'DROP TABLE IF EXISTS nostros_users;',
'DROP TABLE IF EXISTS nostros_relays;',
];
'DROP TABLE IF EXISTS nostros_relays;'
]
return await db.transaction((transaction) => {
dropQueries.forEach((query) => {
transaction.executeSql(query, [], () => {}, errorCallback(query));
});
});
};
transaction.executeSql(query, [], () => {}, errorCallback(query))
})
})
}

View File

@ -1,48 +1,48 @@
import { Event } from '../../../lib/nostr/Events';
import { Event } from '../../../lib/nostr/Events'
export const getMainEventId: (event: Event) => string | null = (event) => {
const eTags = getETags(event);
const eTags = getETags(event)
let mainTag = eTags.find((tag) => {
return tag[3] === 'root';
});
return tag[3] === 'root'
})
if (!mainTag) {
mainTag = eTags[0];
mainTag = eTags[0]
}
return mainTag ? mainTag[1] : null;
};
return mainTag ? mainTag[1] : null
}
export const getReplyEventId: (event: Event) => string | null = (event) => {
const eTags = getETags(event);
const eTags = getETags(event)
let mainTag = eTags.find((tag) => {
return tag[3] === 'reply';
});
return tag[3] === 'reply'
})
if (!mainTag) {
mainTag = eTags[eTags.length - 1];
mainTag = eTags[eTags.length - 1]
}
return mainTag ? mainTag[1] : null;
};
return mainTag ? mainTag[1] : null
}
export const getDirectReplies: (event: Event, replies: Event[]) => Event[] = (event, replies) => {
return replies.filter((item) => isDirectReply(event, item));
};
return replies.filter((item) => isDirectReply(event, item))
}
export const isDirectReply: (mainEvent: Event, reply: Event) => boolean = (mainEvent, reply) => {
const taggedMainEventsIds: string[] = getTaggedEventIds(mainEvent);
const taggedReplyEventsIds: string[] = getTaggedEventIds(reply);
const difference = taggedReplyEventsIds.filter((item) => !taggedMainEventsIds.includes(item));
const taggedMainEventsIds: string[] = getTaggedEventIds(mainEvent)
const taggedReplyEventsIds: string[] = getTaggedEventIds(reply)
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) => {
const mainEventETags: string[][] = getETags(event);
return mainEventETags.map((item) => item[1] ?? '');
};
const mainEventETags: string[][] = getETags(event)
return mainEventETags.map((item) => item[1] ?? '')
}
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

@ -1,28 +1,28 @@
import moment from 'moment';
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import RelayPool from '../../../lib/nostr/RelayPool/intex';
import { getUser, getUsers, User } from '../../DatabaseFunctions/Users';
import { Event } from '../../../lib/nostr/Events';
import moment from 'moment'
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import RelayPool from '../../../lib/nostr/RelayPool/intex'
import { getUser, getUsers, User } from '../../DatabaseFunctions/Users'
import { Event } from '../../../lib/nostr/Events'
export const usersToTags: (users: User[]) => string[][] = (users) => {
return users.map((user): string[] => {
return ['p', user.id, user.main_relay ?? '', user.name ?? ''];
});
};
return ['p', user.id, user.main_relay ?? '', user.name ?? '']
})
}
export const tagsToUsers: (tags: string[][]) => User[] = (tags) => {
return tags.map((tag): User => {
return tagToUser(tag);
});
};
return tagToUser(tag)
})
}
export const tagToUser: (tag: string[]) => User = (tag) => {
return {
id: tag[1],
main_relay: tag[2],
name: tag[3],
};
};
name: tag[3]
}
}
export const populatePets: (
relayPool: RelayPool,
@ -36,12 +36,12 @@ export const populatePets: (
created_at: moment().unix(),
kind: 3,
pubkey: publicKey,
tags: usersToTags(results),
};
relayPool?.sendEvent(event);
tags: usersToTags(results)
}
relayPool?.sendEvent(event)
}
});
};
})
}
export const populateProfile: (
relayPool: RelayPool,
@ -54,16 +54,16 @@ export const populateProfile: (
name: result.name,
main_relay: result.main_relay,
picture: result.picture,
about: result.about,
};
about: result.about
}
const event: Event = {
content: JSON.stringify(profile),
created_at: moment().unix(),
kind: 0,
pubkey: publicKey,
tags: usersToTags([result]),
};
relayPool?.sendEvent(event);
tags: usersToTags([result])
}
relayPool?.sendEvent(event)
}
});
};
})
}

View File

@ -1,12 +1,12 @@
import { SQLiteDatabase } from 'react-native-sqlite-storage';
import RelayPool from '../../lib/nostr/RelayPool/intex';
import { populatePets, populateProfile } from './Users';
import { SQLiteDatabase } from 'react-native-sqlite-storage'
import RelayPool from '../../lib/nostr/RelayPool/intex'
import { populatePets, populateProfile } from './Users'
export const populateRelay: (
relayPool: RelayPool,
database: SQLiteDatabase,
publicKey: string,
) => void = (relayPool, database, publicKey) => {
populateProfile(relayPool, database, publicKey);
populatePets(relayPool, database, publicKey);
};
populateProfile(relayPool, database, publicKey)
populatePets(relayPool, database, publicKey)
}

View File

@ -1,19 +1,19 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './Locales/en.json';
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './Locales/en.json'
i18n.use(initReactI18next).init({
compatibilityJSON: 'v3',
fallbackLng: 'en',
resources: {
en,
en
},
ns: ['common'],
defaultNS: 'common',
debug: true,
interpolation: {
escapeValue: false,
},
});
escapeValue: false
}
})
export default i18n;
export default i18n

View File

@ -1,21 +1,21 @@
import React from 'react';
import { ApplicationProvider, IconRegistry } from '@ui-kitten/components';
import theme from './theme.json';
import { EvaIconsPack } from '@ui-kitten/eva-icons';
import * as eva from '@eva-design/eva';
import { AppContextProvider } from './Contexts/AppContext';
import MainLayout from './Components/MainLayout';
import { RelayPoolContextProvider } from './Contexts/RelayPoolContext';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n.config';
import React from 'react'
import { ApplicationProvider, IconRegistry } from '@ui-kitten/components'
import theme from './theme.json'
import { EvaIconsPack } from '@ui-kitten/eva-icons'
import * as eva from '@eva-design/eva'
import { AppContextProvider } from './Contexts/AppContext'
import MainLayout from './Components/MainLayout'
import { RelayPoolContextProvider } from './Contexts/RelayPoolContext'
import { I18nextProvider } from 'react-i18next'
import i18n from './i18n.config'
export const Frontend: React.FC = () => {
const mapping = {
strict: {
'text-font-family': 'OpenSans-Regular',
'text-font-family': 'OpenSans-Regular'
},
components: {},
};
components: {}
}
return (
<>
@ -30,7 +30,7 @@ export const Frontend: React.FC = () => {
</I18nextProvider>
</ApplicationProvider>
</>
);
};
)
}
export default Frontend;
export default Frontend

View File

@ -1,12 +1,12 @@
import ecurve from 'ecurve';
import BigInteger from 'bigi';
import ecurve from 'ecurve'
import BigInteger from 'bigi'
export const getPublickey: (privateKey: string) => string = (privateKey) => {
const privateKeyBuffer = Buffer.from(privateKey, 'hex');
const ecparams = ecurve.getCurveByName('secp256k1');
const privateKeyBuffer = Buffer.from(privateKey, 'hex')
const ecparams = ecurve.getCurveByName('secp256k1')
const curvePt = ecparams.G.multiply(BigInteger.fromBuffer(privateKeyBuffer));
const publicKey = curvePt.getEncoded(true);
const curvePt = ecparams.G.multiply(BigInteger.fromBuffer(privateKeyBuffer))
const publicKey = curvePt.getEncoded(true)
return publicKey.toString('hex').slice(2);
};
return publicKey.toString('hex').slice(2)
}

View File

@ -1,13 +1,13 @@
import schnorr from 'bip-schnorr';
import schnorr from 'bip-schnorr'
export interface Event {
content: string;
created_at: number;
id?: string;
kind: 0 | 1 | 2 | 3;
pubkey: string;
sig?: string;
tags: string[][];
content: string
created_at: number
id?: string
kind: 0 | 1 | 2 | 3
pubkey: string
sig?: string
tags: string[][]
}
export enum EventKind {
@ -18,56 +18,56 @@ export enum EventKind {
}
export const serializeEvent: (event: Event) => string = (event) => {
return JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
};
return JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content])
}
export const getEventHash: (event: Event) => Buffer = (event) => {
return schnorr.convert.hash(Buffer.from(serializeEvent(event)));
};
return schnorr.convert.hash(Buffer.from(serializeEvent(event)))
}
export const validateEvent: (event: Event) => boolean = (event) => {
if (event.id !== getEventHash(event).toString('hex')) return false;
if (typeof event.content !== 'string') return false;
if (typeof event.created_at !== 'number') return false;
if (event.id !== getEventHash(event).toString('hex')) return false
if (typeof event.content !== 'string') return false
if (typeof event.created_at !== 'number') return false
if (!Array.isArray(event.tags)) return false;
if (!Array.isArray(event.tags)) return false
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i];
if (!Array.isArray(tag)) return false;
const tag = event.tags[i]
if (!Array.isArray(tag)) return false
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === 'object') return false;
if (typeof tag[j] === 'object') return false
}
}
return true;
};
return true
}
export const verifySignature: (event: Event) => Promise<boolean> = async (event) => {
try {
schnorr.verify(
Buffer.from(event.pubkey, 'hex'),
Buffer.from(event?.id, 'hex'),
Buffer.from(event?.sig, 'hex'),
);
return true;
Buffer.from(event?.sig, 'hex')
)
return true
} catch (_e) {
console.error(`The signature verification failed`);
return false;
console.error('The signature verification failed')
return false
}
};
}
export const signEvent: (event: Event, privateKey: string) => Promise<Event> = async (
event,
privateKey,
privateKey
) => {
const hash = getEventHash(event);
const hash = getEventHash(event)
const signature: string = Buffer.from(await schnorr.sign(privateKey, hash), 'hex').toString(
'hex',
);
'hex'
)
event.id = hash.toString('hex');
event.sig = signature;
event.id = hash.toString('hex')
event.sig = signature
return event;
};
return event
}

View File

@ -1,141 +1,141 @@
import { Event } from '../Events';
import { v5 as uuidv5 } from 'uuid';
import { Event } from '../Events'
import { v5 as uuidv5 } from 'uuid'
export interface RelayFilters {
ids?: string[];
authors?: string[];
kinds?: number[];
'#e'?: string[];
'#p'?: string[];
since?: number;
limit?: number;
until?: number;
ids?: string[]
authors?: string[]
kinds?: number[]
'#e'?: string[]
'#p'?: string[]
since?: number
limit?: number
until?: number
}
export interface RelayMessage {
data: string;
data: string
}
export interface RelayOptions {
reconnect?: boolean;
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 = {};
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.onOpen = () => {}
this.onEvent = () => {}
this.onEsoe = () => {}
this.onNotice = () => {}
this.initWebsocket();
this.initWebsocket()
}
private readonly options: RelayOptions;
private socket: WebSocket;
private manualClose: boolean;
private subscriptions: { [subId: string]: string[] };
private readonly options: RelayOptions
private socket: WebSocket
private manualClose: boolean
private subscriptions: { [subId: string]: string[] }
private readonly initWebsocket: () => void = () => {
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);
};
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 = () => {
if (!this.manualClose && this.options.reconnect) this.initWebsocket();
};
if (!this.manualClose && this.options.reconnect) this.initWebsocket()
}
private readonly onError: () => void = () => {
if (this.options.reconnect) this.initWebsocket();
};
if (this.options.reconnect) this.initWebsocket()
}
private readonly handleNostrMessage: (message: RelayMessage) => void = (message) => {
const data: any[] = JSON.parse(message.data);
const data: any[] = JSON.parse(message.data)
if (data.length >= 2) {
const id: string = data[1];
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);
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);
return this.onEsoe(this, id)
} else if (data[0] === 'NOTICE') {
return this.onNotice(this, [...data.slice(1)]);
return this.onNotice(this, [...data.slice(1)])
}
}
};
}
private readonly send: (message: object) => void = async (message) => {
const tosend = JSON.stringify(message);
const tosend = JSON.stringify(message)
if (this.socket.readyState !== WebSocket.OPEN) {
setTimeout(() => {
this.send(message);
}, 500);
this.send(message)
}, 500)
} else {
try {
console.log('SEND =====>', tosend);
this.socket.send(tosend);
console.log('SEND =====>', tosend)
this.socket.send(tosend)
} catch (e) {
console.log('Failed ot send Event', e);
console.log('Failed ot send Event', e)
}
}
};
}
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 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 = () => {
if (this.socket) {
this.manualClose = true;
this.socket.close();
this.manualClose = true
this.socket.close()
}
};
}
public readonly sendEvent: (event: Event) => void = (event) => {
this.send(['EVENT', event]);
};
this.send(['EVENT', event])
}
public readonly subscribe: (subId: string, filters?: RelayFilters) => void = (
subId,
filters = {},
filters = {}
) => {
const uuid = uuidv5(
`${subId}${JSON.stringify(filters)}`,
'57003344-b2cb-4b6f-a579-fae9e82c370a',
);
'57003344-b2cb-4b6f-a579-fae9e82c370a'
)
if (this.subscriptions[subId]?.includes(uuid)) {
console.log('Subscription already done!');
console.log('Subscription already done!')
} else {
this.send(['REQ', subId, filters]);
const newSubscriptions = [...(this.subscriptions[subId] ?? []), uuid];
this.subscriptions[subId] = newSubscriptions;
this.send(['REQ', subId, filters])
const newSubscriptions = [...(this.subscriptions[subId] ?? []), uuid]
this.subscriptions[subId] = newSubscriptions
}
};
}
public readonly unsubscribe: (subId: string) => void = (subId) => {
this.send(['CLOSE', subId]);
delete this.subscriptions[subId];
};
this.send(['CLOSE', subId])
delete this.subscriptions[subId]
}
public readonly unsubscribeAll: () => void = () => {
Object.keys(this.subscriptions).forEach((subId: string) => {
this.send(['CLOSE', subId]);
});
this.subscriptions = {};
};
this.send(['CLOSE', subId])
})
this.subscriptions = {}
}
}
export default Relay;
export default Relay

View File

@ -1,140 +1,140 @@
import { signEvent, validateEvent, Event } from '../Events';
import Relay, { RelayFilters, RelayOptions } from '../Relay';
import { signEvent, validateEvent, Event } from '../Events'
import Relay, { RelayFilters, RelayOptions } from '../Relay'
export interface OnFunctions {
open: { [id: string]: (relay: Relay) => void };
event: { [id: string]: (relay: Relay, subId: string, event: Event) => void };
esoe: { [id: string]: (relay: Relay, subId: string) => void };
notice: { [id: string]: (relay: Relay, events: Event[]) => void };
open: { [id: string]: (relay: Relay) => void }
event: { [id: string]: (relay: Relay, subId: string, event: Event) => void }
esoe: { [id: string]: (relay: Relay, subId: string) => void }
notice: { [id: string]: (relay: Relay, events: Event[]) => void }
}
class RelayPool {
constructor(relaysUrls: string[], privateKey: string, options: RelayOptions = {}) {
this.relays = {};
this.privateKey = privateKey;
this.options = options;
constructor (relaysUrls: string[], privateKey: string, options: RelayOptions = {}) {
this.relays = {}
this.privateKey = privateKey
this.options = options
this.onFunctions = {
open: {},
event: {},
esoe: {},
notice: {},
};
notice: {}
}
relaysUrls.forEach((relayUrl) => {
this.add(relayUrl);
});
this.add(relayUrl)
})
this.setupHandlers();
this.setupHandlers()
}
private readonly privateKey: string;
private readonly options: RelayOptions;
private readonly onFunctions: OnFunctions;
public relays: { [url: string]: Relay };
private readonly privateKey: string
private readonly options: RelayOptions
private readonly onFunctions: OnFunctions
public relays: { [url: string]: Relay }
private readonly setupHandlers: () => void = () => {
Object.keys(this.relays).forEach((relayUrl: string) => {
const relay: Relay = this.relays[relayUrl];
const relay: Relay = this.relays[relayUrl]
relay.onOpen = (openRelay) => {
Object.keys(this.onFunctions.open).forEach((id) => this.onFunctions.open[id](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),
);
};
this.onFunctions.event[id](eventRelay, subId, event)
)
}
relay.onEsoe = (eventRelay, subId) => {
Object.keys(this.onFunctions.esoe).forEach((id) =>
this.onFunctions.esoe[id](eventRelay, subId),
);
};
this.onFunctions.esoe[id](eventRelay, subId)
)
}
relay.onNotice = (eventRelay, events) => {
Object.keys(this.onFunctions.notice).forEach((id) =>
this.onFunctions.notice[id](eventRelay, events),
);
};
});
};
this.onFunctions.notice[id](eventRelay, events)
)
}
})
}
public on: (
method: 'open' | 'event' | 'esoe' | 'notice',
id: string,
fn: (relay: Relay, subId?: string, event?: Event) => void,
) => void = (method, id, fn) => {
this.onFunctions[method][id] = fn;
};
this.onFunctions[method][id] = fn
}
public removeOn: (method: 'open' | 'event' | 'esoe' | 'notice', id: string) => void = (
method,
id,
id
) => {
delete this.onFunctions[method][id];
};
delete this.onFunctions[method][id]
}
public readonly add: (relayUrl: string) => boolean = (relayUrl) => {
if (this.relays[relayUrl]) return false;
if (this.relays[relayUrl]) return false
this.relays[relayUrl] = new Relay(relayUrl, this.options);
this.setupHandlers();
this.relays[relayUrl] = new Relay(relayUrl, this.options)
this.setupHandlers()
return true;
};
return true
}
public readonly close: () => void = () => {
Object.keys(this.relays).forEach((relayUrl: string) => {
const relay: Relay = this.relays[relayUrl];
relay.close();
});
};
const relay: Relay = this.relays[relayUrl]
relay.close()
})
}
public readonly remove: (relayUrl: string) => boolean = (relayUrl) => {
const relay: Relay | undefined = this.relays[relayUrl];
const relay: Relay | undefined = this.relays[relayUrl]
if (relay) {
relay.close();
delete this.relays[relayUrl];
return true;
relay.close()
delete this.relays[relayUrl]
return true
}
return false;
};
return false
}
public readonly sendEvent: (event: Event) => Promise<Event | null> = async (event) => {
const signedEvent: Event = await signEvent(event, this.privateKey);
const signedEvent: Event = await signEvent(event, this.privateKey)
if (validateEvent(signedEvent)) {
Object.keys(this.relays).forEach((relayUrl: string) => {
const relay: Relay = this.relays[relayUrl];
relay.sendEvent(signedEvent);
});
const relay: Relay = this.relays[relayUrl]
relay.sendEvent(signedEvent)
})
return signedEvent;
return signedEvent
} else {
console.log('Not valid event', event);
console.log('Not valid event', event)
}
return null;
};
return null
}
public readonly subscribe: (subId: string, filters?: RelayFilters) => void = (subId, filters) => {
Object.keys(this.relays).forEach((relayUrl: string) => {
this.relays[relayUrl].subscribe(subId, filters);
});
};
this.relays[relayUrl].subscribe(subId, filters)
})
}
public readonly unsubscribe: (subId: string) => void = (subId) => {
Object.keys(this.relays).forEach((relayUrl: string) => {
this.relays[relayUrl].unsubscribe(subId);
});
};
this.relays[relayUrl].unsubscribe(subId)
})
}
public readonly unsubscribeAll: () => void = () => {
Object.keys(this.relays).forEach((relayUrl: string) => {
this.relays[relayUrl].unsubscribeAll();
});
};
this.relays[relayUrl].unsubscribeAll()
})
}
}
export default RelayPool;
export default RelayPool