diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 6a339b7..b2d331e 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -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 diff --git a/android/app/build.gradle b/android/app/build.gradle index f3ade80..23c5f38 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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()) { diff --git a/android/app/src/main/java/com/nostros/classes/Event.java b/android/app/src/main/java/com/nostros/classes/Event.java index 5792c0f..19b21d5 100644 --- a/android/app/src/main/java/com/nostros/classes/Event.java +++ b/android/app/src/main/java/com/nostros/classes/Event.java @@ -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() { diff --git a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java index 26028af..8ab3695 100644 --- a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java +++ b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java @@ -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 { diff --git a/frontend/Components/LnPayment/index.tsx b/frontend/Components/LnPayment/index.tsx index ef628db..ac48716 100644 --- a/frontend/Components/LnPayment/index.tsx +++ b/frontend/Components/LnPayment/index.tsx @@ -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 = ({ open, setOpen, event, user }) => { const theme = useTheme() const { t } = useTranslation('common') + const { getSatoshiSymbol } = React.useContext(AppContext) const bottomSheetLnPaymentRef = React.useRef(null) const bottomSheetInvoiceRef = React.useRef(null) const [monto, setMonto] = useState('') @@ -91,22 +93,13 @@ export const LnPayment: React.FC = ({ open, setOpen, event, us = ({ open, setOpen, event, us - - s - - {monto} + {monto} + {getSatoshiSymbol(23)} {comment && ( diff --git a/frontend/Components/MenuItems/index.tsx b/frontend/Components/MenuItems/index.tsx index e39c97b..95b8e15 100644 --- a/frontend/Components/MenuItems/index.tsx +++ b/frontend/Components/MenuItems/index.tsx @@ -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 = () => { )} {publicKey && ( - + ( @@ -117,6 +119,16 @@ export const MenuItems: React.FC = () => { /> )} + + onPressItem('config', 1)} + onTouchEnd={() => setDrawerItemIndex(-1)} + /> + 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 = ({ note, + showAvatarImage = true, showAnswerData = true, showAction = true, + showActionCount = true, showPreview = true, showRepostPreview = true, onPressUser = () => {}, @@ -59,7 +63,7 @@ export const NoteCard: React.FC = ({ 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(false) const [positiveReactions, setPositiveReactions] = useState(0) const [negaiveReactions, setNegativeReactions] = useState(0) @@ -72,7 +76,7 @@ export const NoteCard: React.FC = ({ const [repost, setRepost] = useState() 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 = ({ )} - {hide ? ( + {hide && !showSensitive ? ( @@ -208,7 +212,7 @@ export const NoteCard: React.FC = ({ {!relayAdded && note && REGEX_SOCKET_LINK.test(note.content) && ( - + )} @@ -217,8 +221,33 @@ export const NoteCard: React.FC = ({ ) } + const blockedContent: () => JSX.Element = () => { + return ( + + + + + + + + {t('noteCard.userBlocked')} + + + + + ) + } + 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 = ({ validNip05={note?.valid_nip05} nip05={note?.nip05} lud06={note?.lnurl} - picture={note?.picture} + picture={showAvatarImage ? note?.picture : undefined} timestamp={note?.created_at} avatarSize={56} /> - - {showAction && ( + {showAction && ( + onPressUser({ id: note.pubkey, name: note.name })} /> - )} - + + )} {getNoteContent()} - {showAction && ( - + {showAction && !note?.blocked && ( + )} @@ -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', diff --git a/frontend/Components/ProfileCard/index.tsx b/frontend/Components/ProfileCard/index.tsx index a5472d5..9581798 100644 --- a/frontend/Components/ProfileCard/index.tsx +++ b/frontend/Components/ProfileCard/index.tsx @@ -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 + showImages: boolean } -export const ProfileCard: React.FC = ({ userPubKey, bottomSheetRef }) => { +export const ProfileCard: React.FC = ({ + 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() + const [blocked, setBlocked] = React.useState() const [openLn, setOpenLn] = React.useState(false) const [isContact, setIsContact] = React.useState() const [showNotification, setShowNotification] = React.useState() @@ -36,6 +48,15 @@ export const ProfileCard: React.FC = ({ 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 = ({ 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 = ({ userPubKey, bottomShee @@ -118,6 +145,14 @@ export const ProfileCard: React.FC = ({ userPubKey, bottomShee {isContact ? t('profileCard.unfollow') : t('profileCard.follow')} )} + + + {t(blocked ? 'profileCard.unblock' : 'profileCard.block')} + = ({ userPubKey, bottomShee /> {t('profileCard.copyNPub')} - - {user?.lnurl && ( + {user?.lnurl && ( + <> = ({ userPubKey, bottomShee /> {t('profileCard.invoice')} - )} - + + )} {showNotification && ( { + const theme = useTheme() + const skeletonBackgroundColor = theme.colors.elevation.level2 + const skeletonForegroundColor = theme.colors.elevation.level5 + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +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, + }, +}) diff --git a/frontend/Contexts/AppContext.tsx b/frontend/Contexts/AppContext.tsx index 722c034..e307123 100644 --- a/frontend/Contexts/AppContext.tsx +++ b/frontend/Contexts/AppContext.tsx @@ -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( + initialAppContext.showPublicImages, + ) + const [showSensitive, setShowSensitive] = React.useState(initialAppContext.showSensitive) + const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi) const [database, setDatabase] = useState(null) const [loadingDb, setLoadingDb] = useState(initialAppContext.loadingDb) @@ -35,14 +56,52 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E }) } + const getSatoshiSymbol: (fontSize?: number) => JSX.Element = (fontSize) => { + return satoshi === 'sats' ? ( + Sats + ) : ( + s + ) + } + 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 ( {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) diff --git a/frontend/Contexts/UserContext.tsx b/frontend/Contexts/UserContext.tsx index 954642f..7f32d4c 100644 --- a/frontend/Contexts/UserContext.tsx +++ b/frontend/Contexts/UserContext.tsx @@ -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() const [user, setUser] = React.useState() const [clipboardLoads, setClipboardLoads] = React.useState([]) - const [contactsCount, setContantsCount] = React.useState(0) - const [followersCount, setFollowersCount] = React.useState(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} diff --git a/frontend/Functions/DatabaseFunctions/Config/index.ts b/frontend/Functions/DatabaseFunctions/Config/index.ts new file mode 100644 index 0000000..c9d239c --- /dev/null +++ b/frontend/Functions/DatabaseFunctions/Config/index.ts @@ -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 = 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 = 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]) +} diff --git a/frontend/Functions/DatabaseFunctions/Notes/index.ts b/frontend/Functions/DatabaseFunctions/Notes/index.ts index df53b17..36d3642 100644 --- a/frontend/Functions/DatabaseFunctions/Notes/index.ts +++ b/frontend/Functions/DatabaseFunctions/Notes/index.ts @@ -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 = async (db, pubKey, limit) => { - const notesQuery = ` + contants: boolean, + filters?: { + until?: number + }, +) => Promise = 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 = 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, diff --git a/frontend/Functions/DatabaseFunctions/Users/index.ts b/frontend/Functions/DatabaseFunctions/Users/index.ts index b9f2e76..a1a5c39 100644 --- a/frontend/Functions/DatabaseFunctions/Users/index.ts +++ b/frontend/Functions/DatabaseFunctions/Users/index.ts @@ -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 = 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 = async ( pubkey, db, diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index b25f064..af50925 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -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": { diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index 2861b3a..ed85c31 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -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" }, diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index 4ea2cbd..fe7ee2a 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -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" }, diff --git a/frontend/Pages/AboutPage/index.tsx b/frontend/Pages/AboutPage/index.tsx index 6965348..f78998a 100644 --- a/frontend/Pages/AboutPage/index.tsx +++ b/frontend/Pages/AboutPage/index.tsx @@ -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: {DeviceInfo.getVersion()}, + right: v0.2.1.5-alpha, onPress: () => {}, }, ], diff --git a/frontend/Pages/ConfigPage/index.tsx b/frontend/Pages/ConfigPage/index.tsx new file mode 100644 index 0000000..d4abd7c --- /dev/null +++ b/frontend/Pages/ConfigPage/index.tsx @@ -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(null) + + React.useEffect(() => {}, [showPublicImages, showSensitive, satoshi]) + + const createOptions = React.useMemo(() => { + return [ + { + key: 1, + title: s, + 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 ( + <> + ( + setShowPublicImages(value)} /> + )} + /> + ( + setShowSensitive(value)} /> + )} + /> + bottomSheetRef.current?.open()} + right={() => getSatoshiSymbol(25)} + /> + + { + return + }} + ItemSeparatorComponent={Divider} + /> + + + ) +} + +const styles = StyleSheet.create({ + satoshi: { + fontFamily: 'Satoshi-Symbol', + fontSize: 25, + }, +}) + +export default ConfigPage diff --git a/frontend/Pages/ContactsFeed/index.tsx b/frontend/Pages/ContactsFeed/index.tsx index b4a5e59..da15d06 100644 --- a/frontend/Pages/ContactsFeed/index.tsx +++ b/frontend/Pages/ContactsFeed/index.tsx @@ -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(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', diff --git a/frontend/Pages/FeedNavigator/index.tsx b/frontend/Pages/FeedNavigator/index.tsx index 9afbd29..336e235 100644 --- a/frontend/Pages/FeedNavigator/index.tsx +++ b/frontend/Pages/FeedNavigator/index.tsx @@ -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 = () => { + diff --git a/frontend/Pages/GlobalFeed/index.tsx b/frontend/Pages/GlobalFeed/index.tsx new file mode 100644 index 0000000..b7373c8 --- /dev/null +++ b/frontend/Pages/GlobalFeed/index.tsx @@ -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 = ({ 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([]) + const [lastLoadAt, setLastLoadAt] = useState(0) + const [newNotesCount, setNewNotesCount] = useState(0) + const [pageSize, setPageSize] = useState(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) => 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 = ({ item, index }) => { + return ( + + { + setProfileCardPubKey(user.id) + }} + showPreview={showPublicImages} + /> + + ) + } + + return ( + + {notes && notes.length > 0 ? ( + } + > + 0} + actions={[]} + icon={() => } + > + {t(newNotesCount < 2 ? 'homeFeed.newMessage' : 'homeFeed.newMessages', { + newNotesCount, + })} + + + {notes.length >= 10 && ( + + )} + + ) : ( + + + + {t('homeFeed.emptyTitle')} + + + {t('homeFeed.emptyDescription')} + + + + )} + + ) +} + +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 diff --git a/frontend/Pages/HomeFeed/index.tsx b/frontend/Pages/HomeFeed/index.tsx index 4f04dba..56fc737 100644 --- a/frontend/Pages/HomeFeed/index.tsx +++ b/frontend/Pages/HomeFeed/index.tsx @@ -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 = ({ 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([]) - const [pageSize, setPageSize] = useState(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() const bottomSheetProfileRef = React.useRef(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 = ({ 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) => 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 = ({ item, index }) => { - return ( - - { - setProfileCardPubKey(user.id) - bottomSheetProfileRef.current?.open() - }} - /> - - ) - } - const bottomSheetStyles = React.useMemo(() => { return { container: { @@ -168,37 +49,74 @@ export const HomeFeed: React.FC = ({ navigation }) => { } }, []) + const renderScene: Record = { + globalFeed: ( + { + setProfileCardPubKey(value) + bottomSheetProfileRef.current?.open() + }} + /> + ), + myFeed: ( + { + setProfileCardPubKey(value) + bottomSheetProfileRef.current?.open() + }} + /> + ), + } + return ( - - {notes && notes.length > 0 ? ( - } + + + - - {notes.length >= 10 && } - - ) : ( - - - - {t('homeFeed.emptyTitle')} - - - {t('homeFeed.emptyDescription')} - - + { + relayPool?.unsubscribe([ + 'homepage-contacts-main', + 'homepage-reactions', + 'homepage-contacts-meta', + 'homepage-replies', + ]) + setTabKey('globalFeed') + }} + > + {t('homeFeed.globalFeed')} + - )} + + { + relayPool?.unsubscribe(['homepage-global-main']) + setTabKey('myFeed') + }} + > + {t('homeFeed.myFeed')} + + + + {renderScene[tabKey]} {privateKey && ( = ({ navigation }) => { height={280} customStyles={bottomSheetStyles} > - + ) @@ -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 diff --git a/frontend/Pages/HomeNavigator/index.tsx b/frontend/Pages/HomeNavigator/index.tsx index a69943f..c878bb0 100644 --- a/frontend/Pages/HomeNavigator/index.tsx +++ b/frontend/Pages/HomeNavigator/index.tsx @@ -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 = () => { + void +} + +export const MyFeed: React.FC = ({ 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([]) + const [pageSize, setPageSize] = useState(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) => 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 = ({ item, index }) => { + return ( + + { + setProfileCardPubKey(user.id) + }} + /> + + ) + } + + return ( + + {notes && notes.length > 0 ? ( + } + > + + {notes.length >= initialPageSize && ( + + )} + + ) : ( + + + + {t('homeFeed.emptyTitle')} + + + {t('homeFeed.emptyDescription')} + + + + )} + + ) +} + +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 diff --git a/frontend/Pages/ProfileConfigPage/index.tsx b/frontend/Pages/ProfileConfigPage/index.tsx index 678c21d..f185a88 100644 --- a/frontend/Pages/ProfileConfigPage/index.tsx +++ b/frontend/Pages/ProfileConfigPage/index.tsx @@ -30,8 +30,7 @@ export const ProfileConfigPage: React.FC = () => { const bottomSheetLud06Ref = React.useRef(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() const [picture, setPicture] = useState() @@ -258,14 +257,6 @@ export const ProfileConfigPage: 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', [ diff --git a/frontend/Pages/RelaysPage/index.tsx b/frontend/Pages/RelaysPage/index.tsx index 2d058fc..8bb4ca1 100644 --- a/frontend/Pages/RelaysPage/index.tsx +++ b/frontend/Pages/RelaysPage/index.tsx @@ -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 ( - (relay.active ? desactiveRelay(relay) : activeRelay(relay))} - /> - ) - } - - const renderItem: ListRenderItem = ({ index, item }) => ( - relayToggle(item)} - onPress={() => { - setSelectedRelay(item) - bottomSheetEditRef.current?.open() - }} - /> - ) - const rbSheetCustomStyles = React.useMemo(() => { return { container: { @@ -113,22 +92,54 @@ export const RelaysPage: React.FC = () => { {t('relaysPage.myList')} - + {myRelays.length > 0 && + myRelays.map((relay, index) => { + return ( + ( + + relay.active ? desactiveRelay(relay) : activeRelay(relay) + } + /> + )} + onPress={() => { + setSelectedRelay(relay) + bottomSheetEditRef.current?.open() + }} + /> + ) + })} )} {t('relaysPage.recommended')} - { - 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 ( + ( + (relay.active ? desactiveRelay(relay) : activeRelay(relay))} + /> + )} + onPress={() => { + setSelectedRelay(relay) + bottomSheetEditRef.current?.open() + }} + /> + ) + })}