Merge branch 'main' into minor-fixes

This commit is contained in:
Pablo Carballeda 2023-01-28 00:08:41 +01:00 committed by GitHub
commit e3ba1cbf00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1154 additions and 305 deletions

View File

@ -31,6 +31,21 @@ jobs:
cd android
./gradlew assembleRelease
- name: 'Check for non-FOSS libraries'
run: |
wget https://github.com/iBotPeaches/Apktool/releases/download/v2.5.0/apktool_2.5.0.jar
wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool
chmod u+x apktool
ln -s apktool_2.5.0.jar apktool.jar
./apktool d android/app/build/outputs/apk/release/app-universal-release.apk
# clone the repo
git clone https://gitlab.com/IzzyOnDroid/repo.git
# create a directory for Apktool and move the apktool* files there
mkdir -p repo/lib/radar/tool
mv app-universal-release* repo/lib/radar/tool
# create an alias for ease of use
repo/bin/scanapk.php android/app/build/outputs/apk/release/app-universal-release.apk
- name: 'Get Commit Hash'
id: commit
uses: pr-mpt/actions-commit-hash@v1

View File

@ -139,8 +139,8 @@ android {
applicationId "com.nostros"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 30
versionName "0.2.1.4-alpha"
versionCode 31
versionName "0.2.1.5-alpha"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {

View File

@ -66,7 +66,7 @@ public class Event {
}
protected boolean isValid() {
return !id.isEmpty() && !sig.isEmpty();
return !id.isEmpty() && !sig.isEmpty() && created_at <= System.currentTimeMillis() / 1000L;
}
protected String getMainEventId() {

View File

@ -68,6 +68,7 @@ public class DatabaseModule {
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN created_at INT DEFAULT 0;");
} catch (SQLException e) { }
try {
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_reactions(\n" +
" id TEXT PRIMARY KEY NOT NULL, \n" +
" content TEXT NOT NULL,\n" +
@ -80,6 +81,7 @@ public class DatabaseModule {
" reacted_event_id TEXT,\n" +
" reacted_user_id TEXT\n" +
" );");
} catch (SQLException e) { }
try {
database.execSQL("CREATE INDEX nostros_notes_pubkey_index ON nostros_notes(pubkey); ");
database.execSQL("CREATE INDEX nostros_notes_main_event_id_index ON nostros_notes(main_event_id); ");
@ -106,6 +108,31 @@ public class DatabaseModule {
try {
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN active BOOLEAN DEFAULT TRUE;");
} catch (SQLException e) { }
try {
database.execSQL("CREATE INDEX nostros_notes_main_index ON nostros_notes(pubkey, main_event_id, created_at);");
database.execSQL("CREATE INDEX nostros_notes_kind_index ON nostros_notes(repost_id, pubkey, created_at); ");
database.execSQL("CREATE INDEX nostros_notes_notifications_index ON nostros_notes(pubkey, user_mentioned, reply_event_id, created_at); ");
database.execSQL("CREATE INDEX nostros_notes_repost_id_index ON nostros_notes(pubkey, repost_id); ");
database.execSQL("CREATE INDEX nostros_notes_reply_event_id_count_index ON nostros_notes(created_at, reply_event_id); ");
database.execSQL("CREATE INDEX nostros_direct_messages_created_at_index ON nostros_direct_messages(created_at); ");
database.execSQL("CREATE INDEX nostros_direct_messages_created_at_conversation_id_index ON nostros_direct_messages(created_at, conversation_id); ");
database.execSQL("CREATE INDEX nostros_reactions_created_at_reacted_event_id_index ON nostros_reactions(created_at, reacted_event_id); ");
database.execSQL("CREATE INDEX nostros_users_contact_index ON nostros_users(contact, follower); ");
database.execSQL("CREATE INDEX nostros_users_contact_index ON nostros_users(id, name); ");
} catch (SQLException e) { }
try {
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_config(\n" +
" satoshi TEXT NOT NULL,\n" +
" show_public_images BOOLEAN DEFAULT FALSE,\n" +
" show_sensitive BOOLEAN DEFAULT FALSE\n" +
" );");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN blocked BOOLEAN DEFAULT FALSE;");
} catch (SQLException e) { }
}
public void saveEvent(JSONObject data, String userPubKey) throws JSONException {

View File

@ -8,6 +8,7 @@ import Clipboard from '@react-native-clipboard/clipboard'
import { useTranslation } from 'react-i18next'
import RBSheet from 'react-native-raw-bottom-sheet'
import { Button, Card, IconButton, Text, TextInput, useTheme } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext'
interface TextContentProps {
open: boolean
@ -19,6 +20,7 @@ interface TextContentProps {
export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, user }) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { getSatoshiSymbol } = React.useContext(AppContext)
const bottomSheetLnPaymentRef = React.useRef<RBSheet>(null)
const bottomSheetInvoiceRef = React.useRef<RBSheet>(null)
const [monto, setMonto] = useState<string>('')
@ -91,22 +93,13 @@ export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, us
<View style={styles.drawerBottom}>
<View style={styles.montoSelection}>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('1000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 1k</Text>
</>
<Text>1k {getSatoshiSymbol(15)}</Text>
</Button>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('5000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 5k</Text>
</>
<Text>5k {getSatoshiSymbol(15)}</Text>
</Button>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('10000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 10k</Text>
</>
<Text>10k {getSatoshiSymbol(15)}</Text>
</Button>
</View>
<TextInput
@ -146,10 +139,8 @@ export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, us
<QRCode value={invoice} size={350} />
</View>
<View style={styles.qrText}>
<Text variant='titleMedium' style={styles.satoshi}>
s
</Text>
<Text variant='titleMedium'>{monto}</Text>
<Text>{monto} </Text>
{getSatoshiSymbol(23)}
</View>
{comment && (
<View style={styles.qrText}>

View File

@ -34,10 +34,12 @@ export const MenuItems: React.FC = () => {
setDrawerItemIndex(index)
if (key === 'relays') {
navigate('Relays')
} else if (key === 'config') {
navigate('Feed', { page: 'Config' })
} else if (key === 'configProfile') {
navigate('Feed', { page: 'ProfileConfig' })
} else if (key === 'about') {
navigate('About')
} else if (key === 'config') {
navigate('Config')
}
}
@ -89,7 +91,7 @@ export const MenuItems: React.FC = () => {
</Card>
)}
{publicKey && (
<Drawer.Section>
<Drawer.Section showDivider={false}>
<Drawer.Item
label={t('menuItems.relays')}
icon={() => (
@ -117,6 +119,16 @@ export const MenuItems: React.FC = () => {
/>
</Drawer.Section>
)}
<Drawer.Section>
<Drawer.Item
label={t('menuItems.configuration')}
icon='cog'
key='configuration'
active={drawerItemIndex === 1}
onPress={() => onPressItem('config', 1)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
</Drawer.Section>
<Drawer.Section showDivider={false}>
<Drawer.Item
label={t('menuItems.about')}

View File

@ -38,8 +38,10 @@ import ProfileData from '../ProfileData'
interface NoteCardProps {
note?: Note
onPressUser?: (user: User) => void
showAvatarImage?: boolean
showAnswerData?: boolean
showAction?: boolean
showActionCount?: boolean
showPreview?: boolean
showRepostPreview?: boolean
numberOfLines?: number
@ -48,8 +50,10 @@ interface NoteCardProps {
export const NoteCard: React.FC<NoteCardProps> = ({
note,
showAvatarImage = true,
showAnswerData = true,
showAction = true,
showActionCount = true,
showPreview = true,
showRepostPreview = true,
onPressUser = () => {},
@ -59,7 +63,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
const theme = useTheme()
const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const { database } = useContext(AppContext)
const { database, showSensitive } = useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>(false)
const [positiveReactions, setPositiveReactions] = useState<number>(0)
const [negaiveReactions, setNegativeReactions] = useState<number>(0)
@ -72,7 +76,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
const [repost, setRepost] = useState<Note>()
useEffect(() => {
if (database && publicKey && showAction && note?.id) {
if (database && publicKey && showAction && showActionCount && note?.id) {
getReactions(database, { eventId: note.id }).then((result) => {
const total = result.length
let positive = 0
@ -152,7 +156,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
</TouchableRipple>
)}
<Card.Content style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
{hide ? (
{hide && !showSensitive ? (
<Button mode='outlined' onPress={() => setHide(false)}>
{t('noteCard.contentWarning')}
</Button>
@ -208,7 +212,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
</View>
</Card.Content>
{!relayAdded && note && REGEX_SOCKET_LINK.test(note.content) && (
<Card.Content style={[styles.actions, { borderColor: theme.colors.onSecondary }]}>
<Card.Content style={[styles.bottomActions, { borderColor: theme.colors.onSecondary }]}>
<Button onPress={addRelayItem}>{t('noteCard.addRelay')}</Button>
</Card.Content>
)}
@ -217,8 +221,33 @@ export const NoteCard: React.FC<NoteCardProps> = ({
)
}
const blockedContent: () => JSX.Element = () => {
return (
<Card.Content style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
<Card>
<Card.Content style={styles.title}>
<View>
<Avatar.Icon
size={54}
icon='account-cancel'
style={{
backgroundColor: theme.colors.tertiaryContainer,
}}
/>
</View>
<View style={styles.userBlocked}>
<Text>{t('noteCard.userBlocked')}</Text>
</View>
</Card.Content>
</Card>
</Card.Content>
)
}
const getNoteContent: () => JSX.Element | undefined = () => {
if (note?.kind === Kind.Text) {
if (note?.blocked) {
return blockedContent()
} else if (note?.kind === Kind.Text) {
return textNote()
} else if (note?.kind === Kind.RecommendRelay) return recommendServer()
}
@ -233,24 +262,24 @@ export const NoteCard: React.FC<NoteCardProps> = ({
validNip05={note?.valid_nip05}
nip05={note?.nip05}
lud06={note?.lnurl}
picture={note?.picture}
picture={showAvatarImage ? note?.picture : undefined}
timestamp={note?.created_at}
avatarSize={56}
/>
</TouchableRipple>
<View>
{showAction && (
{showAction && (
<View style={styles.topAction}>
<IconButton
icon='dots-vertical'
size={25}
onPress={() => onPressUser({ id: note.pubkey, name: note.name })}
/>
)}
</View>
</View>
)}
</Card.Content>
{getNoteContent()}
{showAction && (
<Card.Content style={[styles.actions, { borderColor: theme.colors.onSecondary }]}>
{showAction && !note?.blocked && (
<Card.Content style={[styles.bottomActions, { borderColor: theme.colors.onSecondary }]}>
<Button
icon={() => (
<MaterialCommunityIcons
@ -261,7 +290,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
)}
onPress={() => note.kind !== Kind.RecommendRelay && push('Note', { noteId: note.id })}
>
{repliesCount}
{showActionCount && repliesCount}
</Button>
<Button
icon={() => (
@ -275,7 +304,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
note.kind !== Kind.RecommendRelay && push('Repost', { note, type: 'repost' })
}
>
{repostCount}
{showActionCount && repostCount}
</Button>
<Button
onPress={() => {
@ -293,7 +322,8 @@ export const NoteCard: React.FC<NoteCardProps> = ({
/>
)}
>
{negaiveReactions === undefined || negaiveReactions === 0 ? '-' : negaiveReactions}
{showActionCount &&
(negaiveReactions === undefined || negaiveReactions === 0 ? '-' : negaiveReactions)}
</Button>
<Button
onPress={() => {
@ -311,7 +341,10 @@ export const NoteCard: React.FC<NoteCardProps> = ({
/>
)}
>
{positiveReactions === undefined || positiveReactions === 0 ? '-' : positiveReactions}
{showActionCount &&
(positiveReactions === undefined || positiveReactions === 0
? '-'
: positiveReactions)}
</Button>
</Card.Content>
)}
@ -335,6 +368,10 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignContent: 'center',
},
userBlocked: {
justifyContent: 'center',
textAlign: 'center',
},
titleUser: {
flexDirection: 'row',
alignContent: 'center',
@ -348,12 +385,16 @@ const styles = StyleSheet.create({
padding: 16,
justifyContent: 'space-between',
},
actions: {
bottomActions: {
paddingTop: 16,
flexDirection: 'row',
justifyContent: 'space-around',
borderTopWidth: 1,
},
topAction: {
flexDirection: 'row',
justifyContent: 'space-around',
},
relayActions: {
paddingTop: 16,
flexDirection: 'row',

View File

@ -6,7 +6,13 @@ import { Card, IconButton, Snackbar, Text, useTheme } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext'
import { getUser, updateUserContact, User } from '../../Functions/DatabaseFunctions/Users'
import {
addUser,
getUser,
updateUserBlock,
updateUserContact,
User,
} from '../../Functions/DatabaseFunctions/Users'
import { populatePets, usernamePubKey } from '../../Functions/RelayFunctions/Users'
import LnPayment from '../LnPayment'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
@ -18,14 +24,20 @@ import ProfileData from '../ProfileData'
interface ProfileCardProps {
userPubKey: string
bottomSheetRef: React.RefObject<RBSheet>
showImages: boolean
}
export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomSheetRef }) => {
export const ProfileCard: React.FC<ProfileCardProps> = ({
userPubKey,
bottomSheetRef,
showImages = true,
}) => {
const theme = useTheme()
const { database } = React.useContext(AppContext)
const { publicKey } = React.useContext(UserContext)
const { relayPool } = React.useContext(RelayPoolContext)
const [user, setUser] = React.useState<User>()
const [blocked, setBlocked] = React.useState<boolean>()
const [openLn, setOpenLn] = React.useState<boolean>(false)
const [isContact, setIsContact] = React.useState<boolean>()
const [showNotification, setShowNotification] = React.useState<undefined | string>()
@ -36,6 +48,15 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
loadUser()
}, [])
const onChangeBlockUser: () => void = () => {
if (database && blocked !== undefined) {
updateUserBlock(userPubKey, database, !blocked).then(() => {
setBlocked(!blocked)
loadUser()
})
}
}
const removeContact: () => void = () => {
if (relayPool && database && publicKey) {
updateUserContact(userPubKey, database, false).then(() => {
@ -61,7 +82,13 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
getUser(userPubKey, database).then((result) => {
if (result) {
setUser(result)
setBlocked(result.blocked)
setIsContact(result?.contact)
} else {
addUser(userPubKey, database).then(() => {
setUser({ id: userPubKey })
setBlocked(false)
})
}
})
}
@ -80,11 +107,11 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
<View style={styles.cardUserMain}>
<ProfileData
username={user?.name}
publicKey={user?.id}
publicKey={user?.id ?? userPubKey}
validNip05={user?.valid_nip05}
nip05={user?.nip05}
lud06={user?.lnurl}
picture={user?.picture}
picture={showImages ? user?.picture : undefined}
avatarSize={54}
/>
</View>
@ -118,6 +145,14 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
<Text>{isContact ? t('profileCard.unfollow') : t('profileCard.follow')}</Text>
</View>
)}
<View style={styles.actionButton}>
<IconButton
icon={blocked ? 'account-cancel' : 'account-cancel-outline'}
size={28}
onPress={onChangeBlockUser}
/>
<Text>{t(blocked ? 'profileCard.unblock' : 'profileCard.block')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='message-plus-outline'
@ -140,8 +175,8 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
/>
<Text>{t('profileCard.copyNPub')}</Text>
</View>
<View style={styles.actionButton}>
{user?.lnurl && (
{user?.lnurl && (
<View style={styles.actionButton}>
<>
<IconButton
icon='lightning-bolt'
@ -151,8 +186,8 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ userPubKey, bottomShee
/>
<Text>{t('profileCard.invoice')}</Text>
</>
)}
</View>
</View>
)}
</View>
{showNotification && (
<Snackbar

View File

@ -0,0 +1,149 @@
import * as React from 'react'
import { StyleSheet, View } from 'react-native'
import { Card, useTheme, IconButton } from 'react-native-paper'
import ContentLoader, { Rect, Circle } from 'react-content-loader/native'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
export const SkeletonNote: React.FC = () => {
const theme = useTheme()
const skeletonBackgroundColor = theme.colors.elevation.level2
const skeletonForegroundColor = theme.colors.elevation.level5
return (
<Card style={[styles.container, { backgroundColor: theme.colors.elevation.level1 }]}>
<View style={styles.header}>
<View style={styles.headerContent}>
<ContentLoader
speed={2}
width={285}
height={54}
viewBox='0 0 285 54'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Circle cx='27' cy='27' r='27' />
<Rect x='70' y='0' rx='10' ry='10' width='170' height='12' />
<Rect x='70' y='21' rx='10' ry='10' width='120' height='12' />
<Rect x='70' y='42' rx='7' ry='7' width='70' height='12' />
</ContentLoader>
</View>
<IconButton icon='dots-vertical' iconColor={theme.colors.elevation.level3} size={25} />
</View>
<View style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
<ContentLoader
speed={2}
width={328}
height={32}
viewBox='0 0 328 32'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Rect x='0' y='0' rx='6' ry='6' width='100%' height='12' />
<Rect x='0' y='20' rx='6' ry='6' width='60%' height='12' />
</ContentLoader>
</View>
<View style={styles.footer}>
<View style={styles.action}>
<MaterialCommunityIcons
style={styles.actionIcon}
name='message-outline'
size={25}
color={theme.colors.elevation.level3}
/>
<ContentLoader
animate={false}
width={24}
height={16}
viewBox='0 0 24 16'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Rect x='0' y='0' rx='6' ry='6' width='100%' height='16' />
</ContentLoader>
</View>
<View style={styles.action}>
<MaterialCommunityIcons
style={styles.actionIcon}
name='cached'
size={25}
color={theme.colors.elevation.level3}
/>
<ContentLoader
animate={false}
width={24}
height={16}
viewBox='0 0 24 16'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Rect x='0' y='0' rx='6' ry='6' width='100%' height='16' />
</ContentLoader>
</View>
<View style={styles.action}>
<MaterialCommunityIcons
style={styles.actionIcon}
name={'thumb-down-outline'}
size={25}
color={theme.colors.elevation.level3}
/>
<ContentLoader
animate={false}
width={24}
height={16}
viewBox='0 0 24 16'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Rect x='0' y='0' rx='6' ry='6' width='100%' height='16' />
</ContentLoader>
</View>
<View style={styles.action}>
<MaterialCommunityIcons
style={styles.actionIcon}
name={'thumb-up-outline'}
size={25}
color={theme.colors.elevation.level3}
/>
<ContentLoader
animate={false}
width={24}
height={16}
viewBox='0 0 24 16'
backgroundColor={skeletonBackgroundColor}
foregroundColor={skeletonForegroundColor}
>
<Rect x='0' y='0' rx='6' ry='6' width='100%' height='16' />
</ContentLoader>
</View>
</View>
</Card>
)
}
const styles = StyleSheet.create({
container: {},
header: {
flexDirection: 'row',
padding: 16,
},
headerContent: {
flex: 1,
},
content: {
borderTopWidth: 1,
borderBottomWidth: 1,
padding: 16,
},
footer: {
padding: 16,
flexDirection: 'row',
justifyContent: 'space-around',
},
action: {
flexDirection: 'row',
alignItems: 'center',
},
actionIcon: {
marginRight: 8,
},
})

View File

@ -2,12 +2,21 @@ import React, { useEffect, useState } from 'react'
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { initDatabase } from '../Functions/DatabaseFunctions'
import SInfo from 'react-native-sensitive-info'
import { Linking } from 'react-native'
import { Linking, StyleSheet } from 'react-native'
import { Config, getConfig, updateConfig } from '../Functions/DatabaseFunctions/Config'
import { Text } from 'react-native-paper'
export interface AppContextProps {
init: () => void
loadingDb: boolean
database: QuickSQLiteConnection | null
showPublicImages: boolean
setShowPublicImages: (showPublicImages: boolean) => void
showSensitive: boolean
setShowSensitive: (showPublicImages: boolean) => void
satoshi: 'kebab' | 'sats'
setSatoshi: (showPublicImages: 'kebab' | 'sats') => void
getSatoshiSymbol: (fontSize?: number) => JSX.Element
}
export interface AppContextProviderProps {
@ -18,9 +27,21 @@ export const initialAppContext: AppContextProps = {
init: () => {},
loadingDb: true,
database: null,
showPublicImages: false,
setShowPublicImages: () => {},
showSensitive: false,
setShowSensitive: () => {},
satoshi: 'kebab',
setSatoshi: () => {},
getSatoshiSymbol: () => <></>,
}
export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => {
const [showPublicImages, setShowPublicImages] = React.useState<boolean>(
initialAppContext.showPublicImages,
)
const [showSensitive, setShowSensitive] = React.useState<boolean>(initialAppContext.showSensitive)
const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi)
const [database, setDatabase] = useState<QuickSQLiteConnection | null>(null)
const [loadingDb, setLoadingDb] = useState<boolean>(initialAppContext.loadingDb)
@ -35,14 +56,52 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
})
}
const getSatoshiSymbol: (fontSize?: number) => JSX.Element = (fontSize) => {
return satoshi === 'sats' ? (
<Text>Sats</Text>
) : (
<Text style={[styles.satoshi, { fontSize }]}>s</Text>
)
}
useEffect(init, [])
useEffect(() => {
if (database) {
getConfig(database).then((result) => {
if (result) {
setShowPublicImages(result.show_public_images ?? initialAppContext.showPublicImages)
setShowSensitive(result.show_sensitive ?? initialAppContext.showSensitive)
setSatoshi(result.satoshi)
}
})
}
}, [database])
useEffect(() => {
if (database) {
const config: Config = {
show_public_images: showPublicImages,
show_sensitive: showSensitive,
satoshi,
}
updateConfig(config, database)
}
}, [database])
return (
<AppContext.Provider
value={{
init,
loadingDb,
database,
showPublicImages,
setShowPublicImages,
showSensitive,
setShowSensitive,
satoshi,
setSatoshi,
getSatoshiSymbol,
}}
>
{children}
@ -50,4 +109,10 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
)
}
const styles = StyleSheet.create({
satoshi: {
fontFamily: 'Satoshi-Symbol',
},
})
export const AppContext = React.createContext(initialAppContext)

View File

@ -2,12 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
import SInfo from 'react-native-sensitive-info'
import { RelayPoolContext } from './RelayPoolContext'
import { AppContext } from './AppContext'
import {
getContactsCount,
getFollowersCount,
getUser,
User,
} from '../Functions/DatabaseFunctions/Users'
import { getUser, User } from '../Functions/DatabaseFunctions/Users'
import { getPublicKey } from 'nostr-tools'
import { dropTables } from '../Functions/DatabaseFunctions'
import { navigate } from '../lib/Navigation'
@ -27,10 +22,6 @@ export interface UserContextProps {
setPrivateKey: (privateKey: string | undefined) => void
setUser: (user: User) => void
user?: User
contactsCount: number
followersCount: number
setContantsCount: (count: number) => void
setFollowersCount: (count: number) => void
reloadUser: () => void
logout: () => void
}
@ -47,10 +38,6 @@ export const initialUserContext: UserContextProps = {
setUser: () => {},
reloadUser: () => {},
logout: () => {},
setContantsCount: () => {},
setFollowersCount: () => {},
contactsCount: 0,
followersCount: 0,
}
export const UserContextProvider = ({ children }: UserContextProviderProps): JSX.Element => {
@ -63,8 +50,6 @@ export const UserContextProvider = ({ children }: UserContextProviderProps): JSX
const [privateKey, setPrivateKey] = useState<string>()
const [user, setUser] = React.useState<User>()
const [clipboardLoads, setClipboardLoads] = React.useState<string[]>([])
const [contactsCount, setContantsCount] = React.useState<number>(0)
const [followersCount, setFollowersCount] = React.useState<number>(0)
const reloadUser: () => void = () => {
if (database && publicKey) {
@ -78,8 +63,6 @@ export const UserContextProvider = ({ children }: UserContextProviderProps): JSX
}
checkClipboard()
})
getContactsCount(database).then(setContantsCount)
getFollowersCount(database).then(setFollowersCount)
}
}
@ -169,12 +152,8 @@ export const UserContextProvider = ({ children }: UserContextProviderProps): JSX
privateKey,
setPrivateKey,
user,
contactsCount,
followersCount,
reloadUser,
logout,
setContantsCount,
setFollowersCount,
}}
>
{children}

View File

@ -0,0 +1,34 @@
import { getItems } from '..'
import { QuickSQLiteConnection, QueryResult } from 'react-native-quick-sqlite'
export interface Config {
satoshi: 'kebab' | 'sats'
show_public_images?: boolean
show_sensitive?: boolean
}
const databaseToEntity: (object: object) => Config = (object) => {
return object as Config
}
export const getConfig: (db: QuickSQLiteConnection) => Promise<Config | null> = async (db) => {
const userQuery = `SELECT * FROM nostros_config LIMIT 1;`
const resultSet = await db.execute(userQuery)
if (resultSet.rows && resultSet.rows?.length > 0) {
const items: object[] = getItems(resultSet)
const user: Config = databaseToEntity(items[0])
return user
} else {
return null
}
}
export const updateConfig: (
config: Config,
db: QuickSQLiteConnection,
) => Promise<QueryResult> = async (config, db) => {
const configQuery = `UPDATE nostros_config SET satoshi = ?, show_public_images = ?, show_sensitive = ?`
return db.execute(configQuery, [config.satoshi, config.show_public_images, config.show_sensitive])
}

View File

@ -11,6 +11,7 @@ export interface Note extends Event {
nip05: string
valid_nip05: boolean
repost_id: string
blocked: boolean
}
const databaseToEntity: (object: any) => Note = (object = {}) => {
@ -22,14 +23,26 @@ export const getMainNotes: (
db: QuickSQLiteConnection,
pubKey: string,
limit: number,
) => Promise<Note[]> = async (db, pubKey, limit) => {
const notesQuery = `
contants: boolean,
filters?: {
until?: number
},
) => Promise<Note[]> = async (db, pubKey, limit, contants, filters) => {
let notesQuery = `
SELECT
nostros_notes.*, nostros_users.nip05, nostros_users.valid_nip05, nostros_users.lnurl, nostros_users.name, nostros_users.picture, nostros_users.contact, nostros_users.created_at as user_created_at FROM nostros_notes
nostros_notes.*, nostros_users.nip05, nostros_users.blocked, nostros_users.valid_nip05, nostros_users.lnurl, nostros_users.name, nostros_users.picture, nostros_users.contact, nostros_users.created_at as user_created_at FROM nostros_notes
LEFT JOIN
nostros_users ON nostros_users.id = nostros_notes.pubkey
WHERE (nostros_users.contact = 1 OR nostros_notes.pubkey = '${pubKey}')
AND nostros_notes.main_event_id IS NULL
WHERE
`
if (contants)
notesQuery += `(nostros_users.contact = 1 OR nostros_notes.pubkey = '${pubKey}') AND `
if (filters?.until) notesQuery += `nostros_notes.created_at < ${filters?.until} AND `
notesQuery += `
nostros_notes.main_event_id IS NULL
ORDER BY created_at DESC
LIMIT ${limit}
`
@ -41,6 +54,22 @@ export const getMainNotes: (
return notes
}
export const getMainNotesCount: (
db: QuickSQLiteConnection,
from: number,
) => Promise<number> = async (db, from) => {
const repliesQuery = `
SELECT
COUNT(*)
FROM nostros_notes
WHERE created_at > "${from}"
`
const resultSet = db.execute(repliesQuery)
const item: { 'COUNT(*)': number } = resultSet?.rows?.item(0)
return item['COUNT(*)'] ?? 0
}
export const getMentionNotes: (
db: QuickSQLiteConnection,
pubKey: string,

View File

@ -13,6 +13,7 @@ export interface User {
nip05?: string
created_at?: number
valid_nip05?: boolean
blocked?: boolean
}
const databaseToEntity: (object: object) => User = (object) => {
@ -30,6 +31,17 @@ export const updateUserContact: (
return db.execute(userQuery, [contact ? 1 : 0, userId])
}
export const updateUserBlock: (
userId: string,
db: QuickSQLiteConnection,
blocked: boolean,
) => Promise<QueryResult | null> = async (userId, db, blocked) => {
const userQuery = `UPDATE nostros_users SET blocked = ? WHERE id = ?`
await addUser(userId, db)
return db.execute(userQuery, [blocked ? 1 : 0, userId])
}
export const getUser: (pubkey: string, db: QuickSQLiteConnection) => Promise<User | null> = async (
pubkey,
db,

View File

@ -23,6 +23,7 @@
"Note": "Note",
"Profile": "Profile",
"About": "About",
"Config": "Config",
"Send": "Send",
"Relays": "Relays",
"ProfileConfig": "My profile"
@ -57,9 +58,15 @@
"about": "About",
"logout": "Logout"
},
"configPage": {
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol"
},
"noteCard": {
"answering": "Answer to {{pubkey}}",
"reposting": "Reposted {{pubkey}}",
"userBlocked": "User blocked",
"seeParent": "See note",
"contentWarning": "Sensitive content"
},
@ -128,7 +135,11 @@
"homeFeed": {
"emptyTitle": "You are not following anyone.",
"emptyDescription": "Follow other profiles to see content.",
"emptyButton": "Go to contacts"
"emptyButton": "Go to contacts",
"globalFeed": "Global feed",
"myFeed": "My feed",
"newMessage": "{{newNotesCount}} new note. Pull to refresh.",
"newMessages": "{{newNotesCount}} new notes. Pull to refresh."
},
"relaysPage": {
"labelAdd": "Relay address",
@ -204,6 +215,8 @@
"message": "Message",
"follow": "Follow",
"unfollow": "Following",
"block": "Block",
"unblock": "Unblock",
"copyNPub": "Copy key"
},
"conversationsFeed": {

View File

@ -23,6 +23,7 @@
"Note": "Nota",
"Profile": "Perfil",
"About": "About",
"Config": "Config",
"Send": "Send",
"Relays": "Relays",
"ProfileConfig": "Mi perfil"
@ -57,10 +58,16 @@
"about": "Sobre Nostros",
"logout": "Salir"
},
"configPage": {
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol"
},
"noteCard": {
"answering": "Responder a {{pubkey}}",
"reposting": "Reposted {{pubkey}}",
"seeParent": "Ver nota",
"userBlocked": "Usuario bloqueado",
"contentWarning": "Contenido sensible"
},
"lnPayment": {
@ -129,7 +136,11 @@
"homeFeed": {
"emptyTitle": "No sigues a nadie",
"emptyDescription": "Sigue otros perfiles para ver aquí contenido.",
"emptyButton": "Ir a contactos"
"emptyButton": "Ir a contactos",
"globalFeed": "Fee global",
"newMessage": "{{newNotesCount}} nota nueva. Desliza para recargar.",
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar.",
"myFeed": "Mi feed"
},
"relaysPage": {
"labelAdd": "Dirección de relay",
@ -202,6 +213,8 @@
"invoice": "Propina",
"message": "Mensaje",
"follow": "Seguir",
"block": "Bloquear",
"unblock": "Desbloquear",
"unfollow": "Siguiendo",
"copyNPub": "Copiar clave"
},

View File

@ -23,6 +23,7 @@
"Repost": "Поделиться",
"Profile": "Профиль",
"About": "О нас",
"Config": "Config",
"Send": "Отпраить",
"Relays": "Реле",
"ProfileConfig": "Мой профиль"
@ -57,10 +58,16 @@
"about": "Подроблее",
"logout": "Выйти"
},
"configPage": {
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol"
},
"noteCard": {
"answering": "Ответить {{pubkey}}",
"reposting": "Reposted {{pubkey}}",
"seeParent": "See note",
"userBlocked": "User blocked",
"contentWarning": "Деликатный контент"
},
"lnPayment": {
@ -128,7 +135,11 @@
"homeFeed": {
"emptyTitle": "You are not following anyone.",
"emptyDescription": "Follow other profiles to see content.",
"emptyButton": "Go to contacts"
"emptyButton": "Go to contacts",
"globalFeed": "Global feed",
"newMessage": "{{newNotesCount}} nota nueva. Desliza para recargar.",
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar.",
"myFeed": "My feed"
},
"relaysPage": {
"labelAdd": "Relay address",
@ -201,6 +212,8 @@
"invoice": "Tip",
"message": "Message",
"follow": "Follow",
"block": "Block",
"unblock": "Desbloquear",
"unfollow": "Following",
"copyNPub": "Copy key"
},

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
import { FlatList, Linking, ListRenderItem, StyleSheet } from 'react-native'
import { Divider, List, Text, useTheme } from 'react-native-paper'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import DeviceInfo from 'react-native-device-info'
interface ItemList {
key: number
@ -82,7 +81,7 @@ export const AboutPage: React.FC = () => {
)}
/>
),
right: <Text>{DeviceInfo.getVersion()}</Text>,
right: <Text>v0.2.1.5-alpha</Text>,
onPress: () => {},
},
],

View File

@ -0,0 +1,100 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, StyleSheet, Text } from 'react-native'
import { Divider, List, Switch, useTheme } from 'react-native-paper'
import RBSheet from 'react-native-raw-bottom-sheet'
import { AppContext } from '../../Contexts/AppContext'
export const ConfigPage: React.FC = () => {
const theme = useTheme()
const { t } = useTranslation('common')
const {
getSatoshiSymbol,
showPublicImages,
setShowPublicImages,
showSensitive,
setShowSensitive,
satoshi,
setSatoshi,
} = React.useContext(AppContext)
const bottomSheetRef = React.useRef<RBSheet>(null)
React.useEffect(() => {}, [showPublicImages, showSensitive, satoshi])
const createOptions = React.useMemo(() => {
return [
{
key: 1,
title: <Text style={styles.satoshi}>s</Text>,
onPress: () => {
setSatoshi('kebab')
bottomSheetRef.current?.close()
},
},
{
key: 2,
title: 'sats',
onPress: () => {
setSatoshi('sats')
bottomSheetRef.current?.close()
},
},
]
}, [])
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
padding: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
},
}
}, [])
return (
<>
<List.Item
title={t('configPage.showPublicImages')}
right={() => (
<Switch value={showPublicImages} onValueChange={(value) => setShowPublicImages(value)} />
)}
/>
<List.Item
title={t('configPage.showSensitive')}
right={() => (
<Switch value={showSensitive} onValueChange={(value) => setShowSensitive(value)} />
)}
/>
<List.Item
title={t('configPage.satoshi')}
onPress={() => bottomSheetRef.current?.open()}
right={() => getSatoshiSymbol(25)}
/>
<RBSheet
ref={bottomSheetRef}
closeOnDragDown={true}
height={160}
customStyles={bottomSheetStyles}
>
<FlatList
data={createOptions}
renderItem={({ item }) => {
return <List.Item key={item.key} title={item.title} onPress={item.onPress} />
}}
ItemSeparatorComponent={Divider}
/>
</RBSheet>
</>
)
}
const styles = StyleSheet.create({
satoshi: {
fontFamily: 'Satoshi-Symbol',
fontSize: 25,
},
})
export default ConfigPage

View File

@ -44,8 +44,7 @@ export const ContactsFeed: React.FC = () => {
const { t } = useTranslation('common')
const initialPageSize = 20
const { database } = useContext(AppContext)
const { privateKey, publicKey, setContantsCount, setFollowersCount, nPub } =
React.useContext(UserContext)
const { privateKey, publicKey, nPub } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const theme = useTheme()
const [pageSize, setPageSize] = useState<number>(initialPageSize)
@ -94,8 +93,6 @@ export const ContactsFeed: React.FC = () => {
])
setFollowers(followers)
setFollowing(following)
setContantsCount(following.length)
setFollowersCount(followers.length)
}
})
}
@ -434,7 +431,7 @@ const styles = StyleSheet.create({
alignContent: 'center',
},
tabActive: {
borderBottomWidth: 5,
borderBottomWidth: 3,
},
tabText: {
textAlign: 'center',

View File

@ -14,6 +14,7 @@ import ProfileCard from '../../Components/ProfileCard'
import NotePage from '../NotePage'
import SendPage from '../SendPage'
import ConversationPage from '../ConversationPage'
import ConfigPage from '../ConfigPage'
export const HomeNavigator: React.FC = () => {
const theme = useTheme()
@ -102,6 +103,7 @@ export const HomeNavigator: React.FC = () => {
<Stack.Group>
<Stack.Screen name='Relays' component={RelaysPage} />
<Stack.Screen name='About' component={AboutPage} />
<Stack.Screen name='Config' component={ConfigPage} />
<Stack.Screen name='ProfileConfig' component={ProfileConfigPage} />
<Stack.Screen name='Profile' component={ProfilePage} />
</Stack.Group>

View File

@ -0,0 +1,190 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import {
ListRenderItem,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMainNotes, getMainNotesCount, Note } from '../../Functions/DatabaseFunctions/Notes'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { ActivityIndicator, Banner, Button, Text } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import { useTheme } from '@react-navigation/native'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { t } from 'i18next'
import { FlatList } from 'react-native-gesture-handler'
import { getUnixTime } from 'date-fns'
interface GlobalFeedProps {
navigation: any
setProfileCardPubKey: (profileCardPubKey: string) => void
}
export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation, setProfileCardPubKey }) => {
const theme = useTheme()
const { database, showPublicImages } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { lastEventId, relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>([])
const [lastLoadAt, setLastLoadAt] = useState<number>(0)
const [newNotesCount, setNewNotesCount] = useState<number>(0)
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [refreshing, setRefreshing] = useState(false)
useEffect(() => {
subscribeNotes()
}, [])
useEffect(() => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId, lastLoadAt])
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
}, [pageSize])
const updateLastLoad: () => void = () => {
setLastLoadAt(getUnixTime(new Date()))
}
const onRefresh = useCallback(() => {
setRefreshing(true)
updateLastLoad()
setNewNotesCount(0)
}, [])
const subscribeNotes: (past?: boolean) => void = async (past) => {
if (!database || !publicKey) return
const message: RelayFilters = {
kinds: [Kind.Text, Kind.RecommendRelay],
limit: pageSize,
}
if (past) message.until = notes[0].created_at
relayPool?.subscribe('homepage-global-main', [message])
setRefreshing(false)
updateLastLoad()
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const loadNotes: () => void = () => {
if (database && publicKey) {
if (lastLoadAt > 0) {
getMainNotesCount(database, lastLoadAt).then(setNewNotesCount)
}
getMainNotes(database, publicKey, pageSize, false, { until: lastLoadAt }).then((results) => {
setRefreshing(false)
if (results.length > 0) {
setNotes(results)
relayPool?.subscribe('homepage-contacts-meta', [
{
kinds: [Kind.Metadata],
authors: notes.map((note) => note.pubkey ?? ''),
},
])
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item, index }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard
note={item}
showActionCount={false}
showAvatarImage={showPublicImages}
onPressUser={(user) => {
setProfileCardPubKey(user.id)
}}
showPreview={showPublicImages}
/>
</View>
)
}
return (
<View>
{notes && notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<Banner
visible={newNotesCount > 0}
actions={[]}
icon={() => <MaterialCommunityIcons name='arrow-down-bold' size={20} />}
>
{t(newNotesCount < 2 ? 'homeFeed.newMessage' : 'homeFeed.newMessages', {
newNotesCount,
})}
</Banner>
<FlatList showsVerticalScrollIndicator={false} data={notes} renderItem={renderItem} />
{notes.length >= 10 && (
<ActivityIndicator animating={true} style={styles.activityIndicator} />
)}
</ScrollView>
) : (
<View style={styles.blank}>
<MaterialCommunityIcons
name='account-group-outline'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('homeFeed.emptyTitle')}
</Text>
<Text variant='bodyMedium' style={styles.center}>
{t('homeFeed.emptyDescription')}
</Text>
<Button mode='contained' compact onPress={() => navigation.jumpTo('contacts')}>
{t('homeFeed.emptyButton')}
</Button>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 220,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default GlobalFeed

View File

@ -1,32 +1,16 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import {
Dimensions,
ListRenderItem,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getLastReply, getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import React, { useContext, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { ActivityIndicator, AnimatedFAB, Button, Text } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import { AnimatedFAB, Text, TouchableRipple } from 'react-native-paper'
import RBSheet from 'react-native-raw-bottom-sheet'
import ProfileCard from '../../Components/ProfileCard'
import { useFocusEffect, useTheme } from '@react-navigation/native'
import { navigate } from '../../lib/Navigation'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { t } from 'i18next'
import { FlatList } from 'react-native-gesture-handler'
import { getLastReaction } from '../../Functions/DatabaseFunctions/Reactions'
import GlobalFeed from '../GlobalFeed'
import MyFeed from '../MyFeed'
import { AppContext } from '../../Contexts/AppContext'
interface HomeFeedProps {
navigation: any
@ -34,24 +18,19 @@ interface HomeFeedProps {
export const HomeFeed: React.FC<HomeFeedProps> = ({ navigation }) => {
const theme = useTheme()
const { database } = useContext(AppContext)
const { publicKey, privateKey } = useContext(UserContext)
const { lastEventId, relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>([])
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [refreshing, setRefreshing] = useState(false)
const { showPublicImages } = useContext(AppContext)
const { privateKey } = useContext(UserContext)
const { relayPool } = useContext(RelayPoolContext)
const [tabKey, setTabKey] = React.useState('myFeed')
const [profileCardPubkey, setProfileCardPubKey] = useState<string>()
const bottomSheetProfileRef = React.useRef<RBSheet>(null)
useFocusEffect(
React.useCallback(() => {
subscribeNotes()
loadNotes()
return () =>
relayPool?.unsubscribe([
'homepage-main',
'homepage-global-main',
'homepage-contacts-main',
'homepage-reactions',
'homepage-contacts-meta',
'homepage-replies',
@ -59,104 +38,6 @@ export const HomeFeed: React.FC<HomeFeedProps> = ({ navigation }) => {
}, []),
)
useEffect(() => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId])
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
}, [pageSize])
const onRefresh = useCallback(() => {
setRefreshing(true)
subscribeNotes()
}, [])
const subscribeNotes: (past?: boolean) => void = async (past) => {
if (!database || !publicKey) return
const users: User[] = await getUsers(database, { contacts: true, order: 'created_at DESC' })
const authors: string[] = [...users.map((user) => user.id), publicKey]
const lastNotes: Note[] = await getMainNotes(database, publicKey, initialPageSize)
const lastNote: Note = lastNotes[lastNotes.length - 1]
const message: RelayFilters = {
kinds: [Kind.Text, Kind.RecommendRelay],
authors,
}
if (lastNote && lastNotes.length >= pageSize && !past) {
message.since = lastNote?.created_at
} else {
message.limit = pageSize + initialPageSize
}
relayPool?.subscribe('homepage-main', [message])
relayPool?.subscribe('homepage-contacts-meta', [
{
kinds: [Kind.Metadata],
authors,
},
])
setRefreshing(false)
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMainNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
if (notes.length > 0) {
const notedIds = notes.map((note) => note.id ?? '')
getLastReaction(database, { eventIds: notes.map((note) => note.id ?? '') }).then(
(lastReaction) => {
relayPool?.subscribe('homepage-reactions', [
{
kinds: [Kind.Reaction],
'#e': notedIds,
since: lastReaction?.created_at ?? 0,
},
])
},
)
getLastReply(database, { eventIds: notes.map((note) => note.id ?? '') }).then(
(lastReply) => {
relayPool?.subscribe('homepage-replies', [
{
kinds: [Kind.Text],
'#e': notedIds,
since: lastReply?.created_at ?? 0,
},
])
},
)
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item, index }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard
note={item}
onPressUser={(user) => {
setProfileCardPubKey(user.id)
bottomSheetProfileRef.current?.open()
}}
/>
</View>
)
}
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
@ -168,37 +49,74 @@ export const HomeFeed: React.FC<HomeFeedProps> = ({ navigation }) => {
}
}, [])
const renderScene: Record<string, JSX.Element> = {
globalFeed: (
<GlobalFeed
navigation={navigation}
setProfileCardPubKey={(value) => {
setProfileCardPubKey(value)
bottomSheetProfileRef.current?.open()
}}
/>
),
myFeed: (
<MyFeed
navigation={navigation}
setProfileCardPubKey={(value) => {
setProfileCardPubKey(value)
bottomSheetProfileRef.current?.open()
}}
/>
),
}
return (
<View style={styles.container}>
{notes && notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
<View>
<View style={styles.tabsNavigator}>
<View
style={[
styles.tab,
{
borderBottomColor:
tabKey === 'globalFeed' ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === 'globalFeed' ? 3 : 1,
},
]}
>
<FlatList showsVerticalScrollIndicator={false} data={notes} renderItem={renderItem} />
{notes.length >= 10 && <ActivityIndicator animating={true} />}
</ScrollView>
) : (
<View style={styles.blank}>
<MaterialCommunityIcons
name='account-group-outline'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('homeFeed.emptyTitle')}
</Text>
<Text variant='bodyMedium' style={styles.center}>
{t('homeFeed.emptyDescription')}
</Text>
<Button mode='contained' compact onPress={() => navigation.jumpTo('contacts')}>
{t('homeFeed.emptyButton')}
</Button>
<TouchableRipple
onPress={() => {
relayPool?.unsubscribe([
'homepage-contacts-main',
'homepage-reactions',
'homepage-contacts-meta',
'homepage-replies',
])
setTabKey('globalFeed')
}}
>
<Text style={styles.tabText}>{t('homeFeed.globalFeed')}</Text>
</TouchableRipple>
</View>
)}
<View
style={[
styles.tab,
{
borderBottomColor: tabKey === 'myFeed' ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === 'myFeed' ? 3 : 1,
},
]}
>
<TouchableRipple
onPress={() => {
relayPool?.unsubscribe(['homepage-global-main'])
setTabKey('myFeed')
}}
>
<Text style={styles.tabText}>{t('homeFeed.myFeed')}</Text>
</TouchableRipple>
</View>
</View>
<View style={styles.feed}>{renderScene[tabKey]}</View>
{privateKey && (
<AnimatedFAB
style={[styles.fab, { top: Dimensions.get('window').height - 220 }]}
@ -216,7 +134,11 @@ export const HomeFeed: React.FC<HomeFeedProps> = ({ navigation }) => {
height={280}
customStyles={bottomSheetStyles}
>
<ProfileCard userPubKey={profileCardPubkey ?? ''} bottomSheetRef={bottomSheetProfileRef} />
<ProfileCard
userPubKey={profileCardPubkey ?? ''}
bottomSheetRef={bottomSheetProfileRef}
showImages={showPublicImages}
/>
</RBSheet>
</View>
)
@ -231,9 +153,7 @@ const styles = StyleSheet.create({
position: 'absolute',
},
container: {
paddingLeft: 16,
paddingRight: 16,
flex: 1,
padding: 16,
},
center: {
alignContent: 'center',
@ -244,6 +164,27 @@ const styles = StyleSheet.create({
height: 220,
marginTop: 91,
},
tab: {
flex: 1,
height: '100%',
justifyContent: 'center',
alignContent: 'center',
},
tabText: {
textAlign: 'center',
paddingTop: 25,
height: '100%',
},
tabsNavigator: {
flexDirection: 'row',
justifyContent: 'space-between',
height: 70,
},
feed: {
paddingBottom: 140,
paddingLeft: 16,
paddingRight: 16,
},
})
export default HomeFeed

View File

@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
import ProfileCreatePage from '../../Pages/ProfileCreatePage'
import { DrawerNavigationProp } from '@react-navigation/drawer'
import RelaysPage from '../RelaysPage'
import ConfigPage from '../ConfigPage'
export const HomeNavigator: React.FC = () => {
const theme = useTheme()
@ -107,6 +108,7 @@ export const HomeNavigator: React.FC = () => {
<Stack.Group>
<Stack.Screen name='About' component={AboutPage} />
<Stack.Screen name='Relays' component={RelaysPage} />
<Stack.Screen name='Config' component={ConfigPage} />
</Stack.Group>
</Stack.Navigator>
<RBSheet

View File

@ -0,0 +1,192 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import {
ListRenderItem,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getLastReply, getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { ActivityIndicator, Button, Text } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import { useTheme } from '@react-navigation/native'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { t } from 'i18next'
import { FlatList } from 'react-native-gesture-handler'
import { getLastReaction } from '../../Functions/DatabaseFunctions/Reactions'
interface MyFeedProps {
navigation: any
setProfileCardPubKey: (profileCardPubKey: string) => void
}
export const MyFeed: React.FC<MyFeedProps> = ({ navigation, setProfileCardPubKey }) => {
const theme = useTheme()
const { database } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { lastEventId, relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>([])
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [refreshing, setRefreshing] = useState(false)
useEffect(() => {
subscribeNotes()
loadNotes()
}, [])
useEffect(() => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId])
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
}, [pageSize])
const onRefresh = useCallback(() => {
setRefreshing(true)
subscribeNotes()
}, [])
const subscribeNotes: (past?: boolean) => void = async (past) => {
if (!database || !publicKey) return
const users: User[] = await getUsers(database, { contacts: true, order: 'created_at DESC' })
const authors: string[] = [...users.map((user) => user.id), publicKey]
const message: RelayFilters = {
kinds: [Kind.Text, Kind.RecommendRelay],
authors,
limit: pageSize,
}
relayPool?.subscribe('homepage-contacts-main', [message])
setRefreshing(false)
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMainNotes(database, publicKey, pageSize, true).then((notes) => {
setNotes(notes)
if (notes.length > 0) {
relayPool?.subscribe('homepage-contacts-meta', [
{
kinds: [Kind.Metadata],
authors: notes.map((note) => note.pubkey ?? ''),
},
])
const notedIds = notes.map((note) => note.id ?? '')
getLastReaction(database, { eventIds: notes.map((note) => note.id ?? '') }).then(
(lastReaction) => {
relayPool?.subscribe('homepage-reactions', [
{
kinds: [Kind.Reaction],
'#e': notedIds,
since: lastReaction?.created_at ?? 0,
},
])
},
)
getLastReply(database, { eventIds: notes.map((note) => note.id ?? '') }).then(
(lastReply) => {
relayPool?.subscribe('homepage-replies', [
{
kinds: [Kind.Text],
'#e': notedIds,
since: lastReply?.created_at ?? 0,
},
])
},
)
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item, index }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard
note={item}
onPressUser={(user) => {
setProfileCardPubKey(user.id)
}}
/>
</View>
)
}
return (
<View style={styles.container}>
{notes && notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<FlatList showsVerticalScrollIndicator={false} data={notes} renderItem={renderItem} />
{notes.length >= initialPageSize && (
<ActivityIndicator animating={true} style={styles.activityIndicator} />
)}
</ScrollView>
) : (
<View style={styles.blank}>
<MaterialCommunityIcons
name='account-group-outline'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('homeFeed.emptyTitle')}
</Text>
<Text variant='bodyMedium' style={styles.center}>
{t('homeFeed.emptyDescription')}
</Text>
<Button mode='contained' compact onPress={() => navigation.jumpTo('contacts')}>
{t('homeFeed.emptyButton')}
</Button>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 220,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default MyFeed

View File

@ -30,8 +30,7 @@ export const ProfileConfigPage: React.FC = () => {
const bottomSheetLud06Ref = React.useRef<RBSheet>(null)
const { database } = useContext(AppContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const { user, publicKey, nPub, nSec, contactsCount, followersCount, setUser } =
useContext(UserContext)
const { user, publicKey, nPub, nSec, setUser } = useContext(UserContext)
// State
const [name, setName] = useState<string>()
const [picture, setPicture] = useState<string>()
@ -258,14 +257,6 @@ export const ProfileConfigPage: React.FC = () => {
</View>
</TouchableRipple>
</View>
<View style={styles.cardActions}>
<Button mode='elevated'>
{t('menuItems.following', { following: contactsCount })}
</Button>
<Button mode='elevated'>
{t('menuItems.followers', { followers: followersCount })}
</Button>
</View>
<View style={styles.cardActions}>
<View style={styles.actionButton}>
<IconButton
@ -537,7 +528,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignContent: 'center',
marginBottom: 32,
},
actionButton: {
marginTop: 32,

View File

@ -33,19 +33,16 @@ export const ProfileLoadPage: React.FC = () => {
useEffect(() => {
loadPets()
reloadUser()
if (user?.created_at) {
setProfileFound(true)
loadPets()
}
}, [lastEventId])
useEffect(() => {
if (publicKey && relayPoolReady) loadMeta()
}, [publicKey, relayPoolReady])
useEffect(() => {
if (user) {
setProfileFound(true)
loadPets()
}
}, [user])
const loadMeta: () => void = () => {
if (publicKey && relayPoolReady) {
relayPool?.subscribe('profile-load-meta-pets', [

View File

@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react'
import { FlatList, ListRenderItem, ScrollView, StyleSheet, View } from 'react-native'
import { ScrollView, StyleSheet, View } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
@ -71,27 +71,6 @@ export const RelaysPage: React.FC = () => {
}
}
const relayToggle: (relay: Relay) => JSX.Element = (relay) => {
return (
<Switch
value={relay.active}
onValueChange={() => (relay.active ? desactiveRelay(relay) : activeRelay(relay))}
/>
)
}
const renderItem: ListRenderItem<Relay> = ({ index, item }) => (
<List.Item
key={index}
title={item.url.split('wss://')[1]?.split('/')[0]}
right={() => relayToggle(item)}
onPress={() => {
setSelectedRelay(item)
bottomSheetEditRef.current?.open()
}}
/>
)
const rbSheetCustomStyles = React.useMemo(() => {
return {
container: {
@ -113,22 +92,54 @@ export const RelaysPage: React.FC = () => {
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.myList')}
</Text>
<FlatList style={styles.list} data={myRelays} renderItem={renderItem} />
{myRelays.length > 0 &&
myRelays.map((relay, index) => {
return (
<List.Item
key={index}
title={relay.url.split('wss://')[1]?.split('/')[0]}
right={() => (
<Switch
value={relay.active}
onValueChange={() =>
relay.active ? desactiveRelay(relay) : activeRelay(relay)
}
/>
)}
onPress={() => {
setSelectedRelay(relay)
bottomSheetEditRef.current?.open()
}}
/>
)
})}
</>
)}
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.recommended')}
</Text>
<FlatList
style={styles.list}
data={defaultRelays.map((url) => {
return {
url,
active: relays.find((relay) => relay.url === url) !== undefined,
}
})}
renderItem={renderItem}
/>
{defaultRelays.map((url, index) => {
const relay = {
url,
active: relays.find((relay) => relay.url === url && relay.active) !== undefined,
}
return (
<List.Item
key={index}
title={url.split('wss://')[1]?.split('/')[0]}
right={() => (
<Switch
value={relay.active}
onValueChange={() => (relay.active ? desactiveRelay(relay) : activeRelay(relay))}
/>
)}
onPress={() => {
setSelectedRelay(relay)
bottomSheetEditRef.current?.open()
}}
/>
)
})}
</ScrollView>
<AnimatedFAB
style={styles.fab}

View File

@ -33,11 +33,11 @@
"lodash.debounce": "^4.0.8",
"nostr-tools": "^1.1.1",
"react": "18.1.0",
"react-content-loader": "^6.2.0",
"react-i18next": "^12.1.4",
"react-native": "0.70.6",
"react-native-action-button": "^2.8.5",
"react-native-bidirectional-infinite-scroll": "^0.3.3",
"react-native-device-info": "^10.3.0",
"react-native-gesture-handler": "^2.8.0",
"react-native-multithreading": "^1.1.1",
"react-native-pager-view": "^6.1.2",

View File

@ -7167,6 +7167,11 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
react-content-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.2.0.tgz#cd8fee8160b8fda6610d0c69ce5aee7b8094cba6"
integrity sha512-r1dI6S+uHNLW68qraLE2njJYOuy6976PpCExuCZUcABWbfnF3FMcmuESRI8L4Bj45wnZ7n8g71hkPLzbma7/Cw==
react-devtools-core@4.24.0:
version "4.24.0"
resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.24.0.tgz#7daa196bdc64f3626b3f54f2ff2b96f7c4fdf017"
@ -7225,11 +7230,6 @@ react-native-codegen@^0.70.6:
jscodeshift "^0.13.1"
nullthrows "^1.1.1"
react-native-device-info@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.3.0.tgz#6bab64d84d3415dd00cc446c73ec5e2e61fddbe7"
integrity sha512-/ziZN1sA1REbJTv5mQZ4tXggcTvSbct+u5kCaze8BmN//lbxcTvWsU6NQd4IihLt89VkbX+14IGc9sVApSxd/w==
react-native-gesture-handler@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz#ef9857871c10663c95a51546225b6e00cd4740cf"