├── .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 |
--------------------------------------------------------------------------------