From f83f6d82da2a66f984b15cc2a3cd749519ca56ac Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Sun, 6 Nov 2022 19:57:52 +0100 Subject: [PATCH] Followers list --- frontend/Components/ContactsPage/index.tsx | 71 +++++++++++---- .../Components/LandingPage/Logger/index.tsx | 4 +- frontend/Components/ProfilePage/index.tsx | 19 +++- frontend/Components/RelaysPage/index.tsx | 4 +- frontend/Contexts/AppContext.tsx | 3 +- frontend/Contexts/RelayPoolContext.tsx | 9 +- .../DatabaseFunctions/Migrations/index.ts | 14 ++- .../DatabaseFunctions/Notes/index.ts | 2 +- .../DatabaseFunctions/Users/index.ts | 87 ++++++++++++------- frontend/Locales/en.json | 2 + 10 files changed, 151 insertions(+), 64 deletions(-) diff --git a/frontend/Components/ContactsPage/index.tsx b/frontend/Components/ContactsPage/index.tsx index 62aba1e..08b0da2 100644 --- a/frontend/Components/ContactsPage/index.tsx +++ b/frontend/Components/ContactsPage/index.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Input, Layout, Modal, useTheme } from '@ui-kitten/components' +import { Button, Card, Input, Layout, Modal, Tab, TabBar, useTheme } from '@ui-kitten/components' import React, { useCallback, useContext, useEffect, useState } from 'react' import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native' import { AppContext } from '../../Contexts/AppContext' @@ -6,9 +6,10 @@ import Icon from 'react-native-vector-icons/FontAwesome5' import { Event, EventKind } from '../../lib/nostr/Events' import { useTranslation } from 'react-i18next' import { - addContact, getUsers, - insertUserContact, + insertUserPets, + updateUserContact, + updateUserFollower, User, } from '../../Functions/DatabaseFunctions/Users' import UserCard from '../UserCard' @@ -25,6 +26,7 @@ export const ContactsPage: React.FC = () => { const [refreshing, setRefreshing] = useState(true) const [showAddContact, setShowAddContact] = useState(false) const [contactInput, setContactInput] = useState() + const [selectedTab, setSelectedTab] = useState(0) const { t } = useTranslation('common') @@ -33,15 +35,23 @@ export const ContactsPage: React.FC = () => { }, [lastEventId]) useEffect(() => { + setUsers([]) subscribeContacts() - }, []) + }, [selectedTab]) const loadUsers: () => void = () => { if (database && publicKey) { + setRefreshing(true) setTimeout(() => setRefreshing(false), 5000) - getUsers(database, { contacts: true }).then((results) => { - if (results) setUsers(results) - }) + if (selectedTab === 0) { + getUsers(database, { contacts: true }).then((results) => { + if (results) setUsers(results) + }) + } else { + getUsers(database, { followers: true }).then((results) => { + if (results) setUsers(results) + }) + } } } @@ -49,22 +59,36 @@ export const ContactsPage: React.FC = () => { relayPool?.unsubscribeAll() relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => { console.log('CONTACTS PAGE EVENT =======>', relay.url, event) - if (database && event?.id && event.kind === EventKind.petNames) { - insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? '')) - relayPool?.removeOn('event', 'contacts') + if (database && event?.id && publicKey && event.kind === EventKind.petNames) { + if (event.pubkey === publicKey) { + insertUserPets(event, database).finally(() => setLastEventId(event?.id ?? '')) + relayPool?.removeOn('event', 'contacts') + } else { + const isFollower = event.tags.some((tag) => tag[1] === publicKey) + updateUserFollower(event.pubkey, database, isFollower).finally(() => + setLastEventId(event?.id ?? ''), + ) + } } }) if (publicKey) { - relayPool?.subscribe('main-channel', { - kinds: [EventKind.petNames], - authors: [publicKey], - }) + if (selectedTab === 0) { + relayPool?.subscribe('main-channel', { + kinds: [EventKind.petNames], + authors: [publicKey], + }) + } else if (selectedTab === 1) { + relayPool?.subscribe('main-channel', { + kinds: [EventKind.petNames], + '#p': [publicKey], + }) + } } } const onPressAddContact: () => void = () => { if (contactInput && relayPool && database && publicKey) { - addContact(contactInput, database).then(() => { + updateUserContact(contactInput, database, true).then(() => { populatePets(relayPool, database, publicKey) setShowAddContact(false) loadUsers() @@ -76,6 +100,12 @@ export const ContactsPage: React.FC = () => { setRefreshing(true) relayPool?.unsubscribeAll() subscribeContacts() + if (users && users?.length > 0) { + relayPool?.subscribe('main-channel', { + kinds: [EventKind.meta], + authors: users?.map((user) => user.id), + }) + } }, []) const styles = StyleSheet.create({ @@ -103,10 +133,21 @@ export const ContactsPage: React.FC = () => { backdrop: { backgroundColor: 'rgba(0, 0, 0, 0.5)', }, + topBar: { + height: 64, + }, }) return ( <> + setSelectedTab(index)} + > + + + { if (database) { if (event.tags.length > 0) { setStatus(2) - insertUserContact(event, database).then(() => { + insertUserPets(event, database).then(() => { requestUserData(event) }) } else { diff --git a/frontend/Components/ProfilePage/index.tsx b/frontend/Components/ProfilePage/index.tsx index 760150a..41a3a60 100644 --- a/frontend/Components/ProfilePage/index.tsx +++ b/frontend/Components/ProfilePage/index.tsx @@ -22,7 +22,13 @@ import { AppContext } from '../../Contexts/AppContext' import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes' import NoteCard from '../NoteCard' import { RelayPoolContext } from '../../Contexts/RelayPoolContext' -import { getUser, removeContact, addContact, User } from '../../Functions/DatabaseFunctions/Users' +import { + getUser, + removeContact, + User, + updateUserFollower, + updateUserContact, +} from '../../Functions/DatabaseFunctions/Users' import { EventKind, Event } from '../../lib/nostr/Events' import Relay, { RelayFilters } from '../../lib/nostr/Relay' import Icon from 'react-native-vector-icons/FontAwesome5' @@ -35,7 +41,8 @@ import Avatar from '../Avatar' export const ProfilePage: React.FC = () => { const { database, page, goToPage, goBack } = useContext(AppContext) - const { publicKey, privateKey, lastEventId, relayPool } = useContext(RelayPoolContext) + const { publicKey, privateKey, lastEventId, relayPool, setLastEventId } = + useContext(RelayPoolContext) const theme = useTheme() const initialPageSize = 10 const [notes, setNotes] = useState() @@ -114,6 +121,12 @@ export const ProfilePage: React.FC = () => { setTimeout(() => setRefreshing(false), 5000) storeEvent(event, database) subscribeNotes() + if (publicKey && event.pubkey !== publicKey) { + const isFollower = event.tags.some((tag) => tag[1] === publicKey) + updateUserFollower(event.pubkey, database, isFollower).finally(() => + setLastEventId(event?.id ?? ''), + ) + } } } } @@ -153,7 +166,7 @@ export const ProfilePage: React.FC = () => { const addAuthor: () => void = () => { if (relayPool && database && publicKey) { - addContact(userId, database).then(() => { + updateUserContact(userId, database, true).then(() => { populatePets(relayPool, database, publicKey) setIsContact(true) }) diff --git a/frontend/Components/RelaysPage/index.tsx b/frontend/Components/RelaysPage/index.tsx index 7b03417..ebb00e6 100644 --- a/frontend/Components/RelaysPage/index.tsx +++ b/frontend/Components/RelaysPage/index.tsx @@ -22,7 +22,7 @@ export const RelaysPage: React.FC = () => { const { goBack, database } = useContext(AppContext) const { relayPool, publicKey, setRelayPool } = useContext(RelayPoolContext) const { t } = useTranslation('common') - const [relays, setRelays] = useState() + const [relays, setRelays] = useState([]) const [loading, setLoading] = useState(false) const loadRelays: () => void = () => { @@ -157,7 +157,7 @@ export const RelaysPage: React.FC = () => { /> }> - {relays?.length ? ( + {relays ? ( [...relays, ...defaultList()].map((item, index) => { return renderItem(item, index) }) diff --git a/frontend/Contexts/AppContext.tsx b/frontend/Contexts/AppContext.tsx index 62f04ab..683254e 100644 --- a/frontend/Contexts/AppContext.tsx +++ b/frontend/Contexts/AppContext.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { initDatabase } from '../Functions/DatabaseFunctions' -import { createInitDatabase } from '../Functions/DatabaseFunctions/Migrations' +import { createInitDatabase, runMigrations } from '../Functions/DatabaseFunctions/Migrations' import FlashMessage from 'react-native-flash-message' import SInfo from 'react-native-sensitive-info' import { BackHandler } from 'react-native' @@ -50,6 +50,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E setLoadingDb(false) }) } else { + runMigrations(db) setLoadingDb(false) } }) diff --git a/frontend/Contexts/RelayPoolContext.tsx b/frontend/Contexts/RelayPoolContext.tsx index 1ac5dc4..6805872 100644 --- a/frontend/Contexts/RelayPoolContext.tsx +++ b/frontend/Contexts/RelayPoolContext.tsx @@ -8,7 +8,6 @@ import { getRelays, addRelay } from '../Functions/DatabaseFunctions/Relays' import { showMessage } from 'react-native-flash-message' import SInfo from 'react-native-sensitive-info' import { getPublickey } from '../lib/nostr/Bip' -import { pickRandomItems } from '../Functions/NativeFunctions' import { defaultRelays } from '../Constants/RelayConstants' export interface RelayPoolContextProps { @@ -58,10 +57,10 @@ export const RelayPoolContextProvider = ({ initRelayPool.add(relay.url) }) } else { - pickRandomItems(defaultRelays, 2).forEach((relayUrl) => { - initRelayPool.add(relayUrl) - addRelay({ url: relayUrl }, database) - }) + // pickRandomItems(defaultRelays, 1).forEach((relayUrl) => { + initRelayPool.add(defaultRelays[4]) + addRelay({ url: defaultRelays[4] }, database) + // }) } initRelayPool?.on( diff --git a/frontend/Functions/DatabaseFunctions/Migrations/index.ts b/frontend/Functions/DatabaseFunctions/Migrations/index.ts index e892610..da4007e 100644 --- a/frontend/Functions/DatabaseFunctions/Migrations/index.ts +++ b/frontend/Functions/DatabaseFunctions/Migrations/index.ts @@ -1,7 +1,8 @@ -import { QuickSQLiteConnection } from 'react-native-quick-sqlite' -import { simpleExecute } from '..' +import { ColumnMetadata, QuickSQLiteConnection } from 'react-native-quick-sqlite' +import { dropTables, simpleExecute } from '..' export const createInitDatabase: (db: QuickSQLiteConnection) => Promise = async (db) => { + dropTables(db) await simpleExecute( ` CREATE TABLE IF NOT EXISTS nostros_notes( @@ -40,4 +41,13 @@ export const createInitDatabase: (db: QuickSQLiteConnection) => Promise = `, db, ) + await runMigrations(db) +} + +export const runMigrations: (db: QuickSQLiteConnection) => Promise = async (db) => { + const { metadata } = db.execute('SELECT * FROM nostros_users;') + // v0.1.8.1 alpha + if (!metadata?.some((meta: ColumnMetadata) => meta.columnName === 'follower')) { + await simpleExecute('ALTER TABLE nostros_users ADD COLUMN follower BOOLEAN DEFAULT FALSE;', db) + } } diff --git a/frontend/Functions/DatabaseFunctions/Notes/index.ts b/frontend/Functions/DatabaseFunctions/Notes/index.ts index e3cf2cc..92da690 100644 --- a/frontend/Functions/DatabaseFunctions/Notes/index.ts +++ b/frontend/Functions/DatabaseFunctions/Notes/index.ts @@ -81,7 +81,7 @@ export const getNotes: ( } else { notesQuery += 'WHERE ' } - notesQuery += 'nostros_users.contact = TRUE ' + notesQuery += 'nostros_users.contact = 1 ' } if (includeIds) { if (Object.keys(filters).length > 0 || contacts) { diff --git a/frontend/Functions/DatabaseFunctions/Users/index.ts b/frontend/Functions/DatabaseFunctions/Users/index.ts index cc5471f..5149806 100644 --- a/frontend/Functions/DatabaseFunctions/Users/index.ts +++ b/frontend/Functions/DatabaseFunctions/Users/index.ts @@ -10,6 +10,7 @@ export interface User { picture?: string about?: string contact?: boolean + follower?: boolean } const databaseToEntity: (object: object) => User = (object) => { @@ -32,9 +33,9 @@ export const insertUserMeta: ( const query = ` INSERT OR REPLACE INTO nostros_users - (id, name, picture, about, main_relay, contact) + (id, name, picture, about, main_relay, contact, follower) VALUES - (?,?,?,?,?, (SELECT contact FROM nostros_users WHERE id = ?)); + (?,?,?,?,?,(SELECT contact FROM nostros_users WHERE id = ?),(SELECT follower FROM nostros_users WHERE id = ?)); ` const queryParams = [ id, @@ -43,15 +44,36 @@ export const insertUserMeta: ( about.split("'").join("''"), mainRelay.split("'").join("''"), id, + id, ] - return await db.executeAsync(query, queryParams) } else { return null } } -export const insertUserContact: ( +export const updateUserFollower: ( + userId: string, + db: QuickSQLiteConnection, + follower: boolean, +) => Promise = async (userId, db, follower) => { + const userQuery = `UPDATE nostros_users SET follower = ? WHERE id = ?` + + await addUser(userId, db) + return db.execute(userQuery, [follower ? 1 : 0, userId]) +} +export const updateUserContact: ( + userId: string, + db: QuickSQLiteConnection, + contact: boolean, +) => Promise = async (userId, db, contact) => { + const userQuery = `UPDATE nostros_users SET contact = ? WHERE id = ?` + + await addUser(userId, db) + return db.execute(userQuery, [contact ? 1 : 0, userId]) +} + +export const insertUserPets: ( event: Event, db: QuickSQLiteConnection, ) => Promise = async (event, db) => { @@ -60,7 +82,7 @@ export const insertUserContact: ( if (valid && event.kind === EventKind.petNames) { const users: User[] = event.tags.map((tag) => tagToUser(tag)) users.map(async (user) => { - return await addContact(user.id, db) + return await updateUserContact(user.id, db, true) }) return users } else { @@ -88,51 +110,50 @@ export const removeContact: ( pubkey: string, db: QuickSQLiteConnection, ) => Promise = async (pubkey, db) => { - const userQuery = `UPDATE nostros_users SET contact = FALSE WHERE id = ?` + const userQuery = `UPDATE nostros_users SET contact = 0 WHERE id = ?` return db.execute(userQuery, [pubkey]) } -export const addContact: ( - pubKey: string, - db: QuickSQLiteConnection, -) => Promise = async (pubKey, db) => { +export const addUser: (pubKey: string, db: QuickSQLiteConnection) => Promise = async ( + pubKey, + db, +) => { const query = ` - INSERT INTO nostros_users (id, contact) - VALUES (?, ?) - ON CONFLICT (id) DO - UPDATE SET contact=excluded.contact; + INSERT OR IGNORE INTO nostros_users (id) VALUES (?) ` - - return db.execute(query, [pubKey, '1']) + return db.execute(query, [pubKey]) } export const getUsers: ( db: QuickSQLiteConnection, - options: { exludeIds?: string[]; contacts?: boolean; includeIds?: string[] }, -) => Promise = async (db, { exludeIds, contacts, includeIds }) => { + options: { exludeIds?: string[]; contacts?: boolean; followers?: boolean; includeIds?: string[] }, +) => Promise = async (db, { exludeIds, contacts, followers, includeIds }) => { let userQuery = 'SELECT * FROM nostros_users ' - if (contacts) { - userQuery += 'WHERE contact = TRUE ' - } + const filters = [] - if (exludeIds && exludeIds.length > 0) { - if (!contacts) { - userQuery += 'WHERE ' - } else { - userQuery += 'AND ' - } - userQuery += `id NOT IN ('${exludeIds.join("', '")}') ` + if (contacts) { + filters.push('contact = 1') } if (includeIds && includeIds.length > 0) { - if (!contacts && !exludeIds) { - userQuery += 'WHERE ' - } else { - userQuery += 'OR ' + filters.push(`id IN ('${includeIds.join("', '")}')`) + } + + if (followers) { + filters.push('follower = 1') + } + + if (filters.length > 0) { + userQuery += `WHERE ${filters.join(' OR ')} ` + if (exludeIds && exludeIds.length > 0) { + userQuery += `AND id NOT IN ('${exludeIds.join("', '")}') ` + } + } else { + if (exludeIds && exludeIds.length > 0) { + userQuery += `WHERE id NOT IN ('${exludeIds.join("', '")}') ` } - userQuery += `id IN ('${includeIds.join("', '")}') ` } userQuery += 'ORDER BY name,id' diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index fe374c6..7bb40ac 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -42,6 +42,8 @@ "privateKey": "Private Key" }, "contactsPage": { + "following": "Following", + "followers": "Followers", "add": "Add User", "addContact": { "placeholder": "Public Key",