mirror of
https://github.com/KoalaSat/nostros.git
synced 2024-09-29 06:30:47 +00:00
Config page and global feed (#198)
This commit is contained in:
commit
f70b6be427
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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}>
|
||||
|
@ -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')}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -1,153 +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 { 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
|
||||
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>
|
||||
)
|
||||
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,
|
||||
}
|
||||
})
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
34
frontend/Functions/DatabaseFunctions/Config/index.ts
Normal file
34
frontend/Functions/DatabaseFunctions/Config/index.ts
Normal 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])
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -23,6 +23,7 @@
|
||||
"Note": "Nota",
|
||||
"Profile": "Perfil",
|
||||
"About": "About",
|
||||
"Config": "Config",
|
||||
"Send": "Send",
|
||||
"Relays": "Relays",
|
||||
"ProfileConfig": "My profile"
|
||||
@ -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": "See note",
|
||||
"userBlocked": "User blocked",
|
||||
"contentWarning": "Sensitive content"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
100
frontend/Pages/ConfigPage/index.tsx
Normal file
100
frontend/Pages/ConfigPage/index.tsx
Normal 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
|
@ -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',
|
||||
|
@ -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>
|
||||
|
190
frontend/Pages/GlobalFeed/index.tsx
Normal file
190
frontend/Pages/GlobalFeed/index.tsx
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
192
frontend/Pages/MyFeed/index.tsx
Normal file
192
frontend/Pages/MyFeed/index.tsx
Normal 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
|
@ -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>()
|
||||
@ -256,14 +255,6 @@ export const ProfileConfigPage: React.FC = () => {
|
||||
)}
|
||||
</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
|
||||
@ -535,7 +526,6 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
marginBottom: 32,
|
||||
},
|
||||
actionButton: {
|
||||
marginTop: 32,
|
||||
|
@ -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', [
|
||||
|
@ -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}
|
||||
|
@ -7230,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"
|
||||
|
Loading…
Reference in New Issue
Block a user