diff --git a/android/app/src/main/java/com/nostros/classes/Database.java b/android/app/src/main/java/com/nostros/classes/Database.java index 9d96720..e3ea386 100644 --- a/android/app/src/main/java/com/nostros/classes/Database.java +++ b/android/app/src/main/java/com/nostros/classes/Database.java @@ -237,6 +237,9 @@ public class Database { try { instance.execSQL("ALTER TABLE nostros_relays ADD COLUMN paid INT DEFAULT 0;"); } catch (SQLException e) { } + try { + instance.execSQL("ALTER TABLE nostros_zaps ADD COLUMN preimage TEXT;"); + } catch (SQLException e) { } } public void saveEvent(JSONObject data, String userPubKey, String relayUrl) throws JSONException { 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 5a94413..e0ddddb 100644 --- a/android/app/src/main/java/com/nostros/classes/Event.java +++ b/android/app/src/main/java/com/nostros/classes/Event.java @@ -544,10 +544,12 @@ public class Event { JSONArray eTags = filterTags("e"); JSONArray bolt11Tags = filterTags("bolt11"); JSONArray descriptionTags = filterTags("description"); + JSONArray preimageTags = filterTags("preimage"); String zapped_event_id = ""; String zapped_user_id = ""; String zapper_user_id = ""; + String preimage = ""; double amount = 0; if (descriptionTags.length() > 0) { JSONArray tag = descriptionTags.getJSONArray(0); @@ -564,6 +566,9 @@ public class Event { if (pTags.length() > 0) { zapped_user_id = pTags.getJSONArray(pTags.length() - 1).getString(1); } + if (preimageTags.length() > 0) { + preimage = preimageTags.getJSONArray(preimageTags.length() - 1).getString(1); + } String userQuery = "SELECT created_at FROM nostros_users WHERE zap_pubkey = ? AND id = ?"; @SuppressLint("Recycle") Cursor userCursor = database.rawQuery(userQuery, new String[] {pubkey, zapped_user_id}); @@ -581,6 +586,7 @@ public class Event { values.put("zapped_user_id", zapped_user_id); values.put("zapped_event_id", zapped_event_id); values.put("zapper_user_id", zapper_user_id); + values.put("preimage", preimage); database.insert("nostros_zaps", null, values); } diff --git a/frontend/Components/LnPreview/index.tsx b/frontend/Components/LnPreview/index.tsx index e309137..4fb68ed 100644 --- a/frontend/Components/LnPreview/index.tsx +++ b/frontend/Components/LnPreview/index.tsx @@ -7,6 +7,8 @@ import RBSheet from 'react-native-raw-bottom-sheet' import { Card, IconButton, Text, useTheme } from 'react-native-paper' import { AppContext } from '../../Contexts/AppContext' import { decode, type PaymentRequestObject, type TagsObject } from 'bolt11' +import { WalletContext } from '../../Contexts/WalletContext' +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' interface LnPreviewProps { setOpen?: (open: boolean) => void @@ -21,11 +23,13 @@ export const LnPreview: React.FC = ({ }) => { const theme = useTheme() const { t } = useTranslation('common') + const { active, payInvoice } = React.useContext(WalletContext) const { getSatoshiSymbol } = React.useContext(AppContext) const bottomSheetInvoiceRef = React.useRef(null) const [decodedLnUrl, setDecodedLnUrl] = useState< PaymentRequestObject & { tagsObject: TagsObject } >() + const [paymentDone, setPaymentDone] = useState() useEffect(() => { if (invoice) { @@ -43,6 +47,12 @@ export const LnPreview: React.FC = ({ Clipboard.setString(invoice ?? '') } + const payWithWallet: () => void = () => { + if (invoice) { + payInvoice(invoice).then(setPaymentDone) + } + } + const openApp: () => void = () => { Linking.openURL(`lightning:${invoice}`) } @@ -76,7 +86,15 @@ export const LnPreview: React.FC = ({ - + {paymentDone === undefined ? ( + + ) : ( + + )} {decodedLnUrl?.satoshis} @@ -89,8 +107,14 @@ export const LnPreview: React.FC = ({ {t('lnPayment.copy')} + {active && ( + + + {t('lnPayment.pay')} + + )} - + {t('lnPayment.open')} diff --git a/frontend/Components/MenuItems/index.tsx b/frontend/Components/MenuItems/index.tsx index bca8449..4525af6 100644 --- a/frontend/Components/MenuItems/index.tsx +++ b/frontend/Components/MenuItems/index.tsx @@ -18,10 +18,14 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI import { navigate } from '../../lib/Navigation' import { usernamePubKey } from '../../Functions/RelayFunctions/Users' import ProfileData from '../ProfileData' +import { WalletContext } from '../../Contexts/WalletContext' +import { AppContext } from '../../Contexts/AppContext' export const MenuItems: React.FC = () => { const [drawerItemIndex, setDrawerItemIndex] = React.useState(-1) + const { getSatoshiSymbol } = React.useContext(AppContext) const { relays } = React.useContext(RelayPoolContext) + const { balance, active } = React.useContext(WalletContext) const { nPub, publicKey, @@ -61,6 +65,8 @@ export const MenuItems: React.FC = () => { navigate('Config') } else if (key === 'contacts') { navigate('Contacts') + } else if (key === 'wallet') { + navigate('Wallet') } } @@ -114,49 +120,65 @@ export const MenuItems: React.FC = () => { )} {publicKey && ( - <> - ( - - )} - key='relays' - active={drawerItemIndex === 0} - onPress={() => onPressItem('relays', 0)} - onTouchEnd={() => setDrawerItemIndex(-1)} - right={() => - activerelays < 1 ? ( - {t('menuItems.notConnected')} - ) : ( - - {t('menuItems.connectedRelays', { - number: activerelays, - })} - - ) - } - /> - - onPressItem('contacts', 1)} - onTouchEnd={() => setDrawerItemIndex(-1)} - /> - + ( + + )} + key='relays' + active={drawerItemIndex === 0} + onPress={() => onPressItem('relays', 0)} + onTouchEnd={() => setDrawerItemIndex(-1)} + right={() => + activerelays < 1 ? ( + {t('menuItems.notConnected')} + ) : ( + + {t('menuItems.connectedRelays', { + number: activerelays, + })} + + ) + } + /> + )} + onPressItem('wallet', 1)} + onTouchEnd={() => setDrawerItemIndex(-1)} + right={() => { + if (!active) return <> + return ( + + {`${balance} `} + {getSatoshiSymbol()} + + ) + }} + /> + {publicKey && ( + onPressItem('contacts', 1)} + onTouchEnd={() => setDrawerItemIndex(-1)} + /> )} onPressItem('config', 1)} + active={drawerItemIndex === 2} + onPress={() => onPressItem('config', 2)} onTouchEnd={() => setDrawerItemIndex(-1)} /> @@ -165,16 +187,16 @@ export const MenuItems: React.FC = () => { label={t('menuItems.about')} icon='information-outline' key='about' - active={drawerItemIndex === 2} - onPress={() => onPressItem('about', 2)} + active={drawerItemIndex === 3} + onPress={() => onPressItem('about', 3)} onTouchEnd={() => setDrawerItemIndex(-1)} /> onPressItem('faq', 2)} + active={drawerItemIndex === 4} + onPress={() => onPressItem('faq', 4)} onTouchEnd={() => setDrawerItemIndex(-1)} /> void + balance?: number + transactions: WalletAction[] + invoices: WalletAction[] + refreshLndHub: (login?: string, password?: string, uri?: string) => void + payInvoice: (invoice: string) => Promise + logoutWallet: () => void +} + +export interface WalletContextProviderProps { + children: React.ReactNode +} + +export const initialWalletContext: WalletContextProps = { + active: false, + setLndHub: () => {}, + transactions: [], + invoices: [], + refreshLndHub: () => {}, + payInvoice: async () => false, + logoutWallet: () => {}, +} + +export const WalletContextProvider = ({ children }: WalletContextProviderProps): JSX.Element => { + const [active, setActive] = React.useState(initialWalletContext.active) + const [lndHub, setLndHub] = useState() + const [balance, setBalance] = useState() + const [updatedAt, setUpdatedAt] = useState() + const [transactions, setTransactions] = useState( + initialWalletContext.transactions, + ) + const [invoices, setInvoices] = useState(initialWalletContext.invoices) + + useEffect(() => { + SInfo.getItem('lndHub', {}).then((value) => { + if (value) { + setLndHub(JSON.parse(value)) + } + }) + }, []) + + const refreshLndHub: (login?: string, password?: string, uri?: string) => void = ( + login, + password, + uri, + ) => { + setLndHub(undefined) + let params: + | { type: string; refresh_token?: string; login?: string; password?: string } + | undefined + if (lndHub?.refreshToken) { + params = { + type: 'refresh_token', + refresh_token: lndHub?.refreshToken, + } + uri = lndHub?.url + } else if (login !== '' && password !== '' && uri !== '') { + params = { + type: 'auth', + login, + password, + } + } + if (params && uri) { + axios.post(`${uri}/auth`, {}, { params }).then((response) => { + if (response?.data?.refresh_token && response.data?.access_token && uri) { + setLndHub({ + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + url: uri, + }) + } + }) + } + } + + const updateLndHub: () => void = () => { + if (!lndHub) return + + const headers = { + Authorization: `Bearer ${lndHub.accessToken}`, + } + + axios.get(`${lndHub.url}/balance`, { headers }).then((response) => { + if (response) { + if (response.status === 200) { + setUpdatedAt(`${getUnixTime(new Date())}-balance`) + setBalance(response.data?.BTC?.AvailableBalance ?? 0) + SInfo.setItem('lndHub', JSON.stringify(lndHub), {}) + setActive(true) + } else if (response.status === 401) { + refreshLndHub() + } + } + }) + axios.get(`${lndHub.url}/gettxs`, { headers }).then((response) => { + if (response) { + setTransactions( + response.data.map((item: any) => { + return { + id: item.payment_preimage, + monto: item.value, + type: 'transaction', + description: item.memo, + timestamp: item.timestamp, + } + }), + ) + setUpdatedAt(`${getUnixTime(new Date())}-gettxs`) + } + }) + axios.get(`${lndHub.url}/getuserinvoices`, { headers }).then((response) => { + if (response) { + setInvoices( + response.data + .filter((item: any) => item.ispaid) + .map((item: any) => { + return { + id: item.payment_hash, + monto: item.amt, + type: 'invoice', + description: item.description, + timestamp: item.timestamp, + } + }), + ) + setUpdatedAt(`${getUnixTime(new Date())}-getuserinvoices`) + } + }) + } + + useEffect(updateLndHub, [lndHub]) + + const payInvoice: (invoice: string) => Promise = async (invoice) => { + if (active && invoice && invoice !== '') { + const headers = { + Authorization: `Bearer ${lndHub?.accessToken}`, + } + const params = { + invoice, + } + const response = await axios.post(`${lndHub?.url}/payinvoice`, params, { headers }) + if (response) { + if (response.status === 200) { + updateLndHub() + return response?.data?.payment_error === '' + } else if (response.status === 401) { + refreshLndHub() + } + } + } + + return false + } + + const logoutWallet: () => void = () => { + SInfo.deleteItem('lndHub', {}) + setActive(false) + setLndHub(undefined) + setBalance(undefined) + setUpdatedAt(undefined) + setTransactions([]) + setInvoices([]) + } + + return ( + + {children} + + ) +} + +export const WalletContext = React.createContext(initialWalletContext) diff --git a/frontend/Functions/DatabaseFunctions/Zaps/index.ts b/frontend/Functions/DatabaseFunctions/Zaps/index.ts index 50863f6..08edf21 100644 --- a/frontend/Functions/DatabaseFunctions/Zaps/index.ts +++ b/frontend/Functions/DatabaseFunctions/Zaps/index.ts @@ -14,6 +14,7 @@ export interface Zap extends Event { nip05: string lnurl: string ln_address: string + preimage: string } const databaseToEntity: (object: any) => Zap = (object) => { @@ -109,7 +110,7 @@ export const getUserZaps: ( export const getZaps: ( db: QuickSQLiteConnection, - filters: { eventId?: string; zapperId?: string; limit?: number }, + filters: { eventId?: string; zapperId?: string; limit?: number; preimages?: string[] }, ) => Promise = async (db, filters) => { let groupsQuery = ` SELECT @@ -121,6 +122,12 @@ export const getZaps: ( nostros_users ON nostros_users.id = nostros_zaps.zapper_user_id ` + if (filters.preimages) { + groupsQuery += ` + WHERE preimage IN ("${filters.preimages.join('", "')}") + ` + } + if (filters.eventId) { groupsQuery += ` WHERE zapped_event_id = "${filters.eventId}" diff --git a/frontend/Locales/de.json b/frontend/Locales/de.json index 6bb1470..1e7e695 100644 --- a/frontend/Locales/de.json +++ b/frontend/Locales/de.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Today", - "yerterday": "Yesterday" + "yesterday": "Yesterday" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "Relays", "relaysDescription": "Relays sind Nodes (Netzknoten) im Netzwerk, die als Vermittler von Nachrichten zwischen den Anwendungen dienen.\n\n\nRelays können die Belastbarkeit und die Verfügbarkeit des Netzwerks verbessern, indem sie dafür sorgen, das Nachrichten trotz Unterbrechungen oder Ausfällen der Verfügbarkeit ausgeliefert werden.\n\n\nRelays können Privatsphäre und Netzwerksicherheit erhöhen, indem sie Aufenthaltsort und Identität der Anwendungen bzw. der Benutzer, die miteinander kommunizieren, verbergen\n\n\nDies kann von Wert sein in Umgebungen, in denen Zensur oder Überwachung ein Problem ist.\n\n\nEs ist wichtig darauf hinzuweisen, das Relays für schadhafte Zwecke missbraucht werden können, wie zum Beispiel Sniffing oder das Zensieren von Netzwerkverkehr.\n\n\nDeswegen ist es von Bedeutung, die Nutzung von Relays sorgfältig abzuwägen, und angemessenene Sichheitsmassnahmen anzuwenden, um Identität, Privatsphäre und Netzwerksicherheit zu schützen.", "keysTitle": "Was sind Schlüssel?", @@ -28,6 +29,7 @@ "homeNavigator": { "ProfileCreate": "Profil anlegen", "Search": "", + "Wallet": "Wallet", "ImageGallery": "", "ProfileConnect": "", "Group": "", @@ -172,7 +174,8 @@ "copy": "Kopieren", "open": "Öffne Wallet", "anonTip": "Anonymer Tip", - "zap": "Zap" + "zap": "Zap", + "pay": "Pay" }, "notificationsFeed": { "connectContactRelays": "Reconnect to contacts' relays", diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index 4ca0608..3b51f37 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Today", - "yerterday": "Yesterday" + "yesterday": "Yesterday" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "Relays", "relaysDescription": "Relays are nodes on the network that act as intermediaries for the transmission of messages between applications.\n\n\nRelays can be used to improve network resiliency and availability by allowing messages to be delivered even when there are failures or interruptions in connectivity.\n\n\nRelays can also be used to improve privacy and network security, as they can hide the location and identity of applications that communicate with each other. This can be useful in environments where censorship or surveillance is an issue.\n\n\nIt is important to note that relays can also be used for malicious purposes, such as sniffing or censoring network traffic.\n\n\nTherefore, it is important to carefully evaluate the use of relays and consider appropriate security measures to protect privacy and network security.", "keysTitle": "What are these keys?", @@ -25,9 +26,15 @@ "loginStep3Description": "You can add the relays used by your contacts to your list and connect to them to strengthen the network.", "loginskip": "You can skip this process but you may miss out on the opportunity to connect to a diverse range of relays and expand your network's reach" }, + "walletPage": { + "addLnhub": "Add LNHub", + "lnHub": "LNHub address", + "connect": "Connect" + }, "homeNavigator": { "ProfileCreate": "Create profile", "Search": "", + "Wallet": "Wallet", "ImageGallery": "", "ProfileConnect": "", "Group": "", @@ -106,6 +113,7 @@ "poweredBy": "Powered by {{uri}}" }, "menuItems": { + "wallet": "Wallet", "relays": "Relays", "notConnected": "Not connected", "connectedRelays": "{{number}} connected", @@ -175,7 +183,8 @@ "copy": "Copy", "open": "Open wallet", "anonTip": "Anonymous tip", - "zap": "Zap" + "zap": "Zap", + "pay": "Pay" }, "notificationsFeed": { "reposted": "Reposted", diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index 9c09fc4..978589c 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Hoy", - "yerterday": "Ayer" + "yesterday": "Ayer" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "Relés", "relaysDescription": "Los relays son nodos en la red que actúan como intermediarios para la transmisión demensajes entre aplicaciones.\n\n\nLos relays pueden ser utilizados para mejorar la resiliencia yla disponibilidad de la red, ya que permiten que los mensajes sean entregados aun cuandohay fallos o interrupciones en la conectividad.\n\n\nLos relays también pueden ser utilizadospara mejorar la privacidad y la seguridad de la red, ya que pueden ocultar la ubicación yel identidad de las aplicaciones que se comunican entre sí a través de ellos. Esto puedeser útil en entornos donde la censura o la vigilancia son un problema.\n\n\nEs importante teneren cuenta que los relays también pueden ser utilizados para propósitos malintencionados,como para rastrear o censurar el tráfico de la red.\n\n\nPor lo tanto, es importante evaluarcuidadosamente el uso de relays y considerar medidas de seguridad adecuadas para protegerla privacidad y la seguridad de la red.", "keysTitle": "¿Qué son las claves?", @@ -39,6 +40,7 @@ "Group": "", "QrReader": "", "Search": "", + "Wallet": "Wallet", "ImageGallery": "", "ProfileCreate": "Crear perfil", "ProfileConnect": "", @@ -188,7 +190,8 @@ "copy": "Copiar", "open": "Abrir wallet", "anonTip": "Propina anónima", - "zap": "Zap" + "zap": "Zap", + "pay": "Pagar" }, "notificationsFeed": { "reposted": "Reposteado", diff --git a/frontend/Locales/fr.json b/frontend/Locales/fr.json index 17d9ca4..79c9eb0 100644 --- a/frontend/Locales/fr.json +++ b/frontend/Locales/fr.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Today", - "yerterday": "Yesterday" + "yesterday": "Yesterday" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "Relais", "relaysDescription": "Les relais sont des nœuds du réseau qui servent d'intermédiaires pour la transmission de messages entre les applications.\n\nLes relais peuvent être utilisés pour améliorer la résilience et la disponibilité des réseaux en permettant la transmission des messages même en cas de défaillance ou d'interruption de la connectivité.\n\nLes relais peuvent également être utilisés pour améliorer la confidentialité et la sécurité des réseaux, car ils peuvent cacher l'emplacement et l'identité des applications qui communiquent entre elles par leur intermédiaire. Cela peut être utile dans les environnements où la censure ou la surveillance est un problème.\n\nIl est important de noter que les relais peuvent également être utilisés à des fins malveillantes, par exemple pour suivre ou censurer le trafic réseau.\n\nIl est donc important d'évaluer soigneusement l'utilisation des relais et d'envisager des mesures de sécurité appropriées pour protéger la vie privée et la sécurité du réseau.", "keysTitle": "C'est quoi les clés ?", @@ -39,6 +40,7 @@ "Group": "", "QrReader": "", "ImageGallery": "", + "Wallet": "Wallet", "Search": "", "ProfileCreate": "Create profile", "ProfileConnect": "", @@ -193,7 +195,8 @@ "copy": "Copier", "open": "Ouvrir le wallet", "anonTip": "Anonymous tip", - "zap": "Zap" + "zap": "Zap", + "pay": "Pay" }, "notificationsFeed": { "reposted": "Reposted", diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index 445d9e9..9b2bd0d 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Today", - "yerterday": "Yesterday" + "yesterday": "Yesterday" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "Реле", "relaysDescription": "Реле(ретрансляторы) — это сетевые узлы, которые работают как посредники для передачи сообщений между приложениями.\n\n\nРетрансляторы можно использовать для повышения отказоустойчивости и доступности сети, позволяя доставлять сообщения даже при сбоях и проблемах с подключением. n\n\nРеле также можно использовать для повышения конфиденциальности и сетевой безопасности, поскольку они могут скрывать местоположение и идентификационные данные приложений, которые обмениваются данными. Это может быть полезно в условиях цензуры или слежки.\n\n \nВажно отметить, что реле также могут использоваться в злонамеренных целях, таких как прослушивание или цензура сетевого трафика.\n \n\nПоэтому важно тщательно оценивать использование реле и принимать соответствующие меры безопасности для защиты конфиденциальности и сетевой безопасности.", "keysTitle": "Что такое ключи?", @@ -38,6 +39,7 @@ "homeNavigator": { "Group": "", "QrReader": "", + "Wallet": "Wallet", "ImageGallery": "", "Search": "", "ProfileCreate": "Create profile", @@ -189,7 +191,8 @@ "copy": "Скопировать", "open": "Открыть кошелек", "anonTip": "Anonymous tip", - "zap": "Zap" + "zap": "Zap", + "pay": "Pay" }, "notificationsFeed": { "reposted": "Reposted", diff --git a/frontend/Locales/zhCn.json b/frontend/Locales/zhCn.json index 7c7cc61..782ce27 100644 --- a/frontend/Locales/zhCn.json +++ b/frontend/Locales/zhCn.json @@ -2,9 +2,10 @@ "common": { "time": { "today": "Today", - "yerterday": "Yesterday" + "yesterday": "Yesterday" }, "drawers": { + "walletLogout": "Logout", "relaysTitle": "关于中继", "relaysDescription": "中继是网络上的节点,作为应用程序之间传输消息的中介。\n\n\n中继可用于提高网络的弹性和可用性,即使在连接出现故障或中断的情况下,也能传递消息。 \n\n\n中继还可用于提高隐私和网络安全,因为它们可以隐藏相互通信的用户的位置和身份。 \n\n\n这在审查或监视是一个问题的环境中是很有用的。 \n\n\n需要注意的是,中继也可以用于恶意的目的,如嗅探或审查网络流量。", "keysTitle": "这些密钥是什么?", @@ -40,6 +41,7 @@ "QrReader": "", "ImageGallery": "", "Search": "", + "Wallet": "Wallet", "ProfileCreate": "创建用户", "ProfileConnect": "", "Contacts": "联系人", @@ -187,7 +189,8 @@ "copy": "复制", "open": "打开钱包", "anonTip": "匿名赞赏", - "zap": "赞赏" + "zap": "赞赏", + "pay": "Pay" }, "notificationsFeed": { "reposted": "Reposted", diff --git a/frontend/Pages/FeedNavigator/index.tsx b/frontend/Pages/FeedNavigator/index.tsx index 0bd4ff2..03d1b2a 100644 --- a/frontend/Pages/FeedNavigator/index.tsx +++ b/frontend/Pages/FeedNavigator/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Platform, View } from 'react-native' import type { DrawerNavigationProp } from '@react-navigation/drawer' import { CardStyleInterpolators, createStackNavigator } from '@react-navigation/stack' -import { Appbar, Text, useTheme } from 'react-native-paper' +import { Appbar, Button, Text, useTheme } from 'react-native-paper' import RBSheet from 'react-native-raw-bottom-sheet' import { useTranslation } from 'react-i18next' import HomePage from '../HomePage' @@ -29,11 +29,14 @@ import DatabaseModule from '../../lib/Native/DatabaseModule' import ImageGalleryPage from '../ImageGalleryPage' import { navigate } from '../../lib/Navigation' import SearchPage from '../SearchPage' +import WalletPage from '../WalletPage' +import { WalletContext } from '../../Contexts/WalletContext' export const HomeNavigator: React.FC = () => { const theme = useTheme() const { t } = useTranslation('common') const { displayRelayDrawer, setDisplayrelayDrawer } = React.useContext(RelayPoolContext) + const { logoutWallet } = React.useContext(WalletContext) const { displayUserDrawer, setDisplayNoteDrawer, @@ -46,6 +49,7 @@ export const HomeNavigator: React.FC = () => { const bottomSheetProfileRef = React.useRef(null) const bottomSheetNoteRef = React.useRef(null) const bottomSheetRelayRef = React.useRef(null) + const bottomWalletRef = React.useRef(null) const Stack = React.useMemo(() => createStackNavigator(), []) const cardStyleInterpolator = React.useMemo( () => @@ -131,6 +135,12 @@ export const HomeNavigator: React.FC = () => { route.params?.title ? route.params?.title : t(`homeNavigator.${route.name}`) } /> + {['Wallet'].includes(route.name) && ( + bottomWalletRef.current?.open()} + /> + )} {['Profile', 'Conversation'].includes(route.name) && ( { + @@ -228,6 +239,19 @@ export const HomeNavigator: React.FC = () => { {t('drawers.relaysDescription')} + + + + + ) } diff --git a/frontend/Pages/HomePage/ConversationsFeed/index.tsx b/frontend/Pages/HomePage/ConversationsFeed/index.tsx index 0d7ed65..58feaa9 100644 --- a/frontend/Pages/HomePage/ConversationsFeed/index.tsx +++ b/frontend/Pages/HomePage/ConversationsFeed/index.tsx @@ -38,7 +38,7 @@ import { useTranslation } from 'react-i18next' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import { useFocusEffect } from '@react-navigation/native' import ProfileData from '../../../Components/ProfileData' -import { handleInfinityScroll } from '../../../Functions/NativeFunctions' +import { formatHour, handleInfinityScroll } from '../../../Functions/NativeFunctions' export const ConversationsFeed: React.FC = () => { const initialPageSize = 14 @@ -154,7 +154,7 @@ export const ConversationsFeed: React.FC = () => { - {formatHour(item?.created_at, false)} + {formatHour(item?.created_at)} {item.pubkey !== publicKey && !item.read && } diff --git a/frontend/Pages/ProfileLoadPage/ThirdStep/index.tsx b/frontend/Pages/ProfileLoadPage/ThirdStep/index.tsx index 4f59c8e..efd2164 100644 --- a/frontend/Pages/ProfileLoadPage/ThirdStep/index.tsx +++ b/frontend/Pages/ProfileLoadPage/ThirdStep/index.tsx @@ -130,6 +130,7 @@ export const ThirdStep: React.FC = ({ nextStep, skip }) => { data={asignation} renderItem={renderItem} ItemSeparatorComponent={Divider} + style={styles.list} /> @@ -182,6 +183,9 @@ const styles = StyleSheet.create({ relayColor: { paddingTop: 9, }, + list: { + maxHeight: 230, + }, }) export default ThirdStep diff --git a/frontend/Pages/WalletPage/index.tsx b/frontend/Pages/WalletPage/index.tsx new file mode 100644 index 0000000..0698b18 --- /dev/null +++ b/frontend/Pages/WalletPage/index.tsx @@ -0,0 +1,308 @@ +import Clipboard from '@react-native-clipboard/clipboard' +import { differenceInDays, format, fromUnixTime, isSameDay } from 'date-fns' +import { t } from 'i18next' +import React, { useEffect, useMemo } from 'react' +import { type ListRenderItem, StyleSheet, View } from 'react-native' +import { FlatList } from 'react-native-gesture-handler' +import { + Avatar, + Button, + Snackbar, + Text, + TextInput, + TouchableRipple, + useTheme, +} from 'react-native-paper' +import RBSheet from 'react-native-raw-bottom-sheet' +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' +import NostrosAvatar from '../../Components/NostrosAvatar' +import { AppContext } from '../../Contexts/AppContext' +import { type WalletAction, WalletContext } from '../../Contexts/WalletContext' +import { getZaps, type Zap } from '../../Functions/DatabaseFunctions/Zaps' +import { navigate } from '../../lib/Navigation' + +export const WalletPage: React.FC = () => { + const theme = useTheme() + const { getSatoshiSymbol, database, setDisplayUserDrawer } = React.useContext(AppContext) + const { refreshLndHub, active, balance, transactions, invoices, updatedAt } = + React.useContext(WalletContext) + const [lnHubAddress, setLndHubAddress] = React.useState() + const [showNotification, setShowNotification] = React.useState() + const [actions, setActions] = React.useState([]) + const [zaps, setZaps] = React.useState>({}) + const bottomLndHubRef = React.useRef(null) + + useEffect(refreshLndHub, []) + useEffect(() => { + const array = [...transactions, ...invoices].sort( + (item1, item2) => item2.timestamp - item1.timestamp, + ) + setActions(array) + if (database) { + getZaps(database, { preimages: array.map((item) => item.id) }).then((results) => { + if (results) { + const map: Record = {} + results.forEach((zap) => { + map[zap.preimage] = zap + }) + setZaps(map) + } + }) + } + }, [updatedAt]) + + const pasteLndHub: () => void = () => { + Clipboard.getString().then((value) => { + setLndHubAddress(value ?? '') + }) + } + + const connectLndHub: () => void = () => { + const lnHubRegExp = /^lndhub:\/\/(\S*):(\S*)@(\S*)$/gi + if (lnHubAddress) { + const match = [...lnHubAddress.matchAll(lnHubRegExp)] + if (match.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let [_full, login, password, uri] = match[0] + if (uri[uri.length - 1] === '/') { + uri = uri.substring(0, uri.length - 1) + } + refreshLndHub(login, password, uri) + setLndHubAddress(undefined) + bottomLndHubRef.current?.close() + } + } + } + + const bottomSheetStyles = React.useMemo(() => { + return { + container: { + backgroundColor: theme.colors.background, + paddingTop: 16, + paddingRight: 16, + paddingBottom: 32, + paddingLeft: 16, + borderTopRightRadius: 28, + borderTopLeftRadius: 28, + height: 'auto', + }, + } + }, []) + + const login = useMemo( + () => ( + + + + ), + [], + ) + + const renderAction: ListRenderItem = ({ item, index }) => { + const date = fromUnixTime(item.timestamp) + const prevDate = index > 0 ? fromUnixTime(actions[index - 1].timestamp) : new Date() + + const formatPattern = differenceInDays(new Date(), date) < 7 ? 'EEEE' : 'MM-dd-yy' + + const zap = zaps[item.id] + + return ( + <> + {(index === 0 || !isSameDay(date, prevDate)) && ( + {format(date, formatPattern)} + )} + { + if (zap) { + if (zap.zapped_event_id) { + navigate('Note', { noteId: zap.zapped_event_id }) + } else if (zap.zapper_user_id) { + setDisplayUserDrawer(zap.zapper_user_id) + } + } + }} + disabled={!zap} + > + + + {zap ? ( + + ) : ( + + )} + + + + + + + {item.type === 'transaction' && '-'} + {`${item.monto} `} + + {getSatoshiSymbol()} + + + {format(date, 'HH:mm')} + + + + + {item.description} + + + + {item.type === 'transaction' ? ( + + ) : ( + + )} + + + + + ) + } + + return ( + + {active ? ( + + + + {`${balance} `} + + {getSatoshiSymbol()} + + + + item.id} + /> + + ) : ( + login + )} + {showNotification && ( + setShowNotification(undefined)} + onDismiss={() => setShowNotification(undefined)} + > + {t(`profileCard.notifications.${showNotification}`)} + + )} + + + + {t('walletPage.addLnhub')} + + + } + /> + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + justifyContent: 'space-between', + alignContent: 'center', + }, + center: { + justifyContent: 'center', + alignContent: 'center', + height: '100%', + padding: 16, + }, + drawerParagraph: { + marginBottom: 16, + }, + snackbar: { + marginBottom: 85, + flex: 1, + }, + balance: { + height: 180, + justifyContent: 'center', + }, + balanceNumber: { + justifyContent: 'center', + flexDirection: 'row', + }, + balanceSymbol: { + paddingTop: 18, + }, + list: { + paddingLeft: 16, + paddingTop: 16, + }, + listItem: { + paddingTop: 16, + paddingBottom: 16, + flexDirection: 'row', + }, + row: { + flexDirection: 'row', + }, + listAvatar: { + width: '15%', + flexDirection: 'row', + justifyContent: 'flex-start', + }, + listIcon: { + width: '10%', + flexDirection: 'row', + justifyContent: 'flex-end', + }, + listItemSection: { + width: '70%', + justifyContent: 'space-between', + paddingLeft: 16, + paddingRight: 16, + }, + listItemSymbol: { + paddingTop: 5, + }, + listData: { + justifyContent: 'space-between', + flexDirection: 'row', + width: '100%', + }, +}) + +export default WalletPage diff --git a/frontend/index.tsx b/frontend/index.tsx index c787327..5f3d34c 100644 --- a/frontend/index.tsx +++ b/frontend/index.tsx @@ -20,6 +20,7 @@ import nostrosDarkTheme from './Constants/Theme/theme-dark.json' import { navigationRef } from './lib/Navigation' import { UserContextProvider } from './Contexts/UserContext' import NostrosDrawerNavigator from './Pages/NostrosDrawerNavigator' +import { WalletContextProvider } from './Contexts/WalletContext' export const Frontend: React.FC = () => { const { LightTheme, DarkTheme } = adaptNavigationTheme({ @@ -41,15 +42,17 @@ export const Frontend: React.FC = () => { - - - - - {() => } - - - - + + + + + + {() => } + + + + +