├── .easignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── App.jsx ├── README.md ├── Root.jsx ├── app.json ├── assets ├── 69033-bolt-lightning-sangy.json ├── Montserrat-Bold.ttf ├── Montserrat-Regular.ttf ├── Montserrat-VariableFont_wght.ttf ├── Satoshi-Symbol.ttf ├── adaptive-icon.png ├── amped_logo.png ├── amped_placeholder.png ├── current-android-logo-transparent.png ├── favicon.png ├── icon.png ├── lightning_logo_negativ.png ├── placeholder.png ├── splash.png ├── user_placeholder.jpg └── zap-success.json ├── babel.config.js ├── components ├── AppStateChecker.js ├── BackButton.jsx ├── BackHeader.jsx ├── BackHeaderWithButton.jsx ├── CharacterImagePlaceholder.tsx ├── CustomButton.tsx ├── CustomKeyboardView.tsx ├── CustomTabBar.js ├── ExpandableInput.tsx ├── ExpandingSearch.tsx ├── Images │ ├── FeedImage.js │ └── FullScreenImage.jsx ├── Input.jsx ├── LoadingSkeleton.jsx ├── LoadingSpinner.js ├── MenuBottomSheet.tsx ├── MenuBottomSheetWithData.tsx ├── NumPad.tsx ├── PostMenuBottomSheet.jsx ├── Posts │ ├── ImagePost.js │ ├── PostActionBar.jsx │ ├── TextPost.jsx │ ├── ZapPost.js │ └── index.js ├── PressableIcon.tsx ├── QrScanner.js ├── SelectProfilePicture.js ├── SuccessToast.tsx ├── SwitchBar.tsx ├── TabBarHeaderLeft.jsx ├── TabBarHeaderRight.jsx ├── TabBarIcon.jsx ├── UserSearchList.tsx ├── UserSearchResultItem.jsx ├── VideoPlayer.js ├── WarningSign.tsx ├── WarningToast.tsx └── index.js ├── constants ├── index.js └── regex.js ├── eas.json ├── features ├── authSlice.js ├── badges │ ├── badgeSlice.js │ ├── components │ │ ├── AwardedBadge.jsx │ │ ├── BadgeBar.jsx │ │ ├── BadgeIcon.jsx │ │ ├── IssuedBy.jsx │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ ├── useAwardedBadges.js │ │ └── useBadge.js │ ├── index.js │ ├── utils │ │ ├── getBadge.js │ │ ├── index.js │ │ └── publishProfileBadges.js │ └── views │ │ ├── BadgeDetailView.jsx │ │ ├── ChooseBadgeView.jsx │ │ └── index.js ├── comments │ ├── components │ │ ├── Comment.tsx │ │ ├── CommentHeader.js │ │ ├── ItemSeperator.jsx │ │ ├── PullDownNote.tsx │ │ ├── PullUp.tsx │ │ └── index.js │ ├── hooks │ │ ├── useHeaderNotes.js │ │ ├── useReplies.js │ │ └── useThread.ts │ ├── utils │ │ ├── publishReply.js │ │ └── threads.ts │ └── views │ │ ├── CommentScreen.jsx │ │ └── ThreadScreen.jsx ├── community │ ├── communitySlice.js │ ├── components │ │ ├── CommunitiesTitle.tsx │ │ ├── CommunityList.tsx │ │ ├── CommunityListItem.tsx │ │ ├── JoinPrompt.tsx │ │ ├── Message.tsx │ │ ├── RelayMessage.tsx │ │ ├── SentMessage.tsx │ │ └── index.ts │ ├── hooks │ │ ├── index.js │ │ ├── useChat.js │ │ ├── useCommunities.js │ │ └── useIsMember.js │ ├── index.js │ ├── models │ │ └── Community.js │ ├── nav │ │ └── CommunityNavigator.jsx │ ├── utils │ │ ├── index.ts │ │ └── nostr.ts │ └── views │ │ ├── CommunitiesView.jsx │ │ ├── CommunityView.jsx │ │ └── index.js ├── dvm │ ├── components │ │ ├── DVMHeader.tsx │ │ ├── ImageGenRequest.tsx │ │ └── ImageGenResult.tsx │ ├── hooks │ │ ├── index.ts │ │ └── useImageJob.tsx │ ├── nav │ │ └── DvmNavigator.jsx │ ├── utils │ │ └── publishImageJob.ts │ └── views │ │ ├── DvmSelectionScreen.jsx │ │ ├── ImageGenScreen.jsx │ │ └── index.ts ├── homefeed │ ├── components │ │ ├── ActionBar.jsx │ │ ├── FullImagePost.tsx │ │ ├── GetStartedItems.js │ │ ├── HomeFeed.jsx │ │ ├── ImagePost.js │ │ ├── NewPostButton.tsx │ │ ├── PostItem.js │ │ ├── ReadMoreModal.js │ │ ├── TextPost.tsx │ │ ├── UserBanner.jsx │ │ └── index.js │ └── hooks │ │ └── usePaginatedFeed.js ├── interactionSlice.ts ├── introSlice.js ├── mentions │ ├── components │ │ ├── Mention.jsx │ │ ├── ZapMention.jsx │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ ├── useNoteMentions.js │ │ └── useZapMentions.js │ ├── nav │ │ └── MentionsNavigator.jsx │ └── views │ │ ├── MentionsView.js │ │ ├── ZapMentionsView.js │ │ └── index.js ├── messages │ ├── components │ │ └── ConversationText.jsx │ ├── hooks │ │ ├── useConversations.js │ │ └── useMessages.js │ ├── index.js │ ├── nav │ │ ├── ConversationNavigator.jsx │ │ └── index.js │ ├── utils │ │ └── publishMessage.js │ └── views │ │ ├── ActiveConversationsScreen.jsx │ │ ├── ConversationScreen.jsx │ │ └── index.js ├── messagesSlice.js ├── plebhy │ ├── Views │ │ ├── PlebhyGifView.jsx │ │ ├── PlebhyStickerView.jsx │ │ └── index.js │ ├── components │ │ ├── GifContainer.jsx │ │ └── index.js │ ├── hooks │ │ └── useZapPlebhy.js │ ├── index.js │ └── nav │ │ └── PlebhyNavigator.jsx ├── post │ ├── nav │ │ └── PostNavigator.jsx │ ├── utils │ │ ├── publishNote.ts │ │ └── publishStatus.ts │ └── views │ │ ├── NewPostScreen.jsx │ │ ├── NewStatusScreen.jsx │ │ └── index.js ├── premium │ ├── components │ │ ├── FeatureCard.tsx │ │ └── index.ts │ ├── index.ts │ ├── utils │ │ ├── index.ts │ │ └── utils.ts │ └── views │ │ ├── PremiumView.jsx │ │ └── index.ts ├── profile │ ├── ProfilePost.js │ ├── components │ │ ├── ProfileHeader.js │ │ └── ProfileInfo.jsx │ ├── hooks │ │ └── useIsAuthed.js │ ├── utils │ │ └── getUsersPosts.js │ └── views │ │ └── ProfileQRScreen.js ├── relays │ ├── components │ │ ├── RelayItem.jsx │ │ ├── RelaySettingsHeader.jsx │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ └── useRelayUrls.js │ ├── index.js │ ├── nav │ │ └── RelaySettingsNav.jsx │ ├── relaysSlice.js │ └── views │ │ ├── AddRelayView.jsx │ │ ├── RelayOverviewView.jsx │ │ └── index.js ├── reports │ ├── utils │ │ └── publishReport.js │ └── views │ │ └── ReportPostModal.js ├── search │ ├── components │ │ ├── ResultItem.jsx │ │ ├── SearchResultItem.jsx │ │ ├── TrendingImages.jsx │ │ ├── TrendingItem.jsx │ │ ├── TrendingNote.jsx │ │ ├── TrendingProfile.jsx │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ └── useUsersInStore.js │ ├── index.js │ ├── nav │ │ ├── SearchNavigator.jsx │ │ └── index.js │ ├── utils │ │ └── findUser.js │ └── views │ │ ├── SearchView.jsx │ │ ├── TrendingPostView.jsx │ │ ├── TrendingProfilesView.jsx │ │ └── index.js ├── settings │ ├── components │ │ ├── SettingItem.tsx │ │ └── index.ts │ ├── index.js │ ├── locale │ │ ├── de.json │ │ ├── en.json │ │ └── index.js │ ├── nav │ │ └── SettingsNavigator.jsx │ └── views │ │ ├── DeleteAccountScreen.jsx │ │ ├── KeysScreen.jsx │ │ ├── LanguageSettingsScreen.tsx │ │ ├── ListScreen.jsx │ │ ├── NotifcationsSettingsScreen.jsx │ │ ├── PaymentSettingsScreen.jsx │ │ ├── UserSettingsScreen.jsx │ │ └── index.js ├── shared │ └── utils │ │ └── getAge.js ├── userSlice.js ├── wallet │ ├── components │ │ ├── InTxItem.jsx │ │ ├── OutTxItem.jsx │ │ ├── RedeemModal.tsx │ │ ├── SweepModal.tsx │ │ ├── TopUpCard.tsx │ │ └── index.js │ ├── hooks │ │ ├── useBalance.js │ │ └── useTransactionHistory.js │ └── views │ │ ├── ReceiveScreen.jsx │ │ ├── SendScreen.jsx │ │ └── index.js ├── walletconnect │ ├── components │ │ ├── WalletconnectItem.jsx │ │ ├── WalletconnectSettingsHeader.jsx │ │ └── index.js │ ├── nav │ │ └── WalletconnectSettingsNav.jsx │ ├── utils │ │ ├── keys.js │ │ └── walletApi.js │ ├── views │ │ ├── AddWalletconnectView.jsx │ │ ├── WalletconnectInfoView.jsx │ │ ├── WalletconnectOverviewView.jsx │ │ └── index.js │ └── walletconnectSlice.js ├── welcome │ ├── components │ │ ├── ImportTypeItem.jsx │ │ ├── IntroductionItem.jsx │ │ └── index.js │ ├── index.js │ ├── locale │ │ ├── de.json │ │ ├── en.json │ │ └── index.js │ ├── nav │ │ ├── CreateProfileNavigator.jsx │ │ ├── ImportNavigator.jsx │ │ └── WelcomeNavigator.jsx │ └── views │ │ ├── CreateProfileView.jsx │ │ ├── EULAView.jsx │ │ ├── ImportKeyView.jsx │ │ ├── ImportSelectionView.jsx │ │ ├── ImportWordsView.jsx │ │ ├── IntroductionView.jsx │ │ ├── LoadingProfileView.jsx │ │ ├── NewImportWordsView.jsx │ │ ├── SelectImageView.jsx │ │ ├── StartUpView.jsx │ │ ├── UsernameView.jsx │ │ └── index.js └── zaps │ ├── Zap.js │ ├── components │ └── ZapItem.js │ ├── hooks │ └── useIsZapped.js │ └── utils │ ├── getZapInvoice.js │ └── getZaps.js ├── hooks ├── index.js ├── useErrorToast.tsx ├── useFollowUser.js ├── useInteractions.tsx ├── useIsFollowed.js ├── useLanguage.ts ├── usePaginatedPosts.js ├── useParseContent.jsx ├── useSilentFollow.js ├── useStatus.tsx ├── useSubscribeEvents.js ├── useSubscribeReplies.js ├── useUnfollowUser.js ├── useUpdateFollowing.js ├── useUser.tsx ├── useUsersInStore.js └── useZapNote.js ├── index.js ├── metro.config.js ├── models ├── Kind1063Media.tsx ├── Kind1Note.ts └── Kind65005ImgRequest.ts ├── nav ├── AuthedNavigator.jsx ├── OwnProfileNavigator.jsx ├── ProfileNavigator.jsx └── WalletNavigator.jsx ├── package-lock.json ├── package.json ├── patches ├── @noble+hashes+1.3.0.patch ├── @noble+secp256k1+1.7.1.patch └── react-native-root-toast+3.4.1.patch ├── plugins └── withAndroidNamespace.js ├── services └── walletApi.js ├── store ├── listenerMiddleware.js └── store.js ├── styles ├── colors.js ├── globalStyles.js └── index.js ├── translations ├── locale │ └── common │ │ ├── de.json │ │ └── en.json └── translations.js ├── tsconfig.json ├── utils ├── bitcoin │ └── lnurl.js ├── cache │ └── asyncStorage.js ├── database.js ├── images.js ├── images.ts ├── index.js ├── internal.js ├── keys.js ├── nostr │ ├── Event.js │ └── keys.js ├── nostrV2 │ ├── Event.js │ ├── Message.js │ ├── Note.js │ ├── Reply.js │ ├── createEvent.js │ ├── getEvents.js │ ├── getUserData.js │ ├── getUsersPosts.js │ ├── index.ts │ ├── mentions.ts │ ├── publishEvents.js │ ├── relayPool.js │ ├── relays.ts │ └── tags.ts ├── notifications.js ├── secureStore.js ├── time.js ├── users.js ├── wallet.js └── wordlist.json ├── views ├── HomeView.jsx ├── OwnProfileScreen.jsx ├── ProfileScreen.jsx ├── home │ └── ZapListModal.js ├── profile │ └── EditProfileScreen.jsx ├── wallet │ ├── WalletConfirmScreen.js │ ├── WalletHomeScreen.jsx │ ├── WalletInfoScreen.js │ ├── WalletInvoiceScreen.jsx │ ├── WalletSendLnurlScreen.js │ └── WalletTransactionScreen.jsx └── welcome │ ├── TwitterModal.js │ └── VerifyTwitterModal.js └── yarn.lock /.easignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | *.local 16 | *.xcworkspacedata 17 | *.plist 18 | 19 | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 20 | # The following patterns were generated by expo-cli 21 | 22 | # OSX 23 | # 24 | .DS_Store 25 | 26 | # Xcode 27 | # 28 | build/ 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | xcuserdata 38 | *.xccheckout 39 | *.moved-aside 40 | DerivedData 41 | *.hmap 42 | *.ipa 43 | *.xcuserstate 44 | project.xcworkspace 45 | 46 | # Android/IntelliJ 47 | # 48 | build/ 49 | .idea 50 | .gradle 51 | local.properties 52 | *.iml 53 | *.hprof 54 | .cxx/ 55 | *.keystore 56 | !debug.keystore 57 | 58 | # node.js 59 | # 60 | node_modules/ 61 | npm-debug.log 62 | yarn-error.log 63 | 64 | # Bundle artifacts 65 | *.jsbundle 66 | 67 | # CocoaPods 68 | /ios/Pods/ 69 | 70 | # Temporary files created by Metro to check the health of the file watcher 71 | .metro-health-check* 72 | 73 | # Expo 74 | .expo/ 75 | web-build/ 76 | dist/ 77 | /android 78 | 79 | # @end expo-cli 80 | 81 | ios/ 82 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:react/recommended', 'airbnb'], 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['react'], 13 | settings: { 14 | 'import/resolver': { 15 | typescript: {}, 16 | }, 17 | }, 18 | rules: { 19 | 'react/prop-types': 0, 20 | 'react/function-component-definition': [ 21 | 1, 22 | { 23 | namedComponents: 'arrow-function', 24 | }, 25 | ], 26 | 'object-curly-newline': 0, 27 | 'no-param-reassign': [ 28 | 'error', 29 | { 30 | props: true, 31 | ignorePropertyModificationsFor: ['state'], 32 | }, 33 | ], 34 | 'import/extensions': [ 35 | 'error', 36 | 'ignorePackages', 37 | { 38 | js: 'never', 39 | jsx: 'never', 40 | ts: 'never', 41 | tsx: 'never', 42 | }, 43 | ], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | *.local 16 | *.xcworkspacedata 17 | *.plist 18 | 19 | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 20 | # The following patterns were generated by expo-cli 21 | 22 | # OSX 23 | # 24 | .DS_Store 25 | 26 | # Xcode 27 | # 28 | build/ 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | xcuserdata 38 | *.xccheckout 39 | *.moved-aside 40 | DerivedData 41 | *.hmap 42 | *.ipa 43 | *.xcuserstate 44 | project.xcworkspace 45 | 46 | # Android/IntelliJ 47 | # 48 | build/ 49 | .idea 50 | .gradle 51 | local.properties 52 | *.iml 53 | *.hprof 54 | .cxx/ 55 | *.keystore 56 | !debug.keystore 57 | 58 | # node.js 59 | # 60 | node_modules/ 61 | npm-debug.log 62 | yarn-error.log 63 | 64 | # Bundle artifacts 65 | *.jsbundle 66 | 67 | # CocoaPods 68 | /ios/Pods/ 69 | 70 | # Temporary files created by Metro to check the health of the file watcher 71 | .metro-health-check* 72 | 73 | # Expo 74 | .expo/ 75 | web-build/ 76 | dist/ 77 | google-services.json 78 | /android 79 | 80 | # @end expo-cli 81 | 82 | ios/ 83 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'text-encoding-polyfill'; 3 | import { Provider } from 'react-redux'; 4 | import PolyfillCrypto from 'react-native-webview-crypto'; 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import { RootSiblingParent } from 'react-native-root-siblings'; 7 | import 'react-native-gesture-handler'; 8 | import 'react-native-url-polyfill/auto'; 9 | import { store } from './store/store'; 10 | import Root from './Root'; 11 | import { injectStore } from './utils/nostrV2/Event'; 12 | 13 | const App = () => { 14 | injectStore(store); 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /assets/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /assets/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /assets/Montserrat-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/Montserrat-VariableFont_wght.ttf -------------------------------------------------------------------------------- /assets/Satoshi-Symbol.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/Satoshi-Symbol.ttf -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/amped_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/amped_logo.png -------------------------------------------------------------------------------- /assets/amped_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/amped_placeholder.png -------------------------------------------------------------------------------- /assets/current-android-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/current-android-logo-transparent.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/icon.png -------------------------------------------------------------------------------- /assets/lightning_logo_negativ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/lightning_logo_negativ.png -------------------------------------------------------------------------------- /assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/placeholder.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/splash.png -------------------------------------------------------------------------------- /assets/user_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/assets/user_placeholder.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin', 'transform-inline-environment-variables'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /components/AppStateChecker.js: -------------------------------------------------------------------------------- 1 | import React, {useRef, useState, useEffect} from 'react'; 2 | import {AppState, StyleSheet, Text, View} from 'react-native'; 3 | import { disconnectRelays, reconnectRelays } from '../utils/nostrV2'; 4 | 5 | const AppStateChecker = () => { 6 | const appState = useRef(AppState.currentState); 7 | let timer; 8 | useEffect(() => { 9 | const subscription = AppState.addEventListener('change', nextAppState => { 10 | clearTimeout(timer) 11 | if ( 12 | appState.current.match(/inactive|background/) && 13 | nextAppState === 'active' 14 | ) { 15 | console.log('Runs!') 16 | reconnectRelays(); 17 | } 18 | 19 | if (nextAppState === 'background') { 20 | timer = setTimeout(() => { 21 | console.log('In background for 30 seconds... disconnecting from relays...') 22 | disconnectRelays(); 23 | }, 30000) 24 | } 25 | 26 | appState.current = nextAppState; 27 | }); 28 | 29 | return () => { 30 | subscription.remove(); 31 | }; 32 | }, []); 33 | return ( 34 | <> 35 | 36 | ) 37 | } 38 | 39 | export default AppStateChecker -------------------------------------------------------------------------------- /components/BackButton.jsx: -------------------------------------------------------------------------------- 1 | import { Pressable, StyleSheet, Text } from 'react-native'; 2 | import React from 'react'; 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { colors, globalStyles } from '../styles'; 5 | 6 | const styles = StyleSheet.create({ 7 | container: { 8 | flexDirection: 'row', 9 | paddingVertical: 10, 10 | paddingHorizontal: 12, 11 | alignItems: 'center', 12 | backgroundColor: colors.backgroundSecondary, 13 | alignSelf: 'flex-start', 14 | borderRadius: 10, 15 | }, 16 | containerActive: { 17 | backgroundColor: colors.backgroundActive, 18 | }, 19 | }); 20 | 21 | const BackButton = ({ onPress, text }) => ( 22 | [ 24 | styles.container, 25 | pressed ? styles.containerActive : undefined, 26 | ]} 27 | onPress={onPress} 28 | > 29 | 30 | 36 | {text || 'Back'} 37 | 38 | 39 | ); 40 | 41 | export default BackButton; 42 | -------------------------------------------------------------------------------- /components/BackHeader.jsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import BackButton from './BackButton'; 4 | import colors from '../styles/colors'; 5 | 6 | const BackHeader = memo(({ navigation }) => ( 7 | 14 | { 16 | navigation.goBack(); 17 | }} 18 | /> 19 | 20 | )); 21 | 22 | export default BackHeader; 23 | -------------------------------------------------------------------------------- /components/BackHeaderWithButton.jsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import BackButton from './BackButton'; 4 | import colors from '../styles/colors'; 5 | 6 | const BackHeaderWithButton = memo(({ navigation, rightButton }) => ( 7 | 16 | { 18 | navigation.goBack(); 19 | }} 20 | /> 21 | {rightButton()} 22 | 23 | )); 24 | 25 | export default BackHeaderWithButton; 26 | -------------------------------------------------------------------------------- /components/CharacterImagePlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { colors, globalStyles } from '../styles'; 4 | 5 | type CharacterImagePlaceholderProps = { 6 | name: string; 7 | }; 8 | 9 | const CharacterImagePlaceholder = ({ 10 | name, 11 | }: CharacterImagePlaceholderProps) => { 12 | return ( 13 | 14 | {name.slice(0, 1).toUpperCase()} 15 | 16 | ); 17 | }; 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | height: 50, 22 | width: 50, 23 | borderRadius: 25, 24 | backgroundColor: colors.backgroundSecondary, 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | }, 28 | text: { 29 | marginBottom: 0 30 | } 31 | }); 32 | 33 | export default CharacterImagePlaceholder; 34 | -------------------------------------------------------------------------------- /components/CustomKeyboardView.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | Text, 4 | StyleSheet, 5 | KeyboardAvoidingView, 6 | Platform, 7 | useWindowDimensions, 8 | } from 'react-native'; 9 | import React, { ReactNode, useState } from 'react'; 10 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 11 | 12 | type CustomKeyboardViewProps = { 13 | children: ReactNode; 14 | noBottomBar?: boolean; 15 | }; 16 | 17 | const styles = StyleSheet.create({ 18 | outerContainer: { 19 | flex: 1, 20 | }, 21 | avoidingContainer: { 22 | flex: 1, 23 | flexGrow: 1, 24 | }, 25 | }); 26 | 27 | const CustomKeyboardView = ({ 28 | children, 29 | noBottomBar, 30 | }: CustomKeyboardViewProps) => { 31 | const [outerContainerHeight, setOuterContainerHeight] = 32 | useState(null); 33 | const { height } = useWindowDimensions(); 34 | let bottomTabBarHeight: number; 35 | if (noBottomBar) { 36 | bottomTabBarHeight = 0; 37 | } else { 38 | bottomTabBarHeight = useBottomTabBarHeight(); 39 | } 40 | return ( 41 | { 44 | setOuterContainerHeight(e.nativeEvent.layout.height); 45 | }} 46 | > 47 | 56 | {children} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default CustomKeyboardView; 63 | -------------------------------------------------------------------------------- /components/ExpandingSearch.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import Input from './Input'; 4 | 5 | const ExpandingSearch = () => { 6 | const [isOpen, setIsOpen] = useState(false); 7 | return ( 8 | 9 | { 12 | setIsOpen(true); 13 | console.log('focus'); 14 | }, 15 | }} 16 | /> 17 | 18 | ); 19 | }; 20 | 21 | export default ExpandingSearch; 22 | -------------------------------------------------------------------------------- /components/Images/FeedImage.js: -------------------------------------------------------------------------------- 1 | import { Pressable } from "react-native"; 2 | import React from "react"; 3 | import { useNavigation } from "@react-navigation/native"; 4 | import { Image } from "expo-image"; 5 | 6 | const FeedImage = ({ size, images }) => { 7 | const navigation = useNavigation(); 8 | const blurhash = 9 | "|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj["; 10 | return ( 11 | { 14 | navigation.navigate("ImageModal", { imageUri: images }); 15 | }} 16 | > 17 | 24 | 25 | ); 26 | }; 27 | 28 | export default FeedImage; 29 | -------------------------------------------------------------------------------- /components/Input.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { TextInput } from 'react-native-gesture-handler'; 4 | import { colors, globalStyles } from '../styles'; 5 | 6 | const Input = ({label, textInputConfig, labelStyle, inputStyle, invalid, alignment}) => { 7 | return ( 8 | <> 9 | {label ? {label} : undefined} 10 | 11 | 12 | ) 13 | } 14 | 15 | export default Input 16 | 17 | const styles = StyleSheet.create({ 18 | label: { 19 | marginBottom: 6, 20 | }, 21 | input: { 22 | width: '100%', 23 | backgroundColor: colors.backgroundSecondary, 24 | borderColor: colors.primary500, 25 | borderWidth: 1, 26 | borderRadius: 10, 27 | padding: 8, 28 | color: 'white', 29 | } 30 | }) -------------------------------------------------------------------------------- /components/LoadingSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated, { 3 | useAnimatedStyle, 4 | withRepeat, 5 | withSequence, 6 | withTiming, 7 | } from 'react-native-reanimated'; 8 | import { colors } from '../styles'; 9 | 10 | const LoadingSkeleton = () => { 11 | const pulseStyle = useAnimatedStyle(() => ({ 12 | opacity: withRepeat( 13 | withSequence( 14 | withTiming(0.1, { duration: 1000 }), 15 | withTiming(1, { duration: 1000 }), 16 | ), 17 | -1, 18 | true, 19 | ), 20 | })); 21 | return ( 22 | 25 | ); 26 | }; 27 | 28 | export default LoadingSkeleton; 29 | -------------------------------------------------------------------------------- /components/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | import React,{useEffect} from "react"; 2 | import Animated, { 3 | useAnimatedStyle, 4 | useSharedValue, 5 | withRepeat, 6 | withTiming, 7 | cancelAnimation, 8 | Easing, 9 | } from "react-native-reanimated"; 10 | import { colors } from "../styles"; 11 | 12 | const LoadingSpinner = ({ size }) => { 13 | const rotation = useSharedValue(0); 14 | 15 | const animatedStyles = useAnimatedStyle(() => { 16 | return { 17 | transform: [ 18 | { 19 | rotateZ: `${rotation.value}deg`, 20 | }, 21 | ], 22 | }; 23 | }, [rotation.value]); 24 | 25 | useEffect(() => { 26 | rotation.value = withRepeat( 27 | withTiming(360, { 28 | duration: 1000, 29 | easing: Easing.linear, 30 | }), 31 | -1 32 | ); 33 | return () => cancelAnimation(rotation); 34 | }, []); 35 | 36 | return ( 37 | 38 | ); 39 | }; 40 | 41 | export default LoadingSpinner; 42 | -------------------------------------------------------------------------------- /components/Posts/index.js: -------------------------------------------------------------------------------- 1 | export * from './ImagePost'; 2 | export { default as TextPost } from './TextPost'; 3 | export * from './ZapPost'; 4 | -------------------------------------------------------------------------------- /components/PressableIcon.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | Text, 4 | Pressable, 5 | GestureResponderEvent, 6 | StyleSheet, 7 | } from 'react-native'; 8 | import React from 'react'; 9 | import { Ionicons } from '@expo/vector-icons'; 10 | import { colors, globalStyles } from '../styles'; 11 | 12 | type PressableIconType = { 13 | icon: keyof typeof Ionicons.glyphMap, 14 | onPress: (event: GestureResponderEvent) => void, 15 | label?: string, 16 | }; 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | padding: 10, 21 | borderRadius: 100, 22 | backgroundColor: colors.backgroundSecondary, 23 | marginBottom: 6 24 | }, 25 | active: { 26 | backgroundColor: colors.backgroundActive, 27 | }, 28 | }); 29 | 30 | const PressableIcon = ({ icon, onPress, label }: PressableIconType) => { 31 | return ( 32 | 33 | [ 36 | styles.container, 37 | pressed ? styles.active : undefined, 38 | ]} 39 | > 40 | 41 | 42 | {label ? {label} : undefined} 43 | 44 | ); 45 | }; 46 | 47 | export default PressableIcon; 48 | -------------------------------------------------------------------------------- /components/SuccessToast.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { globalStyles } from '../styles'; 5 | 6 | type SuccessToastProps = { 7 | text: string; 8 | }; 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | flexDirection: 'row', 13 | alignItems: 'center', 14 | gap: 10 15 | } 16 | }) 17 | 18 | const SuccessToast = ({ text }: SuccessToastProps) => { 19 | return ( 20 | 21 | 22 | {text} 23 | 24 | ); 25 | }; 26 | 27 | export default SuccessToast; 28 | -------------------------------------------------------------------------------- /components/SwitchBar.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, SwitchChangeEvent } from 'react-native'; 2 | import React from 'react'; 3 | import { Switch } from 'react-native-gesture-handler'; 4 | import { colors, globalStyles } from '../styles'; 5 | 6 | type SwitchBarProps = { 7 | value: boolean; 8 | onChange: (event: SwitchChangeEvent) => void | Promise; 9 | text: string; 10 | disabled?: boolean; 11 | }; 12 | 13 | const SwitchBar = ({ value, onChange, text, disabled }: SwitchBarProps) => { 14 | return ( 15 | 27 | 33 | {text} 34 | 35 | 41 | 42 | ); 43 | }; 44 | 45 | export default SwitchBar; 46 | -------------------------------------------------------------------------------- /components/TabBarHeaderLeft.jsx: -------------------------------------------------------------------------------- 1 | import { Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import { useSelector } from 'react-redux'; 5 | import { Image } from 'expo-image'; 6 | import Ionicons from '@expo/vector-icons/Ionicons'; 7 | 8 | const TabBarHeaderLeft = () => { 9 | const navigation = useNavigation(); 10 | const pk = useSelector((state) => state.auth.pubKey); 11 | const user = useSelector((state) => state.messages.users[pk]); 12 | return ( 13 | { 23 | navigation.navigate('Profile', { 24 | screen: 'ProfileScreen', 25 | params: { pubkey: pk, name: user?.name || undefined }, 26 | }); 27 | }} 28 | > 29 | {user?.picture ? ( 30 | 38 | ) : ( 39 | 40 | )} 41 | 42 | ); 43 | }; 44 | 45 | export default TabBarHeaderLeft; 46 | -------------------------------------------------------------------------------- /components/TabBarHeaderRight.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import Ionicons from '@expo/vector-icons/Ionicons'; 5 | import { useGetWalletBalanceQuery } from '../services/walletApi'; 6 | import { colors, globalStyles } from '../styles'; 7 | 8 | const TabBarHeaderRight = () => { 9 | const navigation = useNavigation(); 10 | const { data } = useGetWalletBalanceQuery(); 11 | return ( 12 | 13 | { 20 | navigation.navigate('Wallet'); 21 | }} 22 | > 23 | 24 | {data ? `${data.balance.length > 4 ? `${Math.round(data.balance / 1000)}k` : data.balance}` : '----'}{' '} 25 | 26 | SATS 27 | 28 | 29 | 30 | { 36 | navigation.navigate('MentionsModal'); 37 | }} 38 | /> 39 | 40 | ); 41 | }; 42 | 43 | export default TabBarHeaderRight; 44 | -------------------------------------------------------------------------------- /components/TabBarIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Ionicons from '@expo/vector-icons/Ionicons'; 3 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 4 | 5 | const TabBarIcon = ({ route, focused, color, size }) => { 6 | let iconName; 7 | 8 | if (route.name === 'Home') { 9 | iconName = focused ? 'home-sharp' : 'home-outline'; 10 | } else if (route.name === 'Wallet') { 11 | iconName = focused ? 'wallet' : 'wallet-outline'; 12 | } else if (route.name === 'Settings') { 13 | iconName = focused ? 'settings' : 'settings-outline'; 14 | } else if (route.name === 'Search') { 15 | iconName = focused ? 'search-circle' : 'search-circle-outline'; 16 | } else if (route.name === 'New') { 17 | iconName = focused ? 'add-circle' : 'add-circle-outline'; 18 | } else if (route.name === 'Messages') { 19 | iconName = focused ? 'mail-sharp' : 'mail-outline'; 20 | } else if (route.name === 'Community') { 21 | iconName = focused ? 'chatbubbles' : 'chatbubbles-outline'; 22 | } else if (route.name === 'DVM') { 23 | return ; 24 | } 25 | return ; 26 | }; 27 | 28 | export default TabBarIcon; 29 | -------------------------------------------------------------------------------- /components/UserSearchList.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { FlatList } from 'react-native-gesture-handler'; 4 | import { useUsersInStore } from '../hooks'; 5 | import UserSearchResultItem from './UserSearchResultItem'; 6 | 7 | const UserSearchList = ({searchTerm, renderFunction}) => { 8 | const data = useUsersInStore(searchTerm); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default UserSearchList; 18 | -------------------------------------------------------------------------------- /components/UserSearchResultItem.jsx: -------------------------------------------------------------------------------- 1 | import { Text, StyleSheet, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { Image } from 'expo-image'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import { colors, globalStyles } from '../styles'; 6 | import { getValue } from '../utils'; 7 | 8 | const styles = StyleSheet.create({ 9 | container: { 10 | flexDirection: 'row', 11 | alignItems: 'center', 12 | paddingHorizontal: 6, 13 | paddingVertical: 12, 14 | backgroundColor: colors.backgroundSecondary, 15 | width: '100%', 16 | borderBottomColor: '#333333', 17 | borderBottomWidth: 1, 18 | }, 19 | image: { 20 | height: 40, 21 | width: 40, 22 | borderRadius: 20, 23 | marginRight: 12, 24 | }, 25 | }); 26 | 27 | const UserSearchResultItem = ({ userData }) => { 28 | const navigation = useNavigation(); 29 | const pressHandler = async () => { 30 | const sk = await getValue('privKey'); 31 | navigation.navigate('Chat', { pk: userData.pubkey, sk }); 32 | }; 33 | 34 | return ( 35 | 39 | 40 | {userData.name} 41 | 42 | ); 43 | }; 44 | 45 | export default UserSearchResultItem; 46 | -------------------------------------------------------------------------------- /components/VideoPlayer.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { View, StyleSheet, Button } from "react-native"; 3 | import { Video, AVPlaybackStatus } from "expo-av"; 4 | import Ionicons from "@expo/vector-icons/Ionicons"; 5 | import Pressable from "react-native/Libraries/Components/Pressable/Pressable"; 6 | 7 | const VideoPlayer = () => { 8 | const video = React.useRef(null); 9 | const [status, setStatus] = React.useState({}); 10 | return ( 11 | 12 | 42 | ); 43 | }; 44 | 45 | export default VideoPlayer; 46 | -------------------------------------------------------------------------------- /components/WarningSign.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native' 2 | import React from 'react' 3 | import { Ionicons } from '@expo/vector-icons' 4 | import { colors } from '../styles' 5 | 6 | type WarningSignProps = { 7 | text: string, 8 | type: 'warning' | 'danger' 9 | } 10 | 11 | const WarningSign = ({text, type}: WarningSignProps) => { 12 | return ( 13 | 14 | 15 | {text} 16 | 17 | ) 18 | } 19 | 20 | export default WarningSign -------------------------------------------------------------------------------- /components/WarningToast.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { globalStyles } from '../styles'; 5 | 6 | type WarningToastProps = { 7 | text: string; 8 | }; 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | flexDirection: 'row', 13 | alignItems: 'center', 14 | gap: 10 15 | } 16 | }) 17 | 18 | const WarningToast = ({ text }: WarningToastProps) => { 19 | return ( 20 | 21 | 22 | {text} 23 | 24 | ); 25 | }; 26 | 27 | export default WarningToast; 28 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | export { default as BackHeader } from './BackHeader'; 2 | export { default as CustomButton } from './CustomButton'; 3 | export { default as Input } from './Input'; 4 | export { default as LoadingSpinner } from './LoadingSpinner'; 5 | export { default as TabBarHeaderRight } from './TabBarHeaderRight'; 6 | export { default as TabBarHeaderLeft } from './TabBarHeaderLeft'; 7 | export { default as TabBarIcon } from './TabBarIcon'; 8 | export { default as LoadingSkeleton } from './LoadingSkeleton'; 9 | export { default as ExpandableInput } from './ExpandableInput'; 10 | export { default as ExpandingSearch } from './ExpandingSearch'; 11 | export { default as SwitchBar } from './SwitchBar'; 12 | export { default as SuccessToast } from './SuccessToast'; 13 | export { default as WarningToast } from './WarningToast'; 14 | export { default as CustomKeyboardView } from './CustomKeyboardView'; 15 | export { default as NumPad } from './NumPad'; 16 | export { default as MenuBottomSheet } from './MenuBottomSheet'; 17 | export { default as MenuBottomSheetWithData } from './MenuBottomSheetWithData'; 18 | -------------------------------------------------------------------------------- /constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './regex' -------------------------------------------------------------------------------- /constants/regex.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable operator-linebreak */ 2 | export const hexRegex = /^[0-9a-f]{64}$/i; 3 | 4 | export const bech32Regex = /^(npub)[a-zA-HJ-NP-Z0-9]+$/i; 5 | 6 | export const bech32Sk = /^(nsec)[a-zA-HJ-NP-Z0-9]+$/i; 7 | 8 | export const twitterRegex = /^@?(\w){1,15}$/; 9 | 10 | export const mentionRegex = /@\[(.*?)\]\((.*?)\)/g; 11 | 12 | export const emailRegex = 13 | // eslint-disable-next-line no-control-regex 14 | /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i; 15 | 16 | export const httpRegex = 17 | /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/; 18 | 19 | export const imageRegex = 20 | /(https?:[\/|.|\w|\s|\-|_]*\.(?:jpg|gif|png|jpeg))/g; 21 | 22 | export const bolt11Regex = /(lnbc\d+[munp][A-Za-z0-9]+)/i; 23 | 24 | export const nip27Regex = /(nostr:[A-Za-z0-9]+)/gi; 25 | 26 | export const usernameRegex = /^[a-z0-9]{4,32}$/i; 27 | 28 | export const openGraphRegex = 29 | /]*property=["']og:(.*)["'] [^>]*content=["']([^'^"]+?)["'][^>]*>/gi; 30 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.3.2" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "ios": { 10 | "resourceClass": "m-medium" 11 | } 12 | }, 13 | "development-simulator": { 14 | "developmentClient": true, 15 | "distribution": "internal", 16 | "ios": { 17 | "simulator": true, 18 | "resourceClass": "m1-medium" 19 | } 20 | }, 21 | "preview": { 22 | "distribution": "internal", 23 | "ios": { 24 | "resourceClass": "m1-medium" 25 | }, 26 | "android": { 27 | "buildType": "apk" 28 | } 29 | }, 30 | "production": { 31 | "ios": { 32 | "resourceClass": "m1-medium" 33 | } 34 | } 35 | }, 36 | "submit": { 37 | "production": { 38 | "ios": { 39 | "companyName": "Lightning Digital Entertainment Inc." 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /features/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | isLoggedIn: false, 5 | pubKey: null, 6 | username: null, 7 | walletBearer: null, 8 | walletExpires: null, 9 | isPremium: false, 10 | }; 11 | 12 | export const authSlice = createSlice({ 13 | name: 'auth', 14 | initialState, 15 | reducers: { 16 | logIn: (state, action) => { 17 | state.isLoggedIn = true; 18 | state.walletBearer = action.payload.bearer; 19 | const now = Date.now(); 20 | state.walletExpires = now + 1000 * 60 * 60 * 2; 21 | state.username = action.payload.username; 22 | state.pubKey = action.payload.pubKey; 23 | }, 24 | setBearer: (state, action) => { 25 | state.walletBearer = action.payload; 26 | }, 27 | logOut: (state) => { 28 | state.isLoggedIn = false; 29 | state.pubKey = null; 30 | state.username = null; 31 | state.walletBearer = null; 32 | state.walletExpires = null; 33 | }, 34 | setPremium: (state, action) => { 35 | state.isPremium = action.payload; 36 | }, 37 | }, 38 | }); 39 | 40 | export const { logIn, setBearer, logOut, setPremium } = authSlice.actions; 41 | 42 | export default authSlice.reducer; 43 | -------------------------------------------------------------------------------- /features/badges/badgeSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | badges: {}, 5 | }; 6 | 7 | export const badgeSlice = createSlice({ 8 | name: 'badges', 9 | initialState, 10 | reducers: { 11 | addBadge: (state, action) => { 12 | const { badgeUID, event } = action.payload; 13 | state.badges[badgeUID] = event; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { addBadge } = badgeSlice.actions; 19 | 20 | export default badgeSlice.reducer; 21 | -------------------------------------------------------------------------------- /features/badges/components/BadgeBar.jsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import React from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import BadgeIcon from './BadgeIcon'; 6 | import { colors } from '../../../styles'; 7 | 8 | const BadgeBar = ({ badgeDefinition, edit }) => { 9 | const badges = badgeDefinition.slice(0, 5); 10 | const navigation = useNavigation(); 11 | 12 | return ( 13 | 22 | {badges 23 | ? badges.map((badge) => ( 24 | 25 | )) 26 | : undefined} 27 | {edit ? ( 28 | { 34 | navigation.navigate('ChooseBadge'); 35 | }} 36 | /> 37 | ) : undefined} 38 | 39 | ); 40 | }; 41 | 42 | export default BadgeBar; 43 | -------------------------------------------------------------------------------- /features/badges/components/BadgeIcon.jsx: -------------------------------------------------------------------------------- 1 | import { View, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { Image } from 'expo-image'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import useBadge from '../hooks/useBadge'; 6 | import LoadingSkeleton from '../../../components/LoadingSkeleton'; 7 | 8 | const BadgeIcon = ({ badgeDefinition }) => { 9 | const badgeUID = badgeDefinition[1]; 10 | const badge = useBadge(badgeUID); 11 | const navigation = useNavigation(); 12 | let src; 13 | if (badge) { 14 | [[, src]] = badge.tags.filter((tag) => tag[0] === 'thumb'); 15 | } 16 | 17 | const navigationHandler = () => { 18 | navigation.navigate('BadgeDetails', { badgeUID }); 19 | }; 20 | return ( 21 | 22 | {src ? ( 23 | 29 | ) : ( 30 | 31 | 32 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | export default BadgeIcon; 39 | -------------------------------------------------------------------------------- /features/badges/components/IssuedBy.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React, { memo, useMemo } from 'react'; 3 | import { useParseContent } from '../../../hooks'; 4 | import { globalStyles } from '../../../styles'; 5 | 6 | const IssuedBy = memo(({ rawText }) => { 7 | const contentObj = useMemo(() => ({ content: rawText }), [rawText]); 8 | const content = useParseContent(contentObj); 9 | return ( 10 | 11 | {content} 12 | 13 | ); 14 | }); 15 | 16 | export default IssuedBy; 17 | -------------------------------------------------------------------------------- /features/badges/components/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as BadgeBar } from './BadgeBar'; 3 | export { default as AwardedBadge } from './AwardedBadge'; 4 | -------------------------------------------------------------------------------- /features/badges/hooks/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as useBadge } from './useBadge'; 3 | -------------------------------------------------------------------------------- /features/badges/hooks/useAwardedBadges.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { getReadRelays, getRelayUrls, pool } from '../../../utils/nostrV2'; 4 | 5 | const useAwardedBadges = () => { 6 | const [events, setEvents] = useState([]); 7 | const readUrls = getRelayUrls(getReadRelays()); 8 | const pk = useSelector((state) => state.auth.pubKey); 9 | useEffect(() => { 10 | const sub = pool.sub(readUrls, [ 11 | { 12 | kinds: [8], 13 | '#p': [pk], 14 | }, 15 | ]); 16 | sub.on('event', (event) => { 17 | const [[, badgeUID]] = event.tags.filter((tag) => tag[0] === 'a'); 18 | const awardId = event.id; 19 | setEvents((prev) => [...prev, { badgeUID, awardId }]); 20 | }); 21 | return () => { 22 | sub.unsub(); 23 | }; 24 | }, []); 25 | return events; 26 | }; 27 | 28 | export default useAwardedBadges; 29 | -------------------------------------------------------------------------------- /features/badges/hooks/useBadge.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { getBadge } from '../utils'; 4 | import { addBadge } from '../badgeSlice'; 5 | 6 | const useBadge = (badgeUID) => { 7 | const badge = useSelector((state) => state.badges.badges[badgeUID]); 8 | const dispatch = useDispatch(); 9 | 10 | async function addBadgeToStore() { 11 | const event = await getBadge(badgeUID); 12 | dispatch(addBadge({ badgeUID, event })); 13 | } 14 | 15 | useEffect(() => { 16 | if (!badge) { 17 | addBadgeToStore(); 18 | } 19 | }, []); 20 | 21 | return badge; 22 | }; 23 | 24 | export default useBadge; 25 | -------------------------------------------------------------------------------- /features/badges/index.js: -------------------------------------------------------------------------------- 1 | export * from './views'; 2 | export * from './hooks'; 3 | export * from './components'; 4 | -------------------------------------------------------------------------------- /features/badges/utils/getBadge.js: -------------------------------------------------------------------------------- 1 | import { getReadRelays, getRelayUrls, pool } from '../../../utils/nostrV2'; 2 | 3 | async function getBadge(badgeUID) { 4 | try { 5 | const [, author, identifier] = badgeUID.split(':'); 6 | const readUrls = getRelayUrls(getReadRelays()); 7 | const badgeEvent = await new Promise((resolve) => { 8 | const sub = pool.sub(readUrls, [ 9 | { authors: [author], '#d': [identifier], kinds: [30009] }, 10 | ]); 11 | sub.on('event', (event) => { 12 | sub.unsub(); 13 | resolve(event); 14 | }); 15 | }); 16 | return badgeEvent; 17 | } catch (e) { 18 | console.log(e); 19 | return undefined; 20 | } 21 | } 22 | 23 | export default getBadge; 24 | -------------------------------------------------------------------------------- /features/badges/utils/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as getBadge } from './getBadge'; 3 | -------------------------------------------------------------------------------- /features/badges/utils/publishProfileBadges.js: -------------------------------------------------------------------------------- 1 | import { getEventHash, signEvent } from 'nostr-tools'; 2 | import { getRelayUrls, getWriteRelays, pool } from '../../../utils/nostrV2'; 3 | import { getValue } from '../../../utils'; 4 | import devLog from '../../../utils/internal'; 5 | 6 | async function publishProfileBadges(profileBadgeArray, pk) { 7 | try { 8 | const sk = await getValue('privKey'); 9 | if (!sk) { 10 | throw new Error('No privKey in secure storage found'); 11 | } 12 | const event = { 13 | kind: 30008, 14 | pubkey: pk, 15 | created_at: Math.floor(Date.now() / 1000), 16 | tags: [['d', 'profile_badges']], 17 | content: '', 18 | }; 19 | profileBadgeArray.forEach((badge) => { 20 | event.tags.push(['a', badge.badgeUID]); 21 | event.tags.push(['e', badge.awardId]); 22 | }); 23 | event.id = getEventHash(event); 24 | event.sig = signEvent(event, sk); 25 | const writeUrls = getRelayUrls(getWriteRelays()); 26 | await new Promise((resolve) => { 27 | let handled = 0; 28 | const timer = setTimeout(resolve, 3200); 29 | function checkIfHandled() { 30 | if (handled === writeUrls.length) { 31 | clearTimeout(timer); 32 | resolve(); 33 | } 34 | } 35 | const pubs = pool.publish(writeUrls, event); 36 | pubs.on('ok', () => { 37 | handled += 1; 38 | checkIfHandled(); 39 | }); 40 | pubs.on('failed', () => { 41 | handled += 1; 42 | checkIfHandled(); 43 | }); 44 | }); 45 | } catch (e) { 46 | devLog(e); 47 | } 48 | } 49 | 50 | export default publishProfileBadges; 51 | 52 | // { 53 | // "kind": 30008, 54 | // "pubkey": "bob", 55 | // "tags": [ 56 | // ["d", "profile_badges"], 57 | // ["a", "30009:alice:bravery"], 58 | // ["e", "", "wss://nostr.academy"], 59 | // ["a", "30009:alice:honor"], 60 | // ["e", "", "wss://nostr.academy"], 61 | // ], 62 | // ... 63 | // } 64 | -------------------------------------------------------------------------------- /features/badges/views/BadgeDetailView.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, useWindowDimensions } from 'react-native'; 2 | import React, { useMemo } from 'react'; 3 | import { Image } from 'expo-image'; 4 | import { ScrollView } from 'react-native-gesture-handler'; 5 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 6 | import { nip19 } from 'nostr-tools'; 7 | import { colors, globalStyles } from '../../../styles'; 8 | import useBadge from '../hooks/useBadge'; 9 | import IssuedBy from '../components/IssuedBy'; 10 | 11 | const BadgeDetailView = ({ route }) => { 12 | const { badgeUID } = route.params || {}; 13 | const badge = useBadge(badgeUID); 14 | const { width } = useWindowDimensions(); 15 | const insets = useSafeAreaInsets(); 16 | let src; 17 | let name; 18 | let description; 19 | const rawIssuedText = useMemo(() => { 20 | if (badge) { 21 | [[, src]] = badge.tags.filter((tag) => tag[0] === 'image'); 22 | [[, name]] = badge.tags.filter((tag) => tag[0] === 'name'); 23 | [[, description]] = badge.tags.filter((tag) => tag[0] === 'description'); 24 | return `Issued by nostr:${nip19.npubEncode(badge.pubkey)}`; 25 | } 26 | return undefined; 27 | }, [badge]); 28 | return ( 29 | 33 | {name} 34 | 45 | {description} 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default BadgeDetailView; 53 | -------------------------------------------------------------------------------- /features/badges/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as BadgeDetaiView } from './BadgeDetailView'; 2 | -------------------------------------------------------------------------------- /features/comments/components/ItemSeperator.jsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import React from 'react'; 3 | import { colors } from '../../../styles'; 4 | 5 | const ItemSeperator = () => ( 6 | 14 | ); 15 | 16 | export default ItemSeperator; 17 | -------------------------------------------------------------------------------- /features/comments/components/PullDownNote.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { globalStyles } from '../../../styles'; 5 | 6 | const PullDownNote = memo(() => { 7 | return ( 8 | 9 | 10 | Pull down to load thread 11 | 12 | 13 | ); 14 | }); 15 | 16 | export default PullDownNote; 17 | -------------------------------------------------------------------------------- /features/comments/components/PullUp.tsx: -------------------------------------------------------------------------------- 1 | import { RefreshControl } from "react-native-gesture-handler" 2 | import { colors } from "../../../styles" 3 | 4 | export const MyRefreshControl = ({ 5 | refreshing, 6 | onRefresh, 7 | counter, 8 | ...props 9 | }) => { 10 | return ( 11 | 18 | ) 19 | } -------------------------------------------------------------------------------- /features/comments/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as CommentHeader } from './CommentHeader'; 2 | export { default as ItemSeperator } from './ItemSeperator'; 3 | export { default as PullDownNote } from './PullDownNote'; 4 | -------------------------------------------------------------------------------- /features/comments/hooks/useHeaderNotes.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Note } from '../../../utils/nostrV2'; 3 | import { useRelayUrls } from '../../relays'; 4 | import { pool } from '../../../utils/nostrV2/relays.ts'; 5 | 6 | export const useHeaderNotes = (parentEvent) => { 7 | const [root, setRootEvent] = useState(); 8 | const [parent, setParentEvent] = useState(); 9 | const { readUrls } = useRelayUrls(); 10 | 11 | useEffect(() => { 12 | let ids; 13 | const parentETags = parentEvent.tags.filter((tag) => tag[0] === 'e'); 14 | if (parentETags.length > 0) { 15 | ids = [parentEvent.id, parentETags[0][1]]; 16 | } else { 17 | ids = [parentEvent.id]; 18 | } 19 | const sub = pool.sub(readUrls, [ 20 | { 21 | ids, 22 | }, 23 | ]); 24 | 25 | sub.on('event', (event) => { 26 | const note = new Note(event); 27 | const parsedNote = note.save(); 28 | if (parsedNote.id === parentEvent.id) { 29 | setParentEvent(parsedNote); 30 | } else if (parsedNote.id === parentETags[0][1]) { 31 | setRootEvent(parsedNote); 32 | } 33 | }); 34 | }, []); 35 | 36 | return { parent, root }; 37 | }; 38 | 39 | export default useHeaderNotes; 40 | -------------------------------------------------------------------------------- /features/comments/hooks/useReplies.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Note } from '../../../utils/nostrV2'; 4 | import { Zap } from '../../zaps/Zap'; 5 | import { useRelayUrls } from '../../relays'; 6 | import { pool } from '../../../utils/nostrV2/relays'; 7 | 8 | export const useReplies = (eventId) => { 9 | const [replies, setReplies] = useState([]); 10 | const mutedPubkeys = useSelector((state) => state.user.mutedPubkeys); 11 | const { readUrls } = useRelayUrls(); 12 | 13 | const eventCallback = useCallback( 14 | (event) => { 15 | if (mutedPubkeys.includes(event.pubkey)) { 16 | return; 17 | } 18 | if (event.kind === 1) { 19 | const newEvent = new Note(event).saveReply(); 20 | if (newEvent.repliesTo === eventId) { 21 | setReplies((prev) => 22 | [...prev, newEvent].sort((a, b) => b.created_at - a.created_at), 23 | ); 24 | } 25 | } else if (event.kind === 9735) { 26 | const newZap = new Zap(event); 27 | setReplies((prev) => 28 | [...prev, newZap].sort((a, b) => b.created_at - a.created_at), 29 | ); 30 | } 31 | }, 32 | [mutedPubkeys], 33 | ); 34 | 35 | useEffect(() => { 36 | const sub = pool.sub( 37 | readUrls, 38 | [ 39 | { 40 | kinds: [1], 41 | '#e': [eventId], 42 | }, 43 | ], 44 | { skipVerification: true }, 45 | ); 46 | 47 | sub.on('event', eventCallback); 48 | }, []); 49 | return replies; 50 | }; 51 | 52 | export default useReplies; 53 | -------------------------------------------------------------------------------- /features/comments/utils/threads.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'nostr-tools'; 2 | import Kind1Note from '../../../models/Kind1Note'; 3 | 4 | export function repliesTo(event: Event) { 5 | if (event.tags.length < 1) { 6 | return undefined; 7 | } 8 | const eTags = event.tags.filter((tag) => tag[0] === 'e'); 9 | if (eTags.length < 1) { 10 | return undefined; 11 | } 12 | const replyTag = eTags.filter((tag) => tag[3] === 'reply'); 13 | if (replyTag.length > 1) { 14 | return replyTag[0][1]; 15 | } 16 | if (eTags.length === 1) { 17 | return eTags[0][1]; 18 | } 19 | return eTags[eTags.length - 1][1]; 20 | } 21 | 22 | export function getRoot(event: Event) { 23 | const eTags = event.tags.filter((tag) => tag[0] === 'e'); 24 | if (eTags.length === 0) { 25 | return undefined; 26 | } 27 | if (eTags.length === 1) { 28 | return eTags[0][1]; 29 | } 30 | if (eTags.length > 1) { 31 | const rootTag = eTags.filter((tag) => tag[3] === 'root'); 32 | if (rootTag.length > 0) { 33 | return rootTag[0][1]; 34 | } 35 | return eTags[0][1]; 36 | } 37 | } 38 | 39 | export function buildThread(note: Kind1Note, allnotes, thread = []) { 40 | const parentNoteId = note.repliesTo; 41 | if (!parentNoteId || !allnotes[parentNoteId]) { 42 | return [note, ...thread]; 43 | } 44 | const parentNote = allnotes[parentNoteId]; 45 | const newThread = [note, ...thread]; 46 | return buildThread(parentNote, allnotes, newThread); 47 | } 48 | -------------------------------------------------------------------------------- /features/community/communitySlice.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { createSlice } from '@reduxjs/toolkit'; 3 | 4 | const initialState = { 5 | communities: [], 6 | communitySlugs: [], 7 | joinedCommunities: [], 8 | }; 9 | 10 | export const communitySlice = createSlice({ 11 | name: 'community', 12 | initialState, 13 | reducers: { 14 | addCommunity: (state, action) => { 15 | if (!state.communitySlugs.includes(action.payload.communitySlug)) { 16 | state.communitySlugs.push(action.payload.communitySlug); 17 | state.communities.push(action.payload); 18 | } 19 | }, 20 | joinCommunity: (state, action) => { 21 | if (!state.joinedCommunities.includes(action.payload)) { 22 | state.joinedCommunities.push(action.payload); 23 | } 24 | }, 25 | }, 26 | }); 27 | 28 | export const communityListener = async (action, listenerApi) => { 29 | const { 30 | community: { joinedCommunities }, 31 | } = listenerApi.getState(); 32 | const json = JSON.stringify(joinedCommunities); 33 | await AsyncStorage.setItem('joinedCommunities', json); 34 | }; 35 | 36 | export const { addCommunity, joinCommunity } = communitySlice.actions; 37 | 38 | export default communitySlice.reducer; 39 | -------------------------------------------------------------------------------- /features/community/components/CommunitiesTitle.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | 6 | const CommunitiesTitle = () => { 7 | return ( 8 | 9 | Communities 10 | 11 | 12 | ); 13 | }; 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flexDirection: 'row', 18 | alignItems: 'center' 19 | }, 20 | title: { 21 | ...globalStyles.textH2, 22 | marginBottom: 0, 23 | marginRight: 12 24 | }, 25 | icon: { 26 | color: colors.primary500 27 | } 28 | }) 29 | 30 | export default CommunitiesTitle; 31 | -------------------------------------------------------------------------------- /features/community/components/CommunityList.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Pressable } from 'react-native'; 2 | import React, { useLayoutEffect } from 'react'; 3 | import useCommunities from '../hooks/useCommunities'; 4 | import { FlatList } from 'react-native-gesture-handler'; 5 | import CommunityListItem from './CommunityListItem'; 6 | import { useNavigation } from '@react-navigation/native'; 7 | import { Ionicons } from '@expo/vector-icons'; 8 | import { colors } from '../../../styles'; 9 | 10 | const CommunityList = () => { 11 | const [communitites, setRefresh] = useCommunities(); 12 | const { setOptions } = useNavigation(); 13 | useLayoutEffect(() => { 14 | setOptions({ 15 | headerRight: () => ( 16 | {setRefresh((prev) => prev + 1)}} style={{marginRight: 12}}> 17 | 18 | 19 | ), 20 | }); 21 | }, []); 22 | const refresh = () => { 23 | console.log('refreshes!'); 24 | }; 25 | return ( 26 | 27 | { 30 | console.log('Refresh'); 31 | }} 32 | renderItem={({ item }) => ( 33 | 37 | )} 38 | /> 39 | 40 | ); 41 | }; 42 | 43 | export default CommunityList; 44 | -------------------------------------------------------------------------------- /features/community/components/CommunityListItem.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Pressable } from 'react-native'; 2 | import React, { useLayoutEffect } from 'react'; 3 | import { Image } from 'expo-image'; 4 | import CharacterImagePlaceholder from '../../../components/CharacterImagePlaceholder'; 5 | import { colors, globalStyles } from '../../../styles'; 6 | import { useNavigation } from '@react-navigation/native'; 7 | import Community from '../models/Community'; 8 | import { Ionicons } from '@expo/vector-icons'; 9 | 10 | type CommunityListItemProps = { 11 | groupSlug: string; 12 | groupPicture?: string; 13 | communityObject: Community; 14 | }; 15 | 16 | const CommunityListItem = ({ 17 | groupPicture, 18 | groupSlug, 19 | communityObject, 20 | }: CommunityListItemProps) => { 21 | const { navigate } = useNavigation(); 22 | return ( 23 | { 26 | // @ts-ignore 27 | navigate('Community Chat', { communityObject }); 28 | }} 29 | > 30 | {communityObject.communityPicture ? ( 31 | 32 | ) : ( 33 | 36 | )} 37 | 38 | {groupSlug} 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | flexDirection: 'row', 48 | width: '100%', 49 | borderBottomColor: colors.backgroundSecondary, 50 | borderBottomWidth: 1, 51 | paddingVertical: 10, 52 | justifyContent: 'space-between', 53 | }, 54 | image: { 55 | height: 50, 56 | width: 50, 57 | borderRadius: 25 58 | } 59 | }); 60 | 61 | export default CommunityListItem; 62 | -------------------------------------------------------------------------------- /features/community/components/JoinPrompt.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Alert } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { CustomButton } from '../../../components'; 4 | import { useDispatch } from 'react-redux'; 5 | import { joinCommunity } from '../communitySlice'; 6 | import { Image } from 'expo-image'; 7 | import CharacterImagePlaceholder from '../../../components/CharacterImagePlaceholder'; 8 | import Community from '../models/Community'; 9 | import { colors, globalStyles } from '../../../styles'; 10 | import { publishJoinEvent } from '../utils/nostr'; 11 | 12 | type JoinPromptProps = { 13 | communityObject: Community; 14 | onClose: () => void 15 | }; 16 | 17 | const test = () => {return '1234'} 18 | 19 | const JoinPrompt = memo(({ communityObject, onClose }: JoinPromptProps) => { 20 | const dispatch = useDispatch(); 21 | return ( 22 | 23 | {communityObject.communityPicture ? ( 24 | 28 | ) : ( 29 | 32 | )} 33 | Do you want to join {communityObject.communitySlug}? 34 | { 38 | try { 39 | await publishJoinEvent(communityObject.communitySlug) 40 | dispatch(joinCommunity(communityObject.communitySlug)); 41 | onClose(); 42 | } catch(e) { 43 | console.log(e) 44 | } 45 | }, 46 | }} 47 | /> 48 | 49 | ); 50 | }); 51 | 52 | const styles = StyleSheet.create({ 53 | container: { 54 | width: '100%', 55 | alignItems: 'center', 56 | gap: 12, 57 | }, 58 | image: { 59 | width: 50, 60 | height: 50, 61 | borderRadius: 25, 62 | borderColor: colors.primary500, 63 | borderWidth: 1 64 | }, 65 | }); 66 | 67 | export default JoinPrompt; 68 | -------------------------------------------------------------------------------- /features/community/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Platform } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { Event, nip19 } from 'nostr-tools'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | import { useSelector } from 'react-redux'; 6 | import { Image } from 'expo-image'; 7 | import { useParseContent } from '../../../hooks'; 8 | 9 | type MessageProps = { 10 | event: Event; 11 | }; 12 | 13 | const Message = memo(({ event }: MessageProps) => { 14 | //@ts-ignore 15 | const user = useSelector((state) => state.messages.users[event.pubkey]); 16 | const content = useParseContent(event); 17 | return ( 18 | 24 | {user && user.picture ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 | 30 | 31 | {user?.name || nip19.npubEncode(event.pubkey).slice(0, 32)} 32 | 33 | {content} 34 | 35 | 36 | ); 37 | }); 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | padding: 10, 42 | borderRadius: 10, 43 | width: '80%', 44 | backgroundColor: colors.backgroundSecondary, 45 | marginBottom: 12, 46 | }, 47 | username: { 48 | ...globalStyles.textBodyS, 49 | textAlign: 'left', 50 | }, 51 | text: { 52 | ...globalStyles.textBody, 53 | textAlign: 'left', 54 | }, 55 | image: { height: 40, width: 40, borderRadius: 20 }, 56 | placeholder: { 57 | height: 40, 58 | width: 40, 59 | borderRadius: 20, 60 | backgroundColor: colors.backgroundSecondary, 61 | }, 62 | }); 63 | 64 | export default Message; 65 | -------------------------------------------------------------------------------- /features/community/components/RelayMessage.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Platform } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { globalStyles } from '../../../styles'; 4 | import { Event } from 'nostr-tools'; 5 | import { useParseContent } from '../../../hooks'; 6 | 7 | type RelayMessageProps = { 8 | event: Event; 9 | } 10 | 11 | const RelayMessage = memo(({event}: RelayMessageProps) => { 12 | const content = useParseContent(event); 13 | return ( 14 | 15 | {content} 16 | 17 | ); 18 | }); 19 | 20 | export default RelayMessage; 21 | -------------------------------------------------------------------------------- /features/community/components/SentMessage.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Platform } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { Event } from 'nostr-tools'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | import { useParseContent } from '../../../hooks'; 6 | 7 | type MessageProps = { 8 | event: Event; 9 | }; 10 | 11 | const SentMessage = memo(({ event }: MessageProps) => { 12 | const content = useParseContent(event); 13 | return ( 14 | 20 | 21 | {content} 22 | 23 | 24 | ); 25 | }); 26 | 27 | const styles = StyleSheet.create({ 28 | container: { 29 | padding: 10, 30 | borderRadius: 10, 31 | width: '80%', 32 | backgroundColor: colors.backgroundActive, 33 | marginBottom: 12, 34 | }, 35 | username: { 36 | ...globalStyles.textBodyS, 37 | textAlign: 'left', 38 | }, 39 | text: { 40 | ...globalStyles.textBody, 41 | textAlign: 'left', 42 | }, 43 | image: { height: 40, width: 40, borderRadius: 20 }, 44 | placeholder: { 45 | height: 40, 46 | width: 40, 47 | borderRadius: 20, 48 | backgroundColor: colors.backgroundSecondary, 49 | }, 50 | }); 51 | 52 | export default SentMessage; 53 | -------------------------------------------------------------------------------- /features/community/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CommunityList } from './CommunityList'; 2 | export { default as CommunityListItem } from './CommunityListItem'; 3 | export { default as JoinPrompt } from './JoinPrompt'; 4 | export { default as Message } from './Message'; 5 | export { default as SentMessage } from './SentMessage'; 6 | export { default as RelayMessage } from './RelayMessage'; 7 | export { default as CommunitiesTitle } from './CommunitiesTitle'; 8 | -------------------------------------------------------------------------------- /features/community/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useIsMember } from './useIsMember'; 2 | export { default as useChat } from './useChat'; 3 | export { default as useCommunities } from './useCommunities'; 4 | -------------------------------------------------------------------------------- /features/community/hooks/useChat.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { pool } from '../../../utils/nostrV2'; 3 | 4 | const useChat = (communityObject) => { 5 | const [messages, setMessages] = useState([]); 6 | useEffect(() => { 7 | const newMessages = new Set(); 8 | const sub = pool.sub( 9 | ['wss://spool.chat'], 10 | [{ kinds: [9], '#g': [communityObject.communitySlug] }], 11 | { skipVerification: true }, 12 | ); 13 | sub.on('event', (event) => { 14 | newMessages.add(event); 15 | setMessages([...newMessages].sort((a, b) => b.created_at - a.created_at)); 16 | }); 17 | return () => { 18 | sub.unsub(); 19 | }; 20 | }, []); 21 | return messages; 22 | }; 23 | 24 | export default useChat; 25 | -------------------------------------------------------------------------------- /features/community/hooks/useCommunities.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { pool } from '../../../utils/nostrV2'; 4 | import Community from '../models/Community'; 5 | 6 | const useCommunities = () => { 7 | const communities = useSelector((state) => state.community.communities); 8 | const [refresh, setRefresh] = useState(0); 9 | useEffect(() => { 10 | const sub = pool.sub( 11 | ['wss://spool.chat'], 12 | [{ kinds: [39000], since: 1687395734 }], 13 | { skipVerification: true }, 14 | ); 15 | sub.on('event', (event) => { 16 | const [relay] = pool.seenOn(event.id); 17 | const parsedCommunity = new Community(event, relay); 18 | parsedCommunity.save(); 19 | }); 20 | return () => { 21 | sub.unsub(); 22 | }; 23 | }, [refresh]); 24 | return [communities, setRefresh]; 25 | }; 26 | 27 | export default useCommunities; 28 | -------------------------------------------------------------------------------- /features/community/hooks/useIsMember.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | const useIsMember = (communityObject) => { 4 | const joinedCommunities = useSelector( 5 | (state) => state.community.joinedCommunities, 6 | ); 7 | return joinedCommunities.includes(communityObject.communitySlug); 8 | }; 9 | 10 | export default useIsMember; 11 | -------------------------------------------------------------------------------- /features/community/index.js: -------------------------------------------------------------------------------- 1 | export { default as CommunitiesNavigator } from './nav/CommunityNavigator'; 2 | export * from './views'; 3 | -------------------------------------------------------------------------------- /features/community/models/Community.js: -------------------------------------------------------------------------------- 1 | import { store } from '../../../store/store'; 2 | import { addCommunity } from '../communitySlice'; 3 | 4 | function getSlug(communityEvent) { 5 | const [communitySlugTag] = communityEvent.tags.filter( 6 | (tag) => tag[0] === 'd', 7 | ); 8 | if (communitySlugTag) { 9 | return communitySlugTag[1]; 10 | } 11 | return ''; 12 | } 13 | 14 | function getPicture(communityEvent) { 15 | const [pictureTag] = communityEvent.tags.filter( 16 | (tag) => tag[0] === 'picture', 17 | ); 18 | if (pictureTag) { 19 | return pictureTag[1]; 20 | } 21 | return undefined; 22 | } 23 | 24 | class Community { 25 | constructor(communityEvent, relay) { 26 | this.communitySlug = getSlug(communityEvent); 27 | this.relay = relay; 28 | this.relayKey = communityEvent.pubkey; 29 | this.communityPicture = getPicture(communityEvent); 30 | } 31 | 32 | save() { 33 | const serializedObj = { 34 | communitySlug: this.communitySlug, 35 | relay: this.relay, 36 | relayKey: this.relayKey, 37 | communityPicture: this.communityPicture, 38 | }; 39 | store.dispatch( 40 | addCommunity(serializedObj), 41 | ); 42 | } 43 | } 44 | 45 | export default Community; 46 | -------------------------------------------------------------------------------- /features/community/nav/CommunityNavigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { CommunityView, CommunitiesView } from '../views'; 4 | import { CommunitiesTitle } from '../components'; 5 | 6 | const Stack = createStackNavigator(); 7 | 8 | const ConversationNavigator = () => ( 9 | 10 | 15 | ({ 19 | headerTitle: route.params.communityObject.communitySlug, 20 | })} 21 | /> 22 | 23 | ); 24 | 25 | export default ConversationNavigator; 26 | -------------------------------------------------------------------------------- /features/community/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nostr'; -------------------------------------------------------------------------------- /features/community/utils/nostr.ts: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from "nostr-tools"; 2 | import { getPrivateKey } from "../../../utils/cache/asyncStorage"; 3 | import { pool } from "../../../utils/nostrV2"; 4 | import { signRawEvent } from "../../../utils"; 5 | 6 | export async function publishJoinEvent(communitySlug: string) { 7 | const sk = await getPrivateKey(); 8 | const pk = getPublicKey(sk); 9 | const event = { 10 | kind: 9000, 11 | tags: [ 12 | ["g", communitySlug], 13 | ["action", "add", pk, "user"], 14 | ], 15 | content: "Testing", 16 | created_at: Math.floor(Date.now() / 1000), 17 | pubkey: pk, 18 | id: '', 19 | sig: '' 20 | }; 21 | event.id = getEventHash(event); 22 | event.sig = signEvent(event, sk); 23 | console.log(event); 24 | return new Promise((resolve, reject) => { 25 | const pub = pool.publish(['wss://spool.chat'], event); 26 | pub.on('ok', resolve); 27 | pub.on('failed', reject); 28 | }) 29 | }; 30 | 31 | export async function publishCommunityMessage(content: string, communitySlug: string) {+ 32 | console.log('got here!') 33 | const event = { 34 | kind: 9, 35 | content, 36 | tags: [['g', communitySlug]], 37 | created_at: Math.floor(Date.now() / 1000), 38 | } 39 | const signedEvent = await signRawEvent(event) 40 | console.log(signedEvent); 41 | return new Promise((resolve, reject) => { 42 | const pub = pool.publish(['wss://spool.chat'], signedEvent); 43 | pub.on('ok', () => {resolve()}); 44 | pub.on('failed', (reason: string) => {reject(reason)}); 45 | }) 46 | }; -------------------------------------------------------------------------------- /features/community/views/CommunitiesView.jsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from 'react-native'; 2 | import React, { useRef } from 'react'; 3 | import { globalStyles } from '../../../styles'; 4 | import CommunityList from '../components/CommunityList'; 5 | import { CustomButton } from '../../../components'; 6 | import MenuBottomSheet from '../../../components/MenuBottomSheet'; 7 | 8 | const CommunitiesView = () => { 9 | const modalRef = useRef(); 10 | return ( 11 | 12 | 13 | { 17 | modalRef.current.present(); 18 | }, 19 | }} 20 | /> 21 | 22 | 23 | 24 | 25 | 26 | Switching Relays is not yet supported... 27 | 28 | { 32 | modalRef.current.dismiss(); 33 | }, 34 | }} 35 | /> 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default CommunitiesView; 43 | -------------------------------------------------------------------------------- /features/community/views/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as CommunitiesView } from './CommunitiesView'; 3 | export { default as CommunityView } from './CommunityView'; 4 | -------------------------------------------------------------------------------- /features/dvm/components/DVMHeader.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { colors, globalStyles } from '../../../styles'; 4 | import { AntDesign, MaterialCommunityIcons } from '@expo/vector-icons'; 5 | 6 | const DVMHeader = ({onPress}) => { 7 | return ( 8 | 19 | 20 | 21 | Image Generation DVM 22 | 23 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default DVMHeader; 35 | -------------------------------------------------------------------------------- /features/dvm/components/ImageGenRequest.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { Event } from 'nostr-tools'; 4 | import { imageRegex } from '../../../constants'; 5 | import { Image } from 'expo-image'; 6 | import { useNavigation } from '@react-navigation/native'; 7 | import { tags } from 'react-native-svg/lib/typescript/xml'; 8 | import { colors, globalStyles } from '../../../styles'; 9 | 10 | function getPrompt(event: Event): string { 11 | const promptTag = event.tags.filter( 12 | (tag) => tag[0] === 'i' && tag[2] === 'text', 13 | ); 14 | if (promptTag.length > 0) { 15 | return promptTag[0][1]; 16 | } 17 | return undefined; 18 | } 19 | 20 | function getImage(event: Event): string { 21 | const promptTag = event.tags.filter( 22 | (tag) => tag[0] === 'i' && tag[2] === 'url', 23 | ); 24 | if (promptTag.length > 0) { 25 | return promptTag[0][1]; 26 | } 27 | return undefined; 28 | } 29 | 30 | const ImageGenRequest = ({ event }) => { 31 | const nav = useNavigation(); 32 | const prompt = getPrompt(event); 33 | const image = getImage(event); 34 | return ( 35 | //@ts-ignore 36 | 45 | 46 | {image ? 'Remix Request' : 'Generation Request'} 47 | 48 | {image ? ( 49 | 50 | ) : undefined} 51 | 52 | {prompt?.trim()} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default ImageGenRequest; 59 | -------------------------------------------------------------------------------- /features/dvm/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useImageJob } from './useImageJob'; 2 | -------------------------------------------------------------------------------- /features/dvm/hooks/useImageJob.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { pool } from '../../../utils/nostrV2'; 3 | // import { useRelayUrls } from '../../relays'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | const useImageJob = () => { 7 | // const { readUrls } = useRelayUrls(); 8 | //@ts-ignore 9 | const { pubKey } = useSelector((state) => state.auth); 10 | 11 | const [events, setEvents] = useState([]); 12 | useEffect(() => { 13 | const allEvents = new Set(); 14 | const sub = pool.sub( 15 | [ 16 | 'wss://nostrue.com', 17 | 'wss://relayable.org', 18 | 'wss://nos.lol', 19 | 'wss://relay.conxole.io', 20 | 'wss://wc1.current.ninja', 21 | 'wss://pablof7z.nostr1.com', 22 | 'wss://relay.f7z.io', 23 | ], 24 | [ 25 | { kinds: [65005], authors: [pubKey] }, 26 | { 27 | kinds: [65001], 28 | authors: [ 29 | 'c70735fa4b01f77f953883a6e671982e31bd7d906b2b6111a6f518555bed1b1a', 30 | ], 31 | '#p': [pubKey], 32 | }, 33 | ], 34 | ); 35 | sub.on('event', (event) => { 36 | allEvents.add(event); 37 | setEvents([...allEvents]); 38 | }); 39 | }, []); 40 | return events; 41 | }; 42 | 43 | export default useImageJob; 44 | -------------------------------------------------------------------------------- /features/dvm/nav/DvmNavigator.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 4 | import { DvmSelectionScreen, ImageGenScreen } from '../views'; 5 | import { globalStyles } from '../../../styles'; 6 | import DVMHeader from '../components/DVMHeader'; 7 | 8 | const Stack = createNativeStackNavigator(); 9 | 10 | const DvmNavigator = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default DvmNavigator; 20 | -------------------------------------------------------------------------------- /features/dvm/utils/publishImageJob.ts: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from 'nostr-tools'; 2 | import { getPrivateKey } from '../../../utils/cache/asyncStorage'; 3 | import { publishGenericEvent } from '../../../utils/nostrV2'; 4 | import { imageRegex } from '../../../constants'; 5 | 6 | export function parseAndReplaceImages(input: string): [string, string[]] { 7 | const images: string[] = []; 8 | const parsedContent = input.replace(imageRegex, (match) => { 9 | images.push(match); 10 | return ''; 11 | }); 12 | return [parsedContent, images]; 13 | } 14 | 15 | const publishImageJob = async (prompt, image) => { 16 | const sk = await getPrivateKey(); 17 | const event = { 18 | content: '', 19 | kind: 65005, 20 | tags: [ 21 | ['i', prompt, 'text'], 22 | ['output', 'image/png'], 23 | ['param', 'size', '512x768'], 24 | [ 25 | 'relays', 26 | 'wss://nostrue.com', 27 | 'wss://relayable.org', 28 | 'wss://nos.lol', 29 | 'wss://relay.conxole.io', 30 | 'wss://wc1.current.ninja', 31 | 'wss://pablof7z.nostr1.com', 32 | 'wss://relay.f7z.io', 33 | ], 34 | ['p', 'c70735fa4b01f77f953883a6e671982e31bd7d906b2b6111a6f518555bed1b1a'], 35 | ], 36 | created_at: Math.floor(Date.now() / 1000), 37 | pubkey: getPublicKey(sk), 38 | id: '', 39 | sig: '', 40 | }; 41 | if (image) { 42 | event.tags.push(['i', image, 'url']); 43 | } 44 | event.id = getEventHash(event); 45 | event.sig = signEvent(event, sk); 46 | publishGenericEvent(event, [ 47 | 'wss://nostrue.com', 48 | 'wss://relayable.org', 49 | 'wss://nos.lol', 50 | 'wss://relay.conxole.io', 51 | 'wss://wc1.current.ninja', 52 | 'wss://pablof7z.nostr1.com', 53 | 'wss://relay.f7z.io', 54 | ]); 55 | }; 56 | 57 | export default publishImageJob; 58 | -------------------------------------------------------------------------------- /features/dvm/views/DvmSelectionScreen.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { globalStyles } from '../../../styles'; 4 | import { CustomButton } from '../../../components'; 5 | import { pool } from '../../../utils/nostrV2'; 6 | import { useRelayUrls } from '../../relays'; 7 | 8 | const DvmSelectionScreen = ({ navigation }) => { 9 | const { readUrls } = useRelayUrls(); 10 | return ( 11 | 12 | DvmSelectionScreen 13 | { 17 | navigation.navigate('ImageGen'); 18 | }, 19 | }} 20 | /> 21 | { 25 | const events = await pool.list( 26 | readUrls, 27 | [ 28 | { 29 | authors: 30 | '40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451', 31 | kinds: [30001], 32 | }, 33 | ], 34 | { skipVerification: true }, 35 | ); 36 | console.log(JSON.stringify(events)); 37 | }, 38 | }} 39 | /> 40 | 41 | ); 42 | }; 43 | 44 | export default DvmSelectionScreen; 45 | -------------------------------------------------------------------------------- /features/dvm/views/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DvmSelectionScreen } from './DvmSelectionScreen'; 2 | export { default as ImageGenScreen } from './ImageGenScreen'; 3 | -------------------------------------------------------------------------------- /features/homefeed/components/ReadMoreModal.js: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | import React from "react"; 3 | import { ScrollView } from "react-native-gesture-handler"; 4 | import CustomButton from "../../../components/CustomButton"; 5 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 6 | import { useParseContent } from "../../../hooks/useParseContent"; 7 | import { globalStyles } from "../../../styles"; 8 | 9 | 10 | const ReadMoreModal = ({ navigation,route }) => { 11 | const insets = useSafeAreaInsets(); 12 | const { event, author } = route.params; 13 | const parsedContent = useParseContent(event) 14 | return ( 15 | 16 | 17 | {author} 18 | {parsedContent} 19 | 20 | {navigation.goBack()} }} /> 21 | 22 | ); 23 | }; 24 | 25 | export default ReadMoreModal; 26 | -------------------------------------------------------------------------------- /features/homefeed/components/UserBanner.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, useWindowDimensions, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import { Image } from 'expo-image'; 4 | import { TouchableOpacity } from 'react-native-gesture-handler'; 5 | import { useNavigation } from '@react-navigation/native'; 6 | import globalStyles from '../../../styles/globalStyles'; 7 | import useStatus from '../../../hooks/useStatus'; 8 | 9 | const placeholder = require('../../../assets/user_placeholder.jpg'); 10 | 11 | const UserBanner = ({ user, event, width }) => { 12 | const imageDimensions = (width / 100) * 12; 13 | const navigation = useNavigation(); 14 | const [status, statusLoading] = useStatus(event.pubkey); 15 | 16 | let userStatus; 17 | if (statusLoading) { 18 | userStatus = 'Loading...'; 19 | } 20 | if (!statusLoading) { 21 | userStatus = status?.content || ''; 22 | } 23 | 24 | return ( 25 | { 33 | navigation.navigate('Profile', { 34 | screen: 'ProfileScreen', 35 | params: { 36 | pubkey: event.pubkey, 37 | name: user?.name || event.pubkey.slice(0, 16), 38 | }, 39 | }); 40 | }} 41 | > 42 | 51 | 52 | 53 | {user?.name || event.pubkey.slice(0, 16)} 54 | 55 | 56 | {userStatus} 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default UserBanner; 64 | -------------------------------------------------------------------------------- /features/homefeed/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as PostItem} from './PostItem' 2 | export {default as ImagePost} from './ImagePost' -------------------------------------------------------------------------------- /features/introSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | twitterModalShown: false, 5 | getStartedItems: [0, 1, 2, 3], 6 | }; 7 | 8 | export const introSlice = createSlice({ 9 | name: 'intro', 10 | initialState, 11 | reducers: { 12 | setTwitterModal: (state) => { 13 | state.twitterModalShown = true; 14 | }, 15 | setGetStartedItems: (state, action) => { 16 | const newArray = state.getStartedItems.filter( 17 | (item) => item !== action.payload, 18 | ); 19 | state.getStartedItems = newArray; 20 | }, 21 | resetAll: (state) => { 22 | state.twitterModalShown = false; 23 | state.getStartedItems = [0, 1, 2, 3]; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { setTwitterModal, resetAll, setGetStartedItems } = 29 | introSlice.actions; 30 | 31 | export default introSlice.reducer; 32 | -------------------------------------------------------------------------------- /features/mentions/components/ZapMention.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@react-navigation/native"; 2 | import { Image } from "expo-image"; 3 | import { Pressable, Text, View } from "react-native"; 4 | import { useSelector } from "react-redux"; 5 | import { colors, globalStyles } from "../../../styles"; 6 | 7 | const ZapMention = ({ item }) => { 8 | const user = useSelector((state) => state.messages.users[item.payer]); 9 | const navigation = useNavigation(); 10 | return ( 11 | { 14 | navigation.navigate("Profile", { 15 | screen: "ProfileScreen", 16 | params: { 17 | pubkey: item.payer, 18 | }, 19 | }); 20 | }} 21 | > 22 | 30 | 31 | 34 | {user?.name || item.pubkey.slice(0, 16)} 35 | 36 | 37 | 41 | zapped you!{" "} 42 | 43 | {item.amount} SATS 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default ZapMention; 53 | -------------------------------------------------------------------------------- /features/mentions/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as Mention} from './Mention'; 2 | export {default as ZapMention} from './ZapMention' -------------------------------------------------------------------------------- /features/mentions/hooks/index.js: -------------------------------------------------------------------------------- 1 | export {default as useNoteMentions} from './useNoteMentions'; 2 | export {default as useZapMentions} from './useZapMentions'; -------------------------------------------------------------------------------- /features/mentions/hooks/useNoteMentions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Note, pool } from '../../../utils/nostrV2'; 4 | import { useRelayUrls } from '../../relays'; 5 | 6 | const useNoteMentions = () => { 7 | const { readUrls } = useRelayUrls(); 8 | const [data, setData] = useState([]); 9 | const pk = useSelector((state) => state.auth.pubKey); 10 | const mutedPubkeys = useSelector((state) => state.user.mutedPubkeys); 11 | 12 | const receivedEventIds = []; 13 | 14 | const eventCallback = useCallback( 15 | (event) => { 16 | if (mutedPubkeys.includes(event.pubkey)) { 17 | return; 18 | } 19 | if (!receivedEventIds.includes(event.id) && event.pubkey !== pk) { 20 | receivedEventIds.push(event.id); 21 | const newEvent = new Note(event).save(); 22 | setData((prev) => [...prev, newEvent].sort((a, b) => b.created_at - a.created_at)); 23 | } 24 | }, 25 | [mutedPubkeys], 26 | ); 27 | 28 | useEffect(() => { 29 | const sub = pool.sub( 30 | readUrls, 31 | [ 32 | { 33 | kinds: [1], 34 | '#p': [pk], 35 | }, 36 | ], 37 | { skipVerification: true }, 38 | ); 39 | sub.on('event', eventCallback); 40 | return () => { 41 | sub.unsub(); 42 | }; 43 | }, []); 44 | 45 | return data; 46 | }; 47 | 48 | export default useNoteMentions; 49 | -------------------------------------------------------------------------------- /features/mentions/hooks/useZapMentions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Zap } from '../../zaps/Zap'; 4 | import { useRelayUrls } from '../../relays'; 5 | import { pool } from '../../../utils/nostrV2/relays.ts'; 6 | 7 | const useZapMentions = () => { 8 | const [data, setData] = useState([]); 9 | const pk = useSelector((state) => state.auth.pubKey); 10 | const mutedPubkeys = useSelector((state) => state.user.mutedPubkeys); 11 | const { readUrls } = useRelayUrls(); 12 | 13 | const receivedEventIds = []; 14 | 15 | const eventCallback = useCallback( 16 | (event) => { 17 | if (mutedPubkeys.includes(event.pubkey)) { 18 | return; 19 | } 20 | if (!receivedEventIds.includes(event.id) && event.pubkey !== pk) { 21 | receivedEventIds.push(event.id); 22 | const newEvent = new Zap(event); 23 | setData((prev) => [...prev, newEvent].sort((a, b) => b.created_at - a.created_at)); 24 | } 25 | }, 26 | [mutedPubkeys], 27 | ); 28 | 29 | useEffect(() => { 30 | const sub = pool.sub( 31 | readUrls, 32 | [ 33 | { 34 | kinds: [9735], 35 | '#p': [pk], 36 | }, 37 | ], 38 | { skipVerification: true }, 39 | ); 40 | sub.on('event', eventCallback); 41 | return () => { 42 | sub.unsub(); 43 | }; 44 | }, []); 45 | 46 | return data; 47 | }; 48 | 49 | export default useZapMentions; 50 | -------------------------------------------------------------------------------- /features/mentions/nav/MentionsNavigator.jsx: -------------------------------------------------------------------------------- 1 | import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; 2 | import colors from "../../../styles/colors"; 3 | import { MentionsView, ZapMentionsView } from "../views"; 4 | 5 | const Tab = createMaterialTopTabNavigator(); 6 | 7 | const MentionsNavigator = () => { 8 | return ( 9 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default MentionsNavigator; -------------------------------------------------------------------------------- /features/mentions/views/MentionsView.js: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | import React from "react"; 3 | import globalStyles from "../../../styles/globalStyles"; 4 | import { FlashList } from "@shopify/flash-list"; 5 | import BackButton from "../../../components/BackButton"; 6 | import { useNoteMentions } from "../hooks"; 7 | import { Mention } from "../components"; 8 | 9 | const MentionsView = ({ navigation }) => { 10 | const data = useNoteMentions(); 11 | const renderItem = ({ item }) => { 12 | return ; 13 | }; 14 | 15 | return ( 16 | 17 | 18 | ( 22 | 23 | )} 24 | estimatedItemSize={100} 25 | /> 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default MentionsView; -------------------------------------------------------------------------------- /features/mentions/views/ZapMentionsView.js: -------------------------------------------------------------------------------- 1 | import { useZapMentions } from '../hooks'; 2 | import { ZapMention } from '../components'; 3 | import { FlashList } from '@shopify/flash-list'; 4 | import { View } from 'react-native'; 5 | import { globalStyles } from '../../../styles'; 6 | 7 | const ZapMentionsView = ({ navigation }) => { 8 | const data = useZapMentions(); 9 | const renderItem = ({ item }) => { 10 | return ; 11 | }; 12 | 13 | return ( 14 | 15 | 16 | } 20 | estimatedItemSize={100} 21 | /> 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default ZapMentionsView; 28 | -------------------------------------------------------------------------------- /features/mentions/views/index.js: -------------------------------------------------------------------------------- 1 | export {default as MentionsView} from './MentionsView'; 2 | export {default as ZapMentionsView} from './ZapMentionsView'; -------------------------------------------------------------------------------- /features/messages/components/ConversationText.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { colors, globalStyles } from '../../../styles'; 4 | import { useParseContent } from '../../../hooks'; 5 | import { getHourAndMinute } from '../../../utils'; 6 | 7 | const styles = StyleSheet.create({ 8 | outer: { 9 | width: '100%', 10 | }, 11 | container: { 12 | padding: 12, 13 | backgroundColor: colors.backgroundSecondary, 14 | marginVertical: 6, 15 | width: '90%', 16 | borderRadius: 10, 17 | }, 18 | send: { 19 | backgroundColor: colors.backgroundActive, 20 | alignSelf: 'flex-end', 21 | }, 22 | }); 23 | 24 | const ConversationText = ({ type, event }) => { 25 | const parsedContent = useParseContent(event); 26 | return ( 27 | 28 | 31 | 32 | {parsedContent} 33 | 34 | 35 | 36 | {getHourAndMinute(event.created_at)} 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default ConversationText; 45 | -------------------------------------------------------------------------------- /features/messages/hooks/useConversations.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { useRelayUrls } from '../../relays'; 4 | import { pool } from '../../../utils/nostrV2'; 5 | 6 | const useConversations = (timing) => { 7 | const { readUrls } = useRelayUrls(); 8 | const now = Math.floor(Date.now() / 1000); 9 | const pk = useSelector((state) => state.auth.pubKey); 10 | const [activeConversation, setActiveConversations] = useState(); 11 | const authors = new Set(); 12 | const filter1 = { 13 | kinds: [4], 14 | authors: [pk], 15 | }; 16 | const filter2 = { 17 | kinds: [4], 18 | '#p': [pk], 19 | }; 20 | useEffect(() => { 21 | if (timing > 0) { 22 | filter1.since = now - timing; 23 | filter2.since = now - timing; 24 | } 25 | const sub = pool.sub(readUrls, [filter1, filter2], { 26 | skipVerification: true, 27 | }); 28 | sub.on('event', (event) => { 29 | if (event.pubkey === pk) { 30 | authors.add(event.tags[0][1]); 31 | setActiveConversations([...authors]); 32 | } else { 33 | authors.add(event.pubkey); 34 | setActiveConversations([...authors]); 35 | } 36 | }); 37 | return () => { 38 | sub.unsub(); 39 | }; 40 | }, [timing]); 41 | return activeConversation; 42 | }; 43 | 44 | export default useConversations; 45 | -------------------------------------------------------------------------------- /features/messages/index.js: -------------------------------------------------------------------------------- 1 | export * from './views'; 2 | export * from './nav'; 3 | -------------------------------------------------------------------------------- /features/messages/nav/ConversationNavigator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import React from 'react'; 3 | import { createStackNavigator } from '@react-navigation/stack'; 4 | import { useSelector } from 'react-redux'; 5 | import { ActiveConversationScreen, ConversationScreen } from '../views'; 6 | import { colors } from '../../../styles'; 7 | import { CustomButton } from '../../../components'; 8 | import { deleteMessageCache } from '../../../utils/database'; 9 | 10 | const Stack = createStackNavigator(); 11 | 12 | const ConversationNavigator = () => { 13 | const users = useSelector((state) => state.messages.users); 14 | return ( 15 | 16 | 21 | ({ 25 | headerTitle: users[route.params.pk].name, 26 | headerStyle: { 27 | backgroundColor: colors.backgroundSecondary, 28 | }, 29 | headerTintColor: 'white', 30 | headerRight: () => ( 31 | { 34 | deleteMessageCache(route.params.pk); 35 | navigation.goBack(); 36 | }, 37 | }} 38 | text="Clear Cache" 39 | /> 40 | ), 41 | })} 42 | /> 43 | 44 | ); 45 | }; 46 | 47 | export default ConversationNavigator; 48 | -------------------------------------------------------------------------------- /features/messages/nav/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as ConversationNavigator } from './ConversationNavigator'; 3 | -------------------------------------------------------------------------------- /features/messages/utils/publishMessage.js: -------------------------------------------------------------------------------- 1 | import { 2 | getEventHash, 3 | getPublicKey, 4 | nip04, 5 | nip19, 6 | signEvent, 7 | } from 'nostr-tools'; 8 | import { publishGenericEvent } from '../../../utils/nostrV2'; 9 | import { getValue } from '../../../utils'; 10 | import { mentionRegex } from '../../../constants'; 11 | 12 | async function publishMessage(receiverPk, content) { 13 | const parsedContent = content.replaceAll( 14 | mentionRegex, 15 | (m, g1, g2) => `nostr:${nip19.npubEncode(g2)}`, 16 | ); 17 | const sk = await getValue('privKey'); 18 | const pk = getPublicKey(sk); 19 | const encryptedMessage = await nip04.encrypt(sk, receiverPk, parsedContent); 20 | const event = { 21 | pubkey: pk, 22 | kind: 4, 23 | created_at: Math.floor(Date.now() / 1000), 24 | tags: [['p', receiverPk]], 25 | content: encryptedMessage, 26 | }; 27 | event.id = getEventHash(event); 28 | event.sig = signEvent(event, sk); 29 | await publishGenericEvent(event); 30 | } 31 | 32 | export default publishMessage; 33 | -------------------------------------------------------------------------------- /features/messages/views/ConversationScreen.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | KeyboardAvoidingView, 4 | Platform, 5 | useWindowDimensions, 6 | } from 'react-native'; 7 | import React, { useState } from 'react'; 8 | import { FlatList } from 'react-native-gesture-handler'; 9 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 10 | import useMessages from '../hooks/useMessages'; 11 | import { ExpandableInput } from '../../../components'; 12 | import { globalStyles } from '../../../styles'; 13 | import ConversationText from '../components/ConversationText'; 14 | import publishMessage from '../utils/publishMessage'; 15 | import CustomKeyboardView from '../../../components/CustomKeyboardView'; 16 | 17 | const ConversationScreen = ({ route }) => { 18 | const { pk } = route.params || {}; 19 | const [viewHeight, setViewHeight] = useState(); 20 | 21 | const messages = useMessages(pk); 22 | const tabBarHeight = useBottomTabBarHeight(); 23 | const { height } = useWindowDimensions(); 24 | 25 | const renderText = ({ item }) => { 26 | if (item.pubkey === pk) { 27 | return ( 28 | 29 | ); 30 | } 31 | return ; 32 | }; 33 | 34 | const sendHandler = async (content) => { 35 | publishMessage(pk, content); 36 | }; 37 | 38 | return ( 39 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default ConversationScreen; 56 | -------------------------------------------------------------------------------- /features/messages/views/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as ActiveConversationScreen } from './ActiveConversationsScreen'; 3 | export { default as ConversationScreen } from './ConversationScreen'; 4 | -------------------------------------------------------------------------------- /features/plebhy/Views/index.js: -------------------------------------------------------------------------------- 1 | export {default as PlebhyGifView} from './PlebhyGifView'; 2 | export {default as PlebhyStickerView} from './PlebhyStickerView'; -------------------------------------------------------------------------------- /features/plebhy/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as GifContainer} from './GifContainer'; -------------------------------------------------------------------------------- /features/plebhy/index.js: -------------------------------------------------------------------------------- 1 | export {default as PlebhyNavigator} from './nav/PlebhyNavigator' -------------------------------------------------------------------------------- /features/plebhy/nav/PlebhyNavigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; 3 | import { PlebhyGifView, PlebhyStickerView } from '../Views'; 4 | import { colors } from '../../../styles'; 5 | 6 | const Tab = createMaterialTopTabNavigator(); 7 | 8 | const PlebhyNavigator = ({ route }) => { 9 | const { opener } = route?.params || undefined; 10 | console.log(opener); 11 | return ( 12 | 22 | 27 | 32 | 33 | ); 34 | }; 35 | 36 | export default PlebhyNavigator; 37 | -------------------------------------------------------------------------------- /features/post/nav/PostNavigator.jsx: -------------------------------------------------------------------------------- 1 | import { View, Platform } from 'react-native'; 2 | import React from 'react'; 3 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 4 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 5 | import { colors } from '../../../styles'; 6 | import { NewPostScreen, NewStatusScreen } from '../views'; 7 | import { PlebhyNavigator } from '../../plebhy'; 8 | 9 | const Stack = createNativeStackNavigator(); 10 | 11 | const PostNavigator = () => { 12 | const insets = useSafeAreaInsets(); 13 | return ( 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default PostNavigator; 31 | -------------------------------------------------------------------------------- /features/post/utils/publishNote.ts: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from "nostr-tools"; 2 | import { getValue } from "../../../utils"; 3 | import devLog from "../../../utils/internal"; 4 | import { getRelayUrls, getWriteRelays, pool } from "../../../utils/nostrV2"; 5 | 6 | export const publishNote = async (content, tags) => { 7 | try { 8 | const privKey = await getValue('privKey'); 9 | if (!privKey) { 10 | throw new Error('No privKey in secure storage found'); 11 | } 12 | const pubKey = getPublicKey(privKey); 13 | const event = { 14 | kind: 1, 15 | pubkey: pubKey, 16 | created_at: Math.floor(Date.now() / 1000), 17 | tags: tags || [], 18 | content, 19 | id: "", 20 | sig: "" 21 | }; 22 | 23 | try { 24 | const hashtags = event.content 25 | .split(' ') 26 | .filter((v) => v.startsWith('#')); 27 | hashtags.forEach((tag) => { 28 | event.tags.push(['t', tag.replace(/^#/, '')]); 29 | }); 30 | } catch (e) { 31 | console.log('error in setting up hashtags', e); 32 | } 33 | 34 | event.id = getEventHash(event); 35 | event.sig = signEvent(event, privKey); 36 | const writeUrls = getRelayUrls(getWriteRelays()); 37 | const pub = pool.publish(writeUrls, event); 38 | await new Promise((resolve) => { 39 | let handledRelays = 0; 40 | const timer = setTimeout(resolve, 2500); 41 | const checkAllHandled = () => { 42 | if (handledRelays === writeUrls.length) { 43 | devLog('All handled!'); 44 | clearTimeout(timer); 45 | resolve(); 46 | } 47 | }; 48 | pub.on('ok', () => { 49 | handledRelays += 1; 50 | checkAllHandled(); 51 | }); 52 | pub.on('failed', () => { 53 | handledRelays += 1; 54 | checkAllHandled(); 55 | }); 56 | }); 57 | return { event }; 58 | } catch (err) { 59 | devLog(err); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /features/post/utils/publishStatus.ts: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from 'nostr-tools'; 2 | import { getValue } from '../../../utils'; 3 | import devLog from '../../../utils/internal'; 4 | import { getRelayUrls, getWriteRelays, pool } from '../../../utils/nostrV2'; 5 | 6 | export const publishStatus = async ( 7 | content: string, 8 | website: string, 9 | expiresIn: number, 10 | ) => { 11 | try { 12 | const privKey = await getValue('privKey'); 13 | if (!privKey) { 14 | throw new Error('No privKey in secure storage found'); 15 | } 16 | const now = Math.floor(Date.now() / 1000); 17 | const pubKey = getPublicKey(privKey); 18 | const event = { 19 | kind: 30315, 20 | pubkey: pubKey, 21 | created_at: now, 22 | tags: [['d', 'general']], 23 | content, 24 | id: '', 25 | sig: '', 26 | }; 27 | if (expiresIn) { 28 | event.tags.push(['expiration', String(now + expiresIn)]); 29 | } 30 | if (website) { 31 | event.tags.push(['r', website]); 32 | } 33 | 34 | event.id = getEventHash(event); 35 | event.sig = signEvent(event, privKey); 36 | const writeUrls = getRelayUrls(getWriteRelays()); 37 | const pub = pool.publish(writeUrls, event); 38 | await new Promise((resolve) => { 39 | let handledRelays = 0; 40 | const timer = setTimeout(resolve, 2500); 41 | const checkAllHandled = () => { 42 | if (handledRelays === writeUrls.length) { 43 | clearTimeout(timer); 44 | resolve(); 45 | } 46 | }; 47 | pub.on('ok', () => { 48 | handledRelays += 1; 49 | checkAllHandled(); 50 | }); 51 | pub.on('failed', () => { 52 | handledRelays += 1; 53 | checkAllHandled(); 54 | }); 55 | }); 56 | return { event }; 57 | } catch (err) { 58 | devLog(err); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /features/post/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as NewPostScreen } from './NewPostScreen'; 2 | export { default as NewStatusScreen } from './NewStatusScreen'; 3 | -------------------------------------------------------------------------------- /features/premium/components/FeatureCard.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React, { memo } from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | 6 | type FeatureCardProps = { 7 | icon: keyof typeof Ionicons.glyphMap; 8 | title: string; 9 | text: string; 10 | }; 11 | 12 | const style = StyleSheet.create({ 13 | container: { 14 | alignItems: 'center', 15 | flexDirection: 'row', 16 | gap: 12 17 | }, 18 | title: {...globalStyles.textBodyBold}, 19 | text: {...globalStyles.textBody, flex: 1}, 20 | }); 21 | 22 | const FeatureCard = memo(({ icon, title, text }: FeatureCardProps) => { 23 | return ( 24 | 25 | 26 | {text} 27 | 28 | ); 29 | }); 30 | 31 | export default FeatureCard; 32 | -------------------------------------------------------------------------------- /features/premium/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FeatureCard } from './FeatureCard'; 2 | -------------------------------------------------------------------------------- /features/premium/index.ts: -------------------------------------------------------------------------------- 1 | export * from './views'; 2 | export * from './utils'; -------------------------------------------------------------------------------- /features/premium/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /features/premium/views/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PremiumView } from './PremiumView'; 2 | -------------------------------------------------------------------------------- /features/profile/hooks/useIsAuthed.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | export const useIsAuthed = (pubkey) => { 4 | const loggedInKey = useSelector(state => state.auth.pubKey) 5 | return (pubkey === loggedInKey) 6 | } 7 | ; -------------------------------------------------------------------------------- /features/profile/utils/getUsersPosts.js: -------------------------------------------------------------------------------- 1 | import { connectedRelays } from "../../../utils/nostrV2"; 2 | import { ProfilePost } from "../ProfilePost"; 3 | 4 | export const getUsersPosts = async (pubkeyInHex) => { 5 | const posts = {} 6 | await Promise.allSettled( 7 | connectedRelays.map((relay) => new Promise((resolve, reject) => { 8 | let events = [] 9 | let sub = relay.sub([ 10 | { 11 | authors: [pubkeyInHex], 12 | kinds: [1], 13 | limit: 100 14 | }, 15 | ]); 16 | const timer = setTimeout(() => { 17 | resolve(events); 18 | return; 19 | }, 3000) 20 | sub.on("event", (event) => { 21 | const newEvent = new ProfilePost(event) 22 | const formatted = newEvent.saveNote() 23 | events.push(formatted) 24 | }); 25 | sub.on("eose", () => { 26 | clearTimeout(timer); 27 | sub.unsub(); 28 | resolve(events); 29 | return; 30 | }); 31 | })) 32 | ).then(result => result.map(result => result.value.map(post => {posts[post.id] = post}))); 33 | return posts; 34 | }; -------------------------------------------------------------------------------- /features/profile/views/ProfileQRScreen.js: -------------------------------------------------------------------------------- 1 | import { View, Text, useWindowDimensions } from "react-native"; 2 | import React from "react"; 3 | import QRCode from "react-qr-code"; 4 | import { encodePubkey } from "../../../utils/nostr/keys"; 5 | import { Image } from "expo-image"; 6 | import { useSelector } from "react-redux"; 7 | import { colors, globalStyles } from "../../../styles"; 8 | 9 | const ProfileQRScreen = ({ route }) => { 10 | const { pk } = route.params; 11 | const npub = encodePubkey(pk); 12 | const user = useSelector((state) => state.messages.users[pk]); 13 | const { height, width } = useWindowDimensions(); 14 | return ( 15 | 20 | 21 | 34 | 35 | {user.name || pk.slice(0, 16)} 36 | 37 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default ProfileQRScreen; 55 | -------------------------------------------------------------------------------- /features/relays/components/RelayItem.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { View, Text, StyleSheet, Pressable, Alert } from 'react-native'; 3 | import React from 'react'; 4 | import { useDispatch } from 'react-redux'; 5 | import Animated, { SlideOutRight } from 'react-native-reanimated'; 6 | import { Ionicons } from '@expo/vector-icons'; 7 | import { colors, globalStyles } from '../../../styles'; 8 | import { removeRelay } from '../relaysSlice'; 9 | import { pool } from '../../../utils/nostrV2'; 10 | 11 | const style = StyleSheet.create({ 12 | container: { 13 | flexDirection: 'row', 14 | backgroundColor: colors.backgroundSecondary, 15 | paddingHorizontal: 6, 16 | paddingVertical: 12, 17 | borderRadius: 10, 18 | alignItems: 'center', 19 | justifyContent: 'space-between', 20 | marginBottom: 12, 21 | }, 22 | actionItems: { 23 | flexDirection: 'row', 24 | alignItems: 'center', 25 | width: '50%', 26 | justifyContent: 'space-evenly', 27 | }, 28 | action: { 29 | flexDirection: 'row', 30 | alignItems: 'center', 31 | marginLeft: 12, 32 | }, 33 | }); 34 | 35 | const RelayItem = ({ relay }) => { 36 | const relayUrl = new URL(relay.url); 37 | const state = pool._conn[relayUrl].status; 38 | const dispatch = useDispatch(); 39 | const removeHandler = () => { 40 | Alert.alert('Remove Relay?', `Do you really want to remove ${relay.url}?`, [ 41 | { 42 | text: 'Yes', 43 | onPress: () => { 44 | dispatch(removeRelay(relay.url)); 45 | }, 46 | style: 'destructive', 47 | }, 48 | { text: 'No' }, 49 | ]); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | {relay.url} 56 | 57 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default RelayItem; 68 | -------------------------------------------------------------------------------- /features/relays/components/RelaySettingsHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import BackButton from '../../../components/BackButton'; 5 | import { colors } from '../../../styles'; 6 | 7 | const style = StyleSheet.create({ 8 | container: { 9 | flexDirection: 'row', 10 | justifyContent: 'space-between', 11 | backgroundColor: '#18181b', 12 | paddingHorizontal: 6, 13 | paddingVertical: 12, 14 | }, 15 | }); 16 | 17 | const RelaySettingsHeader = ({ route, navigation }) => ( 18 | 19 | { 21 | navigation.goBack(); 22 | }} 23 | /> 24 | {!(route.name === 'Add') ? ( 25 | { 30 | navigation.navigate('Add'); 31 | }} 32 | /> 33 | ) : undefined} 34 | 35 | ); 36 | 37 | export default RelaySettingsHeader; 38 | -------------------------------------------------------------------------------- /features/relays/components/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as RelayItem } from './RelayItem'; 3 | -------------------------------------------------------------------------------- /features/relays/hooks/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as useRelayUrls } from './useRelayUrls'; 3 | -------------------------------------------------------------------------------- /features/relays/hooks/useRelayUrls.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | function useRelayUrls() { 4 | const relays = useSelector((state) => state.relays.relays); 5 | const readUrls = relays 6 | .filter((relay) => relay.read) 7 | .map((relay) => relay.url); 8 | const writeUrls = relays 9 | .filter((relay) => relay.read) 10 | .map((relay) => relay.url); 11 | 12 | return { readUrls, writeUrls }; 13 | } 14 | 15 | export default useRelayUrls; 16 | -------------------------------------------------------------------------------- /features/relays/index.js: -------------------------------------------------------------------------------- 1 | export { useRelayUrls } from './hooks'; 2 | export { default as RelaySettingsNav } from './nav/RelaySettingsNav'; 3 | -------------------------------------------------------------------------------- /features/relays/nav/RelaySettingsNav.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import React from 'react'; 3 | import { createStackNavigator } from '@react-navigation/stack'; 4 | import { useHeaderHeight } from '@react-navigation/elements'; 5 | import { AddRelayView, RelaysOverviewView } from '../views'; 6 | import RelaySettingsHeader from '../components/RelaySettingsHeader'; 7 | 8 | const Stack = createStackNavigator(); 9 | 10 | const RelaySettingsNav = () => { 11 | const headerHeight = useHeaderHeight(); 12 | return ( 13 | ( 16 | 17 | ), 18 | }} 19 | > 20 | 21 | 26 | 27 | ); 28 | }; 29 | 30 | export default RelaySettingsNav; 31 | -------------------------------------------------------------------------------- /features/relays/views/RelayOverviewView.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { useSelector } from 'react-redux'; 4 | import Animated from 'react-native-reanimated'; 5 | import { RelayItem } from '../components'; 6 | import { globalStyles } from '../../../styles'; 7 | 8 | const RelaysSettingsView = () => { 9 | const relays = useSelector((state) => state.relays.relays); 10 | return ( 11 | 12 | Hold to disconnect from relay 13 | } 16 | style={{ width: '100%' }} 17 | keyExtractor={(item) => item.url} 18 | /> 19 | 20 | ); 21 | }; 22 | 23 | export default RelaysSettingsView; 24 | -------------------------------------------------------------------------------- /features/relays/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as RelaysOverviewView } from './RelayOverviewView'; 2 | export { default as AddRelayView } from './AddRelayView'; 3 | -------------------------------------------------------------------------------- /features/reports/utils/publishReport.js: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from "nostr-tools"; 2 | import { connectedRelays } from "../../../utils/nostrV2"; 3 | import { getValue } from "../../../utils/secureStore"; 4 | 5 | export const publishReport = async (reason, eventId, pubkeyInHex) => { 6 | try { 7 | const sk = await getValue("privKey"); 8 | if (!sk) { 9 | throw new Error("No privKey in secure storage found"); 10 | } 11 | let pk = getPublicKey(sk); 12 | let event = { 13 | kind: 1984, 14 | pubkey: pk, 15 | created_at: Math.floor(Date.now() / 1000), 16 | tags: [ 17 | ["e", eventId, reason], 18 | ["p", pubkeyInHex], 19 | ], 20 | content: "", 21 | }; 22 | 23 | event.id = getEventHash(event); 24 | event.sig = signEvent(event, sk); 25 | const successes = await Promise.allSettled( 26 | connectedRelays.map((relay) => { 27 | return new Promise((resolve, reject) => { 28 | let pub = relay.publish(event); 29 | const timer = setTimeout(() => { 30 | reject(); 31 | }, 5000); 32 | pub.on("ok", () => { 33 | clearTimeout(timer); 34 | resolve(relay.url); 35 | return; 36 | }); 37 | pub.on("failed", (error) => { 38 | console.log(error); 39 | clearTimeout(timer); 40 | reject(); 41 | return; 42 | }); 43 | }); 44 | }) 45 | ).then((result) => 46 | result 47 | .filter((promise) => promise.status === "fulfilled") 48 | .map((promise) => promise.value) 49 | ); 50 | return successes; 51 | } catch (error) { 52 | console.log(error); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /features/search/components/ResultItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, StyleSheet, Pressable } from 'react-native'; 3 | import { Image } from 'expo-image'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import { colors, globalStyles } from '../../../styles'; 6 | 7 | const styles = StyleSheet.create({ 8 | container: { 9 | flexDirection: 'row', 10 | alignItems: 'center', 11 | paddingHorizontal: 6, 12 | paddingVertical: 12, 13 | backgroundColor: colors.backgroundSecondary, 14 | width: '100%', 15 | borderBottomColor: '#333333', 16 | borderBottomWidth: 1, 17 | }, 18 | image: { 19 | height: 40, 20 | width: 40, 21 | borderRadius: 20, 22 | marginRight: 12, 23 | }, 24 | }); 25 | 26 | const placeholder = require('../../../assets/user_placeholder.jpg'); 27 | 28 | const ResultItem = ({ userData }) => { 29 | const navigation = useNavigation(); 30 | const pressHandler = () => { 31 | navigation.navigate('Profile', { 32 | screen: 'ProfileScreen', 33 | params: { 34 | pubkey: userData.pubkey, 35 | name: userData?.name || userData.pubkey.slice(0, 16), 36 | }, 37 | }); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | {userData.name} 44 | 45 | ); 46 | }; 47 | 48 | export default ResultItem; 49 | -------------------------------------------------------------------------------- /features/search/components/SearchResultItem.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/features/search/components/SearchResultItem.jsx -------------------------------------------------------------------------------- /features/search/components/TrendingItem.jsx: -------------------------------------------------------------------------------- 1 | import { Text, Pressable, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import { globalStyles, colors } from '../../../styles'; 6 | 7 | const style = StyleSheet.create({ 8 | container: { 9 | flex: 1, 10 | backgroundColor: colors.backgroundSecondary, 11 | padding: 10, 12 | marginHorizontal: 6, 13 | borderRadius: 10, 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | }, 17 | pressed: { 18 | backgroundColor: '#3f3f46', 19 | }, 20 | }); 21 | 22 | const TrendingItem = ({ icon, title }) => { 23 | const navigation = useNavigation(); 24 | return ( 25 | [ 27 | style.container, 28 | pressed ? style.pressed : undefined, 29 | ]} 30 | onPress={() => { 31 | navigation.navigate(title); 32 | }} 33 | > 34 | 35 | {title} 36 | 37 | ); 38 | }; 39 | 40 | export default TrendingItem; 41 | -------------------------------------------------------------------------------- /features/search/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as TrendingItem } from './TrendingItem'; 2 | export { default as TrendingProfile } from './TrendingProfile'; 3 | export { default as ResultItem } from './ResultItem'; 4 | export { default as SearchResultItem } from './SearchResultItem'; 5 | -------------------------------------------------------------------------------- /features/search/hooks/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as useUsersInStore } from './useUsersInStore'; 3 | -------------------------------------------------------------------------------- /features/search/hooks/useUsersInStore.js: -------------------------------------------------------------------------------- 1 | import { matchSorter } from 'match-sorter'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const useUsersInStore = (searchTerm) => { 5 | const users = useSelector((state) => state.messages.users); 6 | const userArray = Object.keys(users).map((user) => users[user]); 7 | 8 | const result = matchSorter(userArray, searchTerm, { 9 | keys: ['name', 'pubkey', 'nip05'], 10 | }).slice(0, 15); 11 | return result; 12 | }; 13 | 14 | export default useUsersInStore; 15 | -------------------------------------------------------------------------------- /features/search/index.js: -------------------------------------------------------------------------------- 1 | export * from './nav'; 2 | -------------------------------------------------------------------------------- /features/search/nav/SearchNavigator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import { View } from 'react-native'; 3 | import React from 'react'; 4 | import { createStackNavigator } from '@react-navigation/stack'; 5 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 6 | import { SearchView, TrendingPostView, TrendingProfilesView } from '../views'; 7 | import { colors } from '../../../styles'; 8 | import { BackHeader } from '../../../components'; 9 | import CommentScreen from '../../comments/views/CommentScreen'; 10 | import ProfileNavigator from '../../../nav/ProfileNavigator'; 11 | 12 | const Stack = createStackNavigator(); 13 | 14 | const SearchNavigator = () => { 15 | const insets = useSafeAreaInsets(); 16 | return ( 17 | 18 | 19 | 24 | ({ 28 | header: () => , 29 | })} 30 | /> 31 | ({ 35 | header: () => , 36 | })} 37 | /> 38 | 43 | ({ 47 | headerShown: false, 48 | })} 49 | /> 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default SearchNavigator; 56 | -------------------------------------------------------------------------------- /features/search/nav/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as SearchNavigator } from './SearchNavigator'; 3 | -------------------------------------------------------------------------------- /features/search/utils/findUser.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starbackr-com/current/f99da9576ad0c45ad75f833d37a53ca0cff42b77/features/search/utils/findUser.js -------------------------------------------------------------------------------- /features/search/views/TrendingProfilesView.jsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from 'react-native'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { FlashList } from '@shopify/flash-list'; 4 | import devLog from '../../../utils/internal'; 5 | import { globalStyles } from '../../../styles'; 6 | import { TrendingProfile } from '../components'; 7 | import { getUserData } from '../../../utils/nostrV2'; 8 | 9 | const TrendingPostView = () => { 10 | const [trendingProfiles, setTrendingProfiles] = useState([]); 11 | 12 | useEffect(() => { 13 | async function getTrendingProfiles() { 14 | try { 15 | const res = await fetch('https://api.nostr.band/v0/trending/profiles'); 16 | if (!res.status === 200) { 17 | throw new Error('Request failed...'); 18 | } 19 | const data = await res.json(); 20 | const trending = data.profiles.map((note) => note.profile).filter(item => !!item); 21 | const trendingPubkeys = data.profiles.map((item) => item.pubkey); 22 | getUserData(trendingPubkeys); 23 | setTrendingProfiles(trending); 24 | } catch (e) { 25 | devLog(e); 26 | } 27 | } 28 | getTrendingProfiles(); 29 | }, []); 30 | return ( 31 | 32 | 33 | } 36 | estimatedItemSize={300} 37 | ListHeaderComponent={Trending Profiles} 38 | /> 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default TrendingPostView; 45 | -------------------------------------------------------------------------------- /features/search/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as SearchView } from './SearchView'; 2 | export { default as TrendingPostView } from './TrendingPostView'; 3 | export { default as TrendingProfilesView } from './TrendingProfilesView'; 4 | -------------------------------------------------------------------------------- /features/settings/components/SettingItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | Text, 4 | StyleSheet, 5 | Pressable, 6 | GestureResponderEvent, 7 | } from 'react-native'; 8 | import React from 'react'; 9 | import { Ionicons } from '@expo/vector-icons'; 10 | import { colors, globalStyles } from '../../../styles'; 11 | 12 | type SettingItemProps = { 13 | title: string; 14 | description: string; 15 | icon: keyof typeof Ionicons.glyphMap; 16 | onPress: (event: GestureResponderEvent) => void; 17 | }; 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | width: '100%', 22 | flexDirection: 'row', 23 | gap: 24, 24 | alignItems: 'center', 25 | // backgroundColor: colors.backgroundSecondary, 26 | padding: 12, 27 | }, 28 | containerActive: { 29 | backgroundColor: colors.backgroundActive, 30 | }, 31 | title: { 32 | ...globalStyles.textBodyBold, 33 | textAlign: 'left', 34 | }, 35 | }); 36 | 37 | const SettingItem = ({ 38 | title, 39 | description, 40 | icon, 41 | onPress, 42 | }: SettingItemProps) => { 43 | return ( 44 | [ 46 | styles.container, 47 | pressed ? styles.containerActive : undefined, 48 | ]} 49 | onPress={onPress} 50 | > 51 | 52 | 53 | {title} 54 | 55 | {description} 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default SettingItem; 63 | -------------------------------------------------------------------------------- /features/settings/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SettingItem } from './SettingItem'; 2 | -------------------------------------------------------------------------------- /features/settings/index.js: -------------------------------------------------------------------------------- 1 | export { default as SettingsNavigator } from './nav/SettingsNavigator'; 2 | export * from './views'; 3 | -------------------------------------------------------------------------------- /features/settings/locale/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "English": "Englisch", 3 | "Deutsch": "Deutsch", 4 | "SettingsLanguageView_H2_Active": "Aktive Sprache:", 5 | "SettingsLanguageView_H2_Select": "Wähle eine andere Sprache:" 6 | } 7 | -------------------------------------------------------------------------------- /features/settings/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "English": "English", 3 | "Deutsch": "German", 4 | "SettingsLanguageView_H2_Active": "Active language:", 5 | "SettingsLanguageView_H2_Select": "Select another language:", 6 | "SettingsLanguageView_Body_Change":"Do you want to change the selected language to " 7 | } 8 | -------------------------------------------------------------------------------- /features/settings/locale/index.js: -------------------------------------------------------------------------------- 1 | import de from './de.json'; 2 | import en from './en.json'; 3 | 4 | export default { de, en }; 5 | -------------------------------------------------------------------------------- /features/settings/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as ListScreen } from './ListScreen'; 2 | export { default as PaymentSettingsScreen } from './PaymentSettingsScreen'; 3 | export { default as KeysScreen } from './KeysScreen'; 4 | export { default as DeleteAccountScreen } from './DeleteAccountScreen'; 5 | export { default as LanguageSettingsScreen } from './LanguageSettingsScreen'; 6 | export { default as NotifcationsSettingsScreen } from './NotifcationsSettingsScreen'; 7 | export { default as UserSettingsScreen } from './UserSettingsScreen'; 8 | -------------------------------------------------------------------------------- /features/shared/utils/getAge.js: -------------------------------------------------------------------------------- 1 | export const getAge = (timestamp) => { 2 | const now = new Date(); 3 | const timePassedInMins = Math.floor( 4 | (now - new Date(timestamp * 1000)) / 1000 / 60 5 | ); 6 | 7 | if (timePassedInMins < 60) { 8 | return `${timePassedInMins}min ago`; 9 | } else if (timePassedInMins >= 60 && timePassedInMins < 1440) { 10 | return `${Math.floor(timePassedInMins / 60)}h ago`; 11 | } else if (timePassedInMins >= 1440 && timePassedInMins < 10080) { 12 | return `${Math.floor(timePassedInMins / 1440)}d ago`; 13 | } else { 14 | return `on ${new Date(timestamp * 1000).toLocaleDateString()}`; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /features/wallet/components/InTxItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | import { getAge } from '../../shared/utils/getAge'; 6 | 7 | const InTxItem = ({ item }) => { 8 | let status = 'grey'; 9 | if (item.status === 'paid' && item.type === 'in') { 10 | status = 'green'; 11 | } 12 | return ( 13 | 24 | 29 | 37 | {item.amount} 38 | 39 | {item.memo || ''} 40 | 41 | {getAge(item.createdat)} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default InTxItem; 48 | -------------------------------------------------------------------------------- /features/wallet/components/OutTxItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | import { getAge } from '../../shared/utils/getAge'; 6 | 7 | const OutTxItem = ({ item }) => { 8 | const status = 'red'; 9 | 10 | return ( 11 | 22 | 27 | 35 | {item.amount} 36 | 37 | {item.memo || ''} 38 | 39 | {getAge(item.createdat)} 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default OutTxItem; 46 | -------------------------------------------------------------------------------- /features/wallet/components/SweepModal.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import React, { memo, useState } from 'react'; 3 | import { CustomButton, Input } from '../../../components'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | import { emailRegex } from '../../../constants'; 6 | import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; 7 | import publishMessage from '../../messages/utils/publishMessage'; 8 | 9 | const styles = StyleSheet.create({ 10 | input: { 11 | width: '100%', 12 | backgroundColor: colors.backgroundSecondary, 13 | borderColor: colors.primary500, 14 | borderWidth: 1, 15 | borderRadius: 10, 16 | padding: 8, 17 | color: 'white', 18 | }, 19 | }); 20 | 21 | const SweepModal = memo(() => { 22 | const [input, setInput] = useState(); 23 | const submitHandler = async () => { 24 | if (input.length < 1 || !input.match(emailRegex)) { 25 | alert('This is not a valid Lightning Address...'); 26 | return; 27 | } 28 | try { 29 | await publishMessage( 30 | 'c7063ccd7e9adc0ddd4b77c6bfabffc8399b41e24de3a668a6ab62ede2c8aabd', 31 | `Initiated sweep to this address: ${input}`, 32 | ); 33 | alert('Sweep initiated! You will be notified once it is completed.'); 34 | } catch (e) { 35 | alert('Could not initiate sweep. Please contact support.'); 36 | console.log(e); 37 | } 38 | }; 39 | return ( 40 | 41 | 42 | Enter a destination Lightning Address: 43 | 44 | 45 | 46 | 47 | ); 48 | }); 49 | 50 | export default SweepModal; 51 | -------------------------------------------------------------------------------- /features/wallet/components/TopUpCard.tsx: -------------------------------------------------------------------------------- 1 | import { Text, StyleSheet, Pressable } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import { colors, globalStyles } from '../../../styles'; 4 | import { FontAwesome5 } from '@expo/vector-icons'; 5 | import Purchases, { PurchasesStoreProduct } from 'react-native-purchases'; 6 | import { LoadingSpinner } from '../../../components'; 7 | 8 | type TopUpCardProps = { 9 | product: PurchasesStoreProduct; 10 | refetchFn: () => void; 11 | }; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | borderRadius: 10, 17 | padding: 12, 18 | backgroundColor: colors.backgroundSecondary, 19 | alignItems: 'center', 20 | gap: 10, 21 | }, 22 | containerPressed: { 23 | backgroundColor: colors.backgroundActive, 24 | }, 25 | price: { 26 | ...globalStyles.textH2, 27 | marginBottom: 0, 28 | }, 29 | }); 30 | 31 | const TopUpCard = ({ product, refetchFn }: TopUpCardProps) => { 32 | const [isLoading, setIsLoading] = useState(false); 33 | if (isLoading) { 34 | return ; 35 | } 36 | return ( 37 | [ 39 | styles.container, 40 | pressed ? styles.containerPressed : undefined, 41 | ]} 42 | onPress={async () => { 43 | setIsLoading(true); 44 | try { 45 | await Purchases.purchaseStoreProduct(product); 46 | setTimeout(() => { 47 | refetchFn(); 48 | setIsLoading(false); 49 | }, 5000); 50 | } catch (e) { 51 | if (e.userCancelled) { 52 | alert('Transaction cancelled!'); 53 | } else { 54 | alert('Something went wrong...'); 55 | } 56 | console.log(e); 57 | } 58 | }} 59 | > 60 | 61 | 2100 SATS 62 | ${product.price} 63 | 64 | ); 65 | }; 66 | 67 | export default TopUpCard; 68 | -------------------------------------------------------------------------------- /features/wallet/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as InTxItem } from './InTxItem'; 2 | export { default as OutTxItem } from './OutTxItem'; 3 | export { default as TopUpCard } from './TopUpCard'; 4 | export { default as SweepModal } from './SweepModal'; 5 | -------------------------------------------------------------------------------- /features/wallet/hooks/useBalance.js: -------------------------------------------------------------------------------- 1 | import { useGetWalletBalanceQuery } from '../../../services/walletApi'; 2 | 3 | const useBalance = () => { 4 | const { data, error } = useGetWalletBalanceQuery(); 5 | if (!error) { 6 | return data.balance; 7 | } 8 | return undefined; 9 | }; 10 | 11 | export default useBalance; 12 | -------------------------------------------------------------------------------- /features/wallet/hooks/useTransactionHistory.js: -------------------------------------------------------------------------------- 1 | import { 2 | useGetIncomingTransactionsQuery, 3 | useGetOutgoingTransactionsQuery, 4 | } from '../../../services/walletApi'; 5 | 6 | const useTransactionHistory = () => { 7 | const { data: incoming = [], isLoading: inLoading } = useGetIncomingTransactionsQuery() || []; 8 | const { data: outgoing = [], isLoading: outLoading } = useGetOutgoingTransactionsQuery() || []; 9 | 10 | if (!inLoading && !outLoading) { 11 | const incomingTx = incoming.map((tx) => ({ ...tx, type: 'in' })); 12 | const outgoingTx = outgoing.map((tx) => ({ ...tx, type: 'out' })); 13 | const merged = [...incomingTx, ...outgoingTx].sort( 14 | (a, b) => b.createdat - a.createdat, 15 | ); 16 | return merged; 17 | } 18 | return undefined; 19 | }; 20 | 21 | export default useTransactionHistory; 22 | -------------------------------------------------------------------------------- /features/wallet/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as SendScreen } from './SendScreen'; 2 | export { default as ReceiveScreen } from './ReceiveScreen'; 3 | -------------------------------------------------------------------------------- /features/walletconnect/components/WalletconnectSettingsHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import BackButton from '../../../components/BackButton'; 5 | import { colors } from '../../../styles'; 6 | 7 | const style = StyleSheet.create({ 8 | container: { 9 | flexDirection: 'row', 10 | justifyContent: 'space-between', 11 | backgroundColor: '#18181b', 12 | paddingHorizontal: 6, 13 | paddingVertical: 12, 14 | }, 15 | }); 16 | 17 | const WalletconnectSettingsHeader = ({ route, navigation }) => ( 18 | 19 | { 21 | navigation.goBack(); 22 | }} 23 | /> 24 | {!(route.name === 'Add') ? ( 25 | { 30 | navigation.navigate('Add'); 31 | }} 32 | /> 33 | ) : undefined} 34 | 35 | ); 36 | 37 | export default WalletconnectSettingsHeader; 38 | -------------------------------------------------------------------------------- /features/walletconnect/components/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as WalletconnectItem } from './WalletconnectItem'; 3 | export { default as WalletconnectSeattingsHeader } from './WalletconnectSettingsHeader'; 4 | -------------------------------------------------------------------------------- /features/walletconnect/nav/WalletconnectSettingsNav.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import React from 'react'; 3 | import { createStackNavigator } from '@react-navigation/stack'; 4 | import { 5 | AddWalletconnectView, 6 | WalletconnectOverviewView, 7 | WalletconnectInfoView, 8 | } from '../views'; 9 | import WalletconnectSettingsHeader from '../components/WalletconnectSettingsHeader'; 10 | import { BackHeader } from '../../../components'; 11 | 12 | const Stack = createStackNavigator(); 13 | 14 | const WalletconnectSettingsNav = () => ( 15 | ( 18 | 19 | ), 20 | }} 21 | > 22 | 26 | ({ 30 | header: () => , 31 | })} 32 | /> 33 | 38 | 39 | ); 40 | 41 | export default WalletconnectSettingsNav; 42 | -------------------------------------------------------------------------------- /features/walletconnect/utils/keys.js: -------------------------------------------------------------------------------- 1 | import { generatePrivateKey, getPublicKey } from 'nostr-tools'; 2 | 3 | function genWalletConnectKey() { 4 | const privKey = generatePrivateKey(); 5 | const pubKey = getPublicKey(privKey); 6 | return { privKey, pubKey }; 7 | } 8 | 9 | export default genWalletConnectKey; 10 | -------------------------------------------------------------------------------- /features/walletconnect/utils/walletApi.js: -------------------------------------------------------------------------------- 1 | import { store } from '../../../store/store'; 2 | 3 | async function postNwc(payload) { 4 | const { walletBearer } = store.getState().auth; 5 | if (!walletBearer) { 6 | throw new Error('WalletBearer is undefined'); 7 | } 8 | const response = await fetch(`${process.env.BASEURL}/v2/walletconnect`, { 9 | method: 'POST', 10 | body: JSON.stringify(payload), 11 | headers: { 12 | 'content-type': 'application/json', 13 | Authorization: `Bearer ${walletBearer}`, 14 | }, 15 | }); 16 | const data = await response.json(); 17 | return data; 18 | } 19 | -------------------------------------------------------------------------------- /features/walletconnect/views/WalletconnectOverviewView.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { useSelector } from 'react-redux'; 4 | import Animated from 'react-native-reanimated'; 5 | import { WalletconnectItem } from '../components'; 6 | import { globalStyles } from '../../../styles'; 7 | 8 | const WalletconnectSettingsView = () => { 9 | const { wcdata } = useSelector((state) => state.walletconnect); 10 | return ( 11 | 12 | Wallet Connect 13 | {wcdata.length > 0 ? ( 14 | 15 | Touch to view QR code or Hold to deactivate 16 | 17 | ) : ( 18 | 19 | Click + button to add new wallet connect link 20 | 21 | )} 22 | } 25 | style={{ width: '100%' }} 26 | keyExtractor={(item) => item.nwcpubkey} 27 | /> 28 | 29 | Nostr Wallet Connect (NIP-47) is an easy way to connect this wallet to 30 | any supported Nostr clients such as Amethyst or Snort.social. Just copy and paste the QR 31 | code. 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default WalletconnectSettingsView; 38 | -------------------------------------------------------------------------------- /features/walletconnect/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as WalletconnectOverviewView } from './WalletconnectOverviewView'; 2 | export { default as AddWalletconnectView } from './AddWalletconnectView'; 3 | export { default as WalletconnectInfoView } from './WalletconnectInfoView'; 4 | -------------------------------------------------------------------------------- /features/walletconnect/walletconnectSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { saveValue } from '../../utils'; 3 | 4 | const initialState = { 5 | wcdata: [], 6 | wcPubkeys: [], 7 | }; 8 | 9 | export const walletconnectSlice = createSlice({ 10 | name: 'walletconnect', 11 | initialState, 12 | reducers: { 13 | addWalletconnect: (state, action) => { 14 | const newWcData = action.payload.filter( 15 | (wcKey) => !state.wcPubkeys.includes(wcKey.nwcpubkey), 16 | ); 17 | const newPubkeys = [ 18 | ...state.wcPubkeys, 19 | ...newWcData.map((item) => item.nwcpubkey), 20 | ]; 21 | state.wcPubkeys = newPubkeys; 22 | state.wcdata = [...newWcData, ...state.wcdata]; 23 | }, 24 | changeWalletconnect: (state, action) => { 25 | const updatedWalletconnectObject = action.payload; 26 | const index = state.wcdata.findIndex( 27 | (item) => item.nwcpubkey === updatedWalletconnectObject.nwcpubkey, 28 | ); 29 | state.wcdata[index] = updatedWalletconnectObject; 30 | }, 31 | hydrate: (state, action) => { 32 | state.wcPubkeys = action.payload.map((item) => item.nwcpubkey); 33 | state.wcdata = [...action.payload]; 34 | }, 35 | }, 36 | }); 37 | 38 | export const wcListener = async (action, listenerApi) => { 39 | const { 40 | walletconnect: { wcdata }, 41 | } = listenerApi.getState(); 42 | const json = JSON.stringify(wcdata); 43 | await saveValue('wcdata', json); 44 | }; 45 | 46 | export const { addWalletconnect, changeWalletconnect, hydrate } = 47 | walletconnectSlice.actions; 48 | 49 | export default walletconnectSlice.reducer; 50 | -------------------------------------------------------------------------------- /features/welcome/components/ImportTypeItem.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Pressable } from 'react-native'; 2 | import React from 'react'; 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { colors, globalStyles } from '../../../styles'; 6 | 7 | const styles = StyleSheet.create({ 8 | container: { 9 | flexDirection: 'row', 10 | alignItems: 'center', 11 | backgroundColor: colors.backgroundSecondary, 12 | padding: 12, 13 | borderRadius: 10, 14 | marginVertical: 12, 15 | width: '96%', 16 | }, 17 | }); 18 | 19 | const ImportTypeItem = ({ title, text, example, icon, onPress }) => { 20 | const { t } = useTranslation('welcome'); 21 | return ( 22 | [ 24 | styles.container, 25 | pressed ? { backgroundColor: '#333333' } : {}, 26 | ]} 27 | onPress={onPress} 28 | > 29 | 30 | 31 | 34 | {title} 35 | 36 | 37 | {text} 38 | 39 | {example ? ( 40 | 46 | {`${t('ImportTypeItem_Body_Example')}: ${example}`} 47 | 48 | ) : undefined} 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default ImportTypeItem; 55 | -------------------------------------------------------------------------------- /features/welcome/components/IntroductionItem.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { colors, globalStyles } from '../../../styles'; 5 | 6 | const IntroductionItem = ({ title, text, icon }) => ( 7 | 18 | 19 | 20 | 23 | {title} 24 | 25 | {text} 26 | 27 | 28 | ); 29 | 30 | export default IntroductionItem; 31 | -------------------------------------------------------------------------------- /features/welcome/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as IntroductionItem } from './IntroductionItem'; 2 | export { default as ImportTypeItem } from './ImportTypeItem'; 3 | -------------------------------------------------------------------------------- /features/welcome/index.js: -------------------------------------------------------------------------------- 1 | export {default as WelcomeNavigator} from './nav/WelcomeNavigator'; -------------------------------------------------------------------------------- /features/welcome/locale/index.js: -------------------------------------------------------------------------------- 1 | import de from './de.json'; 2 | import en from './en.json'; 3 | 4 | export default { de, en }; 5 | -------------------------------------------------------------------------------- /features/welcome/nav/CreateProfileNavigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { CreateProfileView, SelectImageView } from '../views'; 4 | 5 | const Stack = createStackNavigator(); 6 | 7 | const CreateProfileNavigator = () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default CreateProfileNavigator; 15 | -------------------------------------------------------------------------------- /features/welcome/nav/ImportNavigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { ImportKeyView, ImportSelectionView, ImportWordsView } from '../views'; 4 | 5 | const Stack = createStackNavigator(); 6 | 7 | const ImportNavigator = () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default ImportNavigator; 16 | -------------------------------------------------------------------------------- /features/welcome/views/NewImportWordsView.jsx: -------------------------------------------------------------------------------- 1 | import { View, Text, KeyboardAvoidingView, Platform } from 'react-native'; 2 | import React, { useRef, useState } from 'react'; 3 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 4 | import { globalStyles } from '../../../styles'; 5 | import { CustomButton } from '../../../components'; 6 | import MenuBottomSheetWithData from '../../../components/MenuBottomSheetWithData'; 7 | 8 | const NewImportWordsView = () => { 9 | const insets = useSafeAreaInsets(); 10 | const [listLength, setListLength] = useState(12); 11 | const modalRef = useRef(); 12 | 13 | const pressHandler = () => { 14 | modalRef.current.present(); 15 | }; 16 | return ( 17 | 22 | 23 | Select Seed Phrase Length 24 | 28 | 29 | ( 32 | 33 | {setListLength(12)}}}/> 34 | {setListLength(24)}}}/> 35 | 36 | )} 37 | > 38 | 39 | ); 40 | }; 41 | 42 | export default NewImportWordsView; 43 | -------------------------------------------------------------------------------- /features/welcome/views/index.js: -------------------------------------------------------------------------------- 1 | export { default as StartUpView } from './StartUpView'; 2 | export { default as IntroductionView } from './IntroductionView'; 3 | export { default as EULAView } from './EULAView'; 4 | export { default as UsernameView } from './UsernameView'; 5 | export { default as CreateProfileView } from './CreateProfileView'; 6 | export { default as SelectImageView } from './SelectImageView'; 7 | export { default as LoadingProfileView } from './LoadingProfileView'; 8 | export { default as ImportSelectionView } from './ImportSelectionView'; 9 | export { default as ImportKeyView } from './ImportKeyView'; 10 | export { default as ImportWordsView } from './ImportWordsView'; 11 | -------------------------------------------------------------------------------- /features/zaps/Zap.js: -------------------------------------------------------------------------------- 1 | import { decode } from "light-bolt11-decoder"; 2 | 3 | export class Zap { 4 | constructor(eventData) { 5 | this.id = eventData.id; 6 | this.content = eventData.content; 7 | this.created_at = eventData.created_at; 8 | this.kind = eventData.kind; 9 | this.pubkey = eventData.pubkey; 10 | this.sig = eventData.sig; 11 | this.tags = eventData.tags; 12 | this.receiver = this.tags.filter((tag) => tag[0] === "p"); 13 | this.toEvent = this.tags.filter((tag) => tag[0] === "e")[0][1]; 14 | this.invoice = this.tags.filter((tag) => tag[0] === "bolt11")[0][1]; 15 | this.request = JSON.parse( 16 | this.tags.filter((tag) => tag[0] === "description")[0][1] 17 | ); 18 | this.payer = this.request.pubkey; 19 | this.amount = decode(this.invoice).sections[2].value / 1000; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /features/zaps/hooks/useIsZapped.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | export const useIsZapped = (eventId) => { 4 | const zappedEvents = useSelector(state => state.interaction.zappedEvents) 5 | return zappedEvents.includes(eventId); 6 | }; -------------------------------------------------------------------------------- /features/zaps/utils/getZaps.js: -------------------------------------------------------------------------------- 1 | import { 2 | getReadRelays, 3 | getRelayUrls, 4 | pool, 5 | } from '../../../utils/nostrV2/relays.ts'; 6 | import { Zap } from '../Zap'; 7 | 8 | export const getZaps = async (eventIds) => { 9 | const readUrls = getRelayUrls(getReadRelays()); 10 | const allZaps = {}; 11 | const zapsPerPost = {}; 12 | await new Promise((resolve) => { 13 | const sub = pool.sub(readUrls, [{ kinds: [9735], '#e': eventIds }]); 14 | sub.on('event', (event) => { 15 | const zap = new Zap(event); 16 | allZaps[zap.id] = zap; 17 | }); 18 | sub.on('eose', () => { 19 | resolve(); 20 | }); 21 | }); 22 | const array = Object.keys(allZaps).map((key) => allZaps[key]); 23 | array.forEach((zap) => { 24 | if (zapsPerPost[zap.toEvent]) { 25 | zapsPerPost[zap.toEvent].zaps.push(zap); 26 | zapsPerPost[zap.toEvent].amount += zap.amount; 27 | } else { 28 | zapsPerPost[zap.toEvent] = { amount: zap.amount, zaps: [zap] }; 29 | } 30 | }); 31 | return zapsPerPost; 32 | }; 33 | 34 | export default getZaps; 35 | -------------------------------------------------------------------------------- /hooks/index.js: -------------------------------------------------------------------------------- 1 | export * from './useFollowUser'; 2 | export * from './useUnfollowUser'; 3 | export * from './useParseContent'; 4 | export * from './useSubscribeReplies'; 5 | export * from './useUpdateFollowing'; 6 | export * from './useZapNote'; 7 | export * from './usePaginatedPosts'; 8 | export { default as useIsFollowed } from './useIsFollowed'; 9 | 10 | export { default as useSubscribeEvents } from './useSubscribeEvents'; 11 | export { default as useUsersInStore } from './useUsersInStore'; 12 | -------------------------------------------------------------------------------- /hooks/useErrorToast.tsx: -------------------------------------------------------------------------------- 1 | import Toast from "react-native-root-toast"; 2 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 3 | import { SuccessToast, WarningToast } from "../components"; 4 | import { colors } from "../styles"; 5 | 6 | const useErrorToast = (errorMsg: string) => { 7 | const insets = useSafeAreaInsets(); 8 | function trigger() { 9 | Toast.show(errorMsg, { 10 | duration: Toast.durations.SHORT, 11 | position: -insets.bottom - 25, 12 | backgroundColor: colors.primary500, 13 | textStyle: {fontFamily: 'Montserrat-Bold'} 14 | }); 15 | } 16 | return trigger; 17 | }; 18 | 19 | export default useErrorToast; 20 | -------------------------------------------------------------------------------- /hooks/useInteractions.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const useInteractions = (eventId) => { 5 | const { likedEvents, zappedEvents, repostedEvents } = useSelector( 6 | //@ts-ignore 7 | (state) => state.interaction, 8 | ); 9 | const status = useMemo(() => { 10 | const isLiked: boolean = likedEvents.includes(eventId); 11 | const isZapped: boolean = zappedEvents.includes(eventId); 12 | const isReposted: boolean = repostedEvents.includes(eventId); 13 | return {isLiked, isZapped, isReposted} 14 | }, [ 15 | eventId, 16 | likedEvents, 17 | zappedEvents, 18 | repostedEvents, 19 | ]); 20 | return status; 21 | }; 22 | 23 | export default useInteractions -------------------------------------------------------------------------------- /hooks/useIsFollowed.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | const useIsFollower = (pubkey) => { 4 | const followedPubkeys = useSelector((state) => state.user.followedPubkeys); 5 | return followedPubkeys.includes(pubkey); 6 | }; 7 | 8 | export default useIsFollower; 9 | -------------------------------------------------------------------------------- /hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { getLocales } from 'expo-localization'; 3 | import { getValue } from '../utils'; 4 | import { getData } from '../utils/cache/asyncStorage'; 5 | 6 | const useLanguage = () => { 7 | const [language, setLanguage] = useState(); 8 | 9 | useEffect(() => { 10 | async function checkLanguage() { 11 | const savedLanguage = await getData('language'); 12 | if (savedLanguage) { 13 | setLanguage(savedLanguage); 14 | } else { 15 | setLanguage(getLocales()[0].languageCode); 16 | } 17 | } 18 | checkLanguage(); 19 | }, []); 20 | 21 | return language; 22 | }; 23 | 24 | export default useLanguage; 25 | -------------------------------------------------------------------------------- /hooks/useSilentFollow.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { followMultiplePubkeys } from '../features/userSlice'; 3 | import { db } from '../utils/database'; 4 | 5 | const useSilentFollow = () => { 6 | const dispatch = useDispatch(); 7 | const followedPubkeys = useSelector((state) => state.user.followedPubkeys); 8 | 9 | const silentFollow = async (pubkeysInHex) => { 10 | try { 11 | const deduped = pubkeysInHex.filter( 12 | (pubkey) => !followedPubkeys.includes(pubkey), 13 | ); 14 | dispatch(followMultiplePubkeys(deduped)); 15 | const timeNow = new Date().getTime() / 1000; 16 | deduped.forEach((pubkey) => { 17 | try { 18 | const sql = 'INSERT OR REPLACE INTO followed_users (pubkey, followed_at) VALUES (?,?)'; 19 | const params = [pubkey, timeNow]; 20 | try { 21 | db.transaction((tx) => { 22 | tx.executeSql(sql, params, undefined, (_, error) => { 23 | console.error('Save user error', error); 24 | return false; 25 | }); 26 | }); 27 | } catch (e) { 28 | console.error(e); 29 | console.error(e.stack); 30 | } 31 | } catch (e) { 32 | console.log(e); 33 | } 34 | }); 35 | } catch (e) { 36 | console.log(e); 37 | } 38 | }; 39 | 40 | return silentFollow; 41 | }; 42 | 43 | export default useSilentFollow; 44 | -------------------------------------------------------------------------------- /hooks/useSubscribeEvents.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Note, pool } from '../utils/nostrV2'; 3 | import { useRelayUrls } from '../features/relays'; 4 | 5 | const useSubscribeEvents = (pubkeyInHex) => { 6 | const [events, setEvents] = useState([]); 7 | const { readUrls } = useRelayUrls(); 8 | 9 | const eventCallback = (event) => { 10 | const newEvent = new Note(event); 11 | const parsedEvent = newEvent.save(); 12 | setEvents((prev) => 13 | [...prev, parsedEvent].sort((a, b) => b.created_at - a.created_at), 14 | ); 15 | }; 16 | 17 | useEffect(() => { 18 | setEvents([]); 19 | const sub = pool.sub( 20 | readUrls, 21 | [ 22 | { 23 | kinds: [1], 24 | authors: [pubkeyInHex], 25 | limit: 50, 26 | }, 27 | ], 28 | { skipVerification: true }, 29 | ); 30 | sub.on('event', eventCallback); 31 | return () => { 32 | sub.unsub(); 33 | }; 34 | }, [pubkeyInHex]); 35 | return events; 36 | }; 37 | 38 | export default useSubscribeEvents; 39 | -------------------------------------------------------------------------------- /hooks/useSubscribeReplies.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import { Zap } from "../features/zaps/Zap"; 5 | import { Reply } from "../utils/nostrV2/Reply"; 6 | import { connectedRelays } from "../utils/nostrV2"; 7 | 8 | export const useSubscribeReplies = (parentIds) => { 9 | const [data, setData] = useState(0); 10 | const mutedPubkeys = useSelector((state) => state.user.mutedPubkeys); 11 | console.log(data); 12 | useEffect(() => { 13 | let subs = connectedRelays.map((relay) => { 14 | let sub = relay.sub([ 15 | { 16 | kinds: [1], 17 | "#e": parentIds, 18 | }, 19 | ]); 20 | sub.on("event", (event) => { 21 | if (mutedPubkeys.includes(event.pubkey)) { 22 | return; 23 | } else { 24 | if (event.kind === 1) { 25 | const reply = new Reply(event); 26 | console.log('EVENT!'); 27 | } else if (event.kind === 9735) { 28 | const zap = new Zap(event); 29 | events.push(zap); 30 | } 31 | } 32 | }); 33 | return sub; 34 | }); 35 | 36 | // Unsub on Dismount 37 | return () => { 38 | subs.forEach((sub) => sub.unsub()); 39 | }; 40 | }, []); 41 | }; -------------------------------------------------------------------------------- /hooks/useUser.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | const useUser = (pubkeyInHex) => { 4 | const user = useSelector((state) => state.messages.users[pubkeyInHex]); 5 | return user; 6 | }; 7 | 8 | export default useUser; 9 | -------------------------------------------------------------------------------- /hooks/useUsersInStore.js: -------------------------------------------------------------------------------- 1 | import { matchSorter } from 'match-sorter'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const useUsersInStore = (searchTerm) => { 5 | if (searchTerm.length < 1) { 6 | return []; 7 | } 8 | const users = useSelector((state) => state.messages.users); 9 | const userArray = Object.keys(users).map((user) => users[user]); 10 | 11 | const result = matchSorter(userArray, searchTerm, { 12 | keys: ['name', 'pubkey', 'nip05'], 13 | }).slice(0, 25); 14 | return result; 15 | }; 16 | 17 | export default useUsersInStore; 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes'; 3 | 4 | ViewReactNativeStyleAttributes.scaleY = true; 5 | 6 | import App from './App'; 7 | import "react-native-get-random-values"; 8 | import 'big-integer' 9 | 10 | 11 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 12 | // It also ensures that whether you load the app in Expo Go or in a native build, 13 | // the environment is set up appropriately 14 | registerRootComponent(App); 15 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | module.exports = getDefaultConfig(__dirname); 5 | -------------------------------------------------------------------------------- /models/Kind1063Media.tsx: -------------------------------------------------------------------------------- 1 | import { getTagValue } from '../utils/nostrV2/tags'; 2 | 3 | export class Kind1063Media { 4 | id: string; 5 | caption: string; 6 | createdAt: string; 7 | kind: number; 8 | pubkey: string; 9 | tags: string[][] | []; 10 | sig: string; 11 | url: string; 12 | type: string; 13 | blurhash?: string; 14 | dimensions?: number[]; 15 | 16 | constructor(eventData) { 17 | this.id = eventData.id; 18 | this.caption = eventData.content; 19 | this.createdAt = eventData.created_at; 20 | this.kind = eventData.kind; 21 | this.pubkey = eventData.pubkey; 22 | this.sig = eventData.sig; 23 | this.tags = eventData.tags; 24 | this.url = getTagValue(eventData, 'url'); 25 | this.type = getTagValue(eventData, 'm'); 26 | this.blurhash = getTagValue(eventData, 'blurhash'); 27 | this.dimensions = getTagValue(eventData, 'dim') 28 | ?.split('x') 29 | .map((item) => +item); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /models/Kind65005ImgRequest.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'nostr-tools'; 2 | import { imageRegex } from '../constants'; 3 | 4 | export class Kind65005ImgRequest { 5 | content: string; 6 | id: string; 7 | pubkey: string; 8 | createdAt: number; 9 | tags: string[][] | []; 10 | kind: number; 11 | sig: string; 12 | data: string; 13 | inputType: 'url' | 'event' | 'job' | 'text'; 14 | marker?: string; 15 | inputRelay?: string; 16 | output: string; 17 | bid?: number; 18 | relays?: string[]; 19 | serviceProviders?: string[]; 20 | 21 | constructor(event: Event) { 22 | this.createdAt = event.created_at; 23 | this.id = event.id; 24 | this.pubkey = event.pubkey; 25 | this.kind = event.kind; 26 | this.tags = event.tags; 27 | this.sig = event.sig; 28 | } 29 | } 30 | 31 | export default Kind1Note; 32 | -------------------------------------------------------------------------------- /nav/ProfileNavigator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import React from 'react'; 3 | import { createStackNavigator } from '@react-navigation/stack'; 4 | import ProfileScreen from '../views/ProfileScreen'; 5 | import EditProfileScreen from '../views/profile/EditProfileScreen'; 6 | import ProfileHeader from '../features/profile/components/ProfileHeader'; 7 | import ProfileQRScreen from '../features/profile/views/ProfileQRScreen'; 8 | import { BadgeDetaiView } from '../features/badges'; 9 | import ChooseBadgeView from '../features/badges/views/ChooseBadgeView'; 10 | 11 | const Stack = createStackNavigator(); 12 | 13 | const ProfileNavigator = () => ( 14 | ({ 16 | header: () => , 17 | })} 18 | > 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default ProfileNavigator; 28 | -------------------------------------------------------------------------------- /patches/@noble+hashes+1.3.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@noble/hashes/utils.js b/node_modules/@noble/hashes/utils.js 2 | index c0519ac..b5ea31d 100644 3 | --- a/node_modules/@noble/hashes/utils.js 4 | +++ b/node_modules/@noble/hashes/utils.js 5 | @@ -152,10 +152,12 @@ exports.wrapConstructorWithOpts = wrapConstructorWithOpts; 6 | * Secure PRNG. Uses `globalThis.crypto` or node.js crypto module. 7 | */ 8 | function randomBytes(bytesLength = 32) { 9 | - if (crypto_1.crypto && typeof crypto_1.crypto.getRandomValues === 'function') { 10 | - return crypto_1.crypto.getRandomValues(new Uint8Array(bytesLength)); 11 | - } 12 | - throw new Error('crypto.getRandomValues must be defined'); 13 | -} 14 | + try { 15 | + const randomBytes = require('expo-crypto').getRandomBytes; 16 | + return randomBytes(bytesLength); 17 | + } catch (e) { 18 | + throw new Error("The environment doesn't have randomBytes function and we couldn't use expo-crypto"); 19 | + } 20 | +} 21 | exports.randomBytes = randomBytes; 22 | //# sourceMappingURL=utils.js.map 23 | \ No newline at end of file 24 | -------------------------------------------------------------------------------- /patches/@noble+secp256k1+1.7.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@noble/secp256k1/lib/index.js b/node_modules/@noble/secp256k1/lib/index.js 2 | index 33a0843..eab0bdf 100644 3 | --- a/node_modules/@noble/secp256k1/lib/index.js 4 | +++ b/node_modules/@noble/secp256k1/lib/index.js 5 | @@ -1132,16 +1132,12 @@ exports.utils = { 6 | return numTo32b(num); 7 | }, 8 | randomBytes: (bytesLength = 32) => { 9 | - if (crypto.web) { 10 | - return crypto.web.getRandomValues(new Uint8Array(bytesLength)); 11 | - } 12 | - else if (crypto.node) { 13 | - const { randomBytes } = crypto.node; 14 | - return Uint8Array.from(randomBytes(bytesLength)); 15 | - } 16 | - else { 17 | - throw new Error("The environment doesn't have randomBytes function"); 18 | - } 19 | + try { 20 | + const randomBytes = require('expo-crypto').getRandomBytes; 21 | + return randomBytes(bytesLength); 22 | + } catch (e) { 23 | + throw new Error("The environment doesn't have randomBytes function and we couldn't use expo-crypto"); 24 | + } 25 | }, 26 | randomPrivateKey: () => exports.utils.hashToPrivateKey(exports.utils.randomBytes(groupLen + 8)), 27 | precompute(windowSize = 8, point = Point.BASE) { 28 | -------------------------------------------------------------------------------- /patches/react-native-root-toast+3.4.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-root-toast/index.d.ts b/node_modules/react-native-root-toast/index.d.ts 2 | index 7c93779..8493dc5 100644 3 | --- a/node_modules/react-native-root-toast/index.d.ts 4 | +++ b/node_modules/react-native-root-toast/index.d.ts 5 | @@ -45,7 +45,7 @@ declare module "react-native-root-toast"{ 6 | CENTER:number, 7 | } 8 | export default class Toast extends React.Component{ 9 | - static show:(message:string,options?:ToastOptions)=>any; 10 | + static show:(message:string | React.ReactNode ,options?:ToastOptions)=>any; 11 | static hide:(toast:any)=>void; 12 | static durations:Durations; 13 | static positions:Positions; 14 | -------------------------------------------------------------------------------- /plugins/withAndroidNamespace.js: -------------------------------------------------------------------------------- 1 | const { withAppBuildGradle } = require('@expo/config-plugins'); 2 | 3 | const withAndroidNamespace = (config) => { 4 | return withAppBuildGradle(config, (config) => { 5 | const buildGradle = config.modResults.contents; 6 | const namespace = config.android.package.toString(); 7 | const newContents = buildGradle.replace( 8 | /namespace (.*)\n/, 9 | `namespace '${namespace}'\n` 10 | ); 11 | config.modResults.contents = newContents; 12 | console.log(`withAndroidNamespace: change namespace to ${namespace}`); 13 | return config; 14 | }); 15 | }; 16 | 17 | module.exports = withAndroidNamespace; 18 | -------------------------------------------------------------------------------- /store/listenerMiddleware.js: -------------------------------------------------------------------------------- 1 | import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit'; 2 | import { communityListener, joinCommunity } from '../features/community/communitySlice'; 3 | import { 4 | addRelay, 5 | changeRelayMode, 6 | relayListener, 7 | removeRelay, 8 | } from '../features/relays/relaysSlice'; 9 | import { addWalletconnect, changeWalletconnect, wcListener } from '../features/walletconnect/walletconnectSlice'; 10 | import { addLike, addRepost, likeListener, removeLike, removeRepost, repostListener } from '../features/interactionSlice'; 11 | 12 | const listener = createListenerMiddleware(); 13 | 14 | listener.startListening({ 15 | matcher: isAnyOf(addRelay, removeRelay, changeRelayMode), 16 | effect: relayListener, 17 | }); 18 | listener.startListening({ 19 | matcher: isAnyOf(addWalletconnect, changeWalletconnect), 20 | effect: wcListener, 21 | }); 22 | listener.startListening({ 23 | actionCreator: joinCommunity, 24 | effect: communityListener, 25 | }); 26 | listener.startListening({ 27 | matcher: isAnyOf(addLike, removeLike), 28 | effect: likeListener, 29 | }); 30 | listener.startListening({ 31 | matcher: isAnyOf(addRepost, removeRepost), 32 | effect: repostListener, 33 | }); 34 | 35 | export default listener.middleware; 36 | -------------------------------------------------------------------------------- /store/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { configureStore } from '@reduxjs/toolkit'; 3 | import authReducer from '../features/authSlice'; 4 | import introReducer from '../features/introSlice'; 5 | import messagesReducer from '../features/messagesSlice'; 6 | import { walletApi } from '../services/walletApi'; 7 | import userReducer from '../features/userSlice'; 8 | import interactionReducer from '../features/interactionSlice'; 9 | import relaysReducer from '../features/relays/relaysSlice'; 10 | import walletconnectReducer from '../features/walletconnect/walletconnectSlice'; 11 | import listener from './listenerMiddleware'; 12 | import badgeReducer from '../features/badges/badgeSlice'; 13 | import communityReducer from '../features/community/communitySlice'; 14 | 15 | export const store = configureStore({ 16 | reducer: { 17 | auth: authReducer, 18 | intro: introReducer, 19 | messages: messagesReducer, 20 | user: userReducer, 21 | interaction: interactionReducer, 22 | relays: relaysReducer, 23 | walletconnect: walletconnectReducer, 24 | badges: badgeReducer, 25 | community: communityReducer, 26 | [walletApi.reducerPath]: walletApi.reducer, 27 | }, 28 | middleware: (getDefaultMiddleware) => getDefaultMiddleware() 29 | .prepend(listener) 30 | .concat([walletApi.middleware]), 31 | }); 32 | -------------------------------------------------------------------------------- /styles/colors.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | primary500: '#faa200', 3 | primary600: '#CC8500', 4 | secondary500: '#777777', 5 | 6 | backgroundPrimary: '#18181b', 7 | backgroundSecondary: '#27272a', 8 | backgroundActive: '#3f3f46', 9 | }; 10 | 11 | export default colors; 12 | -------------------------------------------------------------------------------- /styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import colors from './colors'; 3 | 4 | const globalStyles = StyleSheet.create({ 5 | screenContainer: { 6 | flex: 1, 7 | paddingTop: 16, 8 | paddingHorizontal: 8, 9 | backgroundColor: colors.backgroundPrimary, 10 | alignItems: 'center', 11 | }, 12 | screenContainerScroll: { 13 | flex: 1, 14 | paddingTop: 32, 15 | paddingHorizontal: 8, 16 | backgroundColor: colors.backgroundPrimary, 17 | }, 18 | textBody: { 19 | fontFamily: 'Montserrat-Regular', 20 | color: 'white', 21 | fontSize: 16, 22 | textAlign: 'center', 23 | }, 24 | textBodyS: { 25 | fontFamily: 'Montserrat-Regular', 26 | color: 'white', 27 | fontSize: 12, 28 | textAlign: 'center', 29 | }, 30 | textBodyG: { 31 | fontFamily: 'Montserrat-Regular', 32 | color: 'grey', 33 | fontSize: 12, 34 | textAlign: 'center', 35 | }, 36 | textBodyBold: { 37 | fontFamily: 'Montserrat-Bold', 38 | color: 'white', 39 | fontSize: 16, 40 | textAlign: 'center', 41 | }, 42 | textBodyError: { 43 | fontFamily: 'Montserrat-Regular', 44 | color: 'red', 45 | fontSize: 16, 46 | textAlign: 'center', 47 | }, 48 | textH1: { 49 | fontFamily: 'Montserrat-Bold', 50 | color: 'white', 51 | fontSize: 32, 52 | marginBottom: 32, 53 | }, 54 | textH2: { 55 | fontFamily: 'Montserrat-Bold', 56 | color: 'white', 57 | fontSize: 24, 58 | marginBottom: 16, 59 | }, 60 | }); 61 | 62 | export default globalStyles; 63 | -------------------------------------------------------------------------------- /styles/index.js: -------------------------------------------------------------------------------- 1 | export { default as colors } from './colors'; 2 | export { default as globalStyles } from './globalStyles'; 3 | -------------------------------------------------------------------------------- /translations/locale/common/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Common_Yes": "Ja", 3 | "Common_No": "Nein" 4 | } 5 | -------------------------------------------------------------------------------- /translations/locale/common/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Common_Yes": "Yes", 3 | "Common_No": "No", 4 | "Common_GoBack": "Go Back" 5 | } 6 | -------------------------------------------------------------------------------- /translations/translations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | import i18n from 'i18next'; 3 | import { initReactI18next } from 'react-i18next'; 4 | import { getLocales } from 'expo-localization'; 5 | import { getData } from '../utils/cache/asyncStorage'; 6 | import welcomeTranslations from '../features/welcome/locale'; 7 | import settingsTranslations from '../features/settings/locale'; 8 | 9 | async function checkLanguage() { 10 | const selectedLanguage = await getData('language'); 11 | if (selectedLanguage) { 12 | i18n.changeLanguage(selectedLanguage); 13 | return; 14 | } 15 | i18n.changeLanguage(getLocales()[0].languageCode); 16 | } 17 | 18 | const resources = { 19 | en: { 20 | welcome: welcomeTranslations.en, 21 | settings: settingsTranslations.en, 22 | }, 23 | de: { 24 | welcome: welcomeTranslations.de, 25 | settings: settingsTranslations.de, 26 | }, 27 | }; 28 | 29 | i18n 30 | .use(initReactI18next) // passes i18n down to react-i18next 31 | .init({ 32 | resources, 33 | compatibilityJSON: 'v3', 34 | lng: 'de', 35 | interpolation: { 36 | escapeValue: false, 37 | }, 38 | }); 39 | 40 | checkLanguage(); 41 | 42 | export default i18n; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "expo/tsconfig.base" 4 | } 5 | -------------------------------------------------------------------------------- /utils/bitcoin/lnurl.js: -------------------------------------------------------------------------------- 1 | import { bech32 } from "bech32"; 2 | 3 | const utf8Encoder = new TextDecoder(); 4 | const utf8Decoder = new TextEncoder(); 5 | 6 | export const decodeLnurl = (lnurl) => { 7 | try { 8 | let { prefix: hrp, words: dataPart } = bech32.decode(lnurl, 2000); 9 | let requestByteArray = bech32.fromWords(dataPart); 10 | let decoded = utf8Encoder.decode(new Uint8Array(requestByteArray)); 11 | return decoded; 12 | } catch (err) { 13 | console.log("Something went wrong while decoding LNURL..."); 14 | } 15 | }; 16 | 17 | export const encodeLnurl = (string) => { 18 | try { 19 | let words = bech32.toWords(utf8Decoder.encode(string)) 20 | let encoded = bech32.encode('lnurl', words, 500) 21 | return encoded 22 | } catch (err) { 23 | console.log(err); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /utils/images.js: -------------------------------------------------------------------------------- 1 | import * as ImagePicker from 'expo-image-picker'; 2 | import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; 3 | 4 | async function resizeImage(image) { 5 | const manipResult = await manipulateAsync( 6 | image.localUri || image.uri, 7 | [{ resize: { width: 1080 } }], 8 | { compress: 1, format: SaveFormat.PNG }, 9 | ); 10 | return manipResult; 11 | } 12 | 13 | async function uploadImage(image, pubKey, bearer) { 14 | const id = pubKey.slice(0, 16); 15 | const localUri = image.uri; 16 | const filename = localUri.split('/').pop(); 17 | const match = /\.(\w+)$/.exec(filename); 18 | const type = match ? `image/${match[1]}` : 'image'; 19 | const formData = new FormData(); 20 | formData.append('asset', { uri: localUri, name: filename, type }); 21 | formData.append( 22 | 'name', 23 | `${id}/uploads/image${Math.floor(Math.random() * 10000000)}.${match[1]}`, 24 | ); 25 | formData.append('type', 'image'); 26 | const response = await fetch(`${process.env.BASEURL}/upload`, { 27 | method: 'POST', 28 | body: formData, 29 | headers: { 30 | 'content-type': 'multipart/form-data', 31 | Authorization: `Bearer ${bearer}`, 32 | }, 33 | }); 34 | const data = await response.json(); 35 | return data; 36 | } 37 | 38 | async function pickImageResizeAndUpload(pk, bearer) { 39 | let result = await ImagePicker.launchImageLibraryAsync({ 40 | mediaTypes: ImagePicker.MediaTypeOptions.All, 41 | quality: 1, 42 | }); 43 | 44 | if (!result.canceled) { 45 | result = await resizeImage(result.assets[0]); 46 | const data = await uploadImage(result, pk, bearer); 47 | console.log(data); 48 | return data; 49 | } 50 | return { error: true, data: '' }; 51 | } 52 | 53 | export default pickImageResizeAndUpload; 54 | -------------------------------------------------------------------------------- /utils/images.ts: -------------------------------------------------------------------------------- 1 | import * as ImagePicker from 'expo-image-picker'; 2 | import { manipulateAsync, ImageResult } from 'expo-image-manipulator'; 3 | 4 | export async function pickSingleImage() { 5 | const result = await ImagePicker.launchImageLibraryAsync({ 6 | mediaTypes: ImagePicker.MediaTypeOptions.Images, 7 | quality: 1, 8 | }); 9 | if (result.canceled) { 10 | throw new Error('User cancelled'); 11 | } 12 | return result.assets[0]; 13 | } 14 | 15 | export async function resizeImageSmall(image: ImagePicker.ImagePickerAsset) { 16 | if (image.width < 1080 && image.height < 1080) { 17 | const manipResult = await manipulateAsync(image.uri, [], { compress: 1 }); 18 | return manipResult; 19 | } 20 | const manipResult = await manipulateAsync( 21 | image.uri, 22 | [ 23 | image.width > image.height 24 | ? { resize: { width: 1024 } } 25 | : { resize: { height: 1024 } }, 26 | ], 27 | { compress: 0.5 }, 28 | ); 29 | return manipResult; 30 | } 31 | 32 | export async function uploadJpeg( 33 | image: ImageResult, 34 | pubkey: string, 35 | bearerToken: string, 36 | ) { 37 | const filename = image.uri.split('/').pop(); 38 | const formData = new FormData(); 39 | //@ts-ignore 40 | formData.append('asset', { 41 | uri: image.uri, 42 | name: filename, 43 | type: 'image/jpeg', 44 | }); 45 | formData.append( 46 | 'name', 47 | `${pubkey.slice(0, 16)}/uploads/image${Math.floor( 48 | Math.random() * 10000000, 49 | )}.jpg`, 50 | ); 51 | formData.append('type', 'image'); 52 | const response = await fetch(`${process.env.BASEURL}/upload`, { 53 | method: 'POST', 54 | body: formData, 55 | headers: { 56 | 'content-type': 'multipart/form-data', 57 | Authorization: `Bearer ${bearerToken}`, 58 | }, 59 | }); 60 | const data = await response.json(); 61 | if (data.error || !data.data) { 62 | throw new Error('Could not upload image') 63 | } 64 | return data.data; 65 | } 66 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './wallet'; 3 | export * from './nostr/keys'; 4 | export * from './secureStore'; 5 | export * from './time'; 6 | -------------------------------------------------------------------------------- /utils/internal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | function devLog(input) { 3 | // eslint-disable-next-line no-undef 4 | if (__DEV__) { 5 | console.log(input); 6 | } 7 | } 8 | 9 | export default devLog; 10 | -------------------------------------------------------------------------------- /utils/keys.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { getEventHash, getPublicKey, nip06, signEvent } from 'nostr-tools'; 3 | import { generateMnemonic } from '@scure/bip39'; 4 | import { wordlist } from '@scure/bip39/wordlists/english'; 5 | import { getValue } from './secureStore'; 6 | 7 | export const generateSeedphrase = () => { 8 | const mem = generateMnemonic(wordlist, 128).split(' '); 9 | return mem; 10 | }; 11 | 12 | export const mnemonicToSeed = (words) => { 13 | const privKey = nip06.privateKeyFromSeedWords(words.join(' ')); 14 | return privKey; 15 | }; 16 | 17 | export async function signRawEvent(event) { 18 | const rawEvent = event; 19 | const sk = await getValue('privKey'); 20 | const pk = getPublicKey(sk); 21 | rawEvent.pubkey = pk; 22 | rawEvent.id = getEventHash(rawEvent); 23 | rawEvent.sig = signEvent(rawEvent, sk); 24 | 25 | return rawEvent; 26 | } 27 | -------------------------------------------------------------------------------- /utils/nostr/keys.js: -------------------------------------------------------------------------------- 1 | import { bech32 } from 'bech32'; 2 | 3 | function buf2hex(buffer) { 4 | return [...new Uint8Array(buffer)] 5 | .map((x) => x.toString(16).padStart(2, '0')) 6 | .join(''); 7 | } 8 | 9 | function toByteArray(hexString) { 10 | const result = []; 11 | for (let i = 0; i < hexString.length; i += 2) { 12 | result.push(parseInt(hexString.substr(i, 2), 16)); 13 | } 14 | return result; 15 | } 16 | 17 | export const decodePubkey = (pubkey) => { 18 | try { 19 | let { prefix: hrp, words: dataPart } = bech32.decode(pubkey, 2000); 20 | let requestByteArray = bech32.fromWords(dataPart); 21 | let decoded = buf2hex(requestByteArray); 22 | return decoded; 23 | } catch (err) { 24 | console.log(err); 25 | } 26 | }; 27 | 28 | export const encodePubkey = (pubkeyInHex) => { 29 | try { 30 | const words = bech32.toWords(toByteArray(pubkeyInHex)); 31 | const encoded = bech32.encode('npub', words); 32 | return encoded; 33 | } catch (err) { 34 | console.log(err); 35 | } 36 | }; 37 | 38 | export const encodeSeckey = (skInHex) => { 39 | try { 40 | const words = bech32.toWords(toByteArray(skInHex)); 41 | const encoded = bech32.encode('nsec', words); 42 | return encoded; 43 | } catch (err) { 44 | console.log(err); 45 | } 46 | }; 47 | 48 | export const encodeNoteID = (noteIdinHey) => { 49 | try { 50 | let words = bech32.toWords(toByteArray(noteIdinHey)); 51 | const encoded = bech32.encode('note', words); 52 | return encoded; 53 | } catch (err) { 54 | console.log(err); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /utils/nostrV2/Message.js: -------------------------------------------------------------------------------- 1 | import { nip04 } from 'nostr-tools'; 2 | 3 | class Message { 4 | constructor(eventData) { 5 | this.eventData = eventData; 6 | this.id = eventData.id; 7 | this.content = eventData.content; 8 | this.created_at = eventData.created_at; 9 | this.kind = eventData.kind; 10 | this.pubkey = eventData.pubkey; 11 | this.sig = eventData.sig; 12 | this.tags = eventData.tags; 13 | this.receiver = eventData.tags[0][1]; 14 | } 15 | 16 | async save() { 17 | try { 18 | if (this.kind === 4) { 19 | // do domething 20 | } else { 21 | console.log( 22 | `Events of kind ${this.kind} are not handled by Message Class`, 23 | ); 24 | } 25 | } catch (err) { 26 | console.log(err); 27 | } 28 | } 29 | 30 | async decrypt(ownPk, sk) { 31 | const { pubkey, content, receiver, id, created_at, kind, sig, tags } = this; 32 | let decryptedText; 33 | try { 34 | console.log(content); 35 | if (pubkey === ownPk) { 36 | decryptedText = await nip04.decrypt(sk, receiver, content); 37 | } 38 | if (receiver === ownPk) { 39 | decryptedText = await nip04.decrypt(sk, pubkey, content); 40 | } 41 | } catch (err) { 42 | console.log(err); 43 | } 44 | return { 45 | id, 46 | content: decryptedText, 47 | created_at, 48 | kind, 49 | pubkey, 50 | sig, 51 | tags, 52 | receiver, 53 | }; 54 | } 55 | } 56 | 57 | export default Message; 58 | -------------------------------------------------------------------------------- /utils/nostrV2/Reply.js: -------------------------------------------------------------------------------- 1 | const checkReply = (tags) => { 2 | const eTags = tags.filter(tag => tag[0] === 'e') 3 | if (eTags.length < 2) { 4 | return undefined 5 | } 6 | const repliesTo = eTags[eTags.length - 1] 7 | return repliesTo[1] 8 | }; 9 | 10 | const checkRoot = (eTags) => { 11 | const root = eTags.filter(tag => tag[3] === 'root') 12 | return root[1] 13 | }; 14 | 15 | export class Reply { 16 | constructor(eventData) { 17 | this.eventData = eventData; 18 | this.id = eventData.id; 19 | this.content = eventData.content; 20 | this.created_at = eventData.created_at; 21 | this.kind = eventData.kind; 22 | this.pubkey = eventData.pubkey; 23 | this.sig = eventData.sig; 24 | this.tags = eventData.tags; 25 | this.eTags = eventData.tags.filter(tag => tag[0] === 'e') 26 | this.repliesTo = checkReply(eventData.tags) 27 | this.root = checkRoot(this.eTags) 28 | }}; 29 | -------------------------------------------------------------------------------- /utils/nostrV2/createEvent.js: -------------------------------------------------------------------------------- 1 | import { getEventHash, getPublicKey, signEvent } from 'nostr-tools'; 2 | 3 | /* eslint-disable import/prefer-default-export */ 4 | export async function createKind3(followedPubkeyArray, relayArray, sk) { 5 | const pk = getPublicKey(sk); 6 | const relayObject = {}; 7 | relayArray.forEach((relay) => { 8 | relayObject[relay.url] = { read: relay.read, write: relay.write }; 9 | }); 10 | const tags = followedPubkeyArray.map((key) => ['p', key]); 11 | 12 | const event = { 13 | kind: 3, 14 | created_at: Math.floor(Date.now() / 1000), 15 | tags, 16 | content: JSON.stringify(relayObject), 17 | pubkey: pk, 18 | }; 19 | 20 | event.id = getEventHash(event); 21 | event.sig = signEvent(event, sk); 22 | return event; 23 | } 24 | 25 | export async function createKind13194(sk) { 26 | const pk = getPublicKey(sk); 27 | const event = { 28 | kind: 13194, 29 | created_at: Math.floor(Date.now() / 1000), 30 | tags: [], 31 | content: 'pay_invoice', 32 | pubkey: pk, 33 | }; 34 | 35 | event.id = getEventHash(event); 36 | event.sig = signEvent(event, sk); 37 | return event; 38 | } 39 | -------------------------------------------------------------------------------- /utils/nostrV2/getEvents.js: -------------------------------------------------------------------------------- 1 | import { Note } from './Note'; 2 | import { getReadRelays, getRelayUrls, pool } from './relays'; 3 | 4 | export const getEventById = async (eventIdInHex) => { 5 | const readUrls = getRelayUrls(getReadRelays()); 6 | const event = await pool.get(readUrls, { ids: [eventIdInHex] }, { 7 | skipVerification: true, 8 | }); 9 | if (!event) { 10 | throw new Error('Event was not found!'); 11 | } 12 | const newEvent = new Note(event).save(); 13 | return newEvent; 14 | }; 15 | 16 | export default getEventById; 17 | -------------------------------------------------------------------------------- /utils/nostrV2/getUsersPosts.js: -------------------------------------------------------------------------------- 1 | // import { Event } from "./Event"; 2 | 3 | // export const getUsersPosts = async (pubkeyInHex) => { 4 | // const posts = {} 5 | // await Promise.allSettled( 6 | // connectedRelays.map((relay) => new Promise((resolve, reject) => { 7 | // let events = [] 8 | // let sub = relay.sub([ 9 | // { 10 | // authors: [pubkeyInHex], 11 | // kinds: [1], 12 | // limit: 50 13 | // }, 14 | // ]); 15 | // const timer = setTimeout(() => { 16 | // resolve(events); 17 | // return; 18 | // }, 3000) 19 | // sub.on("event", (event) => { 20 | // const newEvent = new Event(event) 21 | // const formatted = newEvent.save('return') 22 | // events.push(formatted) 23 | // console.log(formatted) 24 | // }); 25 | // sub.on("eose", () => { 26 | // clearTimeout(timer); 27 | // sub.unsub(); 28 | // resolve(events); 29 | // return; 30 | // }); 31 | // })) 32 | // ).then(result => result.map(result => result.value.map(post => {posts[post.id] = post}))); 33 | // console.log(posts) 34 | // return posts; 35 | // }; -------------------------------------------------------------------------------- /utils/nostrV2/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Event'; 2 | export * from './Note'; 3 | export * from './publishEvents'; 4 | export * from './getUserData'; 5 | export * from './relays'; 6 | export * from './createEvent' 7 | export * from './Message' 8 | -------------------------------------------------------------------------------- /utils/nostrV2/mentions.ts: -------------------------------------------------------------------------------- 1 | import { nip19 } from "nostr-tools"; 2 | import { mentionRegex } from "../../constants"; 3 | 4 | export function parseInputMentions(content: string) { 5 | const fullMentions = [...content.matchAll(mentionRegex)]; 6 | const mentionArray = fullMentions.map((mention) => ['p', mention[2]]); 7 | const parsedContent = content.replaceAll( 8 | mentionRegex, 9 | (m, g1, g2) => `nostr:${nip19.npubEncode(g2)}`, 10 | ); 11 | return {mentionArray, parsedContent}; 12 | } -------------------------------------------------------------------------------- /utils/nostrV2/relayPool.js: -------------------------------------------------------------------------------- 1 | import { SimplePool } from "nostr-tools"; 2 | import { store } from "../../store/store"; 3 | import { addRelays } from "../../features/userSlice"; 4 | 5 | export let connectedRelayPool; 6 | 7 | export let urls; 8 | 9 | export const pool = new SimplePool(); 10 | 11 | export const initRelayPool = async () => { 12 | const response = await fetch(process.env.BASEURL + "/relays"); 13 | const data = await response.json(); 14 | const urlObj = data.result; 15 | urls = urlObj.map((obj) => obj.relay); 16 | const relayObj = {}; 17 | urls.forEach((relay) => (relayObj[relay] = { read: true, write: true })); 18 | store.dispatch(addRelays(relayObj)); 19 | connectedRelayPool = await Promise.allSettled( 20 | urls.map((url) => { 21 | return new Promise(async (resolve, reject) => { 22 | try { 23 | const relay = await pool.ensureRelay(url); 24 | resolve(relay); 25 | } catch (e) { 26 | console.log(e); 27 | reject(); 28 | } 29 | }); 30 | }) 31 | ).then((result) => 32 | result 33 | .filter((promise) => promise.status === "fulfilled") 34 | .map((promise) => promise.value) 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /utils/nostrV2/tags.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "nostr-tools"; 2 | 3 | export function getTagValue(event: Event, tagIdentifier: string ,valueIndex: number = 1) { 4 | const tag = event.tags.find(tag => tag[0] === tagIdentifier); 5 | if(!tag) { 6 | return undefined 7 | } 8 | return tag[valueIndex] 9 | } -------------------------------------------------------------------------------- /utils/secureStore.js: -------------------------------------------------------------------------------- 1 | import * as SecureStore from 'expo-secure-store'; 2 | 3 | const saveValue = async (key, value) => { 4 | await SecureStore.setItemAsync(key, value); 5 | }; 6 | 7 | const getValue = async (key) => { 8 | const result = await SecureStore.getItemAsync(key); 9 | if (result) { 10 | return result; 11 | } 12 | return undefined; 13 | }; 14 | 15 | const deleteValue = async (key) => { 16 | await SecureStore.deleteItemAsync(key); 17 | }; 18 | 19 | async function getPrivateKeyAsync() { 20 | const sk = await getValue('privKey'); 21 | return sk; 22 | } 23 | 24 | export { saveValue, getValue, deleteValue, getPrivateKeyAsync }; 25 | -------------------------------------------------------------------------------- /utils/time.js: -------------------------------------------------------------------------------- 1 | export function getAge(timestamp) { 2 | const now = new Date(); 3 | const timePassedInMins = Math.floor( 4 | (now - new Date(timestamp * 1000)) / 1000 / 60, 5 | ); 6 | 7 | if (timePassedInMins < 60) { 8 | return `${timePassedInMins}min ago`; 9 | } 10 | if (timePassedInMins >= 60 && timePassedInMins < 1440) { 11 | return `${Math.floor(timePassedInMins / 60)}h ago`; 12 | } 13 | if (timePassedInMins >= 1440 && timePassedInMins < 10080) { 14 | return `${Math.floor(timePassedInMins / 1440)}d ago`; 15 | } 16 | return `on ${new Date(timestamp * 1000).toLocaleDateString()}`; 17 | } 18 | 19 | export function getHourAndMinute(unixInSeconds) { 20 | const timePassedInMins = Math.floor( 21 | (now - new Date(unixInSeconds * 1000)) / 1000 / 60, 22 | ); 23 | if (timePassedInMins > 10080) 24 | return `on ${new Date(timestamp * 1000).toLocaleDateString()}`; 25 | const now = new Date(); 26 | const date = new Date(unixInSeconds * 1000); 27 | let day = date.getDay(); 28 | if (now.getDay() - day === 0) day = 7; 29 | const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Today']; 30 | return `${dayNames[day]} ${date.getHours()}:${date.getMinutes()<10?'0'+date.getMinutes() :date.getMinutes()}`; 31 | } 32 | -------------------------------------------------------------------------------- /views/home/ZapListModal.js: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | import React from "react"; 3 | import { FlashList } from "@shopify/flash-list"; 4 | import { colors } from "../../styles"; 5 | 6 | const ZapListModal = ({route}) => { 7 | const zaps = route?.params?.zaps 8 | return ( 9 | 12 | 13 | {item.amount}}/> 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default ZapListModal; 20 | -------------------------------------------------------------------------------- /views/wallet/WalletInvoiceScreen.jsx: -------------------------------------------------------------------------------- 1 | import * as Clipboard from 'expo-clipboard'; 2 | import React from 'react'; 3 | import Toast from 'react-native-root-toast'; 4 | import { View, Text, StyleSheet, Pressable } from 'react-native'; 5 | import QRCode from 'react-qr-code'; 6 | import CustomButton from '../../components/CustomButton'; 7 | import globalStyles from '../../styles/globalStyles'; 8 | import { SuccessToast } from '../../components'; 9 | 10 | const styles = StyleSheet.create({ 11 | qrContainer: { 12 | padding: 10, 13 | backgroundColor: 'white', 14 | borderRadius: 4, 15 | }, 16 | }); 17 | 18 | const WalletInvoiceScreen = ({ route, navigation }) => { 19 | const { invoice } = route.params; 20 | 21 | const copyHandler = async () => { 22 | await Clipboard.setStringAsync(invoice); 23 | Toast.show(, { 24 | duration: Toast.durations.SHORT, 25 | position: -100, 26 | backgroundColor: 'green', 27 | }); 28 | }; 29 | return ( 30 | 36 | 37 | 38 | Your Invoice 39 | 40 | 41 | 42 | 48 | Click QR-Code to copy... 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default WalletInvoiceScreen; 57 | -------------------------------------------------------------------------------- /views/wallet/WalletTransactionScreen.jsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import React from 'react'; 3 | import { FlashList } from '@shopify/flash-list'; 4 | import { globalStyles } from '../../styles'; 5 | import useTransactionHistory from '../../features/wallet/hooks/useTransactionHistory'; 6 | import { InTxItem, OutTxItem } from '../../features/wallet/components'; 7 | import PressableIcon from '../../components/PressableIcon'; 8 | 9 | const WalletTransactionScreen = () => { 10 | const txHistory = useTransactionHistory(); 11 | const renderTx = ({ item }) => { 12 | if (item.type === 'in') { 13 | return ; 14 | } 15 | return ; 16 | }; 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default WalletTransactionScreen; 31 | --------------------------------------------------------------------------------