Profile reactions 2

This commit is contained in:
KoalaSat 2023-02-24 22:36:40 +01:00
parent 88f109a7e6
commit d87968dbb6
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
32 changed files with 1094 additions and 366 deletions

View File

@ -183,7 +183,7 @@ public class Event {
String name = parts[0];
String domain = parts[1];
if (!name.matches("^[a-zA-Z0-9-_]+$")) return false;
if (name.length() == 0) return false;
try {
String url = "https://" + domain + "/.well-known/nostr.json?name=" + name;
@ -213,7 +213,7 @@ public class Event {
String name = parts[0];
String domain = parts[1];
if (!name.matches("^[a-zA-Z0-9-_]+$")) return "";
if (name.length() == 0) return "";
try {
String url = "https://" + domain + "/.well-known/lnurlp/" + name;

View File

@ -55,14 +55,4 @@ public class Relay {
values.put("deleted_at", 0);
database.replace("nostros_relays", null, values);
}
public void delete(SQLiteDatabase database) {
String whereClause = "url = ?";
String[] whereArgs = new String[] {
url
};
ContentValues values = new ContentValues();
values.put("deleted_at", System.currentTimeMillis() / 1000L);
database.update ("nostros_relays", values, whereClause, whereArgs);
}
}

View File

@ -1,6 +1,7 @@
package com.nostros.modules;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
@ -172,18 +173,7 @@ public class DatabaseModule {
database.execSQL("DROP INDEX nostros_notes_notifications_index;");
} catch (SQLException e) { }
try {
database.execSQL("CREATE INDEX nostros_notes_relays_notes_index ON nostros_notes_relays(note_id, relay_url);");
database.execSQL("CREATE INDEX nostros_notes_relays_users_index ON nostros_notes_relays(pubkey, relay_url);");
database.execSQL("CREATE INDEX nostros_direct_messages_feed_index ON nostros_direct_messages(pubkey, created_at); ");
database.execSQL("CREATE INDEX nostros_direct_messages_notification_index ON nostros_direct_messages(pubkey, read); ");
database.execSQL("CREATE INDEX nostros_direct_messages_conversation_index ON nostros_direct_messages(created_at, conversation_id); ");
database.execSQL("CREATE INDEX nostros_reactions_pubkey_index ON nostros_reactions(pubkey); ");
database.execSQL("CREATE INDEX nostros_reactions_reacted_event_id_index ON nostros_reactions(reacted_event_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_follower_index ON nostros_users(contact, follower); ");
database.execSQL("CREATE INDEX nostros_users_names_index ON nostros_users(id, name); ");
database.execSQL("CREATE INDEX nostros_users_contacts_index ON nostros_users(id, contact); ");
database.execSQL("CREATE INDEX nostros_users_blocked_index ON nostros_users(id, blocked); ");
@ -199,6 +189,24 @@ public class DatabaseModule {
database.execSQL("CREATE INDEX nostros_group_messages_mentions_index ON nostros_group_messages(group_id, pubkey, created_at);");
database.execSQL("CREATE INDEX nostros_group_messages_group_index ON nostros_group_messages(group_id, created_at);");
database.execSQL("CREATE INDEX nostros_group_messages_feed_index ON nostros_group_messages(user_mentioned, read, group_id);");
database.execSQL("CREATE INDEX nostros_notes_relays_notes_index ON nostros_notes_relays(note_id, relay_url);");
database.execSQL("CREATE INDEX nostros_notes_relays_users_index ON nostros_notes_relays(pubkey, relay_url);");
database.execSQL("CREATE INDEX nostros_direct_messages_feed_index ON nostros_direct_messages(pubkey, created_at); ");
database.execSQL("CREATE INDEX nostros_direct_messages_notification_index ON nostros_direct_messages(pubkey, read); ");
database.execSQL("CREATE INDEX nostros_direct_messages_conversation_index ON nostros_direct_messages(created_at, conversation_id); ");
// Previous
database.execSQL("CREATE INDEX nostros_users_contact_follower_index ON nostros_users(contact, follower); ");
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_notes_pubkey_index ON nostros_notes(pubkey); ");
database.execSQL("CREATE INDEX nostros_notes_main_event_id_index ON nostros_notes(main_event_id); ");
database.execSQL("CREATE INDEX nostros_direct_messages_pubkey_index ON nostros_direct_messages(pubkey); ");
database.execSQL("CREATE INDEX nostros_direct_messages_conversation_id_index ON nostros_direct_messages(conversation_id); ");
database.execSQL("CREATE INDEX nostros_reactions_reacted_event_id_index ON nostros_reactions(reacted_event_id); ");
database.execSQL("CREATE INDEX nostros_users_contact_index ON nostros_users(contact); ");
database.execSQL("CREATE INDEX nostros_reactions_pubkey_index ON nostros_reactions(pubkey); ");
} catch (SQLException e) { }
}
@ -211,8 +219,14 @@ public class DatabaseModule {
relay.save(database);
}
public void deleteRelay(Relay relay) {
relay.delete(database);
public void deleteRelay(String relayUrl) {
String whereClause = "url = ?";
String[] whereArgs = new String[] {
relayUrl
};
ContentValues values = new ContentValues();
values.put("deleted_at", System.currentTimeMillis() / 1000L);
database.update ("nostros_relays", values, whereClause, whereArgs);
}
public List<Relay> getRelays(ReactApplicationContext reactContext) {

View File

@ -55,10 +55,9 @@ public class RelayPoolModule extends ReactContextBaseJavaModule {
if(url.equals(relay.url)){
relay.disconnect();
iterator.remove();
database.deleteRelay(relay);
}
}
database.deleteRelay(url);
callback.invoke();
}

View File

@ -50,7 +50,7 @@ export const LnPayment: React.FC<LnPaymentProps> = ({ open, setOpen, note, user
setMonto('')
if (open) {
if (database && note?.id) {
getZaps(database, note?.id).then((results) => {
getZaps(database, { eventId: note?.id }).then((results) => {
relayPool?.subscribe('zappers-meta', [
{
kinds: [Kind.Metadata],
@ -68,7 +68,7 @@ export const LnPayment: React.FC<LnPaymentProps> = ({ open, setOpen, note, user
useEffect(() => {
if (database && note?.id) {
getZaps(database, note?.id).then((results) => {
getZaps(database, { eventId: note?.id }).then((results) => {
setZaps(results)
setZapsUpdated(getUnixTime(new Date()))
})

View File

@ -30,7 +30,7 @@ export const NoteActions: React.FC<NoteActionsProps> = ({ bottomSheetRef }) => {
const loadNote: () => void = () => {
if (database && displayNoteDrawer) {
getNotes(database, { filters: { id: displayNoteDrawer } }).then((results) => {
getNotes(database, { filters: { id: [displayNoteDrawer] } }).then((results) => {
if (results.length > 0) {
setNote(results[0])
}

View File

@ -8,7 +8,7 @@ import {
Note,
NoteRelay,
} from '../../Functions/DatabaseFunctions/Notes'
import { StyleSheet, TouchableNativeFeedback, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { AppContext } from '../../Contexts/AppContext'
import { t } from 'i18next'
@ -134,7 +134,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({
})
}
if (showRepostPreview && note.repost_id) {
getNotes(database, { filters: { id: note.repost_id } }).then((events) => {
getNotes(database, { filters: { id: [note.repost_id] } }).then((events) => {
if (events.length > 0) {
setRepost(events[0])
}
@ -482,16 +482,15 @@ export const NoteCard: React.FC<NoteCardProps> = ({
{relayColouring &&
showRelayColors &&
relays.map((relay, index) => (
<TouchableNativeFeedback key={relay.relay_url}>
<View
style={[
styles.relay,
{ borderBottomColor: relayToColor(relay.relay_url) },
index === 0 ? { borderBottomLeftRadius: 50 } : {},
index === relays.length - 1 ? { borderBottomRightRadius: 50 } : {},
]}
/>
</TouchableNativeFeedback>
<View
key={relay.relay_url}
style={[
styles.relay,
{ borderBottomColor: relayToColor(relay.relay_url) },
index === 0 ? { borderBottomLeftRadius: 50 } : {},
index === relays.length - 1 ? { borderBottomRightRadius: 50 } : {},
]}
/>
))}
</Card.Content>
</Card>

View File

@ -0,0 +1,64 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Text, TouchableRipple, useTheme } from 'react-native-paper'
interface TabsProps {
tabs: string[]
activeTab: string
setActiveTab: (activeTab: string) => void
}
export const Tabs: React.FC<TabsProps> = ({ tabs, activeTab, setActiveTab }) => {
const theme = useTheme()
const { t } = useTranslation('common')
return (
<View style={styles.tabsNavigator}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{tabs.map((tabKey) => (
<View
key={tabKey}
style={[
styles.tab,
{
borderBottomColor: tabKey === activeTab ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === activeTab ? 3 : 1,
},
]}
>
<TouchableRipple
style={styles.textWrapper}
onPress={() => {
setActiveTab(tabKey)
}}
>
<Text style={styles.tabText}>{t(`tabs.${tabKey}`)}</Text>
</TouchableRipple>
</View>
))}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
tabText: {
textAlign: 'center',
},
tabsNavigator: {
flexDirection: 'row',
alignItems: 'center',
height: 48,
},
tab: {
width: 160,
},
textWrapper: {
justifyContent: 'center',
height: '100%',
textAlign: 'center',
},
})
export default Tabs

View File

@ -36,11 +36,11 @@ export const getMainNotes: (
db: QuickSQLiteConnection,
pubKey: string,
limit: number,
contants: boolean,
filters?: {
contants?: boolean,
until?: number
},
) => Promise<Note[]> = async (db, pubKey, limit, contants, filters) => {
) => Promise<Note[]> = async (db, pubKey, limit, filters) => {
let notesQuery = `
SELECT
nostros_notes.*, nostros_users.zap_pubkey, nostros_users.nip05, nostros_users.blocked, nostros_users.valid_nip05,
@ -51,8 +51,11 @@ export const getMainNotes: (
WHERE
`
if (contants)
if (filters?.contants) {
notesQuery += `(nostros_users.contact = 1 OR nostros_notes.pubkey = '${pubKey}') AND `
} else {
notesQuery += `nostros_notes.pubkey = '${pubKey}' AND `
}
if (filters?.until) notesQuery += `nostros_notes.created_at <= ${filters?.until} AND `
@ -68,6 +71,40 @@ export const getMainNotes: (
return notes
}
export const getReplyNotes: (
db: QuickSQLiteConnection,
pubKey: string,
limit: number,
filters?: {
until?: number
},
) => Promise<Note[]> = async (db, pubKey, limit, filters) => {
let notesQuery = `
SELECT
nostros_notes.*, nostros_users.zap_pubkey, nostros_users.nip05, nostros_users.blocked, nostros_users.valid_nip05,
nostros_users.ln_address, 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_notes.pubkey = '${pubKey}' AND
`
if (filters?.until) notesQuery += `nostros_notes.created_at <= ${filters?.until} AND `
notesQuery += `
nostros_notes.main_event_id IS NOT NULL AND
nostros_notes.repost_id IS NULL
ORDER BY created_at DESC
LIMIT ${limit}
`
const resultSet = await db.execute(notesQuery)
const items: object[] = getItems(resultSet)
const notes: Note[] = items.map((object) => databaseToEntity(object))
return notes
}
export const getMainNotesCount: (
db: QuickSQLiteConnection,
@ -299,7 +336,7 @@ export const getRawUserNotes: (
export const getNotes: (
db: QuickSQLiteConnection,
options: {
filters?: Record<string, string>
filters?: Record<string, string[]>
limit?: number
contacts?: boolean
includeIds?: string[]
@ -319,7 +356,7 @@ export const getNotes: (
if (Object.keys(filters).length > 0) {
notesQuery += 'WHERE '
keys.forEach((column, index) => {
notesQuery += `nostros_notes.${column} = '${filters[column]}' `
notesQuery += `nostros_notes.${column} IN ('${filters[column].join("', '")}') `
if (index < keys.length - 1) notesQuery += 'AND '
})
}

View File

@ -1,3 +1,4 @@
import { getUnixTime } from 'date-fns'
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..'
import { Event } from '../../../lib/nostr/Events'
@ -38,11 +39,58 @@ export const getZapsAmount: (
return item['SUM(amount)'] ?? 0
}
export const getZaps: (db: QuickSQLiteConnection, eventId: string) => Promise<Zap[]> = async (
export const getMostZapedNotes: (
db: QuickSQLiteConnection,
publicKey: string,
limit: number,
since: number
) => Promise<Zap[]> = async (db, publicKey, limit, since) => {
const zapsQuery = `
SELECT
SUM(amount) as total, *
FROM
nostros_zaps
WHERE zapped_user_id = '${publicKey}'
AND created_at > ${since}
GROUP BY zapped_event_id
ORDER BY total DESC
LIMIT ${limit}
`
const resultSet = await db.execute(zapsQuery)
const items: object[] = getItems(resultSet)
const zaps: Zap[] = items.map((object) => databaseToEntity(object))
return zaps
}
export const getMostZapedNotesContacts: (
db: QuickSQLiteConnection,
since: number
) => Promise<Zap[]> = async (db, since) => {
const zapsQuery = `
SELECT
SUM(amount) as total, nostros_zaps.*
FROM
nostros_zaps
LEFT JOIN
nostros_users ON nostros_users.id = nostros_zaps.zapped_user_id
WHERE nostros_zaps.created_at > ${since}
AND nostros_users.contact = 1
GROUP BY nostros_zaps.zapped_event_id
ORDER BY total DESC
`
const resultSet = await db.execute(zapsQuery)
const items: object[] = getItems(resultSet)
const zaps: Zap[] = items.map((object) => databaseToEntity(object))
return zaps
}
export const getZaps: (db: QuickSQLiteConnection, filters: { eventId?: string, zapperId?: string, limit?: number }) => Promise<Zap[]> = async (
db,
eventId,
filters,
) => {
const groupsQuery = `
let groupsQuery = `
SELECT
nostros_zaps.*, nostros_users.name, nostros_users.id as user_id, nostros_users.picture, nostros_users.valid_nip05,
nostros_users.nip05, nostros_users.lnurl, nostros_users.ln_address
@ -50,11 +98,28 @@ export const getZaps: (db: QuickSQLiteConnection, eventId: string) => Promise<Za
nostros_zaps
LEFT JOIN
nostros_users ON nostros_users.id = nostros_zaps.zapper_user_id
WHERE zapped_event_id = "${eventId}"
`
if (filters.eventId) {
groupsQuery += `
WHERE zapped_event_id = "${filters.eventId}"
`
}
if (filters.zapperId) {
groupsQuery += `
WHERE zapped_user_id = "${filters.zapperId}"
`
}
if (filters.limit) {
groupsQuery += `
LIMIT ${filters.limit}
`
}
const resultSet = await db.execute(groupsQuery)
const items: object[] = getItems(resultSet)
const notes: Zap[] = items.map((object) => databaseToEntity(object))
const zaps: Zap[] = items.map((object) => databaseToEntity(object))
return notes
return zaps
}

View File

@ -199,15 +199,20 @@
"emptyTitle": "Du hast noch nichts repostet",
"emptyDescription": "Hier sind Notizen, du du repostet hast"
},
"homeFeed": {
"emptyTitle": "Du folgst niemandem",
"emptyDescription": "Folge anderen um hier etwas zu sehen",
"emptyButton": "Kontakte",
"tabs": {
"globalFeed": "Globaler Feed",
"myFeed": "Mein Feed",
"reactions": "Reaktionen",
"mentions": "Erwähnungen",
"reposts": "Reposts",
"zaps": "Most zapped",
"notes": "Notes",
"replies": "Replies"
},
"homeFeed": {
"emptyTitle": "Du folgst niemandem",
"emptyDescription": "Folge anderen um hier etwas zu sehen",
"emptyButton": "Kontakte",
"newMessage": "{{newNotesCount}} neue Nachricht. Zum Aktualisieren tippen.",
"newMessages": "{{newNotesCount}} neue Nachrichten. Zum Aktualisieren tippen."
},

View File

@ -199,15 +199,20 @@
"emptyTitle": "You still didn't reposted",
"emptyDescription": "See here the notes you reposted."
},
"homeFeed": {
"emptyTitle": "You are not following anyone",
"emptyDescription": "Follow other profiles to see content.",
"emptyButton": "Go to contacts",
"tabs": {
"globalFeed": "Global feed",
"myFeed": "My feed",
"reactions": "Reactions",
"mentions": "Mentions",
"reposts": "Reposts",
"zaps": "Most zapped",
"notes": "Notes",
"replies": "Replies"
},
"homeFeed": {
"emptyTitle": "You are not following anyone",
"emptyDescription": "Follow other profiles to see content.",
"emptyButton": "Go to contacts",
"newMessage": "{{newNotesCount}} new note. Pull to refresh.",
"newMessages": "{{newNotesCount}} new notes. Pull to refresh."
},

View File

@ -212,14 +212,22 @@
"nips": "NIPs",
"version": "Version"
},
"tabs": {
"myFeed": "Mi feed",
"globalFeed": "Feed global",
"reactions": "Reacciones",
"mentions": "Menciones",
"reposts": "Reposteados",
"zaps": "Más zappeados",
"notes": "Notas",
"replies": "Respuestas"
},
"homeFeed": {
"emptyTitle": "No sigues a nadie",
"emptyDescription": "Sigue otros perfiles para ver contenido aquí.",
"emptyButton": "Ir a contactos",
"globalFeed": "Feed global",
"newMessage": "{{newNotesCount}} nota nueva. Desliza para recargar.",
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar.",
"myFeed": "Mi feed"
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar."
},
"relayCard": {
"pushDone": "Completado",

View File

@ -218,14 +218,21 @@
"nips": "NIPs",
"version": "Version"
},
"tabs": {
"myFeed": "Mon flux",
"globalFeed": "Flux global",
"reactions": "Reactions",
"mentions": "Mentions",
"reposts": "Reposts",
"zaps": "Most zapped",
"notes": "Notes",
"replies": "Replies"
},
"homeFeed": {
"emptyTitle": "Vous ne suivez personne",
"emptyDescription": "Suivez les autres profils pour voir le contenu ici.",
"emptyButton": "Accéder aux contacts",
"globalFeed": "Flux global",
"newMessage": "{{newNotesCount}} nouvelle note. Balayez pour recharger.",
"newMessages": "{{newNotesCount}} nouvelles notes. Balayez pour recharger.",
"myFeed": "Mon flux"
"newMessages": "{{newNotesCount}} nouvelles notes. Balayez pour recharger."
},
"relayCard": {
"pushDone": "Completed",

View File

@ -212,14 +212,22 @@
"nips": "NIPs",
"version": "Version"
},
"tabs": {
"myFeed": "My feed",
"globalFeed": "Global feed",
"reactions": "Reactions",
"mentions": "Mentions",
"reposts": "Reposts",
"zaps": "Most zapped",
"notes": "Notes",
"replies": "Replies"
},
"homeFeed": {
"emptyTitle": "Вы ни на кого не подписаны",
"emptyDescription": "Подпишитесь на другие профили, что бы увидеть, чем они делятся",
"emptyButton": "Посмотреть контакты",
"globalFeed": "Global feed",
"newMessage": "{{newNotesCount}} nota nueva. Desliza para recargar.",
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar.",
"myFeed": "My feed"
"newMessages": "{{newNotesCount}} notas nuevas. Desliza para refrescar."
},
"relayCard": {
"pushDone": "Completed",

View File

@ -218,15 +218,20 @@
"emptyTitle": "您还没有转发任何 Notes",
"emptyDescription": "查看您您转发过的 Notes"
},
"homeFeed": {
"emptyTitle": "您还没有关注任何人",
"emptyDescription": "关注其他人以查看内容",
"emptyButton": "查看联系人",
"tabs": {
"globalFeed": "发现",
"myFeed": "关注中",
"reactions": "回应",
"mentions": "提及",
"reposts": "转发",
"zaps": "Most zapped",
"notes": "Notes",
"replies": "Replies"
},
"homeFeed": {
"emptyTitle": "您还没有关注任何人",
"emptyDescription": "关注其他人以查看内容",
"emptyButton": "查看联系人",
"newMessage": "有{{newNotesCount}}条新的 Notes下拉刷新",
"newMessages": "有{{newNotesCount}}条新的 Notes下拉刷新"
},

View File

@ -364,7 +364,6 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
renderItem={renderDirectMessageItem}
horizontal={false}
ref={scrollViewRef}
estimatedItemSize={100}
onScroll={onScroll}
/>
{reply ? (

View File

@ -120,6 +120,10 @@ export const GroupsFeed: React.FC = () => {
kinds: [Kind.ChannelMetadata],
'#e': results.map((group) => group.id ?? ''),
},
{
kinds: [Kind.ChannelMessage],
'#e': results.map((group) => group.id ?? '')
},
])
}
})

View File

@ -7,47 +7,45 @@ import {
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 { 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 { RelayFilters } from '../../../lib/nostr/RelayPool/intex'
import { Chip, Button, Text } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import NoteCard from '../../../Components/NoteCard'
import { useTheme } from '@react-navigation/native'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { t } from 'i18next'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import { getUnixTime } from 'date-fns'
interface GlobalFeedProps {
navigation: any
updateLastLoad: () => void
lastLoadAt: number
pageSize: number
setPageSize: (pageSize: number) => void
}
export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
export const GlobalFeed: React.FC<GlobalFeedProps> = ({
navigation,
updateLastLoad,
lastLoadAt,
pageSize,
setPageSize,
}) => {
const initialPageSize = 10
const theme = useTheme()
const { database, showPublicImages, pushedTab } = 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)
const flashListRef = React.useRef<FlashList<Note>>(null)
const unsubscribe: () => void = () => {
relayPool?.unsubscribe(['homepage-global-main', 'homepage-global-meta-repost'])
}
useEffect(() => {
unsubscribe()
subscribeNotes()
}, [])
useEffect(() => {
if (pushedTab) {
flashListRef.current?.scrollToIndex({ animated: true, index: 0 })
@ -58,42 +56,15 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId, lastLoadAt])
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
}, [pageSize])
const updateLastLoad: () => void = () => {
setLastLoadAt(getUnixTime(new Date()) - 5)
}
}, [lastEventId, lastConfirmationtId, lastLoadAt, relayPool, publicKey])
const onRefresh = useCallback(() => {
setRefreshing(true)
updateLastLoad()
setNewNotesCount(0)
unsubscribe()
subscribeNotes()
updateLastLoad()
flashListRef.current?.scrollToIndex({ animated: true, index: 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 = lastLoadAt
relayPool?.subscribe('homepage-global-main', [message])
setRefreshing(false)
updateLastLoad()
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
@ -105,7 +76,7 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
if (lastLoadAt > 0) {
getMainNotesCount(database, lastLoadAt).then(setNewNotesCount)
}
getMainNotes(database, publicKey, pageSize, false, {
getMainNotes(database, publicKey, pageSize, {
until: lastLoadAt,
}).then((results) => {
setRefreshing(false)
@ -121,7 +92,7 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
ids: repostIds,
},
]
relayPool?.subscribe('homepage-global-meta-repost', message)
relayPool?.subscribe('homepage-global-reposts', message)
}
}
})
@ -168,7 +139,6 @@ export const GlobalFeed: React.FC<GlobalFeedProps> = ({ navigation }) => {
<View>
<View style={styles.list}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}

View File

@ -1,5 +1,4 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import {
NativeScrollEvent,
NativeSyntheticEvent,
@ -7,15 +6,15 @@ import {
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { AppContext } from '../../../Contexts/AppContext'
import { 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 { RelayFilters } from '../../../lib/nostr/RelayPool/intex'
import { ActivityIndicator, Button, Text } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import NoteCard from '../../../Components/NoteCard'
import { useTheme } from '@react-navigation/native'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
@ -23,9 +22,12 @@ import { useTranslation } from 'react-i18next'
interface MyFeedProps {
navigation: any
updateLastLoad: () => void
pageSize: number
setPageSize: (pageSize: number) => void
}
export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
export const MyFeed: React.FC<MyFeedProps> = ({ navigation, updateLastLoad, pageSize, setPageSize }) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { database, pushedTab } = useContext(AppContext)
@ -33,7 +35,6 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
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 flashListRef = React.useRef<FlashList<Note>>(null)
@ -43,26 +44,15 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
}
}, [pushedTab])
useEffect(() => {
subscribeNotes()
loadNotes()
}, [])
useEffect(() => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId])
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
}, [pageSize])
}, [lastEventId, lastConfirmationtId, relayPool, publicKey])
const onRefresh = useCallback(() => {
setRefreshing(true)
subscribeNotes()
updateLastLoad()
}, [])
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
@ -71,24 +61,11 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
}
}
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 loadNotes: () => void = async () => {
if (database && publicKey) {
getMainNotes(database, publicKey, pageSize, true).then(async (notes) => {
getMainNotes(database, publicKey, pageSize, { contants: true }).then(async (notes) => {
setNotes(notes)
setRefreshing(false)
if (notes.length > 0) {
const noteIds = notes.map((note) => note.id ?? '')
const authors = notes.map((note) => note.pubkey ?? '')
@ -110,7 +87,7 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
}
relayPool?.subscribe('homepage-contacts-reactions',reactionFilters )
if (repostIds.length > 0) {
relayPool?.subscribe('homepage-contacts-repost', [
relayPool?.subscribe('homepage-contacts-reposts', [
{
kinds: [Kind.Text],
ids: repostIds,
@ -156,7 +133,6 @@ export const MyFeed: React.FC<MyFeedProps> = ({ navigation }) => {
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}

View File

@ -0,0 +1,204 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import {
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../../Contexts/AppContext'
import { getNotes, 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 { FlashList, ListRenderItem } from '@shopify/flash-list'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useTranslation } from 'react-i18next'
import { getMostZapedNotesContacts } from '../../../Functions/DatabaseFunctions/Zaps'
import { getUnixTime } from 'date-fns'
interface ZapsFeedProps {
navigation: any
updateLastLoad: () => void
pageSize: number
setPageSize: (pageSize: number) => void
}
export const ZapsFeed: React.FC<ZapsFeedProps> = ({
navigation,
updateLastLoad,
pageSize,
setPageSize,
}) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { database, pushedTab } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { lastEventId, relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>()
const [refreshing, setRefreshing] = useState(false)
const flashListRef = React.useRef<FlashList<Note>>(null)
useEffect(() => {
if (pushedTab) {
flashListRef.current?.scrollToIndex({ animated: true, index: 0 })
}
}, [pushedTab])
useEffect(() => {
if (relayPool && publicKey) {
loadNotes()
}
}, [lastEventId, lastConfirmationtId, relayPool, publicKey])
const onRefresh = useCallback(() => {
setRefreshing(true)
updateLastLoad()
}, [])
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const loadNotes: () => void = async () => {
if (database && publicKey) {
getMostZapedNotesContacts(database, getUnixTime(new Date()) - 86400).then((zaps) => {
const zappedEventIds = zaps
.map((zap) => zap.zapped_event_id)
.filter((id) => id !== '')
.slice(0, pageSize)
if (zaps.length > 0) {
relayPool?.subscribe('homepage-zapped-notes', [
{
kinds: [Kind.Text, Kind.RecommendRelay],
ids: zappedEventIds,
},
])
getNotes(database, { filters: { id: zappedEventIds } }).then((notes) => {
setNotes(
zappedEventIds
.map((zappedEventId) => {
return notes.find((note) => note && note.id === zappedEventId) as Note
})
.filter((note) => note !== undefined),
)
setRefreshing(false)
if (notes.length > 0) {
const noteIds = notes.map((note) => note.id ?? '')
const authors = notes.map((note) => note.pubkey ?? '')
const repostIds = notes
.filter((note) => note.repost_id)
.map((note) => note.repost_id ?? '')
const reactionFilters: RelayFilters[] = [
{
kinds: [Kind.Reaction, Kind.Text, 9735],
'#e': noteIds,
},
]
if (authors.length > 0) {
reactionFilters.push({
kinds: [Kind.Metadata],
authors,
})
}
relayPool?.subscribe('homepage-contacts-reactions', reactionFilters)
if (repostIds.length > 0) {
relayPool?.subscribe('homepage-contacts-reposts', [
{
kinds: [Kind.Text],
ids: repostIds,
},
])
}
}
})
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard note={item} />
</View>
)
}
const ListEmptyComponent = React.useMemo(
() => (
<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>
),
[],
)
return (
<View style={styles.list}>
<FlashList
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
onScroll={onScroll}
refreshing={refreshing}
ListEmptyComponent={notes ? ListEmptyComponent : <></>}
horizontal={false}
ListFooterComponent={
notes && notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef}
/>
</View>
)
}
const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: {
height: '100%',
},
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 220,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default ZapsFeed

View File

@ -1,137 +1,120 @@
import React, { useContext } from 'react'
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
import React, { useContext, useEffect, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { AnimatedFAB, Text, TouchableRipple } from 'react-native-paper'
import { useFocusEffect, useTheme } from '@react-navigation/native'
import { AnimatedFAB } from 'react-native-paper'
import { useFocusEffect } from '@react-navigation/native'
import { navigate } from '../../lib/Navigation'
import { t } from 'i18next'
import GlobalFeed from '../GlobalFeed'
import MyFeed from '../MyFeed'
import ReactionsFeed from '../ReactionsFeed'
import RepostsFeed from '../RepostsFeed'
import GlobalFeed from './GlobalFeed'
import MyFeed from './MyFeed'
import { getUnixTime } from 'date-fns'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { Kind } from 'nostr-tools'
import { AppContext } from '../../Contexts/AppContext'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import Tabs from '../../Components/Tabs'
import ZapsFeed from './ZapsFeed'
interface HomeFeedProps {
navigation: any
}
export const HomeFeed: React.FC<HomeFeedProps> = ({ navigation }) => {
const theme = useTheme()
const { privateKey } = useContext(UserContext)
const initialPageSize = 10
const { database } = useContext(AppContext)
const { publicKey, privateKey } = useContext(UserContext)
const { relayPool } = useContext(RelayPoolContext)
const [tabKey, setTabKey] = React.useState('myFeed')
const [activeTab, setActiveTab] = React.useState('myFeed')
const [lastLoadAt, setLastLoadAt] = useState<number>(0)
const [pageSize, setPageSize] = useState<number>(initialPageSize)
useFocusEffect(
React.useCallback(() => {
subscribeNotes()
subscribeGlobal()
updateLastLoad()
return () =>
relayPool?.unsubscribe([
'homepage-global-main',
'homepage-contacts-main',
'homepage-reactions',
'homepage-contacts-meta',
'homepage-replies',
'homepage-global-reposts',
'homepage-myfeed-main',
'homepage-myfeed-reactions',
'homepage-myfeed-reposts',
'homepage-zapped-notes',
])
}, []),
)
useEffect(() => {
if (pageSize > initialPageSize) {
subscribeGlobal(true)
subscribeNotes(true)
updateLastLoad()
}
}, [pageSize, lastLoadAt])
const updateLastLoad: () => void = () => {
setLastLoadAt(getUnixTime(new Date()) - 5)
}
const subscribeGlobal: (past?: boolean) => void = (past) => {
const message: RelayFilters = {
kinds: [Kind.Text, Kind.RecommendRelay],
limit: pageSize,
}
if (past) message.until = lastLoadAt
relayPool?.subscribe('homepage-global-main', [message])
}
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,
}
if (past) message.until = lastLoadAt
relayPool?.subscribe('homepage-myfeed-main', [message])
}
const renderScene: Record<string, JSX.Element> = {
globalFeed: <GlobalFeed navigation={navigation} />,
myFeed: <MyFeed navigation={navigation} />,
reactions: <ReactionsFeed navigation={navigation} />,
reposts: <RepostsFeed navigation={navigation} />,
globalFeed: (
<GlobalFeed
navigation={navigation}
updateLastLoad={updateLastLoad}
lastLoadAt={lastLoadAt}
pageSize={pageSize}
setPageSize={setPageSize}
/>
),
myFeed: (
<MyFeed
navigation={navigation}
updateLastLoad={updateLastLoad}
pageSize={pageSize}
setPageSize={setPageSize}
/>
),
zaps: (
<ZapsFeed
navigation={navigation}
updateLastLoad={updateLastLoad}
pageSize={pageSize}
setPageSize={setPageSize}
/>
),
}
return (
<View>
<View style={styles.tabsNavigator}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View
style={[
styles.tab,
{
borderBottomColor:
tabKey === 'globalFeed' ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === 'globalFeed' ? 3 : 1,
},
]}
>
<TouchableRipple
style={styles.textWrapper}
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
style={styles.textWrapper}
onPress={() => {
relayPool?.unsubscribe(['homepage-global-main'])
setTabKey('myFeed')
}}
>
<Text style={styles.tabText}>{t('homeFeed.myFeed')}</Text>
</TouchableRipple>
</View>
<View
style={[
styles.tab,
{
borderBottomColor:
tabKey === 'reactions' ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === 'reactions' ? 3 : 1,
},
]}
>
<TouchableRipple
style={styles.textWrapper}
onPress={() => {
relayPool?.unsubscribe(['homepage-global-main'])
setTabKey('reactions')
}}
>
<Text style={styles.tabText}>{t('homeFeed.reactions')}</Text>
</TouchableRipple>
</View>
<View
style={[
styles.tab,
{
borderBottomColor:
tabKey === 'reposts' ? theme.colors.primary : theme.colors.border,
borderBottomWidth: tabKey === 'reposts' ? 3 : 1,
},
]}
>
<TouchableRipple
style={styles.textWrapper}
onPress={() => {
relayPool?.unsubscribe(['homepage-global-main'])
setTabKey('reposts')
}}
>
<Text style={styles.tabText}>{t('homeFeed.reposts')}</Text>
</TouchableRipple>
</View>
</ScrollView>
</View>
<View style={styles.feed}>{renderScene[tabKey]}</View>
<Tabs tabs={['globalFeed', 'myFeed', 'zaps']} activeTab={activeTab} setActiveTab={setActiveTab} />
<View style={styles.feed}>{renderScene[activeTab]}</View>
{privateKey && (
<AnimatedFAB
style={[styles.fab, { top: Dimensions.get('window').height - 191 }]}

View File

@ -57,7 +57,7 @@ export const HomePage: React.FC = () => {
relayPool?.subscribe('notification-icon', [
{
kinds: [Kind.ChannelMessage],
'#e': [publicKey],
'#p': [publicKey],
limit: 30,
},
{
@ -91,7 +91,7 @@ export const HomePage: React.FC = () => {
const key = decode(clipboardNip21.replace('nostr:', ''))
if (key?.data) {
if (key.type === 'nevent') {
navigate('Note', { noteId: key.data })
navigate('Note', { noteId: key.data.id })
} else if (key.type === 'npub') {
navigate('Profile', { pubKey: key.data })
} else if (key.type === 'nprofile' && key.data.pubkey) {

View File

@ -48,12 +48,12 @@ export const NotePage: React.FC<NotePageProps> = ({ route }) => {
const loadNote: () => void = async () => {
if (database && publicKey) {
const events = await getNotes(database, { filters: { id: route.params.noteId } })
const events = await getNotes(database, { filters: { id: [route.params.noteId] } })
if (events.length > 0) {
const event = events[0]
setNote(event)
const notes = await getNotes(database, { filters: { reply_event_id: route.params.noteId } })
const notes = await getNotes(database, { filters: { reply_event_id: [route.params.noteId] } })
const rootReplies = getDirectReplies(event, notes)
const filters: RelayFilters[] = [
{
@ -117,7 +117,6 @@ export const NotePage: React.FC<NotePageProps> = ({ route }) => {
<NoteCard note={note} />
<View style={[styles.list, { borderColor: theme.colors.onSecondary }]}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={replies}
renderItem={renderItem}

View File

@ -167,7 +167,6 @@ export const NotificationsFeed: React.FC = () => {
return (
<View style={styles.container}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}

View File

@ -29,6 +29,8 @@ export const ProfileLoadPage: React.FC = () => {
setLastEventId(event.eventId),
)
setTimeout(() => loadMeta(), 1000)
reloadUser()
if (profileFound) loadPets()
return () =>
relayPool?.unsubscribe(['profile-load-meta', 'profile-load-notes', 'profile-load-others'])
}, []),

View File

@ -0,0 +1,123 @@
import React, { useContext, useState, useEffect } from 'react'
import {
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../../Contexts/AppContext'
import { getMainNotes, Note } from '../../../Functions/DatabaseFunctions/Notes'
import { RelayPoolContext } from '../../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { ActivityIndicator, Text, useTheme } from 'react-native-paper'
import NoteCard from '../../../Components/NoteCard'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useTranslation } from 'react-i18next'
interface NotesFeedProps {
publicKey: string
setRefreshing: (refreshing: boolean) => void
pageSize: number
refreshing: boolean
}
export const NotesFeed: React.FC<NotesFeedProps> = ({ publicKey, pageSize, setRefreshing, refreshing }) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { database } = useContext(AppContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const [notes, setNotes] = useState<Note[]>([])
const flashListRef = React.useRef<FlashList<Note>>(null)
useEffect(() => {
if(refreshing) loadNotes()
}, [refreshing])
useEffect(() => {
loadNotes()
}, [pageSize, lastEventId])
const loadNotes: (main?: boolean) => void = () => {
if (database) {
getMainNotes(database, publicKey, pageSize).then((results) => {
setNotes(results)
setRefreshing(false)
if (results.length > 0) {
relayPool?.subscribe(`profile-notes-answers${publicKey.substring(0, 8)}`, [
{
kinds: [Kind.Reaction, Kind.Text, Kind.RecommendRelay, 9735],
'#e': results.map((note) => note.id ?? ''),
},
])
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard note={item} />
</View>
)
}
const ListEmptyComponent = React.useMemo(
() => (
<View style={styles.blank}>
<MaterialCommunityIcons
name='lightning-bolt'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('profilePage.repliesFeed.emptyTitle')}
</Text>
</View>
),
[],
)
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={210}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}
horizontal={false}
ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef}
ListEmptyComponent={ListEmptyComponent}
/>
</View>
)
}
const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: {
height: '100%',
},
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 220,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default NotesFeed

View File

@ -0,0 +1,127 @@
import React, { useContext, useState, useEffect } from 'react'
import { StyleSheet, View } from 'react-native'
import { AppContext } from '../../../Contexts/AppContext'
import { getReplyNotes, Note } from '../../../Functions/DatabaseFunctions/Notes'
import { RelayPoolContext } from '../../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { ActivityIndicator, Text, useTheme } from 'react-native-paper'
import NoteCard from '../../../Components/NoteCard'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useTranslation } from 'react-i18next'
interface RepliesFeedProps {
publicKey: string
setRefreshing: (refreshing: boolean) => void
pageSize: number
refreshing: boolean
}
export const RepliesFeed: React.FC<RepliesFeedProps> = ({
publicKey,
pageSize,
refreshing,
setRefreshing,
}) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { database } = useContext(AppContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const [notes, setNotes] = useState<Note[]>([])
const flashListRef = React.useRef<FlashList<Note>>(null)
useEffect(() => {
if (refreshing) loadNotes()
}, [refreshing])
useEffect(() => {
loadNotes()
}, [pageSize, lastEventId])
const loadNotes: (main?: boolean) => void = () => {
if (database) {
getReplyNotes(database, publicKey, pageSize).then((results) => {
setNotes(results)
setRefreshing(false)
if (results.length > 0) {
relayPool?.subscribe(`profile-replies-answers${publicKey.substring(0, 8)}`, [
{
kinds: [Kind.Reaction, Kind.Text, Kind.RecommendRelay, 9735],
'#e': results.map((note) => note.id ?? ''),
},
])
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard note={item} />
</View>
)
}
const ListEmptyComponent = React.useMemo(
() => (
<View style={styles.blank}>
<MaterialCommunityIcons
name='lightning-bolt'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('profilePage.repliesFeed.emptyTitle')}
</Text>
</View>
),
[],
)
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={210}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}
horizontal={false}
ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef}
ListEmptyComponent={ListEmptyComponent}
/>
</View>
)
}
const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: {
height: '100%',
},
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 220,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default RepliesFeed

View File

@ -0,0 +1,150 @@
import React, { useContext, useState, useEffect } from 'react'
import { StyleSheet, View } from 'react-native'
import { AppContext } from '../../../Contexts/AppContext'
import { getNotes, Note } from '../../../Functions/DatabaseFunctions/Notes'
import { RelayPoolContext } from '../../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools'
import { ActivityIndicator, Text, useTheme } from 'react-native-paper'
import NoteCard from '../../../Components/NoteCard'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import { getMostZapedNotes } from '../../../Functions/DatabaseFunctions/Zaps'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useTranslation } from 'react-i18next'
import { getUnixTime } from 'date-fns'
interface ZapsFeedProps {
publicKey: string
setRefreshing: (refreshing: boolean) => void
pageSize: number
refreshing: boolean
}
export const ZapsFeed: React.FC<ZapsFeedProps> = ({
publicKey,
pageSize,
refreshing,
setRefreshing,
}) => {
const theme = useTheme()
const { t } = useTranslation('common')
const { database } = useContext(AppContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const [notes, setNotes] = useState<Note[]>([])
const flashListRef = React.useRef<FlashList<Note>>(null)
useEffect(() => {
if (refreshing) loadNotes()
}, [refreshing])
useEffect(() => {
loadNotes()
}, [pageSize, lastEventId])
const loadNotes: (main?: boolean) => void = () => {
if (database) {
getMostZapedNotes(database, publicKey, pageSize, getUnixTime(new Date()) - 604800).then((zaps) => {
const zappedEventIds = zaps.map((zap) => zap.zapped_event_id)
if (zaps.length > 0) {
relayPool?.subscribe(`profile-zap-notes${publicKey.substring(0, 8)}`, [
{
kinds: [Kind.Text, Kind.RecommendRelay],
ids: zappedEventIds,
},
])
getNotes(database, { filters: { id: zappedEventIds } }).then((results) => {
if (results.length > 0) {
setNotes(
zappedEventIds
.map((zappedEventId) => {
return results.find((note) => note && note.id === zappedEventId) as Note
})
.filter((note) => note !== undefined),
)
setRefreshing(false)
if (results.length > 0) {
relayPool?.subscribe(`profile-zaps-answers${publicKey.substring(0, 8)}`, [
{
kinds: [Kind.Reaction, Kind.Text, Kind.RecommendRelay, 9735],
'#e': results.map((note) => note.id ?? ''),
},
{
kinds: [Kind.Metadata],
ids: zappedEventIds,
},
])
}
}
})
}
})
}
}
const renderItem: ListRenderItem<Note> = ({ item }) => {
return (
<View style={styles.noteCard} key={item.id}>
<NoteCard note={item} />
</View>
)
}
const ListEmptyComponent = React.useMemo(
() => (
<View style={styles.blank}>
<MaterialCommunityIcons
name='lightning-bolt'
size={64}
style={styles.center}
color={theme.colors.onPrimaryContainer}
/>
<Text variant='headlineSmall' style={styles.center}>
{t('profilePage.zapsFeed.emptyTitle')}
</Text>
</View>
),
[],
)
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={210}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}
horizontal={false}
ListFooterComponent={
notes.length > 0 ? <ActivityIndicator style={styles.loading} animating={true} /> : <></>
}
ref={flashListRef}
ListEmptyComponent={ListEmptyComponent}
/>
</View>
)
}
const styles = StyleSheet.create({
loading: {
paddingTop: 16,
},
list: {
height: '100%',
},
noteCard: {
marginTop: 16,
},
center: {
alignContent: 'center',
textAlign: 'center',
},
blank: {
justifyContent: 'space-between',
height: 120,
marginTop: 91,
},
activityIndicator: {
padding: 16,
},
})
export default ZapsFeed

View File

@ -7,22 +7,22 @@ import {
StyleSheet,
View,
} from 'react-native'
import { Surface, Text, ActivityIndicator, Snackbar, Divider } from 'react-native-paper'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import { Surface, Text, Snackbar } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { getUser, User } from '../../Functions/DatabaseFunctions/Users'
import { Kind } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import NoteCard from '../../Components/NoteCard'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { useFocusEffect } from '@react-navigation/native'
import ProfileData from '../../Components/ProfileData'
import ProfileActions from '../../Components/ProfileActions'
import TextContent from '../../Components/TextContent'
import Tabs from '../../Components/Tabs'
import NotesFeed from './NotesFeed'
import RepliesFeed from './RepliesFeed'
import ZapsFeed from './ZapsFeed'
import { getUnixTime } from 'date-fns'
interface ProfilePageProps {
route: { params: { pubKey: string } }
@ -33,47 +33,47 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
const { publicKey } = useContext(UserContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
const initialPageSize = 10
const initialPageSize = 20
const [showNotification, setShowNotification] = useState<undefined | string>()
const [notes, setNotes] = useState<Note[]>()
const [user, setUser] = useState<User>()
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [refreshing, setRefreshing] = useState(false)
const [firstLoad, setFirstLoad] = useState(true)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [firstLoad, setFirstLoad] = useState<boolean>(true)
const [activeTab, setActiveTab] = React.useState('notes')
useFocusEffect(
React.useCallback(() => {
subscribeProfile()
subscribeNotes()
loadUser()
loadNotes()
setFirstLoad(false)
return () =>
relayPool?.unsubscribe([
`profile${route.params.pubKey}`,
`profile-user${route.params.pubKey}`,
`profile-answers${route.params.pubKey}`,
`profile-user${route.params.pubKey.substring(0, 8)}`,
`profile-zaps${route.params.pubKey.substring(0, 8)}`,
`profile-zap-notes${route.params.pubKey.substring(0, 8)}`,
`profile-replies-answers${route.params.pubKey.substring(0, 8)}`,
`profile-notes-answers${route.params.pubKey.substring(0, 8)}`,
])
}, []),
)
useEffect(() => {
if (!firstLoad) {
if (pageSize > initialPageSize) {
subscribeNotes(true)
}
loadUser()
loadNotes()
if (!firstLoad && pageSize > initialPageSize) {
subscribeProfile()
}
}, [pageSize, lastEventId])
}, [pageSize])
useEffect(() => {
if (!firstLoad) {
loadUser()
}
}, [pageSize, lastEventId, activeTab])
const onRefresh = useCallback(() => {
setRefreshing(true)
loadUser()
loadNotes()
subscribeProfile()
subscribeNotes()
}, [])
const loadUser: () => void = () => {
@ -90,25 +90,6 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
}
}
const loadNotes: (past?: boolean) => void = () => {
if (database) {
getNotes(database, { filters: { pubkey: route.params.pubKey }, limit: pageSize }).then(
(results) => {
setNotes(results)
setRefreshing(false)
if (results.length > 0) {
relayPool?.subscribe(`profile-answers${route.params.pubKey.substring(0, 8)}`, [
{
kinds: [Kind.Reaction, Kind.Text, Kind.RecommendRelay, 9735],
'#e': results.map((note) => note.id ?? ''),
},
])
}
},
)
}
}
const subscribeProfile: () => Promise<void> = async () => {
relayPool?.subscribe(`profile-user${route.params.pubKey.substring(0, 8)}`, [
{
@ -119,31 +100,51 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
kinds: [Kind.Contacts],
authors: [route.params.pubKey],
},
{
kinds: [Kind.Text, Kind.RecommendRelay],
authors: [route.params.pubKey],
limit: pageSize,
},
{
kinds: [9735],
"#p": [route.params.pubKey],
since: getUnixTime(new Date()) - 604800
},
])
}
const subscribeNotes: (past?: boolean) => void = (past) => {
if (!database) return
const message: RelayFilters = {
kinds: [Kind.Text, Kind.RecommendRelay],
authors: [route.params.pubKey],
limit: pageSize,
}
relayPool?.subscribe(`profile${route.params.pubKey.substring(0, 8)}`, [message])
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const renderItem: ListRenderItem<Note> = ({ item }) => (
<View style={styles.noteCard} key={item.id}>
<NoteCard note={item} />
</View>
)
const renderScene: Record<string, JSX.Element> = {
notes: (
<NotesFeed
publicKey={route.params.pubKey}
pageSize={pageSize}
setRefreshing={setRefreshing}
refreshing={refreshing}
/>
),
replies: (
<RepliesFeed
publicKey={route.params.pubKey}
pageSize={pageSize}
setRefreshing={setRefreshing}
refreshing={refreshing}
/>
),
zaps: (
<ZapsFeed
publicKey={route.params.pubKey}
pageSize={pageSize}
setRefreshing={setRefreshing}
refreshing={refreshing}
/>
),
}
return (
<View>
@ -173,30 +174,17 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
<View>
<TextContent content={user?.about} showPreview={false} numberOfLines={10} />
</View>
<Divider style={styles.divider} />
{/* <Divider style={styles.divider} />
<View style={styles.profileActions}>
{user && <ProfileActions user={user} setUser={setUser} />}
</View>
</View> */}
</Surface>
<View style={styles.list}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
onScroll={onScroll}
refreshing={refreshing}
horizontal={false}
ListFooterComponent={
notes && notes.length > 0 ? (
<ActivityIndicator style={styles.loading} animating={true} />
) : (
<></>
)
}
/>
</View>
<Tabs
tabs={['notes', 'replies', 'zaps']}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
<View style={styles.list}>{renderScene[activeTab]}</View>
</ScrollView>
{showNotification && (
<Snackbar

View File

@ -156,7 +156,6 @@ export const ReactionsFeed: React.FC<ReactionsFeedProps> = ({ navigation }) => {
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}

View File

@ -155,7 +155,6 @@ export const RepostsFeed: React.FC<RepostsFeedProps> = ({ navigation }) => {
return (
<View style={styles.list}>
<FlashList
estimatedItemSize={200}
showsVerticalScrollIndicator={false}
data={notes}
renderItem={renderItem}