From d87968dbb69ff1b1e3bc935e157052ec4691728e Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Fri, 24 Feb 2023 22:36:40 +0100 Subject: [PATCH] Profile reactions 2 --- .../main/java/com/nostros/classes/Event.java | 4 +- .../main/java/com/nostros/classes/Relay.java | 10 - .../com/nostros/modules/DatabaseModule.java | 40 ++-- .../com/nostros/modules/RelayPoolModule.java | 3 +- frontend/Components/LnPayment/index.tsx | 4 +- frontend/Components/NoteActions/index.tsx | 2 +- frontend/Components/NoteCard/index.tsx | 23 +- frontend/Components/Tabs/index.tsx | 64 ++++++ .../DatabaseFunctions/Notes/index.ts | 47 +++- .../Functions/DatabaseFunctions/Zaps/index.ts | 77 ++++++- frontend/Locales/de.json | 13 +- frontend/Locales/en.json | 13 +- frontend/Locales/es.json | 14 +- frontend/Locales/fr.json | 15 +- frontend/Locales/ru.json | 14 +- frontend/Locales/zhCn.json | 13 +- frontend/Pages/ConversationPage/index.tsx | 1 - frontend/Pages/GroupsFeed/index.tsx | 4 + .../Pages/{ => HomeFeed}/GlobalFeed/index.tsx | 76 ++----- .../Pages/{ => HomeFeed}/MyFeed/index.tsx | 56 ++--- frontend/Pages/HomeFeed/ZapsFeed/index.tsx | 204 ++++++++++++++++++ frontend/Pages/HomeFeed/index.tsx | 201 ++++++++--------- frontend/Pages/HomePage/index.tsx | 4 +- frontend/Pages/NotePage/index.tsx | 5 +- frontend/Pages/NotificationsFeed/index.tsx | 1 - frontend/Pages/ProfileLoadPage/index.tsx | 2 + .../Pages/ProfilePage/NotesFeed/index.tsx | 123 +++++++++++ .../Pages/ProfilePage/RepliesFeed/index.tsx | 127 +++++++++++ frontend/Pages/ProfilePage/ZapsFeed/index.tsx | 150 +++++++++++++ frontend/Pages/ProfilePage/index.tsx | 148 ++++++------- frontend/Pages/ReactionsFeed/index.tsx | 1 - frontend/Pages/RepostsFeed/index.tsx | 1 - 32 files changed, 1094 insertions(+), 366 deletions(-) create mode 100644 frontend/Components/Tabs/index.tsx rename frontend/Pages/{ => HomeFeed}/GlobalFeed/index.tsx (74%) rename frontend/Pages/{ => HomeFeed}/MyFeed/index.tsx (73%) create mode 100644 frontend/Pages/HomeFeed/ZapsFeed/index.tsx create mode 100644 frontend/Pages/ProfilePage/NotesFeed/index.tsx create mode 100644 frontend/Pages/ProfilePage/RepliesFeed/index.tsx create mode 100644 frontend/Pages/ProfilePage/ZapsFeed/index.tsx diff --git a/android/app/src/main/java/com/nostros/classes/Event.java b/android/app/src/main/java/com/nostros/classes/Event.java index 79a743e..ce0f2d7 100644 --- a/android/app/src/main/java/com/nostros/classes/Event.java +++ b/android/app/src/main/java/com/nostros/classes/Event.java @@ -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; diff --git a/android/app/src/main/java/com/nostros/classes/Relay.java b/android/app/src/main/java/com/nostros/classes/Relay.java index 7113780..9fb2e79 100644 --- a/android/app/src/main/java/com/nostros/classes/Relay.java +++ b/android/app/src/main/java/com/nostros/classes/Relay.java @@ -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); - } } diff --git a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java index e858571..831f195 100644 --- a/android/app/src/main/java/com/nostros/modules/DatabaseModule.java +++ b/android/app/src/main/java/com/nostros/modules/DatabaseModule.java @@ -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 getRelays(ReactApplicationContext reactContext) { diff --git a/android/app/src/main/java/com/nostros/modules/RelayPoolModule.java b/android/app/src/main/java/com/nostros/modules/RelayPoolModule.java index 087f214..b51eabe 100644 --- a/android/app/src/main/java/com/nostros/modules/RelayPoolModule.java +++ b/android/app/src/main/java/com/nostros/modules/RelayPoolModule.java @@ -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(); } diff --git a/frontend/Components/LnPayment/index.tsx b/frontend/Components/LnPayment/index.tsx index 23fbca5..fe02944 100644 --- a/frontend/Components/LnPayment/index.tsx +++ b/frontend/Components/LnPayment/index.tsx @@ -50,7 +50,7 @@ export const LnPayment: React.FC = ({ 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 = ({ 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())) }) diff --git a/frontend/Components/NoteActions/index.tsx b/frontend/Components/NoteActions/index.tsx index 74d6069..908a972 100644 --- a/frontend/Components/NoteActions/index.tsx +++ b/frontend/Components/NoteActions/index.tsx @@ -30,7 +30,7 @@ export const NoteActions: React.FC = ({ 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]) } diff --git a/frontend/Components/NoteCard/index.tsx b/frontend/Components/NoteCard/index.tsx index 409db5d..77f8f7d 100644 --- a/frontend/Components/NoteCard/index.tsx +++ b/frontend/Components/NoteCard/index.tsx @@ -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 = ({ }) } 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 = ({ {relayColouring && showRelayColors && relays.map((relay, index) => ( - - - + ))} diff --git a/frontend/Components/Tabs/index.tsx b/frontend/Components/Tabs/index.tsx new file mode 100644 index 0000000..7129d9a --- /dev/null +++ b/frontend/Components/Tabs/index.tsx @@ -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 = ({ tabs, activeTab, setActiveTab }) => { + const theme = useTheme() + const { t } = useTranslation('common') + + return ( + + + {tabs.map((tabKey) => ( + + { + setActiveTab(tabKey) + }} + > + {t(`tabs.${tabKey}`)} + + + ))} + + + ) +} + +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 diff --git a/frontend/Functions/DatabaseFunctions/Notes/index.ts b/frontend/Functions/DatabaseFunctions/Notes/index.ts index fb92504..e291a8c 100644 --- a/frontend/Functions/DatabaseFunctions/Notes/index.ts +++ b/frontend/Functions/DatabaseFunctions/Notes/index.ts @@ -36,11 +36,11 @@ export const getMainNotes: ( db: QuickSQLiteConnection, pubKey: string, limit: number, - contants: boolean, filters?: { + contants?: boolean, until?: number }, -) => Promise = async (db, pubKey, limit, contants, filters) => { +) => Promise = 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 = 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 + filters?: Record 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 ' }) } diff --git a/frontend/Functions/DatabaseFunctions/Zaps/index.ts b/frontend/Functions/DatabaseFunctions/Zaps/index.ts index 860e0db..485e06c 100644 --- a/frontend/Functions/DatabaseFunctions/Zaps/index.ts +++ b/frontend/Functions/DatabaseFunctions/Zaps/index.ts @@ -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 = async ( +export const getMostZapedNotes: ( + db: QuickSQLiteConnection, + publicKey: string, + limit: number, + since: number +) => Promise = 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 = 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 = 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 databaseToEntity(object)) + const zaps: Zap[] = items.map((object) => databaseToEntity(object)) - return notes + return zaps } diff --git a/frontend/Locales/de.json b/frontend/Locales/de.json index a1c4f0e..58de248 100644 --- a/frontend/Locales/de.json +++ b/frontend/Locales/de.json @@ -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." }, diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index 05227c4..fd19dfc 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -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." }, diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index bed8a3f..964cf04 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -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", diff --git a/frontend/Locales/fr.json b/frontend/Locales/fr.json index f4a6c94..9279770 100644 --- a/frontend/Locales/fr.json +++ b/frontend/Locales/fr.json @@ -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", diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index 21fdc45..f26ce3d 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -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", diff --git a/frontend/Locales/zhCn.json b/frontend/Locales/zhCn.json index 48789f5..1aa84a9 100644 --- a/frontend/Locales/zhCn.json +++ b/frontend/Locales/zhCn.json @@ -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,下拉刷新" }, diff --git a/frontend/Pages/ConversationPage/index.tsx b/frontend/Pages/ConversationPage/index.tsx index a7b2ad4..754d84e 100644 --- a/frontend/Pages/ConversationPage/index.tsx +++ b/frontend/Pages/ConversationPage/index.tsx @@ -364,7 +364,6 @@ export const ConversationPage: React.FC = ({ route }) => renderItem={renderDirectMessageItem} horizontal={false} ref={scrollViewRef} - estimatedItemSize={100} onScroll={onScroll} /> {reply ? ( diff --git a/frontend/Pages/GroupsFeed/index.tsx b/frontend/Pages/GroupsFeed/index.tsx index f590c76..130ff5b 100644 --- a/frontend/Pages/GroupsFeed/index.tsx +++ b/frontend/Pages/GroupsFeed/index.tsx @@ -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 ?? '') + }, ]) } }) diff --git a/frontend/Pages/GlobalFeed/index.tsx b/frontend/Pages/HomeFeed/GlobalFeed/index.tsx similarity index 74% rename from frontend/Pages/GlobalFeed/index.tsx rename to frontend/Pages/HomeFeed/GlobalFeed/index.tsx index 06ef1d9..0c7ebb3 100644 --- a/frontend/Pages/GlobalFeed/index.tsx +++ b/frontend/Pages/HomeFeed/GlobalFeed/index.tsx @@ -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 = ({ navigation }) => { +export const GlobalFeed: React.FC = ({ + 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([]) - const [lastLoadAt, setLastLoadAt] = useState(0) const [newNotesCount, setNewNotesCount] = useState(0) - const [pageSize, setPageSize] = useState(initialPageSize) const [refreshing, setRefreshing] = useState(false) const flashListRef = React.useRef>(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 = ({ 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) => void = (event) => { if (handleInfinityScroll(event)) { setPageSize(pageSize + initialPageSize) @@ -105,7 +76,7 @@ export const GlobalFeed: React.FC = ({ 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 = ({ 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 = ({ navigation }) => { void + pageSize: number + setPageSize: (pageSize: number) => void } -export const MyFeed: React.FC = ({ navigation }) => { +export const MyFeed: React.FC = ({ 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 = ({ navigation }) => { const { lastEventId, relayPool, lastConfirmationtId } = useContext(RelayPoolContext) const initialPageSize = 10 const [notes, setNotes] = useState([]) - const [pageSize, setPageSize] = useState(initialPageSize) const [refreshing, setRefreshing] = useState(false) const flashListRef = React.useRef>(null) @@ -43,26 +44,15 @@ export const MyFeed: React.FC = ({ 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) => void = (event) => { @@ -71,24 +61,11 @@ export const MyFeed: React.FC = ({ 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 = ({ 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 = ({ navigation }) => { return ( void + pageSize: number + setPageSize: (pageSize: number) => void +} + +export const ZapsFeed: React.FC = ({ + 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() + const [refreshing, setRefreshing] = useState(false) + const flashListRef = React.useRef>(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) => 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 = ({ item }) => { + return ( + + + + ) + } + + const ListEmptyComponent = React.useMemo( + () => ( + + + + {t('homeFeed.emptyTitle')} + + + {t('homeFeed.emptyDescription')} + + + + ), + [], + ) + + return ( + + } + onScroll={onScroll} + refreshing={refreshing} + ListEmptyComponent={notes ? ListEmptyComponent : <>} + horizontal={false} + ListFooterComponent={ + notes && notes.length > 0 ? : <> + } + ref={flashListRef} + /> + + ) +} + +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 diff --git a/frontend/Pages/HomeFeed/index.tsx b/frontend/Pages/HomeFeed/index.tsx index 74513c6..d8c110e 100644 --- a/frontend/Pages/HomeFeed/index.tsx +++ b/frontend/Pages/HomeFeed/index.tsx @@ -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 = ({ 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(0) + const [pageSize, setPageSize] = useState(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 = { - globalFeed: , - myFeed: , - reactions: , - reposts: , + globalFeed: ( + + ), + myFeed: ( + + ), + zaps: ( + + ), } return ( - - - - { - relayPool?.unsubscribe([ - 'homepage-contacts-main', - 'homepage-reactions', - 'homepage-contacts-meta', - 'homepage-replies', - ]) - setTabKey('globalFeed') - }} - > - {t('homeFeed.globalFeed')} - - - - { - relayPool?.unsubscribe(['homepage-global-main']) - setTabKey('myFeed') - }} - > - {t('homeFeed.myFeed')} - - - - { - relayPool?.unsubscribe(['homepage-global-main']) - setTabKey('reactions') - }} - > - {t('homeFeed.reactions')} - - - - { - relayPool?.unsubscribe(['homepage-global-main']) - setTabKey('reposts') - }} - > - {t('homeFeed.reposts')} - - - - - {renderScene[tabKey]} + + {renderScene[activeTab]} {privateKey && ( { 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) { diff --git a/frontend/Pages/NotePage/index.tsx b/frontend/Pages/NotePage/index.tsx index d201fe8..55213aa 100644 --- a/frontend/Pages/NotePage/index.tsx +++ b/frontend/Pages/NotePage/index.tsx @@ -48,12 +48,12 @@ export const NotePage: React.FC = ({ 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 = ({ route }) => { { return ( { setLastEventId(event.eventId), ) setTimeout(() => loadMeta(), 1000) + reloadUser() + if (profileFound) loadPets() return () => relayPool?.unsubscribe(['profile-load-meta', 'profile-load-notes', 'profile-load-others']) }, []), diff --git a/frontend/Pages/ProfilePage/NotesFeed/index.tsx b/frontend/Pages/ProfilePage/NotesFeed/index.tsx new file mode 100644 index 0000000..e3d0737 --- /dev/null +++ b/frontend/Pages/ProfilePage/NotesFeed/index.tsx @@ -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 = ({ publicKey, pageSize, setRefreshing, refreshing }) => { + const theme = useTheme() + const { t } = useTranslation('common') + const { database } = useContext(AppContext) + const { lastEventId, relayPool } = useContext(RelayPoolContext) + const [notes, setNotes] = useState([]) + const flashListRef = React.useRef>(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 = ({ item }) => { + return ( + + + + ) + } + + const ListEmptyComponent = React.useMemo( + () => ( + + + + {t('profilePage.repliesFeed.emptyTitle')} + + + ), + [], + ) + + return ( + + 0 ? : <> + } + ref={flashListRef} + ListEmptyComponent={ListEmptyComponent} + /> + + ) +} + +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 diff --git a/frontend/Pages/ProfilePage/RepliesFeed/index.tsx b/frontend/Pages/ProfilePage/RepliesFeed/index.tsx new file mode 100644 index 0000000..ae05d4d --- /dev/null +++ b/frontend/Pages/ProfilePage/RepliesFeed/index.tsx @@ -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 = ({ + publicKey, + pageSize, + refreshing, + setRefreshing, +}) => { + const theme = useTheme() + const { t } = useTranslation('common') + const { database } = useContext(AppContext) + const { lastEventId, relayPool } = useContext(RelayPoolContext) + const [notes, setNotes] = useState([]) + const flashListRef = React.useRef>(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 = ({ item }) => { + return ( + + + + ) + } + + + + const ListEmptyComponent = React.useMemo( + () => ( + + + + {t('profilePage.repliesFeed.emptyTitle')} + + + ), + [], + ) + + return ( + + 0 ? : <> + } + ref={flashListRef} + ListEmptyComponent={ListEmptyComponent} + /> + + ) +} + +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 diff --git a/frontend/Pages/ProfilePage/ZapsFeed/index.tsx b/frontend/Pages/ProfilePage/ZapsFeed/index.tsx new file mode 100644 index 0000000..e32160d --- /dev/null +++ b/frontend/Pages/ProfilePage/ZapsFeed/index.tsx @@ -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 = ({ + publicKey, + pageSize, + refreshing, + setRefreshing, +}) => { + const theme = useTheme() + const { t } = useTranslation('common') + const { database } = useContext(AppContext) + const { lastEventId, relayPool } = useContext(RelayPoolContext) + const [notes, setNotes] = useState([]) + const flashListRef = React.useRef>(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 = ({ item }) => { + return ( + + + + ) + } + + const ListEmptyComponent = React.useMemo( + () => ( + + + + {t('profilePage.zapsFeed.emptyTitle')} + + + ), + [], + ) + + return ( + + 0 ? : <> + } + ref={flashListRef} + ListEmptyComponent={ListEmptyComponent} + /> + + ) +} + +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 diff --git a/frontend/Pages/ProfilePage/index.tsx b/frontend/Pages/ProfilePage/index.tsx index 13a6e13..4fc6351 100644 --- a/frontend/Pages/ProfilePage/index.tsx +++ b/frontend/Pages/ProfilePage/index.tsx @@ -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 = ({ route }) => { const { publicKey } = useContext(UserContext) const { lastEventId, relayPool } = useContext(RelayPoolContext) const { t } = useTranslation('common') - const initialPageSize = 10 + const initialPageSize = 20 const [showNotification, setShowNotification] = useState() - const [notes, setNotes] = useState() const [user, setUser] = useState() const [pageSize, setPageSize] = useState(initialPageSize) - const [refreshing, setRefreshing] = useState(false) - const [firstLoad, setFirstLoad] = useState(true) + const [refreshing, setRefreshing] = useState(false) + const [firstLoad, setFirstLoad] = useState(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 = ({ 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 = async () => { relayPool?.subscribe(`profile-user${route.params.pubKey.substring(0, 8)}`, [ { @@ -119,31 +100,51 @@ export const ProfilePage: React.FC = ({ 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) => void = (event) => { if (handleInfinityScroll(event)) { setPageSize(pageSize + initialPageSize) } } - const renderItem: ListRenderItem = ({ item }) => ( - - - - ) + const renderScene: Record = { + notes: ( + + ), + replies: ( + + ), + zaps: ( + + ), + } return ( @@ -173,30 +174,17 @@ export const ProfilePage: React.FC = ({ route }) => { - + {/* {user && } - + */} - - } - onScroll={onScroll} - refreshing={refreshing} - horizontal={false} - ListFooterComponent={ - notes && notes.length > 0 ? ( - - ) : ( - <> - ) - } - /> - + + {renderScene[activeTab]} {showNotification && ( = ({ navigation }) => { return ( = ({ navigation }) => { return (