Push to relay

This commit is contained in:
KoalaSat 2023-02-09 23:10:50 +01:00
parent 9161e7ad90
commit a125001e63
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
28 changed files with 452 additions and 143 deletions

View File

@ -188,7 +188,7 @@ public class Event {
protected void saveNote(SQLiteDatabase database, String userPubKey, String relayUrl) { protected void saveNote(SQLiteDatabase database, String userPubKey, String relayUrl) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put("id", id); values.put("id", id);
values.put("content", content.replace("'", "''")); values.put("content", content);
values.put("created_at", created_at); values.put("created_at", created_at);
values.put("kind", kind); values.put("kind", kind);
values.put("pubkey", pubkey); values.put("pubkey", pubkey);

View File

@ -110,4 +110,22 @@ public class RelayPoolModule extends ReactContextBaseJavaModule {
} }
} }
} }
@ReactMethod
public void sendAll(String message, boolean isGlobalFeed) {
for (Relay relay : relays) {
if (relay.active() > 0 && (!isGlobalFeed || relay.globalFeed > 0)) {
relay.send(message);
}
}
}
@ReactMethod
public void sendRelay(String message, String relayUrl) {
for (Relay relay : relays) {
if (relay.active() > 0 && relayUrl.equals(relay.url)) {
relay.send(message);
}
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -6,6 +6,7 @@ import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext' import { UserContext } from '../../Contexts/UserContext'
import { import {
addUser,
getUser, getUser,
updateUserBlock, updateUserBlock,
updateUserContact, updateUserContact,
@ -32,6 +33,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
const { publicKey } = React.useContext(UserContext) const { publicKey } = React.useContext(UserContext)
const { relayPool, updateRelayItem } = React.useContext(RelayPoolContext) const { relayPool, updateRelayItem } = React.useContext(RelayPoolContext)
const [isContact, setIsContact] = React.useState<boolean>() const [isContact, setIsContact] = React.useState<boolean>()
const [isBlocked, setIsBlocked] = React.useState<boolean>()
const [showNotification, setShowNotification] = React.useState<undefined | string>() const [showNotification, setShowNotification] = React.useState<undefined | string>()
const [showNotificationRelay, setShowNotificationRelay] = React.useState<undefined | string>() const [showNotificationRelay, setShowNotificationRelay] = React.useState<undefined | string>()
const bottomSheetRelaysRef = React.useRef<RBSheet>(null) const bottomSheetRelaysRef = React.useRef<RBSheet>(null)
@ -59,6 +61,8 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
getUser(user.id, database).then((result) => { getUser(user.id, database).then((result) => {
if (result) { if (result) {
setUser(result) setUser(result)
setIsContact(result.contact)
setIsBlocked(result.blocked !== undefined && result.blocked > 0)
} else if (user.id === publicKey) { } else if (user.id === publicKey) {
setUser({ setUser({
id: publicKey, id: publicKey,
@ -69,9 +73,12 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
} }
const onChangeBlockUser: () => void = () => { const onChangeBlockUser: () => void = () => {
if (database && user?.blocked !== undefined) { if (database) {
updateUserBlock(user.id, database, !user?.blocked).then(() => { addUser(user.id, database).then(() => {
updateUserBlock(user.id, database, !isBlocked).then(() => {
loadUser() loadUser()
setShowNotificationRelay(isBlocked ? 'userUnblocked' : 'userBlocked')
})
}) })
} }
} }
@ -160,7 +167,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
}} }}
disabled={user.id === publicKey} disabled={user.id === publicKey}
/> />
<Text>{isContact ? t('profilePage.unfollow') : t('profilePage.follow')}</Text> <Text>{isContact ? t('profileCard.unfollow') : t('profileCard.follow')}</Text>
</View> </View>
<View style={styles.actionButton}> <View style={styles.actionButton}>
<IconButton <IconButton
@ -173,7 +180,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
}) })
}} }}
/> />
<Text>{t('profilePage.message')}</Text> <Text>{t('profileCard.message')}</Text>
</View> </View>
<View style={styles.actionButton}> <View style={styles.actionButton}>
<IconButton <IconButton
@ -183,7 +190,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
disabled={!user?.lnurl} disabled={!user?.lnurl}
iconColor='#F5D112' iconColor='#F5D112'
/> />
<Text>{t('profilePage.invoice')}</Text> <Text>{t('profileCard.invoice')}</Text>
</View> </View>
</View> </View>
<View style={styles.mainLayout}> <View style={styles.mainLayout}>
@ -211,14 +218,14 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
size={28} size={28}
onPress={() => bottomSheetRelaysRef.current?.open()} onPress={() => bottomSheetRelaysRef.current?.open()}
/> />
<Text>{t('profilePage.relaysTitle')}</Text> <Text>{t('profileCard.relaysTitle')}</Text>
</View> </View>
</View> </View>
<RBSheet ref={bottomSheetRelaysRef} closeOnDragDown={true} customStyles={bottomSheetStyles}> <RBSheet ref={bottomSheetRelaysRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<View> <View>
<Text variant='titleLarge'>{t('profilePage.relaysTitle')}</Text> <Text variant='titleLarge'>{t('profileCard.relaysTitle')}</Text>
<Text variant='bodyMedium'> <Text variant='bodyMedium'>
{t('profilePage.relaysDescription', { username: username(user) })} {t('profileCard.relaysDescription', { username: username(user) })}
</Text> </Text>
<List.Item <List.Item
title={t('relaysPage.relayName')} title={t('relaysPage.relayName')}
@ -242,7 +249,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
onIconPress={() => setShowNotificationRelay(undefined)} onIconPress={() => setShowNotificationRelay(undefined)}
onDismiss={() => setShowNotificationRelay(undefined)} onDismiss={() => setShowNotificationRelay(undefined)}
> >
{t(`profilePage.${showNotificationRelay}`)} {t(`profileCard.notifications.${showNotificationRelay}`)}
</Snackbar> </Snackbar>
)} )}
</RBSheet> </RBSheet>
@ -258,7 +265,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
onIconPress={() => setShowNotification(undefined)} onIconPress={() => setShowNotification(undefined)}
onDismiss={() => setShowNotification(undefined)} onDismiss={() => setShowNotification(undefined)}
> >
{t(`profilePage.${showNotification}`)} {t(`profileCard.notifications.${showNotification}`)}
</Snackbar> </Snackbar>
)} )}
</View> </View>

View File

@ -1,7 +1,16 @@
import { t } from 'i18next' import { t } from 'i18next'
import * as React from 'react' import * as React from 'react'
import { StyleSheet, Switch, View } from 'react-native' import { StyleSheet, Switch, View } from 'react-native'
import { Divider, IconButton, List, Snackbar, Text } from 'react-native-paper' import {
Button,
Checkbox,
Divider,
IconButton,
List,
Snackbar,
Text,
useTheme,
} from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import RBSheet from 'react-native-raw-bottom-sheet' import RBSheet from 'react-native-raw-bottom-sheet'
@ -12,6 +21,10 @@ import axios from 'axios'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import Logo from '../Logo' import Logo from '../Logo'
import { getRawUserNotes } from '../../Functions/DatabaseFunctions/Notes'
import { UserContext } from '../../Contexts/UserContext'
import { getRawUserReactions } from '../../Functions/DatabaseFunctions/Reactions'
import { getRawUserConversation } from '../../Functions/DatabaseFunctions/DirectMessages'
interface RelayCardProps { interface RelayCardProps {
url?: string url?: string
@ -19,18 +32,49 @@ interface RelayCardProps {
} }
export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) => { export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) => {
const { updateRelayItem } = React.useContext(RelayPoolContext) const theme = useTheme()
const { publicKey } = React.useContext(UserContext)
const { updateRelayItem, relayPool } = React.useContext(RelayPoolContext)
const { database } = React.useContext(AppContext) const { database } = React.useContext(AppContext)
const [relay, setRelay] = React.useState<Relay>() const [relay, setRelay] = React.useState<Relay>()
const [uri, setUri] = React.useState<string>() const [uri, setUri] = React.useState<string>()
const [relayInfo, setRelayInfo] = React.useState<RelayInfo>() const [relayInfo, setRelayInfo] = React.useState<RelayInfo>()
const [showNotification, setShowNotification] = React.useState<string>() const [showNotification, setShowNotification] = React.useState<string>()
const [checkedPush, setCheckedPush] = React.useState<'checked' | 'unchecked' | 'indeterminate'>(
'unchecked',
)
const [moreInfo, setMoreInfo] = React.useState<boolean>(false) const [moreInfo, setMoreInfo] = React.useState<boolean>(false)
const [pushDone, setPushDone] = React.useState<boolean>(false)
const [pushUserHistoric, setPushUserHistoric] = React.useState<boolean>(false)
const bottomSheetPushRelayRef = React.useRef<RBSheet>(null)
React.useEffect(() => { React.useEffect(() => {
loadRelay() loadRelay()
}, []) }, [])
React.useEffect(() => {
if (pushUserHistoric && url && database && publicKey && relayPool) {
getRawUserNotes(database, publicKey).then((resultNotes) => {
resultNotes.forEach((note) => {
note.content = note.content.replace("''", "'")
relayPool.sendEvent(note, url)
})
})
getRawUserReactions(database, publicKey).then((resultReactions) => {
resultReactions.forEach((reaction) => {
relayPool.sendEvent(reaction, url)
})
})
getRawUserConversation(database, publicKey).then((resultConversations) => {
resultConversations.forEach((conversation) => {
conversation.content = conversation.content.replace("''", "'")
relayPool.sendEvent(conversation, url)
})
setPushDone(true)
})
}
}, [pushUserHistoric])
React.useEffect(() => { React.useEffect(() => {
if (relay) { if (relay) {
setUri(relay.url) setUri(relay.url)
@ -110,6 +154,21 @@ export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) =>
} }
} }
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
paddingTop: 16,
paddingRight: 16,
paddingBottom: 32,
paddingLeft: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
height: 'auto',
},
}
}, [])
return relay ? ( return relay ? (
<View> <View>
<View style={styles.relayDescription}> <View style={styles.relayDescription}>
@ -215,6 +274,14 @@ export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) =>
</View> </View>
<Divider /> <Divider />
<View style={styles.actions}> <View style={styles.actions}>
<View style={styles.actionButton}>
<IconButton
icon={'upload-multiple'}
size={28}
onPress={() => bottomSheetPushRelayRef.current?.open()}
/>
<Text>{t('relayCard.pushRelay')}</Text>
</View>
<View style={styles.actionButton}> <View style={styles.actionButton}>
<IconButton <IconButton
icon={'share-variant-outline'} icon={'share-variant-outline'}
@ -238,6 +305,50 @@ export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) =>
{t(`relayCard.notifications.${showNotification}`)} {t(`relayCard.notifications.${showNotification}`)}
</Snackbar> </Snackbar>
)} )}
<RBSheet
ref={bottomSheetPushRelayRef}
closeOnPressMask={!pushUserHistoric}
customStyles={bottomSheetStyles}
onClose={() => {}}
>
<Text variant='titleLarge'>{t('relayCard.pushHistoricTitle')}</Text>
<Text>{t('relayCard.pushHistoricDescription')}</Text>
<View style={[styles.warning, { backgroundColor: '#683D00' }]}>
<Text variant='titleSmall' style={[styles.warningTitle, { color: '#FFDCBB' }]}>
{t('relayCard.pushHistoricAlertTitle')}
</Text>
<Text style={{ color: '#FFDCBB' }}>{t('relayCard.pushHistoricAlert')}</Text>
</View>
<View style={styles.row}>
<Text>{t('relayCard.pushConsent')}</Text>
<Checkbox
status={checkedPush}
onPress={() => setCheckedPush(checkedPush === 'checked' ? 'unchecked' : 'checked')}
/>
</View>
<Button
style={styles.buttonSpacer}
mode='contained'
onPress={() => setPushUserHistoric(true)}
disabled={pushUserHistoric || checkedPush !== 'checked'}
loading={pushUserHistoric && !pushDone}
>
{pushDone
? t('relayCard.pushDone')
: pushUserHistoric
? t('relayCard.pushingEvent')
: t('relayCard.pushHistoricTitle')}
</Button>
<Button
mode='outlined'
onPress={() => {
bottomSheetPushRelayRef.current?.close()
}}
disabled={pushUserHistoric}
>
{t('relayCard.cancel')}
</Button>
</RBSheet>
</View> </View>
) : ( ) : (
<></> <></>
@ -245,6 +356,12 @@ export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) =>
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
row: {
alignItems: 'center',
flexDirection: 'row',
height: 56,
justifyContent: 'space-between',
},
container: { container: {
padding: 16, padding: 16,
}, },
@ -287,6 +404,19 @@ const styles = StyleSheet.create({
marginBottom: 4, marginBottom: 4,
width: '33%', width: '33%',
}, },
buttonSpacer: {
marginTop: 16,
marginBottom: 16,
},
warning: {
borderRadius: 4,
padding: 16,
marginTop: 16,
marginBottom: 16,
},
warningTitle: {
marginBottom: 8,
},
}) })
export default RelayCard export default RelayCard

View File

@ -48,6 +48,7 @@ export const TextContent: React.FC<TextContentProps> = ({
const DEFAULT_COVER = '../../../assets/images/placeholders/placeholder_url.png' const DEFAULT_COVER = '../../../assets/images/placeholders/placeholder_url.png'
const MEDIA_COVER = '../../../assets/images/placeholders/placeholder_media.png' const MEDIA_COVER = '../../../assets/images/placeholders/placeholder_media.png'
const IMAGE_COVER = '../../../assets/images/placeholders/placeholder_image.png' const IMAGE_COVER = '../../../assets/images/placeholders/placeholder_image.png'
const BLUEBIRD_COVER = '../../../assets/images/placeholders/placeholder_bluebird.png'
useEffect(() => { useEffect(() => {
if (!linkPreview && url) { if (!linkPreview && url) {
@ -162,11 +163,9 @@ export const TextContent: React.FC<TextContentProps> = ({
const getDefaultCover: () => number = () => { const getDefaultCover: () => number = () => {
if (!linkPreview) return require(DEFAULT_COVER) if (!linkPreview) return require(DEFAULT_COVER)
if (linkType === 'blueBird') return require(BLUEBIRD_COVER)
if (linkType === 'audio') return require(MEDIA_COVER) if (linkType === 'audio') return require(MEDIA_COVER)
if (linkType === 'video') return require(MEDIA_COVER) if (linkType === 'video') return require(MEDIA_COVER)
if (linkType === 'blueBird') return require(DEFAULT_COVER)
if (linkType === 'image') return require(IMAGE_COVER)
return require(DEFAULT_COVER) return require(DEFAULT_COVER)
} }
@ -197,6 +196,7 @@ export const TextContent: React.FC<TextContentProps> = ({
{url && ( {url && (
<View style={styles.previewCard}> <View style={styles.previewCard}>
<Card onPress={() => handleUrlPress(url)}> <Card onPress={() => handleUrlPress(url)}>
{linkType === 'image' ? (
<FastImage <FastImage
style={[ style={[
styles.cardCover, styles.cardCover,
@ -208,9 +208,13 @@ export const TextContent: React.FC<TextContentProps> = ({
uri: getRequireCover(), uri: getRequireCover(),
priority: FastImage.priority.high, priority: FastImage.priority.high,
}} }}
defaultSource={getDefaultCover()}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
defaultSource={require(IMAGE_COVER)}
/> />
) : (
<Card.Cover source={getDefaultCover()} resizeMode='contain' />
)}
{linkType !== 'image' && (
<Card.Content style={styles.previewContent}> <Card.Content style={styles.previewContent}>
<Text variant='bodyMedium' numberOfLines={3}> <Text variant='bodyMedium' numberOfLines={3}>
{/* {linkPreview?.title ?? linkPreview?.url ?? url} */} {/* {linkPreview?.title ?? linkPreview?.url ?? url} */}
@ -222,6 +226,7 @@ export const TextContent: React.FC<TextContentProps> = ({
</Text> </Text>
)} */} )} */}
</Card.Content> </Card.Content>
)}
</Card> </Card>
</View> </View>
)} )}
@ -274,8 +279,7 @@ const styles = StyleSheet.create({
cardCover: { cardCover: {
flex: 1, flex: 1,
height: 180, height: 180,
borderTopLeftRadius: 16, borderRadius: 16,
borderTopRightRadius: 16,
}, },
url: { url: {
textDecorationLine: 'underline', textDecorationLine: 'underline',

View File

@ -1,6 +1,6 @@
import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite' import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..' import { getItems } from '..'
import { Event } from '../../../lib/nostr/Events' import { Event, evetDatabaseToEntity } from '../../../lib/nostr/Events'
export interface DirectMessage extends Event { export interface DirectMessage extends Event {
conversation_id: string conversation_id: string
@ -13,6 +13,21 @@ const databaseToEntity: (object: any) => DirectMessage = (object = {}) => {
return object as DirectMessage return object as DirectMessage
} }
export const getRawUserConversation: (
db: QuickSQLiteConnection,
pubKey: string,
) => Promise<Event[]> = async (db, pubKey) => {
const notesQuery = `SELECT * FROM nostros_direct_messages
WHERE pubkey = ?
ORDER BY created_at DESC
`
const resultSet = await db.execute(notesQuery, [pubKey])
const items: object[] = getItems(resultSet)
const notes: Event[] = items.map((object) => evetDatabaseToEntity(object))
return notes
}
export const updateConversationRead: ( export const updateConversationRead: (
conversationId: string, conversationId: string,
db: QuickSQLiteConnection, db: QuickSQLiteConnection,

View File

@ -1,6 +1,6 @@
import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..' import { getItems } from '..'
import { Event } from '../../../lib/nostr/Events' import { Event, evetDatabaseToEntity } from '../../../lib/nostr/Events'
export interface Note extends Event { export interface Note extends Event {
name: string name: string
@ -271,6 +271,21 @@ export const getLastReply: (
return reaction return reaction
} }
export const getRawUserNotes: (
db: QuickSQLiteConnection,
pubKey: string,
) => Promise<Event[]> = async (db, pubKey) => {
const notesQuery = `SELECT * FROM nostros_notes
WHERE pubkey = ?
ORDER BY created_at DESC
`
const resultSet = await db.execute(notesQuery, [pubKey])
const items: object[] = getItems(resultSet)
const notes: Event[] = items.map((object) => evetDatabaseToEntity(object))
return notes
}
export const getNotes: ( export const getNotes: (
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
options: { options: {

View File

@ -1,6 +1,6 @@
import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..' import { getItems } from '..'
import { Event } from '../../../lib/nostr/Events' import { Event, evetDatabaseToEntity } from '../../../lib/nostr/Events'
export interface Reaction extends Event { export interface Reaction extends Event {
positive: boolean positive: boolean
@ -12,6 +12,21 @@ const databaseToEntity: (object: object) => Reaction = (object) => {
return object as Reaction return object as Reaction
} }
export const getRawUserReactions: (
db: QuickSQLiteConnection,
pubKey: string,
) => Promise<Event[]> = async (db, pubKey) => {
const notesQuery = `SELECT * FROM nostros_reactions
WHERE pubkey = ?
ORDER BY created_at DESC
`
const resultSet = await db.execute(notesQuery, [pubKey])
const items: object[] = getItems(resultSet)
const notes: Event[] = items.map((object) => evetDatabaseToEntity(object))
return notes
}
export const getReactions: ( export const getReactions: (
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
filters: { filters: {

View File

@ -36,7 +36,7 @@ export const relayToColor: (string: string) => string = (string) => {
for (let i = 0; i < string.length; i++) { for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash) hash = string.charCodeAt(i) + ((hash << 5) - hash)
} }
return relayColors[(Math.abs(hash) % relayColors.length) - 1] return relayColors[Math.abs(hash) % (relayColors.length - 1)]
} }
export const pickRandomItems = <T extends unknown>(arr: T[], n: number): T[] => { export const pickRandomItems = <T extends unknown>(arr: T[], n: number): T[] => {
@ -65,7 +65,7 @@ export const validMediaUrl: (url: string | undefined) => boolean = (url) => {
export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => { export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => {
if (url) { if (url) {
const serviceRegexp = /^(https?:\/\/(?:twitter.com).*)$/ const serviceRegexp = /^(https?:\/\/(?:twitter.com|t.co).*)$/
return serviceRegexp.test(url) return serviceRegexp.test(url)
} else { } else {
return false return false

View File

@ -102,6 +102,8 @@
"copied": "Privaten Schlüssel kopiert. Verwahre ihn an einem sicheren Ort!", "copied": "Privaten Schlüssel kopiert. Verwahre ihn an einem sicheren Ort!",
"wrongWords": "Die Wörter stimmen nicht überein" "wrongWords": "Die Wörter stimmen nicht überein"
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"snackbarDescription": "Wichtig. Verwahre den Schlüssel an einem sicheren Ort. Bei Verlust kann dieses Konto nie wieder benutzt werden!", "snackbarDescription": "Wichtig. Verwahre den Schlüssel an einem sicheren Ort. Bei Verlust kann dieses Konto nie wieder benutzt werden!",
"snackbarAction": "Privaten Schlüssel kopieren", "snackbarAction": "Privaten Schlüssel kopieren",
"warningTitle": "Wichtig!", "warningTitle": "Wichtig!",
@ -172,6 +174,7 @@
"newMessages": "{{newNotesCount}} neue Nachrichten. Zum Aktualisieren tippen." "newMessages": "{{newNotesCount}} neue Nachrichten. Zum Aktualisieren tippen."
}, },
"relayCard": { "relayCard": {
"pushDone": "Completed",
"moreInfo": "Mehr Infos", "moreInfo": "Mehr Infos",
"lessInfo": "Weniger Infos", "lessInfo": "Weniger Infos",
"globalFeed": "Globaler Feed", "globalFeed": "Globaler Feed",
@ -273,7 +276,6 @@
"isFollower": "folgt dir", "isFollower": "folgt dir",
"unfollow": "Nicht mehr folgen", "unfollow": "Nicht mehr folgen",
"copyNPub": "Schlüssel kopieren", "copyNPub": "Schlüssel kopieren",
"relaysTitle": "Relays",
"relaysDescription": "Dies sind {{username}}'s Relays, aktiviere diejenigen, zu denen du verbunden werden möchtest." "relaysDescription": "Dies sind {{username}}'s Relays, aktiviere diejenigen, zu denen du verbunden werden möchtest."
}, },
"homePage": { "homePage": {
@ -301,7 +303,9 @@
"profileCard": { "profileCard": {
"notifications": { "notifications": {
"contactAdded": "Aboniert", "contactAdded": "Aboniert",
"contactRemoved": "Abo entfernt" "contactRemoved": "Abo entfernt",
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
}, },
"invoice": "Zap", "invoice": "Zap",
"message": "Nachricht", "message": "Nachricht",
@ -309,7 +313,8 @@
"block": "Blockieren", "block": "Blockieren",
"share": "Teilen", "share": "Teilen",
"unblock": "Erlauben", "unblock": "Erlauben",
"unfollow": "Abo entfernen" "unfollow": "Abo entfernen",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Unterhaltung beginnen", "openMessage": "Unterhaltung beginnen",

View File

@ -103,6 +103,8 @@
"copied": "Private key copied.\n\nStore this key in a safe place.", "copied": "Private key copied.\n\nStore this key in a safe place.",
"wrongWords": "The words don't match" "wrongWords": "The words don't match"
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"warningTitle": "Important", "warningTitle": "Important",
"warningDescription": "Store your key in a safe place. If lose it, you won't be able to access or recover your profile.", "warningDescription": "Store your key in a safe place. If lose it, you won't be able to access or recover your profile.",
"warningAction": "Copy private key", "warningAction": "Copy private key",
@ -186,7 +188,7 @@
"resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.", "resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.",
"centralized": "Centralized", "centralized": "Centralized",
"small": "Small", "small": "Small",
"contacts": "# Contacts", "contacts": "Assigned Contacts",
"resilienceMode": "Resilience (experimental)", "resilienceMode": "Resilience (experimental)",
"relayName": "Address", "relayName": "Address",
"globalFeed": "Global feed", "globalFeed": "Global feed",
@ -210,6 +212,15 @@
} }
}, },
"relayCard": { "relayCard": {
"pushDone": "Completed",
"pushHistoricTitle": "Push all my data",
"pushHistoricDescription": "Nostros stores on his own database all the data received from relays. With this option you can push to any relay all the data Nostros has associated to your profile.",
"pushHistoricAlertTitle": "Important",
"pushHistoricAlert": "Public relays have strong anti-spam policies and this action might ban your key. Make sure you are allowed to send big chunks of data to this relay.\n\nWe recomend to use this feature only for private relays like Umbrel's Nostr Relay.",
"pushConsent": "I'm pushing my events to a private relay",
"pushingEvent": "Event {{eventId}} pushed",
"pushRelay": "Push all my data",
"cancel": "Cancel",
"moreInfo": "More info", "moreInfo": "More info",
"lessInfo": "Less info", "lessInfo": "Less info",
"globalFeed": "Global Feed", "globalFeed": "Global Feed",
@ -284,7 +295,6 @@
"follow": "Follow", "follow": "Follow",
"unfollow": "Following", "unfollow": "Following",
"copyNPub": "Copy key", "copyNPub": "Copy key",
"relaysTitle": "Relays",
"isFollower": "follows you", "isFollower": "follows you",
"relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected." "relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected."
}, },
@ -312,7 +322,9 @@
"profileCard": { "profileCard": {
"notifications": { "notifications": {
"contactAdded": "Profile followed", "contactAdded": "Profile followed",
"contactRemoved": "Profile unfollowed" "contactRemoved": "Profile unfollowed",
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
}, },
"invoice": "Tip", "invoice": "Tip",
"message": "Message", "message": "Message",
@ -320,7 +332,8 @@
"unfollow": "Following", "unfollow": "Following",
"block": "Block", "block": "Block",
"unblock": "Unblock", "unblock": "Unblock",
"share": "Share" "share": "Share",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Start conversation", "openMessage": "Start conversation",

View File

@ -102,6 +102,8 @@
"copied": "Clave privada copiada. Guárdala en un lugar seguro.", "copied": "Clave privada copiada. Guárdala en un lugar seguro.",
"wrongWords": "Las palabras no coinciden." "wrongWords": "Las palabras no coinciden."
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"snackbarDescription": "Importante. Guarda tu clave en un lugar seguro, si la pierdes, no será posible acceder a tu perfil.", "snackbarDescription": "Importante. Guarda tu clave en un lugar seguro, si la pierdes, no será posible acceder a tu perfil.",
"snackbarAction": "Copiar clave privada", "snackbarAction": "Copiar clave privada",
"warningTitle": "Importante", "warningTitle": "Importante",
@ -169,6 +171,7 @@
"myFeed": "Mi feed" "myFeed": "Mi feed"
}, },
"relayCard": { "relayCard": {
"pushDone": "Completado",
"moreInfo": "Más información", "moreInfo": "Más información",
"lessInfo": "Menos información", "lessInfo": "Menos información",
"globalFeed": "Feed Global", "globalFeed": "Feed Global",
@ -267,7 +270,6 @@
"follow": "Seguir", "follow": "Seguir",
"unfollow": "Siguiendo", "unfollow": "Siguiendo",
"copyNPub": "Copiar clave", "copyNPub": "Copiar clave",
"relaysTitle": "Relays",
"relaysDescription": "Estos son los relays de {{username}}, activa aquellos a los que quieras estar conectado." "relaysDescription": "Estos son los relays de {{username}}, activa aquellos a los que quieras estar conectado."
}, },
"homePage": { "homePage": {
@ -297,7 +299,9 @@
"profileCard": { "profileCard": {
"notifications": { "notifications": {
"contactAdded": "Siguiendo", "contactAdded": "Siguiendo",
"contactRemoved": "Dejando de seguir" "contactRemoved": "Dejando de seguir",
"userUnblocked": "Perfil desbloqueado",
"userBlocked": "Perfil bloqueado"
}, },
"invoice": "Propina", "invoice": "Propina",
"message": "Mensaje", "message": "Mensaje",
@ -305,7 +309,8 @@
"block": "Bloquear", "block": "Bloquear",
"share": "Share", "share": "Share",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unfollow": "Siguiendo" "unfollow": "Siguiendo",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Comenzar conversación", "openMessage": "Comenzar conversación",

View File

@ -102,6 +102,8 @@
"copied": "Clé privée copiée. Gardez-la dans un endroit sûr.", "copied": "Clé privée copiée. Gardez-la dans un endroit sûr.",
"wrongWords": "The words doesn't match" "wrongWords": "The words doesn't match"
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"snackbarDescription": "Important. Conservez votre clé dans un endroit sûr, si vous le perdez, vous ne pourrez pas accéder à votre compte.", "snackbarDescription": "Important. Conservez votre clé dans un endroit sûr, si vous le perdez, vous ne pourrez pas accéder à votre compte.",
"snackbarAction": "Copier la clé privée", "snackbarAction": "Copier la clé privée",
"warningTitle": "Important", "warningTitle": "Important",
@ -170,6 +172,7 @@
"myFeed": "Mon flux" "myFeed": "Mon flux"
}, },
"relayCard": { "relayCard": {
"pushDone": "Completed",
"moreInfo": "Plus d'info", "moreInfo": "Plus d'info",
"lessInfo": "Moins d'info", "lessInfo": "Moins d'info",
"globalFeed": "Flux Global", "globalFeed": "Flux Global",
@ -199,7 +202,7 @@
"resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.", "resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.",
"centralized": "Centralized", "centralized": "Centralized",
"small": "Small", "small": "Small",
"contacts": "# Contacts", "contacts": "Assigned Contacts",
"resilienceMode": "Resilience (experimental)", "resilienceMode": "Resilience (experimental)",
"relayName": "Adresse", "relayName": "Adresse",
"globalFeed": "Flux global", "globalFeed": "Flux global",
@ -268,7 +271,6 @@
"isFollower": "follows you", "isFollower": "follows you",
"unfollow": "Abonné", "unfollow": "Abonné",
"copyNPub": "Copier la clé", "copyNPub": "Copier la clé",
"relaysTitle": "Relays",
"relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected." "relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected."
}, },
"profileShare": { "profileShare": {
@ -285,14 +287,17 @@
"profileCard": { "profileCard": {
"notifications": { "notifications": {
"contactAdded": "Abonné", "contactAdded": "Abonné",
"contactRemoved": "Vous ne suivez plus ce profil" "contactRemoved": "Vous ne suivez plus ce profil",
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
}, },
"invoice": "Tip", "invoice": "Tip",
"message": "Message", "message": "Message",
"follow": "Abonné", "follow": "Abonné",
"block": "Bloquer", "block": "Bloquer",
"unblock": "Débloquer", "unblock": "Débloquer",
"unfollow": "Abonné" "unfollow": "Abonné",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Commencer la conversation", "openMessage": "Commencer la conversation",

View File

@ -103,6 +103,8 @@
"copied": "Приватный ключ скопирован.\n\nСохраните этот ключ в надежном месте.", "copied": "Приватный ключ скопирован.\n\nСохраните этот ключ в надежном месте.",
"wrongWords": "The words doesn't match" "wrongWords": "The words doesn't match"
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"warningTitle": "Важно", "warningTitle": "Важно",
"warningDescription": "Сохраните этот ключ в надежном месте. В случае утери ключа, Вы не сможете войти или восстановить доступ к профилю.", "warningDescription": "Сохраните этот ключ в надежном месте. В случае утери ключа, Вы не сможете войти или восстановить доступ к профилю.",
"warningAction": "Скопировать приватный ключ", "warningAction": "Скопировать приватный ключ",
@ -169,6 +171,7 @@
"myFeed": "My feed" "myFeed": "My feed"
}, },
"relayCard": { "relayCard": {
"pushDone": "Completed",
"moreInfo": "Узнать больше", "moreInfo": "Узнать больше",
"lessInfo": "Скрыть", "lessInfo": "Скрыть",
"globalFeed": "Общая лента", "globalFeed": "Общая лента",
@ -198,7 +201,7 @@
"resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.", "resilienceCategoriesDescription": "An initial approach that Nostros is testing is to randomly calculate (based on received contact events) which relays are most balanced. This tries to avoid both relays with too many events (centralization) and relays with almost no events (battery discharge).\n\nThe goal is to create a list of 5 relays that cover all contacts. However, if this is not possible, Nostros will search among other relays and warn the user about it.",
"centralized": "Centralized", "centralized": "Centralized",
"small": "Small", "small": "Small",
"contacts": "# Contacts", "contacts": "Assigned Contacts",
"resilienceMode": "Resilience (experimental)", "resilienceMode": "Resilience (experimental)",
"relayName": "Address", "relayName": "Address",
"globalFeed": "Общая лента", "globalFeed": "Общая лента",
@ -267,7 +270,6 @@
"unfollow": "Following", "unfollow": "Following",
"isFollower": "follows you", "isFollower": "follows you",
"copyNPub": "Скопировать ключ", "copyNPub": "Скопировать ключ",
"relaysTitle": "Relays",
"relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected." "relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected."
}, },
"homePage": { "homePage": {
@ -297,7 +299,9 @@
"profileCard": { "profileCard": {
"notifications": { "notifications": {
"contactAdded": "Profile followed", "contactAdded": "Profile followed",
"contactRemoved": "Profile unfollowed" "contactRemoved": "Profile unfollowed",
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
}, },
"invoice": "Tip", "invoice": "Tip",
"message": "Message", "message": "Message",
@ -305,7 +309,8 @@
"block": "Block", "block": "Block",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unfollow": "Following", "unfollow": "Following",
"share": "Share" "share": "Share",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Start conversation", "openMessage": "Start conversation",

View File

@ -101,6 +101,8 @@
"copied": "已复制私钥 \n\n 请妥善保管您的私钥", "copied": "已复制私钥 \n\n 请妥善保管您的私钥",
"wrongWords": "助记词不匹配" "wrongWords": "助记词不匹配"
}, },
"skip": "Skip",
"confirmTitle": "Type the words in the right order.",
"warningTitle": "注意", "warningTitle": "注意",
"warningDescription": "请妥善保管您的私钥。如果遗失,您将无法登入或恢复您的账号。", "warningDescription": "请妥善保管您的私钥。如果遗失,您将无法登入或恢复您的账号。",
"warningAction": "复制私钥", "warningAction": "复制私钥",
@ -208,6 +210,7 @@
} }
}, },
"relayCard": { "relayCard": {
"pushDone": "Completed",
"moreInfo": "更多", "moreInfo": "更多",
"lessInfo": "收起", "lessInfo": "收起",
"globalFeed": "全局信息流", "globalFeed": "全局信息流",
@ -280,7 +283,6 @@
"isFollower": "follows you", "isFollower": "follows you",
"unfollow": "关注中", "unfollow": "关注中",
"copyNPub": "复制公钥", "copyNPub": "复制公钥",
"relaysTitle": "Relays",
"relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected." "relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected."
}, },
"homePage": { "homePage": {
@ -300,7 +302,9 @@
"notifications": { "notifications": {
"contactAdded": "已关注", "contactAdded": "已关注",
"contactRemoved": "已取消关注", "contactRemoved": "已取消关注",
"npubCopied": "已复制公钥" "npubCopied": "已复制公钥",
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
}, },
"invoice": "赞赏", "invoice": "赞赏",
"message": "私信", "message": "私信",
@ -310,7 +314,8 @@
"unblock": "取消屏蔽", "unblock": "取消屏蔽",
"share": "分享", "share": "分享",
"copyNip05": "复制 NIP-05", "copyNip05": "复制 NIP-05",
"copyNPub": "复制公钥" "copyNPub": "复制公钥",
"relaysTitle": "Relays"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "发送私信", "openMessage": "发送私信",

View File

@ -69,6 +69,8 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
setRefreshing(true) setRefreshing(true)
updateLastLoad() updateLastLoad()
setNewNotesCount(0) setNewNotesCount(0)
relayPool?.unsubscribe(['homepage-global-main', 'homepage-global-meta'])
subscribeNotes()
}, []) }, [])
const subscribeNotes: (past?: boolean) => void = async (past) => { const subscribeNotes: (past?: boolean) => void = async (past) => {
@ -189,7 +191,9 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
refreshing={refreshing} refreshing={refreshing}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef} ref={flashListRef}
/> />
</View> </View>
@ -198,6 +202,9 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: { list: {
height: '100%', height: '100%',
}, },

View File

@ -63,12 +63,16 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
setRefreshing(true) setRefreshing(true)
relayPool?.unsubscribe([
'homepage-contacts-main',
'homepage-contacts-meta',
'homepage-contacts-replies',
])
subscribeNotes() subscribeNotes()
}, []) }, [])
const subscribeNotes: (past?: boolean) => void = async (past) => { const subscribeNotes: (past?: boolean) => void = async (past) => {
if (!database || !publicKey) return if (!database || !publicKey) return
const users: User[] = await getUsers(database, { contacts: true, order: 'created_at DESC' }) const users: User[] = await getUsers(database, { contacts: true, order: 'created_at DESC' })
const authors: string[] = [...users.map((user) => user.id), publicKey] const authors: string[] = [...users.map((user) => user.id), publicKey]
@ -89,7 +93,6 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
const loadNotes: () => void = async () => { const loadNotes: () => void = async () => {
if (database && publicKey) { if (database && publicKey) {
relayPool?.unsubscribe(['homepage-reactions', 'homepage-replies', 'homepage-main'])
getMainNotes(database, publicKey, pageSize, true).then(async (notes) => { getMainNotes(database, publicKey, pageSize, true).then(async (notes) => {
setNotes(notes) setNotes(notes)
if (notes.length > 0) { if (notes.length > 0) {
@ -175,7 +178,9 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
refreshing={refreshing} refreshing={refreshing}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef} ref={flashListRef}
/> />
</View> </View>
@ -183,6 +188,9 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: { list: {
height: '100%', height: '100%',
}, },

View File

@ -186,7 +186,9 @@ export const NotificationsFeed: React.FC = () => {
refreshing={refreshing} refreshing={refreshing}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef} ref={flashListRef}
/> />
</View> </View>
@ -194,6 +196,9 @@ export const NotificationsFeed: React.FC = () => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
container: { container: {
flex: 1, flex: 1,
paddingLeft: 16, paddingLeft: 16,

View File

@ -71,6 +71,11 @@ export const ProfileCreatePage: React.FC<ProfileCreatePageProps> = ({ navigation
} }
} }
const onPressSkip: () => void = () => {
setPrivateKey(key)
setUserState('ready')
}
const onChangeTextConfirm: (value: string, position: number) => void = (value, position) => { const onChangeTextConfirm: (value: string, position: number) => void = (value, position) => {
setConfirmWords((prev) => { setConfirmWords((prev) => {
prev[position] = value prev[position] = value
@ -208,6 +213,13 @@ export const ProfileCreatePage: React.FC<ProfileCreatePageProps> = ({ navigation
{` ${step + 1}/3`} {` ${step + 1}/3`}
</Button> </Button>
</View> </View>
{step > 1 && (
<View style={styles.bottomButton}>
<Button mode='outlined' compact onPress={onPressSkip}>
{t('profileCreatePage.skip')}
</Button>
</View>
)}
</View> </View>
)} )}
{showNotification && ( {showNotification && (
@ -289,6 +301,9 @@ const styles = StyleSheet.create({
bold: { bold: {
fontWeight: 'bold', fontWeight: 'bold',
}, },
bottomButton: {
marginTop: 16,
},
}) })
export default ProfileCreatePage export default ProfileCreatePage

View File

@ -179,7 +179,13 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
onScroll={onScroll} onScroll={onScroll}
refreshing={refreshing} refreshing={refreshing}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes && notes.length > 0 ? (
<ActivityIndicator style={styles.loading} animating={true} />
) : (
<></>
)
}
/> />
</View> </View>
</ScrollView> </ScrollView>
@ -199,6 +205,9 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
container: { container: {
padding: 16, padding: 16,
}, },

View File

@ -61,6 +61,11 @@ export const ReactionsFeed: React.FC<ReactionsFeedProps> = ({ navigation }) => {
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
setRefreshing(true) setRefreshing(true)
relayPool?.unsubscribe([
'homepage-contacts-main',
'homepage-contacts-meta',
'homepage-contacts-replies',
])
subscribeNotes() subscribeNotes()
}, []) }, [])
@ -84,7 +89,6 @@ export const ReactionsFeed: React.FC<ReactionsFeedProps> = ({ navigation }) => {
const loadNotes: () => void = async () => { const loadNotes: () => void = async () => {
if (database && publicKey) { if (database && publicKey) {
relayPool?.unsubscribe(['homepage-reactions', 'homepage-replies', 'homepage-main'])
getReactedNotes(database, publicKey, pageSize).then(async (notes) => { getReactedNotes(database, publicKey, pageSize).then(async (notes) => {
setNotes(notes) setNotes(notes)
if (notes.length > 0) { if (notes.length > 0) {
@ -175,7 +179,9 @@ export const ReactionsFeed: React.FC<ReactionsFeedProps> = ({ navigation }) => {
refreshing={refreshing} refreshing={refreshing}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef} ref={flashListRef}
/> />
</View> </View>
@ -183,6 +189,9 @@ export const ReactionsFeed: React.FC<ReactionsFeedProps> = ({ navigation }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: { list: {
height: '100%', height: '100%',
}, },

View File

@ -1,6 +1,5 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react'
import { FlatList, ListRenderItem, ScrollView, StyleSheet, View } from 'react-native' import { FlatList, ListRenderItem, ScrollView, StyleSheet, View } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays' import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays'
@ -40,15 +39,14 @@ export const defaultRelays = [
export const RelaysPage: React.FC = () => { export const RelaysPage: React.FC = () => {
const defaultRelayInput = React.useMemo(() => 'wss://', []) const defaultRelayInput = React.useMemo(() => 'wss://', [])
const { updateRelayItem, addRelayItem, removeRelayItem, relayPool } = useContext(RelayPoolContext) const { updateRelayItem, addRelayItem, relayPool, setDisplayrelayDrawer } =
useContext(RelayPoolContext)
const { database } = useContext(AppContext) const { database } = useContext(AppContext)
const { t } = useTranslation('common') const { t } = useTranslation('common')
const theme = useTheme() const theme = useTheme()
const bottomSheetAddRef = React.useRef<RBSheet>(null) const bottomSheetAddRef = React.useRef<RBSheet>(null)
const bottomSheetEditRef = React.useRef<RBSheet>(null)
const bottomSheetResilenseRef = React.useRef<RBSheet>(null) const bottomSheetResilenseRef = React.useRef<RBSheet>(null)
const [relays, setRelays] = React.useState<Relay[]>([]) const [relays, setRelays] = React.useState<Relay[]>([])
const [selectedRelay, setSelectedRelay] = useState<Relay>()
const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput) const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput)
const [showNotification, setShowNotification] = useState<string>() const [showNotification, setShowNotification] = useState<string>()
@ -66,6 +64,7 @@ export const RelaysPage: React.FC = () => {
} }
const addRelay: (url: string) => void = (url) => { const addRelay: (url: string) => void = (url) => {
if (!relayList.find((relay) => relay.url === url)) {
addRelayItem({ addRelayItem({
url, url,
active: 1, active: 1,
@ -75,16 +74,17 @@ export const RelaysPage: React.FC = () => {
setShowNotification('add') setShowNotification('add')
}) })
} }
const removeRelay: (url: string) => void = (url) => {
removeRelayItem({
url,
}).then(() => {
updateRelays()
setShowNotification('remove')
})
} }
// const removeRelay: (url: string) => void = (url) => {
// removeRelayItem({
// url,
// }).then(() => {
// updateRelays()
// setShowNotification('remove')
// })
// }
const activeRelay: (relay: Relay) => void = (relay) => { const activeRelay: (relay: Relay) => void = (relay) => {
relay.active = 1 relay.active = 1
updateRelayItem(relay).then(() => { updateRelayItem(relay).then(() => {
@ -184,8 +184,7 @@ export const RelaysPage: React.FC = () => {
/> />
)} )}
onPress={() => { onPress={() => {
setSelectedRelay(item) setDisplayrelayDrawer(item.url)
bottomSheetEditRef.current?.open()
}} }}
/> />
) )
@ -377,35 +376,6 @@ export const RelaysPage: React.FC = () => {
</Button> </Button>
</View> </View>
</RBSheet> </RBSheet>
<RBSheet ref={bottomSheetEditRef} closeOnDragDown={true} customStyles={rbSheetCustomStyles}>
<View>
<View style={styles.relayActions}>
<View style={styles.actionButton}>
<IconButton
icon='trash-can-outline'
size={28}
onPress={() => {
if (selectedRelay) removeRelay(selectedRelay.url)
bottomSheetEditRef.current?.close()
}}
/>
<Text>{t('relaysPage.removeRelay')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='content-copy'
size={28}
onPress={() => {
if (selectedRelay) Clipboard.setString(selectedRelay.url)
}}
/>
<Text>{t('relaysPage.copyRelay')}</Text>
</View>
</View>
<Divider style={styles.divider} />
<Text variant='titleLarge'>{selectedRelay?.url}</Text>
</View>
</RBSheet>
</View> </View>
) )
} }

View File

@ -61,6 +61,11 @@ export const RepostsFeed: React.FC<RepostsFeedProps> = ({ navigation }) => {
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
setRefreshing(true) setRefreshing(true)
relayPool?.unsubscribe([
'homepage-contacts-main',
'homepage-contacts-meta',
'homepage-contacts-replies',
])
subscribeNotes() subscribeNotes()
}, []) }, [])
@ -84,7 +89,6 @@ export const RepostsFeed: React.FC<RepostsFeedProps> = ({ navigation }) => {
const loadNotes: () => void = async () => { const loadNotes: () => void = async () => {
if (database && publicKey) { if (database && publicKey) {
relayPool?.unsubscribe(['homepage-reactions', 'homepage-replies', 'homepage-main'])
getRepostedNotes(database, publicKey, pageSize).then(async (notes) => { getRepostedNotes(database, publicKey, pageSize).then(async (notes) => {
setNotes(notes) setNotes(notes)
if (notes.length > 0) { if (notes.length > 0) {
@ -167,7 +171,9 @@ export const RepostsFeed: React.FC<RepostsFeedProps> = ({ navigation }) => {
refreshing={refreshing} refreshing={refreshing}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
horizontal={false} horizontal={false}
ListFooterComponent={<ActivityIndicator animating={true} />} ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef} ref={flashListRef}
/> />
</View> </View>
@ -175,6 +181,9 @@ export const RepostsFeed: React.FC<RepostsFeedProps> = ({ navigation }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: { list: {
height: '100%', height: '100%',
}, },

View File

@ -260,7 +260,9 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
<Button <Button
mode='contained' mode='contained'
onPress={onPressSend} onPress={onPressSend}
disabled={route.params?.type !== 'repost' && (!content || content === '')} disabled={
isSending || (route.params?.type !== 'repost' && (!content || content === ''))
}
loading={isSending || uploadingFile} loading={isSending || uploadingFile}
> >
{t('sendPage.send')} {t('sendPage.send')}

View File

@ -2,7 +2,8 @@ import { NativeModules } from 'react-native'
const { RelayPoolModule } = NativeModules const { RelayPoolModule } = NativeModules
interface RelayPoolInterface { interface RelayPoolInterface {
send: (message: string, globalFeed: boolean) => void sendAll: (message: string, globalFeed: boolean) => void
sendRelay: (message: string, relayUrl: string) => void
connect: (pubKey: string, callback: (eventId: string) => void) => void connect: (pubKey: string, callback: (eventId: string) => void) => void
add: (url: string, callback: () => void) => void add: (url: string, callback: () => void) => void
remove: (url: string, callback: () => void) => void remove: (url: string, callback: () => void) => void

View File

@ -11,6 +11,19 @@ export interface Event {
tags: string[][] tags: string[][]
} }
export const evetDatabaseToEntity: (object: any) => Event = (object = {}) => {
const event: Event = {
created_at: object.created_at,
content: object.content,
id: object.id,
kind: object.kind,
pubkey: object.pubkey,
sig: object.sig,
tags: object.tags ? JSON.parse(object.tags) : [],
}
return event
}
export const serializeEvent: (event: Event) => string = (event) => { 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])
} }

View File

@ -29,6 +29,7 @@ export interface ResilientAssignation {
resilientRelays: Record<string, string[]> resilientRelays: Record<string, string[]>
smallRelays: Record<string, string[]> smallRelays: Record<string, string[]>
centralizedRelays: Record<string, string[]> centralizedRelays: Record<string, string[]>
fallback: Record<string, string[]>
} }
export const fallbackRelays = [ export const fallbackRelays = [
@ -62,12 +63,20 @@ class RelayPool {
private subscriptions: Record<string, string[]> private subscriptions: Record<string, string[]>
public resilientAssignation: ResilientAssignation public resilientAssignation: ResilientAssignation
private readonly send: (message: object, globalFeed?: boolean) => void = async ( private readonly sendAll: (message: object, globalFeed?: boolean) => void = async (
message, message,
globalFeed, globalFeed,
) => { ) => {
const tosend = JSON.stringify(message) const tosend = JSON.stringify(message)
RelayPoolModule.send(tosend, globalFeed ?? false) RelayPoolModule.sendAll(tosend, globalFeed ?? false)
}
private readonly sendRelay: (message: object, relayUrl: string) => void = async (
message,
relayUrl,
) => {
const tosend = JSON.stringify(message)
RelayPoolModule.sendRelay(tosend, relayUrl)
} }
public readonly connect: (publicKey: string, onEventId: (eventId: string) => void) => void = public readonly connect: (publicKey: string, onEventId: (eventId: string) => void) => void =
@ -181,13 +190,23 @@ class RelayPool {
RelayPoolModule.update(relayUrl, active, globalfeed, callback) RelayPoolModule.update(relayUrl, active, globalfeed, callback)
} }
public readonly sendEvent: (event: Event) => Promise<Event | null> = async (event) => { public readonly sendEvent: (event: Event, relayUrl?: string) => Promise<Event | null> = async (
event,
relayUrl,
) => {
if (this.privateKey) { if (this.privateKey) {
const signedEvent: Event = await signEvent(event, this.privateKey) let signedEvent: Event = event
if (!event.sig) {
signedEvent = await signEvent(event, this.privateKey)
}
if (validateEvent(signedEvent)) { if (validateEvent(signedEvent)) {
this.send(['EVENT', event]) if (relayUrl) {
this.sendRelay(['EVENT', event], relayUrl)
} else {
this.sendAll(['EVENT', event])
}
return signedEvent return signedEvent
} else { } else {
console.log('Not valid event', event) console.log('Not valid event', event)
@ -206,7 +225,7 @@ class RelayPool {
if (this.subscriptions[subId]?.includes(id)) { if (this.subscriptions[subId]?.includes(id)) {
console.log('Subscription already done!', subId) console.log('Subscription already done!', subId)
} else { } else {
this.send([...['REQ', subId], ...(filters ?? [])], subId.includes('-global-')) this.sendAll([...['REQ', subId], ...(filters ?? [])], subId.includes('-global-'))
const newSubscriptions = [...(this.subscriptions[subId] ?? []), id] const newSubscriptions = [...(this.subscriptions[subId] ?? []), id]
this.subscriptions[subId] = newSubscriptions this.subscriptions[subId] = newSubscriptions
} }
@ -214,7 +233,7 @@ class RelayPool {
public readonly unsubscribe: (subIds: string[]) => void = async (subIds) => { public readonly unsubscribe: (subIds: string[]) => void = async (subIds) => {
subIds.forEach((subId: string) => { subIds.forEach((subId: string) => {
this.send(['CLOSE', subId]) this.sendAll(['CLOSE', subId])
delete this.subscriptions[subId] delete this.subscriptions[subId]
}) })
} }