Followers list

This commit is contained in:
KoalaSat 2022-11-06 19:57:52 +01:00
parent acb36bcc94
commit f83f6d82da
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
10 changed files with 151 additions and 64 deletions

View File

@ -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 React, { useCallback, useContext, useEffect, useState } from 'react'
import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native' import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'
import { AppContext } from '../../Contexts/AppContext' 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 { Event, EventKind } from '../../lib/nostr/Events'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
addContact,
getUsers, getUsers,
insertUserContact, insertUserPets,
updateUserContact,
updateUserFollower,
User, User,
} from '../../Functions/DatabaseFunctions/Users' } from '../../Functions/DatabaseFunctions/Users'
import UserCard from '../UserCard' import UserCard from '../UserCard'
@ -25,6 +26,7 @@ export const ContactsPage: React.FC = () => {
const [refreshing, setRefreshing] = useState(true) const [refreshing, setRefreshing] = useState(true)
const [showAddContact, setShowAddContact] = useState<boolean>(false) const [showAddContact, setShowAddContact] = useState<boolean>(false)
const [contactInput, setContactInput] = useState<string>() const [contactInput, setContactInput] = useState<string>()
const [selectedTab, setSelectedTab] = useState(0)
const { t } = useTranslation('common') const { t } = useTranslation('common')
@ -33,15 +35,23 @@ export const ContactsPage: React.FC = () => {
}, [lastEventId]) }, [lastEventId])
useEffect(() => { useEffect(() => {
setUsers([])
subscribeContacts() subscribeContacts()
}, []) }, [selectedTab])
const loadUsers: () => void = () => { const loadUsers: () => void = () => {
if (database && publicKey) { if (database && publicKey) {
setRefreshing(true)
setTimeout(() => setRefreshing(false), 5000) setTimeout(() => setRefreshing(false), 5000)
getUsers(database, { contacts: true }).then((results) => { if (selectedTab === 0) {
if (results) setUsers(results) 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?.unsubscribeAll()
relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => { relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => {
console.log('CONTACTS PAGE EVENT =======>', relay.url, event) console.log('CONTACTS PAGE EVENT =======>', relay.url, event)
if (database && event?.id && event.kind === EventKind.petNames) { if (database && event?.id && publicKey && event.kind === EventKind.petNames) {
insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? '')) if (event.pubkey === publicKey) {
relayPool?.removeOn('event', 'contacts') 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) { if (publicKey) {
relayPool?.subscribe('main-channel', { if (selectedTab === 0) {
kinds: [EventKind.petNames], relayPool?.subscribe('main-channel', {
authors: [publicKey], kinds: [EventKind.petNames],
}) authors: [publicKey],
})
} else if (selectedTab === 1) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.petNames],
'#p': [publicKey],
})
}
} }
} }
const onPressAddContact: () => void = () => { const onPressAddContact: () => void = () => {
if (contactInput && relayPool && database && publicKey) { if (contactInput && relayPool && database && publicKey) {
addContact(contactInput, database).then(() => { updateUserContact(contactInput, database, true).then(() => {
populatePets(relayPool, database, publicKey) populatePets(relayPool, database, publicKey)
setShowAddContact(false) setShowAddContact(false)
loadUsers() loadUsers()
@ -76,6 +100,12 @@ export const ContactsPage: React.FC = () => {
setRefreshing(true) setRefreshing(true)
relayPool?.unsubscribeAll() relayPool?.unsubscribeAll()
subscribeContacts() subscribeContacts()
if (users && users?.length > 0) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors: users?.map((user) => user.id),
})
}
}, []) }, [])
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -103,10 +133,21 @@ export const ContactsPage: React.FC = () => {
backdrop: { backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.5)',
}, },
topBar: {
height: 64,
},
}) })
return ( return (
<> <>
<TabBar
style={styles.topBar}
selectedIndex={selectedTab}
onSelect={(index) => setSelectedTab(index)}
>
<Tab title={t('contactsPage.following')} />
<Tab title={t('contactsPage.followers')} />
</TabBar>
<Layout style={styles.container} level='3'> <Layout style={styles.container} level='3'>
<ScrollView <ScrollView
horizontal={false} horizontal={false}

View File

@ -7,7 +7,7 @@ import { tagToUser } from '../../../Functions/RelayFunctions/Users'
import Relay from '../../../lib/nostr/Relay' import Relay from '../../../lib/nostr/Relay'
import { Event, EventKind } from '../../../lib/nostr/Events' import { Event, EventKind } from '../../../lib/nostr/Events'
import { AppContext } from '../../../Contexts/AppContext' import { AppContext } from '../../../Contexts/AppContext'
import { insertUserContact, insertUserMeta } from '../../../Functions/DatabaseFunctions/Users' import { insertUserPets, insertUserMeta } from '../../../Functions/DatabaseFunctions/Users'
import SInfo from 'react-native-sensitive-info' import SInfo from 'react-native-sensitive-info'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
import { generateRandomKey, getPublickey } from '../../../lib/nostr/Bip' import { generateRandomKey, getPublickey } from '../../../lib/nostr/Bip'
@ -75,7 +75,7 @@ export const Logger: React.FC = () => {
if (database) { if (database) {
if (event.tags.length > 0) { if (event.tags.length > 0) {
setStatus(2) setStatus(2)
insertUserContact(event, database).then(() => { insertUserPets(event, database).then(() => {
requestUserData(event) requestUserData(event)
}) })
} else { } else {

View File

@ -22,7 +22,13 @@ import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes' import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../NoteCard' import NoteCard from '../NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' 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 { EventKind, Event } from '../../lib/nostr/Events'
import Relay, { RelayFilters } from '../../lib/nostr/Relay' import Relay, { RelayFilters } from '../../lib/nostr/Relay'
import Icon from 'react-native-vector-icons/FontAwesome5' import Icon from 'react-native-vector-icons/FontAwesome5'
@ -35,7 +41,8 @@ import Avatar from '../Avatar'
export const ProfilePage: React.FC = () => { export const ProfilePage: React.FC = () => {
const { database, page, goToPage, goBack } = useContext(AppContext) 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 theme = useTheme()
const initialPageSize = 10 const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>() const [notes, setNotes] = useState<Note[]>()
@ -114,6 +121,12 @@ export const ProfilePage: React.FC = () => {
setTimeout(() => setRefreshing(false), 5000) setTimeout(() => setRefreshing(false), 5000)
storeEvent(event, database) storeEvent(event, database)
subscribeNotes() 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 = () => { const addAuthor: () => void = () => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
addContact(userId, database).then(() => { updateUserContact(userId, database, true).then(() => {
populatePets(relayPool, database, publicKey) populatePets(relayPool, database, publicKey)
setIsContact(true) setIsContact(true)
}) })

View File

@ -22,7 +22,7 @@ export const RelaysPage: React.FC = () => {
const { goBack, database } = useContext(AppContext) const { goBack, database } = useContext(AppContext)
const { relayPool, publicKey, setRelayPool } = useContext(RelayPoolContext) const { relayPool, publicKey, setRelayPool } = useContext(RelayPoolContext)
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [relays, setRelays] = useState<Relay[]>() const [relays, setRelays] = useState<Relay[]>([])
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const loadRelays: () => void = () => { const loadRelays: () => void = () => {
@ -157,7 +157,7 @@ export const RelaysPage: React.FC = () => {
/> />
<Layout style={styles.actionContainer} level='2'> <Layout style={styles.actionContainer} level='2'>
<ScrollView refreshControl={<RefreshControl refreshing={loading} />}> <ScrollView refreshControl={<RefreshControl refreshing={loading} />}>
{relays?.length ? ( {relays ? (
[...relays, ...defaultList()].map((item, index) => { [...relays, ...defaultList()].map((item, index) => {
return renderItem(item, index) return renderItem(item, index)
}) })

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { initDatabase } from '../Functions/DatabaseFunctions' 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 FlashMessage from 'react-native-flash-message'
import SInfo from 'react-native-sensitive-info' import SInfo from 'react-native-sensitive-info'
import { BackHandler } from 'react-native' import { BackHandler } from 'react-native'
@ -50,6 +50,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
setLoadingDb(false) setLoadingDb(false)
}) })
} else { } else {
runMigrations(db)
setLoadingDb(false) setLoadingDb(false)
} }
}) })

View File

@ -8,7 +8,6 @@ import { getRelays, addRelay } from '../Functions/DatabaseFunctions/Relays'
import { showMessage } from 'react-native-flash-message' import { showMessage } from 'react-native-flash-message'
import SInfo from 'react-native-sensitive-info' import SInfo from 'react-native-sensitive-info'
import { getPublickey } from '../lib/nostr/Bip' import { getPublickey } from '../lib/nostr/Bip'
import { pickRandomItems } from '../Functions/NativeFunctions'
import { defaultRelays } from '../Constants/RelayConstants' import { defaultRelays } from '../Constants/RelayConstants'
export interface RelayPoolContextProps { export interface RelayPoolContextProps {
@ -58,10 +57,10 @@ export const RelayPoolContextProvider = ({
initRelayPool.add(relay.url) initRelayPool.add(relay.url)
}) })
} else { } else {
pickRandomItems(defaultRelays, 2).forEach((relayUrl) => { // pickRandomItems(defaultRelays, 1).forEach((relayUrl) => {
initRelayPool.add(relayUrl) initRelayPool.add(defaultRelays[4])
addRelay({ url: relayUrl }, database) addRelay({ url: defaultRelays[4] }, database)
}) // })
} }
initRelayPool?.on( initRelayPool?.on(

View File

@ -1,7 +1,8 @@
import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { ColumnMetadata, QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { simpleExecute } from '..' import { dropTables, simpleExecute } from '..'
export const createInitDatabase: (db: QuickSQLiteConnection) => Promise<void> = async (db) => { export const createInitDatabase: (db: QuickSQLiteConnection) => Promise<void> = async (db) => {
dropTables(db)
await simpleExecute( await simpleExecute(
` `
CREATE TABLE IF NOT EXISTS nostros_notes( CREATE TABLE IF NOT EXISTS nostros_notes(
@ -40,4 +41,13 @@ export const createInitDatabase: (db: QuickSQLiteConnection) => Promise<void> =
`, `,
db, db,
) )
await runMigrations(db)
}
export const runMigrations: (db: QuickSQLiteConnection) => Promise<void> = 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)
}
} }

View File

@ -81,7 +81,7 @@ export const getNotes: (
} else { } else {
notesQuery += 'WHERE ' notesQuery += 'WHERE '
} }
notesQuery += 'nostros_users.contact = TRUE ' notesQuery += 'nostros_users.contact = 1 '
} }
if (includeIds) { if (includeIds) {
if (Object.keys(filters).length > 0 || contacts) { if (Object.keys(filters).length > 0 || contacts) {

View File

@ -10,6 +10,7 @@ export interface User {
picture?: string picture?: string
about?: string about?: string
contact?: boolean contact?: boolean
follower?: boolean
} }
const databaseToEntity: (object: object) => User = (object) => { const databaseToEntity: (object: object) => User = (object) => {
@ -32,9 +33,9 @@ export const insertUserMeta: (
const query = ` const query = `
INSERT OR REPLACE INTO nostros_users INSERT OR REPLACE INTO nostros_users
(id, name, picture, about, main_relay, contact) (id, name, picture, about, main_relay, contact, follower)
VALUES VALUES
(?,?,?,?,?, (SELECT contact FROM nostros_users WHERE id = ?)); (?,?,?,?,?,(SELECT contact FROM nostros_users WHERE id = ?),(SELECT follower FROM nostros_users WHERE id = ?));
` `
const queryParams = [ const queryParams = [
id, id,
@ -43,15 +44,36 @@ export const insertUserMeta: (
about.split("'").join("''"), about.split("'").join("''"),
mainRelay.split("'").join("''"), mainRelay.split("'").join("''"),
id, id,
id,
] ]
return await db.executeAsync(query, queryParams) return await db.executeAsync(query, queryParams)
} else { } else {
return null return null
} }
} }
export const insertUserContact: ( export const updateUserFollower: (
userId: string,
db: QuickSQLiteConnection,
follower: boolean,
) => Promise<QueryResult | null> = 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<QueryResult | null> = 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, event: Event,
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
) => Promise<User[] | null> = async (event, db) => { ) => Promise<User[] | null> = async (event, db) => {
@ -60,7 +82,7 @@ export const insertUserContact: (
if (valid && event.kind === EventKind.petNames) { if (valid && event.kind === EventKind.petNames) {
const users: User[] = event.tags.map((tag) => tagToUser(tag)) const users: User[] = event.tags.map((tag) => tagToUser(tag))
users.map(async (user) => { users.map(async (user) => {
return await addContact(user.id, db) return await updateUserContact(user.id, db, true)
}) })
return users return users
} else { } else {
@ -88,51 +110,50 @@ export const removeContact: (
pubkey: string, pubkey: string,
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
) => Promise<QueryResult> = async (pubkey, db) => { ) => Promise<QueryResult> = 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]) return db.execute(userQuery, [pubkey])
} }
export const addContact: ( export const addUser: (pubKey: string, db: QuickSQLiteConnection) => Promise<QueryResult> = async (
pubKey: string, pubKey,
db: QuickSQLiteConnection, db,
) => Promise<QueryResult> = async (pubKey, db) => { ) => {
const query = ` const query = `
INSERT INTO nostros_users (id, contact) INSERT OR IGNORE INTO nostros_users (id) VALUES (?)
VALUES (?, ?)
ON CONFLICT (id) DO
UPDATE SET contact=excluded.contact;
` `
return db.execute(query, [pubKey])
return db.execute(query, [pubKey, '1'])
} }
export const getUsers: ( export const getUsers: (
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
options: { exludeIds?: string[]; contacts?: boolean; includeIds?: string[] }, options: { exludeIds?: string[]; contacts?: boolean; followers?: boolean; includeIds?: string[] },
) => Promise<User[]> = async (db, { exludeIds, contacts, includeIds }) => { ) => Promise<User[]> = async (db, { exludeIds, contacts, followers, includeIds }) => {
let userQuery = 'SELECT * FROM nostros_users ' let userQuery = 'SELECT * FROM nostros_users '
if (contacts) { const filters = []
userQuery += 'WHERE contact = TRUE '
}
if (exludeIds && exludeIds.length > 0) { if (contacts) {
if (!contacts) { filters.push('contact = 1')
userQuery += 'WHERE '
} else {
userQuery += 'AND '
}
userQuery += `id NOT IN ('${exludeIds.join("', '")}') `
} }
if (includeIds && includeIds.length > 0) { if (includeIds && includeIds.length > 0) {
if (!contacts && !exludeIds) { filters.push(`id IN ('${includeIds.join("', '")}')`)
userQuery += 'WHERE ' }
} else {
userQuery += 'OR ' 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' userQuery += 'ORDER BY name,id'

View File

@ -42,6 +42,8 @@
"privateKey": "Private Key" "privateKey": "Private Key"
}, },
"contactsPage": { "contactsPage": {
"following": "Following",
"followers": "Followers",
"add": "Add User", "add": "Add User",
"addContact": { "addContact": {
"placeholder": "Public Key", "placeholder": "Public Key",