├── .dockerignore
├── .drone.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .vscode
├── extensions.json
└── settings.json
├── .yarn
├── releases
│ └── yarn-4.1.1.cjs
└── sdks
│ ├── eslint
│ ├── bin
│ │ └── eslint.js
│ ├── lib
│ │ ├── api.js
│ │ └── unsupported-api.js
│ └── package.json
│ ├── integrations.yml
│ ├── prettier
│ ├── bin
│ │ └── prettier.cjs
│ ├── index.cjs
│ └── package.json
│ └── typescript
│ ├── bin
│ ├── tsc
│ └── tsserver
│ ├── lib
│ ├── tsc.js
│ ├── tsserver.js
│ ├── tsserverlibrary.js
│ └── typescript.js
│ └── package.json
├── .yarnrc
├── .yarnrc.yml
├── Dockerfile
├── Dockerfile.prebuilt
├── LICENSE
├── README.md
├── crowdin.yml
├── dev-docs
└── query.md
├── docker
└── nginx.conf
├── functions
├── _middleware.ts
└── tsconfig.json
├── maintainers.yaml
├── nap.yaml
├── package.json
├── packages
├── app
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── babel.config.json
│ ├── config
│ │ ├── README.md
│ │ ├── default.json
│ │ ├── iris.json
│ │ ├── meku.json
│ │ ├── nostr.json
│ │ ├── snort.json
│ │ └── soloco.json
│ ├── custom.d.ts
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── iris
│ │ │ ├── .well-known
│ │ │ │ └── assetlinks.json
│ │ │ ├── _headers
│ │ │ ├── favicon.png
│ │ │ ├── img
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── icon128.png
│ │ │ │ ├── irisconnects.png
│ │ │ │ ├── maskable_icon.png
│ │ │ │ └── maskable_icon_x192.png
│ │ │ ├── manifest.json
│ │ │ └── robots.txt
│ │ ├── nostr
│ │ │ ├── _headers
│ │ │ ├── favicon.png
│ │ │ ├── img
│ │ │ │ └── apple-touch-icon.png
│ │ │ └── nostr.jpg
│ │ ├── phoenix
│ │ │ ├── .well-known
│ │ │ │ ├── apple-app-site-association
│ │ │ │ └── assetlinks.json
│ │ │ ├── _headers
│ │ │ ├── favicon.png
│ │ │ ├── img
│ │ │ │ └── apple-touch-icon.png
│ │ │ ├── logo.svg
│ │ │ ├── logo_256.png
│ │ │ ├── manifest.json
│ │ │ └── robots.txt
│ │ └── snort
│ │ │ ├── .well-known
│ │ │ ├── apple-app-site-association
│ │ │ └── assetlinks.json
│ │ │ ├── _headers
│ │ │ ├── favicon.png
│ │ │ ├── img
│ │ │ └── apple-touch-icon.png
│ │ │ ├── manifest.json
│ │ │ ├── nostrich_256.png
│ │ │ ├── nostrich_512.png
│ │ │ ├── nostrich_orig.jpeg
│ │ │ └── robots.txt
│ ├── src
│ │ ├── Cache
│ │ │ ├── ChatCache.ts
│ │ │ ├── CommunityLeadersStore.tsx
│ │ │ ├── EventCacheWorker.ts
│ │ │ ├── GiftWrapCache.ts
│ │ │ ├── ProfileWorkerCache.ts
│ │ │ ├── RefreshFeedCache.ts
│ │ │ ├── TextCache.tsx
│ │ │ ├── UserFollowsWorker.ts
│ │ │ └── index.ts
│ │ ├── Components
│ │ │ ├── Button
│ │ │ │ ├── AsyncButton.css
│ │ │ │ ├── AsyncButton.tsx
│ │ │ │ ├── AsyncIcon.tsx
│ │ │ │ ├── BackButton.css
│ │ │ │ ├── BackButton.tsx
│ │ │ │ ├── CloseButton.tsx
│ │ │ │ ├── IconButton.tsx
│ │ │ │ ├── LogoutButton.tsx
│ │ │ │ └── NavLink.tsx
│ │ │ ├── Collapsed.tsx
│ │ │ ├── CommunityLeaders
│ │ │ │ ├── Award.tsx
│ │ │ │ └── LeaderBadge.tsx
│ │ │ ├── Copy
│ │ │ │ ├── Copy.css
│ │ │ │ └── Copy.tsx
│ │ │ ├── Embed
│ │ │ │ ├── AppleMusicEmbed.tsx
│ │ │ │ ├── CashuNuts.css
│ │ │ │ ├── CashuNuts.tsx
│ │ │ │ ├── GenericPlayer.tsx
│ │ │ │ ├── Hashtag.css
│ │ │ │ ├── Hashtag.tsx
│ │ │ │ ├── HyperText.tsx
│ │ │ │ ├── Invoice.css
│ │ │ │ ├── Invoice.tsx
│ │ │ │ ├── LinkPreview.css
│ │ │ │ ├── LinkPreview.tsx
│ │ │ │ ├── MagnetLink.tsx
│ │ │ │ ├── MediaElement.tsx
│ │ │ │ ├── Mention.tsx
│ │ │ │ ├── MixCloudEmbed.tsx
│ │ │ │ ├── NostrLink.tsx
│ │ │ │ ├── NostrNestsEmbed.tsx
│ │ │ │ ├── PubkeyList.tsx
│ │ │ │ ├── SoundCloudEmded.tsx
│ │ │ │ ├── SpotifyEmbed.tsx
│ │ │ │ ├── TidalEmbed.tsx
│ │ │ │ ├── TwitchEmbed.tsx
│ │ │ │ ├── WavlakeEmbed.tsx
│ │ │ │ ├── YoutubeEmbed.tsx
│ │ │ │ ├── ZapstrEmbed.css
│ │ │ │ └── ZapstrEmbed.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── ErrorOrOffline.tsx
│ │ │ ├── Event
│ │ │ │ ├── Application.tsx
│ │ │ │ ├── Create
│ │ │ │ │ ├── NoteCreator.tsx
│ │ │ │ │ ├── NoteCreatorButton.tsx
│ │ │ │ │ ├── OkResponseRow.tsx
│ │ │ │ │ └── util.ts
│ │ │ │ ├── EventComponent.css
│ │ │ │ ├── EventComponent.tsx
│ │ │ │ ├── FileUpload.tsx
│ │ │ │ ├── HiddenNote.tsx
│ │ │ │ ├── LoadMore.tsx
│ │ │ │ ├── LongFormText.css
│ │ │ │ ├── LongFormText.tsx
│ │ │ │ ├── Markdown.css
│ │ │ │ ├── Markdown.tsx
│ │ │ │ ├── NostrFileHeader.tsx
│ │ │ │ ├── Note
│ │ │ │ │ ├── ClientTag.tsx
│ │ │ │ │ ├── Note.tsx
│ │ │ │ │ ├── NoteAppHandler.tsx
│ │ │ │ │ ├── NoteContextMenu.tsx
│ │ │ │ │ ├── NoteFooter
│ │ │ │ │ │ ├── AsyncFooterIcon.tsx
│ │ │ │ │ │ ├── FooterZapButton.tsx
│ │ │ │ │ │ ├── LikeButton.tsx
│ │ │ │ │ │ ├── NoteFooter.tsx
│ │ │ │ │ │ ├── PowIcon.tsx
│ │ │ │ │ │ ├── ReplyButton.tsx
│ │ │ │ │ │ ├── RepostButton.tsx
│ │ │ │ │ │ └── ZapperQueue.tsx
│ │ │ │ │ ├── NoteGhost.tsx
│ │ │ │ │ ├── NoteHeader.tsx
│ │ │ │ │ ├── NoteQuote.tsx
│ │ │ │ │ ├── NoteText.tsx
│ │ │ │ │ ├── NoteTime.tsx
│ │ │ │ │ ├── ReactionsModal.css
│ │ │ │ │ ├── ReactionsModal.tsx
│ │ │ │ │ ├── ReplyTag.tsx
│ │ │ │ │ ├── TranslationInfo.tsx
│ │ │ │ │ └── types.tsx
│ │ │ │ ├── NoteReaction.css
│ │ │ │ ├── NoteReaction.tsx
│ │ │ │ ├── Poll.tsx
│ │ │ │ ├── Reveal.tsx
│ │ │ │ ├── RevealMedia.tsx
│ │ │ │ ├── Thread
│ │ │ │ │ ├── Divider.tsx
│ │ │ │ │ ├── Subthread.tsx
│ │ │ │ │ ├── Thread.css
│ │ │ │ │ ├── Thread.tsx
│ │ │ │ │ ├── ThreadNote.tsx
│ │ │ │ │ ├── ThreadRoute.tsx
│ │ │ │ │ ├── TierThree.tsx
│ │ │ │ │ ├── TierTwo.tsx
│ │ │ │ │ └── util.ts
│ │ │ │ ├── Zap.css
│ │ │ │ ├── Zap.tsx
│ │ │ │ ├── ZapButton.css
│ │ │ │ ├── ZapButton.tsx
│ │ │ │ ├── ZapGoal.css
│ │ │ │ ├── ZapGoal.tsx
│ │ │ │ └── ZapsSummary.tsx
│ │ │ ├── Feed
│ │ │ │ ├── DisplayAsSelector.tsx
│ │ │ │ ├── Generic.tsx
│ │ │ │ ├── ImageGridItem.tsx
│ │ │ │ ├── LoadMore.tsx
│ │ │ │ ├── RootTabItems.tsx
│ │ │ │ ├── RootTabs.css
│ │ │ │ ├── RootTabs.tsx
│ │ │ │ ├── Timeline.css
│ │ │ │ ├── Timeline.tsx
│ │ │ │ ├── TimelineChunk.tsx
│ │ │ │ ├── TimelineFollows.tsx
│ │ │ │ ├── TimelineFragment.tsx
│ │ │ │ ├── TimelineRenderer.tsx
│ │ │ │ └── UsersFeed.tsx
│ │ │ ├── Icons
│ │ │ │ ├── Alby.tsx
│ │ │ │ ├── BlueWallet.tsx
│ │ │ │ ├── Cashu.tsx
│ │ │ │ ├── ECash.tsx
│ │ │ │ ├── Icon.tsx
│ │ │ │ ├── NWC.tsx
│ │ │ │ ├── Nostrich.tsx
│ │ │ │ ├── Spinner.css
│ │ │ │ ├── Spinner.tsx
│ │ │ │ ├── Toggle.css
│ │ │ │ ├── Toggle.tsx
│ │ │ │ └── icons.svg
│ │ │ ├── IntlProvider
│ │ │ │ ├── IntlProvider.tsx
│ │ │ │ ├── IntlProviderUtils.tsx
│ │ │ │ ├── langStore.tsx
│ │ │ │ └── useLocale.tsx
│ │ │ ├── Invite.tsx
│ │ │ ├── IrisAccount
│ │ │ │ ├── AccountName.tsx
│ │ │ │ ├── ActiveAccount.tsx
│ │ │ │ ├── IrisAccount.tsx
│ │ │ │ └── ReservedAccount.tsx
│ │ │ ├── LineChart.tsx
│ │ │ ├── LiveStream
│ │ │ │ ├── LiveEvent.tsx
│ │ │ │ ├── LiveStreams.tsx
│ │ │ │ ├── VU.tsx
│ │ │ │ ├── livekit.tsx
│ │ │ │ └── nests-participants.tsx
│ │ │ ├── Modal
│ │ │ │ ├── Modal.css
│ │ │ │ └── Modal.tsx
│ │ │ ├── Nip5Service.tsx
│ │ │ ├── Offline.tsx
│ │ │ ├── PageSpinner.tsx
│ │ │ ├── PinPrompt
│ │ │ │ ├── PinPrompt.css
│ │ │ │ └── PinPrompt.tsx
│ │ │ ├── Progress
│ │ │ │ ├── Progress.css
│ │ │ │ └── Progress.tsx
│ │ │ ├── ProxyImg.tsx
│ │ │ ├── QrCode.tsx
│ │ │ ├── ReBroadcaster.tsx
│ │ │ ├── Relay
│ │ │ │ ├── Relay.tsx
│ │ │ │ ├── RelaysMetadata.tsx
│ │ │ │ ├── paid.tsx
│ │ │ │ ├── permissions.tsx
│ │ │ │ ├── software.tsx
│ │ │ │ ├── status-label.tsx
│ │ │ │ ├── uptime-label.tsx
│ │ │ │ └── uptime.tsx
│ │ │ ├── Review.tsx
│ │ │ ├── RightWidgets
│ │ │ │ ├── articles.tsx
│ │ │ │ ├── base.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── invite-friends.tsx
│ │ │ │ └── mini-stream.tsx
│ │ │ ├── ScrollToTop.tsx
│ │ │ ├── SearchBox
│ │ │ │ ├── SearchBox.css
│ │ │ │ └── SearchBox.tsx
│ │ │ ├── Spotlight
│ │ │ │ ├── SpotlightMedia.tsx
│ │ │ │ └── SpotlightThreadModal.tsx
│ │ │ ├── SuggestedProfiles.tsx
│ │ │ ├── TabSelectors
│ │ │ │ └── TabSelectors.tsx
│ │ │ ├── Tasks
│ │ │ │ ├── BackupKey.tsx
│ │ │ │ ├── DonateTask.tsx
│ │ │ │ ├── FollowMorePeople.tsx
│ │ │ │ ├── Nip5Task.tsx
│ │ │ │ ├── NoticeZapPool.tsx
│ │ │ │ ├── RenewSubscription.tsx
│ │ │ │ ├── TaskList.css
│ │ │ │ ├── TaskList.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Text
│ │ │ │ ├── DisableMedia.tsx
│ │ │ │ ├── HighlightedText.tsx
│ │ │ │ ├── Text.css
│ │ │ │ ├── Text.tsx
│ │ │ │ └── const.ts
│ │ │ ├── Textarea
│ │ │ │ ├── Textarea.css
│ │ │ │ └── Textarea.tsx
│ │ │ ├── Toaster
│ │ │ │ ├── Toaster.css
│ │ │ │ └── Toaster.tsx
│ │ │ ├── Trending
│ │ │ │ ├── ShortNote.tsx
│ │ │ │ ├── TrendingHashtags.tsx
│ │ │ │ ├── TrendingPosts.tsx
│ │ │ │ └── TrendingUsers.tsx
│ │ │ ├── Upload
│ │ │ │ └── file-picker.tsx
│ │ │ ├── User
│ │ │ │ ├── AnimalName.ts
│ │ │ │ ├── Avatar.tsx
│ │ │ │ ├── AvatarEditor.css
│ │ │ │ ├── AvatarEditor.tsx
│ │ │ │ ├── AvatarGroup.tsx
│ │ │ │ ├── BadgeList.css
│ │ │ │ ├── BadgeList.tsx
│ │ │ │ ├── Bookmarks.tsx
│ │ │ │ ├── Debug.tsx
│ │ │ │ ├── DisplayName.css
│ │ │ │ ├── DisplayName.tsx
│ │ │ │ ├── FollowButton.tsx
│ │ │ │ ├── FollowDistanceIndicator.tsx
│ │ │ │ ├── FollowListBase.tsx
│ │ │ │ ├── FollowedBy.tsx
│ │ │ │ ├── Following.css
│ │ │ │ ├── Following.tsx
│ │ │ │ ├── FollowsYou.css
│ │ │ │ ├── FollowsYou.tsx
│ │ │ │ ├── MuteButton.tsx
│ │ │ │ ├── MutedList.tsx
│ │ │ │ ├── Nip05.css
│ │ │ │ ├── Nip05.tsx
│ │ │ │ ├── NoteToSelf.css
│ │ │ │ ├── NoteToSelf.tsx
│ │ │ │ ├── ProfileCard.css
│ │ │ │ ├── ProfileCard.tsx
│ │ │ │ ├── ProfileImage.tsx
│ │ │ │ ├── ProfileLink.tsx
│ │ │ │ ├── ProfilePreview.tsx
│ │ │ │ ├── UserWebsiteLink.css
│ │ │ │ ├── UserWebsiteLink.tsx
│ │ │ │ └── Username.tsx
│ │ │ ├── WarningNotice
│ │ │ │ ├── WarningNotice.css
│ │ │ │ └── WarningNotice.tsx
│ │ │ ├── ZapModal
│ │ │ │ ├── SuccessAction.tsx
│ │ │ │ ├── ZapModal.css
│ │ │ │ ├── ZapModal.tsx
│ │ │ │ ├── ZapModalInput.tsx
│ │ │ │ ├── ZapModalInvoice.tsx
│ │ │ │ ├── ZapModalTitle.tsx
│ │ │ │ ├── ZapType.tsx
│ │ │ │ └── ZapTypeSelector.tsx
│ │ │ ├── flyout.tsx
│ │ │ ├── kind-name.tsx
│ │ │ ├── messages.ts
│ │ │ ├── nip.tsx
│ │ │ └── zap-amount.tsx
│ │ ├── Db
│ │ │ ├── FuzzySearch.ts
│ │ │ └── index.ts
│ │ ├── External
│ │ │ ├── NostrBand.ts
│ │ │ ├── NostrServices.ts
│ │ │ ├── SemisolDev.ts
│ │ │ ├── SnortApi.ts
│ │ │ └── index.ts
│ │ ├── Feed
│ │ │ ├── ArticlesFeed.ts
│ │ │ ├── BadgesFeed.ts
│ │ │ ├── FollowersFeed.ts
│ │ │ ├── FollowsFeed.ts
│ │ │ ├── HashtagsFeed.ts
│ │ │ ├── LoginFeed.ts
│ │ │ ├── RelayState.ts
│ │ │ ├── RelaysFeed.tsx
│ │ │ ├── StatusFeed.ts
│ │ │ ├── ThreadFeed.ts
│ │ │ ├── TimelineFeed.ts
│ │ │ ├── WorkerRelayView.ts
│ │ │ └── ZapsFeed.ts
│ │ ├── Hooks
│ │ │ ├── useAppHandler.ts
│ │ │ ├── useCachedFetch.ts
│ │ │ ├── useCloseRelays.ts
│ │ │ ├── useCommunityLeaders.tsx
│ │ │ ├── useCopy.ts
│ │ │ ├── useDiscoverMediaServers.ts
│ │ │ ├── useEventPublisher.tsx
│ │ │ ├── useFollowControls.ts
│ │ │ ├── useHistoryState.tsx
│ │ │ ├── useHorizontalScroll.tsx
│ │ │ ├── useImgProxy.ts
│ │ │ ├── useInteractionCache.tsx
│ │ │ ├── useKeyboardShortcut.ts
│ │ │ ├── useLists.tsx
│ │ │ ├── useLiveStreams.ts
│ │ │ ├── useLoading.tsx
│ │ │ ├── useLogin.tsx
│ │ │ ├── useLoginHandler.tsx
│ │ │ ├── useLoginRelays.tsx
│ │ │ ├── useMediaServerList.ts
│ │ │ ├── useModeration.tsx
│ │ │ ├── usePageDimensions.tsx
│ │ │ ├── usePreferences.ts
│ │ │ ├── useProfileLink.ts
│ │ │ ├── useProfileSearch.tsx
│ │ │ ├── useRates.tsx
│ │ │ ├── useRelays.tsx
│ │ │ ├── useTextTransformCache.tsx
│ │ │ ├── useTheme.tsx
│ │ │ ├── useTimelineChunks.ts
│ │ │ ├── useTimelineWindow.tsx
│ │ │ ├── useWindowSize.ts
│ │ │ └── useWoT.ts
│ │ ├── Pages
│ │ │ ├── About.tsx
│ │ │ ├── CacheDebug.tsx
│ │ │ ├── Deck
│ │ │ │ ├── Articles.tsx
│ │ │ │ ├── Columns.tsx
│ │ │ │ ├── Deck.css
│ │ │ │ └── DeckLayout.tsx
│ │ │ ├── Discover.tsx
│ │ │ ├── Donate
│ │ │ │ ├── DonatePage.tsx
│ │ │ │ ├── ZapPoolDonateSection.tsx
│ │ │ │ └── const.ts
│ │ │ ├── ErrorPage.tsx
│ │ │ ├── FreeNostrAddressPage.tsx
│ │ │ ├── HashTagsPage.tsx
│ │ │ ├── HelpPage.tsx
│ │ │ ├── Layout
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── HasNotificationsMarker.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Layout.css
│ │ │ │ ├── LogoHeader.tsx
│ │ │ │ ├── NavSidebar.tsx
│ │ │ │ ├── NotificationsHeader.tsx
│ │ │ │ ├── ProfileMenu.tsx
│ │ │ │ ├── RightColumn.tsx
│ │ │ │ ├── WalletBalance.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ListFeedPage.tsx
│ │ │ ├── Messages
│ │ │ │ ├── ChatParticipant.tsx
│ │ │ │ ├── DM.css
│ │ │ │ ├── DM.tsx
│ │ │ │ ├── DmWindow.tsx
│ │ │ │ ├── MessagesPage.tsx
│ │ │ │ ├── NewChatWindow.tsx
│ │ │ │ ├── UnreadCount.css
│ │ │ │ ├── UnreadCount.tsx
│ │ │ │ └── WriteMessage.tsx
│ │ │ ├── NostrAddressPage.tsx
│ │ │ ├── NostrLinkHandler.tsx
│ │ │ ├── Notifications
│ │ │ │ ├── NotificationGroup.tsx
│ │ │ │ ├── Notifications.css
│ │ │ │ ├── Notifications.tsx
│ │ │ │ ├── getNotificationContext.tsx
│ │ │ │ └── notificationContext.tsx
│ │ │ ├── Profile
│ │ │ │ ├── AvatarSection.tsx
│ │ │ │ ├── MusicStatus.tsx
│ │ │ │ ├── ProfileDetails.tsx
│ │ │ │ ├── ProfilePage.css
│ │ │ │ ├── ProfilePage.tsx
│ │ │ │ ├── ProfileTabComponents.tsx
│ │ │ │ ├── ProfileTabSelectors.tsx
│ │ │ │ └── ProfileTabType.tsx
│ │ │ ├── Root
│ │ │ │ ├── ConversationsTab.tsx
│ │ │ │ ├── DefaultTab.tsx
│ │ │ │ ├── FollowSets.tsx
│ │ │ │ ├── FollowedByFriendsTab.tsx
│ │ │ │ ├── ForYouTab.tsx
│ │ │ │ ├── Media.tsx
│ │ │ │ ├── NotesTab.tsx
│ │ │ │ ├── RootRoutes.tsx
│ │ │ │ ├── RootTabRoutes.tsx
│ │ │ │ └── TagsTab.tsx
│ │ │ ├── SearchPage.tsx
│ │ │ ├── TopicsPage.tsx
│ │ │ ├── ZapPool
│ │ │ │ ├── ZapPool.css
│ │ │ │ ├── ZapPool.tsx
│ │ │ │ ├── ZapPoolPageInner.tsx
│ │ │ │ └── ZapPoolTarget.tsx
│ │ │ ├── messages.ts
│ │ │ ├── onboarding
│ │ │ │ ├── discover.tsx
│ │ │ │ ├── fixedModeration.tsx
│ │ │ │ ├── fixedTopics.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.tsx
│ │ │ │ ├── moderation.tsx
│ │ │ │ ├── profile.tsx
│ │ │ │ ├── start.tsx
│ │ │ │ └── topics.tsx
│ │ │ ├── settings
│ │ │ │ ├── Accounts.tsx
│ │ │ │ ├── Cache.tsx
│ │ │ │ ├── Keys.css
│ │ │ │ ├── Keys.tsx
│ │ │ │ ├── Menu
│ │ │ │ │ ├── Menu.tsx
│ │ │ │ │ └── SettingsMenuComponent.tsx
│ │ │ │ ├── Moderation.tsx
│ │ │ │ ├── Notifications.tsx
│ │ │ │ ├── Preferences.css
│ │ │ │ ├── Preferences.tsx
│ │ │ │ ├── Profile.css
│ │ │ │ ├── Profile.tsx
│ │ │ │ ├── Referrals.tsx
│ │ │ │ ├── RelayInfo.tsx
│ │ │ │ ├── Relays.tsx
│ │ │ │ ├── Routes.tsx
│ │ │ │ ├── SnortNostrAddressService.tsx
│ │ │ │ ├── WalletSettings.tsx
│ │ │ │ ├── handle
│ │ │ │ │ ├── LNAddress.tsx
│ │ │ │ │ ├── ListHandles.tsx
│ │ │ │ │ ├── Manage.tsx
│ │ │ │ │ ├── TransferHandle.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── media-settings.tsx
│ │ │ │ ├── messages.ts
│ │ │ │ ├── relays
│ │ │ │ │ └── discover.tsx
│ │ │ │ ├── saveRelays.tsx
│ │ │ │ ├── tools
│ │ │ │ │ ├── follows-relay-health.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── prune-follows.tsx
│ │ │ │ │ └── sync-account.tsx
│ │ │ │ └── wallet
│ │ │ │ │ ├── Alby.tsx
│ │ │ │ │ ├── LNDHub.tsx
│ │ │ │ │ ├── NWC.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── utils.ts
│ │ │ ├── subscribe
│ │ │ │ ├── ManageSubscription.tsx
│ │ │ │ ├── RenewSub.tsx
│ │ │ │ ├── SubscriptionCard.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.tsx
│ │ │ │ └── utils.tsx
│ │ │ └── wallet
│ │ │ │ ├── index.tsx
│ │ │ │ ├── price-chart.tsx
│ │ │ │ ├── receive.tsx
│ │ │ │ └── send.tsx
│ │ ├── State
│ │ │ └── NoteCreator.ts
│ │ ├── Utils
│ │ │ ├── Const.ts
│ │ │ ├── Login
│ │ │ │ ├── Functions.ts
│ │ │ │ ├── LoginSession.ts
│ │ │ │ ├── MultiAccountStore.ts
│ │ │ │ ├── Nip7OsSigner.ts
│ │ │ │ ├── Preferences.ts
│ │ │ │ └── index.ts
│ │ │ ├── Nip05
│ │ │ │ ├── ServiceProvider.ts
│ │ │ │ ├── SnortServiceProvider.ts
│ │ │ │ └── Verifier.ts
│ │ │ ├── Notifications.ts
│ │ │ ├── Number.ts
│ │ │ ├── Subscription
│ │ │ │ └── index.ts
│ │ │ ├── Thread
│ │ │ │ ├── ChainKey.tsx
│ │ │ │ ├── ThreadContext.tsx
│ │ │ │ └── ThreadContextWrapper.tsx
│ │ │ ├── Upload
│ │ │ │ ├── blossom.ts
│ │ │ │ └── index.ts
│ │ │ ├── Utils.test.ts
│ │ │ ├── ZapPoolController.ts
│ │ │ ├── emoji-search.ts
│ │ │ ├── getEventMedia.ts
│ │ │ ├── index.ts
│ │ │ ├── nip6.ts
│ │ │ ├── stream.ts
│ │ │ └── wasm.ts
│ │ ├── Wallet
│ │ │ └── index.ts
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2
│ │ │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2
│ │ │ │ └── inter.css
│ │ │ └── img
│ │ │ │ ├── cashu.png
│ │ │ │ ├── lnd-logo.png
│ │ │ │ ├── nostrich.webp
│ │ │ │ └── telegram.svg
│ │ ├── bench.html
│ │ ├── benchmarks.ts
│ │ ├── chat
│ │ │ ├── index.ts
│ │ │ └── nip17.ts
│ │ ├── hug.json
│ │ ├── index.css
│ │ ├── index.tsx
│ │ ├── lang.json
│ │ ├── service-worker.ts
│ │ ├── setupTests.ts
│ │ ├── system.ts
│ │ ├── translations
│ │ │ ├── af_ZA.json
│ │ │ ├── ar_SA.json
│ │ │ ├── az_AZ.json
│ │ │ ├── ca_ES.json
│ │ │ ├── cs_CZ.json
│ │ │ ├── da_DK.json
│ │ │ ├── de_DE.json
│ │ │ ├── el_GR.json
│ │ │ ├── en.json
│ │ │ ├── es_ES.json
│ │ │ ├── fa_IR.json
│ │ │ ├── fi_FI.json
│ │ │ ├── fr_FR.json
│ │ │ ├── he_IL.json
│ │ │ ├── hr_HR.json
│ │ │ ├── hu_HU.json
│ │ │ ├── id_ID.json
│ │ │ ├── it_IT.json
│ │ │ ├── ja_JP.json
│ │ │ ├── ko_KR.json
│ │ │ ├── ms_MY.json
│ │ │ ├── nl_NL.json
│ │ │ ├── no_NO.json
│ │ │ ├── pa_IN.json
│ │ │ ├── pl_PL.json
│ │ │ ├── pt_BR.json
│ │ │ ├── pt_PT.json
│ │ │ ├── ro_RO.json
│ │ │ ├── ru_RU.json
│ │ │ ├── sr_SP.json
│ │ │ ├── sv_SE.json
│ │ │ ├── sw_KE.json
│ │ │ ├── ta_IN.json
│ │ │ ├── th_TH.json
│ │ │ ├── tr_TR.json
│ │ │ ├── uk_UA.json
│ │ │ ├── vi_VN.json
│ │ │ ├── zh_CN.json
│ │ │ └── zh_TW.json
│ │ ├── tz.json
│ │ └── wdyr.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.ts
├── bot
│ ├── README.md
│ ├── example
│ │ └── simple.ts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── shared
│ ├── package.json
│ ├── src
│ │ ├── LRUSet.ts
│ │ ├── SortedMap
│ │ │ ├── SortedMap.test.ts
│ │ │ └── SortedMap.ts
│ │ ├── const.ts
│ │ ├── custom.d.ts
│ │ ├── dexie-like.ts
│ │ ├── external-store.ts
│ │ ├── feed-cache.ts
│ │ ├── index.ts
│ │ ├── invoices.ts
│ │ ├── lnurl.ts
│ │ ├── utils.ts
│ │ └── work-queue.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── system-react
│ ├── README.md
│ ├── example
│ │ └── example.tsx
│ ├── package.json
│ ├── src
│ │ ├── context.tsx
│ │ ├── index.ts
│ │ ├── useEventFeed.ts
│ │ ├── useEventReactions.tsx
│ │ ├── useReactions.ts
│ │ ├── useRequestBuilder.tsx
│ │ ├── useSystemState.tsx
│ │ ├── useUserProfile.ts
│ │ └── useUserSearch.tsx
│ ├── tsconfig.json
│ └── typedoc.json
├── system-svelte
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── request-builder.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── system-wasm
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ ├── benches
│ │ └── basic.rs
│ ├── package.json
│ ├── pkg
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── system_wasm.d.ts
│ │ ├── system_wasm.js
│ │ ├── system_wasm_bg.js
│ │ ├── system_wasm_bg.wasm
│ │ └── system_wasm_bg.wasm.d.ts
│ ├── src
│ │ ├── diff.rs
│ │ ├── filter.rs
│ │ ├── lib.rs
│ │ ├── merge.rs
│ │ └── pow.rs
│ ├── system-query.iml
│ └── typedoc.json
├── system-web
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── system
│ ├── .npmignore
│ ├── README.md
│ ├── examples
│ │ └── simple.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── background-loader.ts
│ │ ├── cache-relay.ts
│ │ ├── cache
│ │ │ ├── events.ts
│ │ │ ├── index.ts
│ │ │ ├── relay-metric.ts
│ │ │ ├── user-follows-lists.ts
│ │ │ ├── user-metadata.ts
│ │ │ └── user-relays.ts
│ │ ├── connection-cache-relay.ts
│ │ ├── connection-pool.ts
│ │ ├── connection-stats.ts
│ │ ├── connection.ts
│ │ ├── const.ts
│ │ ├── encryption
│ │ │ ├── index.ts
│ │ │ ├── nip44.ts
│ │ │ └── pin-encrypted.ts
│ │ ├── event-builder.ts
│ │ ├── event-ext.ts
│ │ ├── event-kind.ts
│ │ ├── event-publisher.ts
│ │ ├── filter-cache-layer.ts
│ │ ├── impl
│ │ │ ├── nip10.ts
│ │ │ ├── nip22.ts
│ │ │ ├── nip4.ts
│ │ │ ├── nip44.ts
│ │ │ ├── nip46.ts
│ │ │ ├── nip55.ts
│ │ │ ├── nip57.ts
│ │ │ ├── nip7.ts
│ │ │ ├── nip92.ts
│ │ │ └── nip94.ts
│ │ ├── index.ts
│ │ ├── links.ts
│ │ ├── negentropy
│ │ │ ├── accumulator.ts
│ │ │ ├── negentropy-flow.ts
│ │ │ ├── negentropy.ts
│ │ │ ├── utils.ts
│ │ │ ├── vector-storage.ts
│ │ │ └── wrapped-buffer.ts
│ │ ├── nips.ts
│ │ ├── nostr-link.ts
│ │ ├── nostr-system.ts
│ │ ├── nostr.ts
│ │ ├── note-collection.ts
│ │ ├── outbox
│ │ │ ├── index.ts
│ │ │ ├── outbox-model.ts
│ │ │ └── relay-loader.ts
│ │ ├── pow-util.ts
│ │ ├── pow-worker.ts
│ │ ├── pow.ts
│ │ ├── profile-cache.ts
│ │ ├── query-manager.ts
│ │ ├── query-optimizer
│ │ │ ├── index.ts
│ │ │ ├── request-expander.ts
│ │ │ ├── request-merger.ts
│ │ │ └── request-splitter.ts
│ │ ├── query.ts
│ │ ├── relay-info.ts
│ │ ├── relay-metric-handler.ts
│ │ ├── request-builder.ts
│ │ ├── request-matcher.ts
│ │ ├── request-router.ts
│ │ ├── request-trim.ts
│ │ ├── signer.ts
│ │ ├── sync
│ │ │ ├── connection.ts
│ │ │ ├── diff-sync.ts
│ │ │ ├── index.ts
│ │ │ ├── json-in-event-sync.ts
│ │ │ ├── range-sync.ts
│ │ │ └── safe-sync.ts
│ │ ├── system-base.ts
│ │ ├── system.ts
│ │ ├── text.ts
│ │ ├── user-state.ts
│ │ └── utils.ts
│ ├── tests
│ │ ├── Impl.test.ts
│ │ ├── Query.test.ts
│ │ ├── event-ext.test.ts
│ │ ├── negentropy.test.ts
│ │ ├── node.ts
│ │ ├── note-collection.test.ts
│ │ ├── outbox-model.test.ts
│ │ ├── request-builder.test.ts
│ │ ├── request-expander.test.ts
│ │ ├── request-matcher.test.ts
│ │ ├── request-merger.test.ts
│ │ ├── request-splitter.test.ts
│ │ ├── setupTests.ts
│ │ └── utils.test.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── wallet
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── AlbyWallet.ts
│ │ ├── LNDHub.ts
│ │ ├── NostrWalletConnect.ts
│ │ ├── WebLN.ts
│ │ ├── custom.d.ts
│ │ ├── index.ts
│ │ └── zapper.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── webrtc-server
│ ├── README.md
│ ├── index.js
│ └── package.json
└── worker-relay
│ ├── README.md
│ ├── example
│ └── basic.ts
│ ├── package.json
│ ├── src
│ ├── custom.d.ts
│ ├── debug.ts
│ ├── forYouFeed.ts
│ ├── index.ts
│ ├── interface.ts
│ ├── memory-relay.ts
│ ├── queue.ts
│ ├── sqlite
│ │ ├── fixers.ts
│ │ ├── migrations.ts
│ │ ├── sqlite-relay.ts
│ │ └── sqlite3.wasm
│ ├── types.ts
│ └── worker.ts
│ ├── tsconfig.json
│ └── typedoc.json
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── capabilities
│ └── migrated.json
├── gen
│ └── schemas
│ │ ├── acl-manifests.json
│ │ ├── capabilities.json
│ │ ├── desktop-schema.json
│ │ └── linux-schema.json
├── icons
│ ├── 128x128.png
│ └── 128x128@2x.png
├── src
│ └── main.rs
└── tauri.conf.json
├── yarn.lock
└── zapstore.yaml
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/.pnp.*
3 | **/.yarn/*
4 | !**/.yarn/patches
5 | !**/.yarn/plugins
6 | !**/.yarn/releases
7 | !**/.yarn/sdks
8 | !**/.yarn/versions
9 | **/.idea
10 | **/target
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 |
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 |
15 | Steps to reproduce the behavior:
16 |
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 |
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 |
28 | If applicable, add screenshots to help explain your problem.
29 |
30 | **Desktop (please complete the following information):**
31 |
32 | - OS: [e.g. iOS]
33 | - Browser: [e.g. chrome, safari]
34 | - Version: [e.g. 22]
35 |
36 | **Smartphone (please complete the following information):**
37 |
38 | - Device: [e.g. iPhone6]
39 | - OS: [e.g. iOS8.1]
40 | - Browser: [e.g. stock browser, safari]
41 | - Version: [e.g. 22]
42 |
43 | **Additional context**
44 | Add any other context about the problem here.
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea
3 | .pnp.*
4 | .yarn/*
5 | !.yarn/patches
6 | !.yarn/plugins
7 | !.yarn/releases
8 | !.yarn/sdks
9 | !.yarn/versions
10 | dist/
11 | *.tgz
12 | *.log
13 | .DS_Store
14 | .pnp*
15 | docs/
16 | .wrangler/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .yarn/
2 | build/
3 | .vscode/
4 | .github/
5 | transifex.yml
6 | dist/
7 | src-tauri/
8 | target/
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "arcanis.vscode-zipfs",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.svn": true,
5 | "**/.hg": true,
6 | "**/CVS": true,
7 | "**/.DS_Store": true,
8 | "**/Thumbs.db": true,
9 | "**/node_modules": true
10 | },
11 | "search.exclude": {
12 | "**/.yarn": true,
13 | "**/.pnp.*": true
14 | },
15 | "typescript.tsdk": ".yarn/sdks/typescript/lib",
16 | "typescript.enablePromptUseWorkspaceTsdk": true,
17 | "eslint.nodePath": ".yarn/sdks",
18 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs"
19 | }
20 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/bin/eslint.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require eslint/bin/eslint.js
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real eslint/bin/eslint.js your application uses
20 | module.exports = absRequire(`eslint/bin/eslint.js`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/lib/api.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require eslint
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real eslint your application uses
20 | module.exports = absRequire(`eslint`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/lib/unsupported-api.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require eslint/use-at-your-own-risk
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real eslint/use-at-your-own-risk your application uses
20 | module.exports = absRequire(`eslint/use-at-your-own-risk`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint",
3 | "version": "8.53.0-sdk",
4 | "main": "./lib/api.js",
5 | "type": "commonjs",
6 | "bin": {
7 | "eslint": "./bin/eslint.js"
8 | },
9 | "exports": {
10 | "./package.json": "./package.json",
11 | ".": "./lib/api.js",
12 | "./use-at-your-own-risk": "./lib/unsupported-api.js"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.yarn/sdks/integrations.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by @yarnpkg/sdks.
2 | # Manual changes might be lost!
3 |
4 | integrations:
5 | - vscode
6 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/bin/prettier.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require prettier/bin/prettier.cjs
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real prettier/bin/prettier.cjs your application uses
20 | module.exports = absRequire(`prettier/bin/prettier.cjs`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/index.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require prettier
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real prettier your application uses
20 | module.exports = absRequire(`prettier`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prettier",
3 | "version": "3.1.0-sdk",
4 | "main": "./index.cjs",
5 | "type": "commonjs",
6 | "bin": "./bin/prettier.cjs"
7 | }
8 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/bin/tsc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require typescript/bin/tsc
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real typescript/bin/tsc your application uses
20 | module.exports = absRequire(`typescript/bin/tsc`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/bin/tsserver:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require typescript/bin/tsserver
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real typescript/bin/tsserver your application uses
20 | module.exports = absRequire(`typescript/bin/tsserver`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/lib/tsc.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require typescript/lib/tsc.js
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real typescript/lib/tsc.js your application uses
20 | module.exports = absRequire(`typescript/lib/tsc.js`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/lib/typescript.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire} = require(`module`);
5 | const {resolve} = require(`path`);
6 |
7 | const relPnpApiPath = "../../../../.pnp.cjs";
8 |
9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
10 | const absRequire = createRequire(absPnpApiPath);
11 |
12 | if (existsSync(absPnpApiPath)) {
13 | if (!process.versions.pnp) {
14 | // Setup the environment to be able to require typescript
15 | require(absPnpApiPath).setup();
16 | }
17 | }
18 |
19 | // Defer to the real typescript your application uses
20 | module.exports = absRequire(`typescript`);
21 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript",
3 | "version": "5.2.2-sdk",
4 | "main": "./lib/typescript.js",
5 | "type": "commonjs",
6 | "bin": {
7 | "tsc": "./bin/tsc",
8 | "tsserver": "./bin/tsserver"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | yarn-path ".yarn/releases/yarn-1.22.19.cjs"
6 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | compressionLevel: mixed
2 |
3 | enableGlobalCache: false
4 |
5 | npmScopes:
6 | here:
7 | npmRegistryServer: "https://repo.platform.here.com/artifactory/api/npm/maps-api-for-javascript/"
8 |
9 | yarnPath: .yarn/releases/yarn-4.1.1.cjs
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:current as build
2 | WORKDIR /src
3 | RUN apt update \
4 | && apt install -y --no-install-recommends git \
5 | && git clone --single-branch -b main https://git.v0l.io/Kieran/snort \
6 | && cd snort \
7 | && yarn --network-timeout 1000000 \
8 | && yarn build
9 |
10 | FROM nginxinc/nginx-unprivileged:mainline-alpine
11 | COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
12 | COPY --from=build /src/snort/packages/app/build /usr/share/nginx/html
13 |
--------------------------------------------------------------------------------
/Dockerfile.prebuilt:
--------------------------------------------------------------------------------
1 | FROM nginxinc/nginx-unprivileged:mainline-alpine
2 | COPY packages/app/build /usr/share/nginx/html
3 | COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Kieran (v0l)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | project_id: 568149
2 | preserve_hierarchy: true
3 | files:
4 | - source: packages/app/src/translations/en.json
5 | translation: packages/app/src/translations/%locale_with_underscore%.json
6 |
--------------------------------------------------------------------------------
/docker/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080 default_server;
3 | server_name _;
4 | root /usr/share/nginx/html;
5 | index index.html;
6 | add_header Content-Security-Policy "default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com";
7 | add_header Cross-Origin-Opener-Policy same-origin;
8 | add_header Cross-Origin-Embedder-Policy require-corp;
9 |
10 | location / {
11 | try_files $uri $uri/ /index.html =404;
12 | }
13 | }
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "lib": ["esnext"],
6 | "types": ["@cloudflare/workers-types"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/maintainers.yaml:
--------------------------------------------------------------------------------
1 | maintainers:
2 | - npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk
3 | - npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49
4 | relays:
5 | - wss://relay.snort.social/
6 | - wss://pyramid.fiatjaf.com/
7 | - wss://nos.lol/
8 | - ws://skzzn6cimfdv5e2phjc4yr5v7ikbxtn5f7dkwn5c7v47tduzlbosqmqd.onion/
9 |
--------------------------------------------------------------------------------
/nap.yaml:
--------------------------------------------------------------------------------
1 | id: "social.snort.app"
2 | name: "Snort"
3 | description: ""
4 | icon: "https://snort.social/nostrich_256.png"
5 | images:
6 | - "https://snort.social/nostrich_512.png"
7 | repository: "https://github.com/v0l/snort"
8 | license: "MIT"
9 | tags:
10 | - "social"
11 | - "twitter"
12 |
--------------------------------------------------------------------------------
/packages/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .idea
26 |
27 | dist/
28 | dev-dist/
29 | .wrangler/
--------------------------------------------------------------------------------
/packages/app/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "formatjs",
5 | {
6 | "idInterpolationPattern": "[sha512:contenthash:base64:6]",
7 | "ast": true
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/app/config/README.md:
--------------------------------------------------------------------------------
1 | Choose config with NODE_CONFIG_ENV: `NODE_CONFIG_ENV=iris yarn start`
2 |
--------------------------------------------------------------------------------
/packages/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/packages/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("tailwindcss"), require("autoprefixer")],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/app/public/iris/.well-known/assetlinks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "relation": ["delegate_permission/common.handle_all_urls"],
4 | "target": {
5 | "namespace": "android_app",
6 | "package_name": "to.iris.twa",
7 | "sha256_cert_fingerprints": [
8 | "63:B5:70:E8:F1:75:7E:D6:EF:81:11:66:F4:9D:47:AB:49:3C:2E:00:B9:67:92:40:89:A5:03:0B:96:B9:40:09"
9 | ]
10 | }
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/packages/app/public/iris/_headers:
--------------------------------------------------------------------------------
1 | /*
2 | Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
3 | /service-worker.js
4 | Cache-Control: max-age=604800, must-revalidate;
--------------------------------------------------------------------------------
/packages/app/public/iris/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/favicon.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/icon128.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/irisconnects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/irisconnects.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/maskable_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/maskable_icon.png
--------------------------------------------------------------------------------
/packages/app/public/iris/img/maskable_icon_x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/iris/img/maskable_icon_x192.png
--------------------------------------------------------------------------------
/packages/app/public/iris/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Iris",
3 | "name": "Iris",
4 | "description": "Fast nostr web ui",
5 | "id": "/",
6 | "icons": [
7 | {
8 | "src": "/img/android-chrome-192x192.png",
9 | "sizes": "192x192",
10 | "type": "image/png"
11 | },
12 | {
13 | "src": "/img/android-chrome-512x512.png",
14 | "sizes": "512x512",
15 | "type": "image/png",
16 | "purpose": "any"
17 | },
18 | {
19 | "src": "/img/maskable_icon.png",
20 | "sizes": "640x640",
21 | "type": "image/png",
22 | "purpose": "maskable"
23 | },
24 | {
25 | "src": "/img/maskable_icon_x192.png",
26 | "sizes": "192x192",
27 | "type": "image/png",
28 | "purpose": "maskable"
29 | }
30 | ],
31 | "start_url": "/",
32 | "display": "standalone",
33 | "theme_color": "#000000",
34 | "background_color": "#000000",
35 | "protocol_handlers": [
36 | {
37 | "protocol": "web+nostr",
38 | "url": "/%s"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/packages/app/public/iris/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/packages/app/public/nostr/_headers:
--------------------------------------------------------------------------------
1 | /*
2 | Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
--------------------------------------------------------------------------------
/packages/app/public/nostr/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/nostr/favicon.png
--------------------------------------------------------------------------------
/packages/app/public/nostr/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/nostr/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/app/public/nostr/nostr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/nostr/nostr.jpg
--------------------------------------------------------------------------------
/packages/app/public/phoenix/.well-known/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "details": [
4 | {
5 | "appIDs": [
6 | "snort.social.app"
7 | ]
8 | }
9 | ]
10 | },
11 | "webcredentials": {
12 | "apps": [
13 | "snort.social.app"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/public/phoenix/.well-known/assetlinks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "relation": ["delegate_permission/common.handle_all_urls"],
4 | "target": {
5 | "namespace": "android_app",
6 | "package_name": "social.snort.app",
7 | "sha256_cert_fingerprints": [
8 | "78:CE:8A:F7:C1:E2:30:12:77:55:BF:0E:86:E4:5C:BA:99:93:A0:D7:D7:42:F8:27:8B:C9:1B:AC:FC:8A:85:05",
9 | "FC:C1:CA:02:C0:81:81:0C:1F:EC:1E:38:CA:38:61:62:6B:6E:90:88:62:DE:4A:66:FC:EC:08:33:B6:94:EE:3C"
10 | ]
11 | }
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/app/public/phoenix/_headers:
--------------------------------------------------------------------------------
1 | /*
2 | Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
3 | /service-worker.js
4 | Cache-Control: max-age=604800, must-revalidate;
--------------------------------------------------------------------------------
/packages/app/public/phoenix/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/phoenix/favicon.png
--------------------------------------------------------------------------------
/packages/app/public/phoenix/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/phoenix/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/app/public/phoenix/logo_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/phoenix/logo_256.png
--------------------------------------------------------------------------------
/packages/app/public/phoenix/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Phoenix",
3 | "name": "phoenix.social - Nostr interface",
4 | "description": "Fast nostr web ui",
5 | "id": "/",
6 | "icons": [
7 | {
8 | "src": "phoenix_256.png",
9 | "type": "image/png",
10 | "sizes": "256x256"
11 | }
12 | ],
13 | "start_url": "/",
14 | "display": "standalone",
15 | "theme_color": "#000000",
16 | "background_color": "#000000",
17 | "protocol_handlers": [
18 | {
19 | "protocol": "web+nostr",
20 | "url": "/%s"
21 | }
22 | ],
23 | "screenshots": [],
24 | "display_override": [
25 | "fullscreen"
26 | ],
27 | "related_applications": [
28 | {
29 | "platform": "play",
30 | "url": "https://play.google.com/store/apps/details?id=social.snort.app",
31 | "id": "social.snort.app"
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/packages/app/public/phoenix/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 | Sitemap: https://api.snort.social/api/v1/sitemap/index.xml
--------------------------------------------------------------------------------
/packages/app/public/snort/.well-known/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "details": [
4 | {
5 | "appIDs": [
6 | "snort.social.app"
7 | ]
8 | }
9 | ]
10 | },
11 | "webcredentials": {
12 | "apps": [
13 | "snort.social.app"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/public/snort/.well-known/assetlinks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "relation": ["delegate_permission/common.handle_all_urls"],
4 | "target": {
5 | "namespace": "android_app",
6 | "package_name": "social.snort.app",
7 | "sha256_cert_fingerprints": [
8 | "78:CE:8A:F7:C1:E2:30:12:77:55:BF:0E:86:E4:5C:BA:99:93:A0:D7:D7:42:F8:27:8B:C9:1B:AC:FC:8A:85:05",
9 | "FC:C1:CA:02:C0:81:81:0C:1F:EC:1E:38:CA:38:61:62:6B:6E:90:88:62:DE:4A:66:FC:EC:08:33:B6:94:EE:3C"
10 | ]
11 | }
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/app/public/snort/_headers:
--------------------------------------------------------------------------------
1 | /*
2 | Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
3 | /service-worker.js
4 | Cache-Control: max-age=604800, must-revalidate;
--------------------------------------------------------------------------------
/packages/app/public/snort/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/snort/favicon.png
--------------------------------------------------------------------------------
/packages/app/public/snort/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/snort/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/app/public/snort/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Snort",
3 | "name": "snort.social - Nostr interface",
4 | "description": "Fast nostr web ui",
5 | "id": "/",
6 | "icons": [
7 | {
8 | "src": "nostrich_256.png",
9 | "type": "image/png",
10 | "sizes": "256x256"
11 | },
12 | {
13 | "src": "nostrich_512.png",
14 | "type": "image/png",
15 | "sizes": "512x512"
16 | }
17 | ],
18 | "start_url": "/",
19 | "display": "standalone",
20 | "theme_color": "#000000",
21 | "background_color": "#000000",
22 | "protocol_handlers": [
23 | {
24 | "protocol": "web+nostr",
25 | "url": "/%s"
26 | }
27 | ],
28 | "screenshots": [],
29 | "display_override": ["fullscreen"],
30 | "related_applications": [
31 | {
32 | "platform": "play",
33 | "url": "https://play.google.com/store/apps/details?id=social.snort.app",
34 | "id": "social.snort.app"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/packages/app/public/snort/nostrich_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/snort/nostrich_256.png
--------------------------------------------------------------------------------
/packages/app/public/snort/nostrich_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/snort/nostrich_512.png
--------------------------------------------------------------------------------
/packages/app/public/snort/nostrich_orig.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/public/snort/nostrich_orig.jpeg
--------------------------------------------------------------------------------
/packages/app/public/snort/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 | Sitemap: https://api.snort.social/api/v1/sitemap/index.xml
--------------------------------------------------------------------------------
/packages/app/src/Cache/ChatCache.ts:
--------------------------------------------------------------------------------
1 | import { FeedCache } from "@snort/shared";
2 | import { NostrEvent } from "@snort/system";
3 |
4 | import { db } from "@/Db";
5 |
6 | export class ChatCache extends FeedCache {
7 | constructor() {
8 | super("ChatCache", db.chats);
9 | }
10 |
11 | key(of: NostrEvent): string {
12 | return of.id;
13 | }
14 |
15 | override async preload(): Promise {
16 | await super.preload();
17 | // load all dms to memory
18 | await this.buffer([...this.onTable]);
19 | }
20 |
21 | newest(): number {
22 | let ret = 0;
23 | this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
24 | return ret;
25 | }
26 |
27 | takeSnapshot(): Array {
28 | return [...this.cache.values()];
29 | }
30 |
31 | async search() {
32 | return >[];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/app/src/Cache/CommunityLeadersStore.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalStore } from "@snort/shared";
2 |
3 | class CommunityLeadersStore extends ExternalStore> {
4 | #leaders: Array = [];
5 |
6 | setLeaders(arr: Array) {
7 | this.#leaders = arr;
8 | this.notifyChange();
9 | }
10 |
11 | takeSnapshot(): string[] {
12 | return [...this.#leaders];
13 | }
14 | }
15 |
16 | export const LeadersStore = new CommunityLeadersStore();
17 |
--------------------------------------------------------------------------------
/packages/app/src/Cache/RefreshFeedCache.ts:
--------------------------------------------------------------------------------
1 | import { FeedCache } from "@snort/shared";
2 | import { EventPublisher, RequestBuilder, TaggedNostrEvent } from "@snort/system";
3 |
4 | import { LoginSession } from "@/Utils/Login";
5 |
6 | export type TWithCreated = (T | Readonly) & { created_at: number };
7 |
8 | export abstract class RefreshFeedCache extends FeedCache> {
9 | abstract buildSub(session: LoginSession, rb: RequestBuilder): void;
10 | abstract onEvent(evs: Readonly>, pubKey: string, pub?: EventPublisher): void;
11 |
12 | /**
13 | * Get latest event
14 | */
15 | protected newest(filter?: (e: TWithCreated) => boolean) {
16 | let ret = 0;
17 | this.cache.forEach(v => {
18 | if (!filter || filter(v)) {
19 | ret = v.created_at > ret ? v.created_at : ret;
20 | }
21 | });
22 | return ret;
23 | }
24 |
25 | override async preload(): Promise {
26 | await super.preload();
27 | await this.buffer([...this.onTable]);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/app/src/Cache/TextCache.tsx:
--------------------------------------------------------------------------------
1 | import { ParsedFragment } from "@snort/system";
2 | import { LRUCache } from "typescript-lru-cache";
3 |
4 | export const TextCache = new LRUCache>({
5 | maxSize: 1000,
6 | });
7 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/AsyncButton.css:
--------------------------------------------------------------------------------
1 | .spinner-wrapper {
2 | position: absolute;
3 | width: 100%;
4 | height: 100%;
5 | top: 0;
6 | left: 0;
7 | }
8 |
9 | .spinner-button > span {
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | gap: 8px;
14 | }
15 |
16 | .light .spinner-button {
17 | border: 1px solid var(--border-color);
18 | color: var(--font-secondary);
19 | box-shadow: rgba(0, 0, 0, 0.08) 0 1px 1px;
20 | }
21 |
22 | .light .spinner-button:not(.primary):hover {
23 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 3px;
24 | }
25 |
26 | .light .spinner-button:not(.primary) > span {
27 | color: black;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/AsyncIcon.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@/Components/Icons/Icon";
2 | import Spinner from "@/Components/Icons/Spinner";
3 | import useLoading from "@/Hooks/useLoading";
4 |
5 | export type AsyncIconProps = React.HTMLProps & {
6 | iconName: string;
7 | iconSize?: number;
8 | onClick?: (e: React.MouseEvent) => Promise | void;
9 | };
10 |
11 | export function AsyncIcon(props: AsyncIconProps) {
12 | const { loading, handle } = useLoading(props.onClick, props.disabled);
13 |
14 | const mergedProps = { ...props } as Record;
15 | delete mergedProps["iconName"];
16 | delete mergedProps["iconSize"];
17 | delete mergedProps["loading"];
18 | return (
19 |
20 | {loading ? : }
21 | {props.children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/BackButton.css:
--------------------------------------------------------------------------------
1 | .back-button {
2 | background: none;
3 | padding: 0;
4 | color: var(--highlight);
5 | font-weight: 400;
6 | font-size: var(--font-size);
7 | display: flex;
8 | align-items: center;
9 | border: none !important;
10 | box-shadow: none !important;
11 | }
12 |
13 | .back-button svg {
14 | margin-right: 0.5em;
15 | }
16 |
17 | .back-button:hover:hover,
18 | .light .back-button:hover {
19 | text-decoration: underline;
20 | box-shadow: none !important;
21 | background: none !important;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/BackButton.tsx:
--------------------------------------------------------------------------------
1 | import "./BackButton.css";
2 |
3 | import { useIntl } from "react-intl";
4 |
5 | import Icon from "@/Components/Icons/Icon";
6 |
7 | import messages from "../messages";
8 |
9 | interface BackButtonProps {
10 | text?: string;
11 | onClick?(): void;
12 | }
13 |
14 | const BackButton = ({ text, onClick }: BackButtonProps) => {
15 | const { formatMessage } = useIntl();
16 | const onClickHandler = () => {
17 | if (onClick) {
18 | onClick();
19 | }
20 | };
21 |
22 | return (
23 |
27 | );
28 | };
29 |
30 | export default BackButton;
31 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/CloseButton.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | import Icon from "@/Components/Icons/Icon";
4 |
5 | export default function CloseButton({ onClick, className }: { onClick?: () => void; className?: string }) {
6 | return (
7 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import type { ReactNode } from "react";
3 |
4 | import Icon, { IconProps } from "@/Components/Icons/Icon";
5 |
6 | interface IconButtonProps {
7 | onClick?: () => void;
8 | icon: IconProps;
9 | className?: string;
10 | children?: ReactNode;
11 | }
12 |
13 | const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
14 | return (
15 |
24 | );
25 | };
26 |
27 | export default IconButton;
28 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | import useLogin from "@/Hooks/useLogin";
5 | import { logout } from "@/Utils/Login";
6 |
7 | import messages from "../messages";
8 |
9 | export default function LogoutButton() {
10 | const navigate = useNavigate();
11 | const login = useLogin(s => ({ publicKey: s.publicKey, id: s.id }));
12 |
13 | if (!login.publicKey) return;
14 | return (
15 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Button/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink as RouterNavLink, NavLinkProps, useLocation } from "react-router-dom";
2 |
3 | export default function NavLink(props: NavLinkProps) {
4 | const { to, onClick, ...rest } = props;
5 | const location = useLocation();
6 |
7 | const isActive = location.pathname === to.toString();
8 |
9 | const handleClick = event => {
10 | if (onClick) {
11 | onClick(event);
12 | }
13 |
14 | if (isActive) {
15 | window.scrollTo({ top: 0, behavior: "instant" });
16 | }
17 | };
18 |
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Copy/Copy.css:
--------------------------------------------------------------------------------
1 | .copy .copy-body {
2 | font-size: var(--font-size-small);
3 | color: var(--font-color);
4 | }
5 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/AppleMusicEmbed.tsx:
--------------------------------------------------------------------------------
1 | const AppleMusicEmbed = ({ link }: { link: string }) => {
2 | const convertedUrl = link.replace("music.apple.com", "embed.music.apple.com");
3 | const isSongLink = /\?i=\d+$/.test(convertedUrl);
4 |
5 | return (
6 |
15 | );
16 | };
17 |
18 | export default AppleMusicEmbed;
19 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/CashuNuts.css:
--------------------------------------------------------------------------------
1 | .cashu {
2 | background: var(--cashu-gradient);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/Hashtag.css:
--------------------------------------------------------------------------------
1 | .hashtag {
2 | color: var(--highlight);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/Hashtag.tsx:
--------------------------------------------------------------------------------
1 | import "./Hashtag.css";
2 |
3 | import { Link } from "react-router-dom";
4 |
5 | const Hashtag = ({ tag }: { tag: string }) => {
6 | return (
7 |
8 | e.stopPropagation()}>
9 | #{tag}
10 |
11 |
12 | );
13 | };
14 |
15 | export default Hashtag;
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/MagnetLink.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 |
3 | import { Magnet } from "@/Utils";
4 |
5 | interface MagnetLinkProps {
6 | magnet: Magnet;
7 | }
8 |
9 | const MagnetLink = ({ magnet }: MagnetLinkProps) => {
10 | return (
11 |
19 | );
20 | };
21 |
22 | export default MagnetLink;
23 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/MixCloudEmbed.tsx:
--------------------------------------------------------------------------------
1 | import usePreferences from "@/Hooks/usePreferences";
2 | import { MixCloudRegex } from "@/Utils/Const";
3 |
4 | const MixCloudEmbed = ({ link }: { link: string }) => {
5 | const match = link.match(MixCloudRegex);
6 | if (!match) return;
7 | const feedPath = match[1] + "%2F" + match[2];
8 |
9 | const theme = usePreferences(s => s.theme);
10 | const lightParams = theme === "light" ? "light=1" : "light=0";
11 | return (
12 |
20 | );
21 | };
22 |
23 | export default MixCloudEmbed;
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/NostrNestsEmbed.tsx:
--------------------------------------------------------------------------------
1 | const NostrNestsEmbed = ({ link }: { link: string }) => (
2 |
3 | );
4 |
5 | export default NostrNestsEmbed;
6 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/SoundCloudEmded.tsx:
--------------------------------------------------------------------------------
1 | const SoundCloudEmbed = ({ link }: { link: string }) => {
2 | return (
3 |
10 | );
11 | };
12 |
13 | export default SoundCloudEmbed;
14 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/SpotifyEmbed.tsx:
--------------------------------------------------------------------------------
1 | const SpotifyEmbed = ({ link }: { link: string }) => {
2 | const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
3 |
4 | return (
5 |
14 | );
15 | };
16 |
17 | export default SpotifyEmbed;
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/TwitchEmbed.tsx:
--------------------------------------------------------------------------------
1 | const TwitchEmbed = ({ link }: { link: string }) => {
2 | const channel = link.split("/").slice(-1);
3 |
4 | const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
5 | return (
6 |
12 | );
13 | };
14 |
15 | export default TwitchEmbed;
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/WavlakeEmbed.tsx:
--------------------------------------------------------------------------------
1 | const WavlakeEmbed = ({ link }: { link: string }) => {
2 | const convertedUrl = link.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
3 |
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default WavlakeEmbed;
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/YoutubeEmbed.tsx:
--------------------------------------------------------------------------------
1 | import { YoutubeUrlRegex } from "@/Utils/Const";
2 |
3 | export default function YoutubeEmbed({ link }: { link: string }) {
4 | const m = link.match(YoutubeUrlRegex);
5 | if (!m) return;
6 |
7 | return (
8 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Embed/ZapstrEmbed.css:
--------------------------------------------------------------------------------
1 | .zapstr {
2 | }
3 |
4 | .zapstr > img {
5 | margin: 0 10px 0 0;
6 | }
7 |
8 | .zapstr audio {
9 | margin: 0;
10 | height: 2em;
11 | }
12 |
13 | .zapstr .pfp .avatar {
14 | width: 35px;
15 | height: 35px;
16 | }
17 |
18 | .zapstr .pfp .subheader {
19 | text-transform: capitalize;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/Components/ErrorOrOffline.tsx:
--------------------------------------------------------------------------------
1 | import { OfflineError } from "@snort/shared";
2 | import classNames from "classnames";
3 |
4 | import Icon from "@/Components/Icons/Icon";
5 |
6 | import { Offline } from "./Offline";
7 |
8 | export function ErrorOrOffline({
9 | error,
10 | onRetry,
11 | className,
12 | }: {
13 | error: Error;
14 | onRetry?: () => void | Promise;
15 | className?: string;
16 | }) {
17 | if (error instanceof OfflineError) {
18 | return ;
19 | } else {
20 | return (
21 |
22 |
23 | {error.message}
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Create/util.ts:
--------------------------------------------------------------------------------
1 | import { removeUndefined } from "@snort/shared";
2 | import { NostrEvent, OkResponse, SystemInterface } from "@snort/system";
3 |
4 | export async function sendEventToRelays(
5 | system: SystemInterface,
6 | ev: NostrEvent,
7 | customRelays?: Array,
8 | setResults?: (x: Array) => void,
9 | ) {
10 | if (customRelays) {
11 | system.HandleEvent("*", { ...ev, relays: [] });
12 | return removeUndefined(
13 | await Promise.all(
14 | customRelays.map(async r => {
15 | try {
16 | return await system.WriteOnceToRelay(r, ev);
17 | } catch (e) {
18 | console.error(e);
19 | }
20 | }),
21 | ),
22 | );
23 | } else {
24 | const responses: OkResponse[] = await system.BroadcastEvent(ev);
25 | setResults?.(responses);
26 | return responses;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/FileUpload.tsx:
--------------------------------------------------------------------------------
1 | import Progress from "@/Components/Progress/Progress";
2 | import { UploadProgress } from "@/Utils/Upload";
3 |
4 | export default function FileUploadProgress({ progress }: { progress: Array }) {
5 | return (
6 |
7 | {progress.map(p => (
8 |
9 | {p.file.name}
10 |
11 |
12 | ))}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/HiddenNote.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { FormattedMessage } from "react-intl";
3 |
4 | import usePreferences from "@/Hooks/usePreferences";
5 |
6 | const HiddenNote = ({ children }: { children: React.ReactNode }) => {
7 | const hideMutedNotes = usePreferences(s => s.hideMutedNotes);
8 | const [show, setShow] = useState(false);
9 | if (hideMutedNotes) return;
10 |
11 | return show ? (
12 | children
13 | ) : (
14 |
15 |
16 |
17 |
18 |
21 |
22 | );
23 | };
24 |
25 | export default HiddenNote;
26 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/LongFormText.css:
--------------------------------------------------------------------------------
1 | .long-form-note p {
2 | font-family: Georgia;
3 | line-height: 1.7;
4 | }
5 |
6 | .long-form-note hr {
7 | border: 0;
8 | height: 1px;
9 | background-color: var(--gray);
10 | margin: 5px 0px;
11 | }
12 |
13 | .long-form-note .reading {
14 | border: 1px dashed var(--highlight);
15 | }
16 |
17 | .long-form-note .header-image {
18 | height: 360px;
19 | background: var(--img);
20 | background-position: center;
21 | background-size: cover;
22 | }
23 |
24 | .long-form-note h1 {
25 | font-size: 32px;
26 | font-weight: 700;
27 | line-height: 40px; /* 125% */
28 | margin: 0;
29 | }
30 |
31 | .long-form-note small {
32 | font-weight: 400;
33 | line-height: 24px; /* 150% */
34 | }
35 |
36 | .long-form-note img:not(.custom-emoji),
37 | .long-form-note video,
38 | .long-form-note iframe,
39 | .long-form-note audio {
40 | width: 100%;
41 | display: block;
42 | }
43 |
44 | .long-form-note iframe,
45 | .long-form-note video {
46 | width: -webkit-fill-available;
47 | aspect-ratio: 16 / 9;
48 | }
49 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Markdown.css:
--------------------------------------------------------------------------------
1 | .markdown a {
2 | color: var(--highlight);
3 | }
4 |
5 | .markdown blockquote {
6 | margin: 0;
7 | color: var(--font-secondary-color);
8 | border-left: 2px solid var(--font-secondary-color);
9 | padding-left: 12px;
10 | }
11 |
12 | .markdown hr {
13 | border: 0;
14 | height: 1px;
15 | background-image: var(--gray-gradient);
16 | margin: 20px;
17 | }
18 |
19 | .markdown img:not(.custom-emoji),
20 | .markdown video,
21 | .markdown iframe,
22 | .markdown audio {
23 | width: 100%;
24 | display: block;
25 | }
26 |
27 | .markdown iframe,
28 | .markdown video {
29 | width: -webkit-fill-available;
30 | aspect-ratio: 16 / 9;
31 | }
32 |
33 | .markdown ul,
34 | .markdown ol {
35 | padding-inline-start: 20px;
36 | }
37 |
38 | .markdown ul {
39 | list-style: circle;
40 | }
41 |
42 | .markdown ol {
43 | list-style: decimal;
44 | }
45 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/ClientTag.tsx:
--------------------------------------------------------------------------------
1 | import { NostrLink, TaggedNostrEvent } from "@snort/system";
2 | import { FormattedMessage } from "react-intl";
3 | import { Link } from "react-router-dom";
4 |
5 | export function ClientTag({ ev }: { ev: TaggedNostrEvent }) {
6 | const tag = ev.tags.find(a => a[0] === "client");
7 | if (!tag) return;
8 | const link = tag[2] && tag[2].includes(":") ? NostrLink.tryFromTag(["a", tag[2]]) : undefined;
9 | return (
10 |
11 | {" "}
12 | {tag[1]} : tag[1],
17 | }}
18 | />
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | import { AsyncIcon, AsyncIconProps } from "@/Components/Button/AsyncIcon";
4 | import { formatShort } from "@/Utils/Number";
5 |
6 | export const AsyncFooterIcon = (props: AsyncIconProps & { value: number }) => {
7 | const mergedProps = {
8 | ...props,
9 | iconSize: 18,
10 | className: classNames(
11 | "transition duration-200 ease-in-out flex flex-row reaction-pill cursor-pointer gap-2 items-center",
12 | props.className,
13 | ),
14 | };
15 |
16 | return (
17 |
18 | {props.value > 0 && {formatShort(props.value)}
}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/NoteFooter/PowIcon.tsx:
--------------------------------------------------------------------------------
1 | import { countLeadingZeros, TaggedNostrEvent } from "@snort/system";
2 | import { useIntl } from "react-intl";
3 |
4 | import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
5 | import { findTag } from "@/Utils";
6 |
7 | export const PowIcon = ({ ev }: { ev: TaggedNostrEvent }) => {
8 | const { formatMessage } = useIntl();
9 |
10 | const powValue = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined;
11 | if (!powValue) return null;
12 |
13 | return (
14 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/NoteFooter/ZapperQueue.tsx:
--------------------------------------------------------------------------------
1 | import { processWorkQueue, WorkQueueItem } from "@snort/shared";
2 |
3 | export const ZapperQueue: Array = [];
4 |
5 | processWorkQueue(ZapperQueue);
6 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/NoteGhost.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import { FormattedMessage } from "react-intl";
3 |
4 | interface NoteGhostProps {
5 | className?: string;
6 | link: string;
7 | }
8 |
9 | export default function NoteGhost(props: NoteGhostProps) {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Note/types.tsx:
--------------------------------------------------------------------------------
1 | import { TaggedNostrEvent } from "@snort/system";
2 |
3 | export interface NoteTranslation {
4 | text: string;
5 | fromLanguage: string;
6 | confidence: number;
7 | skipped?: boolean;
8 | }
9 |
10 | export interface NoteContextMenuProps {
11 | ev: TaggedNostrEvent;
12 |
13 | setShowReactions(b: boolean): void;
14 |
15 | react(content: string): Promise;
16 |
17 | onTranslated?: (t: NoteTranslation) => void;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/NoteReaction.css:
--------------------------------------------------------------------------------
1 | .reaction {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 8px;
5 | }
6 |
7 | .reaction > div:nth-child(1) {
8 | font-size: 16px;
9 | font-weight: 600;
10 | }
11 |
12 | .reaction > div:nth-child(1) svg {
13 | opacity: 0.5;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Reveal.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import { WarningNotice } from "@/Components/WarningNotice/WarningNotice";
4 |
5 | interface RevealProps {
6 | message: React.ReactNode;
7 | children: React.ReactNode;
8 | }
9 |
10 | export default function Reveal(props: RevealProps) {
11 | const [reveal, setReveal] = useState(false);
12 |
13 | if (!reveal) {
14 | return setReveal(true)}>{props.message};
15 | } else if (props.children) {
16 | return props.children;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Thread/Divider.tsx:
--------------------------------------------------------------------------------
1 | interface DividerProps {
2 | variant?: "regular" | "small";
3 | }
4 |
5 | export const Divider = ({ variant = "regular" }: DividerProps) => {
6 | const className = variant === "small" ? "divider divider-small" : "divider";
7 | return (
8 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Thread/ThreadRoute.tsx:
--------------------------------------------------------------------------------
1 | import { NostrPrefix, parseNostrLink } from "@snort/system";
2 | import { useParams } from "react-router-dom";
3 |
4 | import { Thread } from "@/Components/Event/Thread/Thread";
5 | import { ThreadContextWrapper } from "@/Utils/Thread/ThreadContextWrapper";
6 |
7 | export function ThreadRoute({ id }: { id?: string }) {
8 | const params = useParams();
9 | const resolvedId = id ?? params.id;
10 | const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/Thread/util.ts:
--------------------------------------------------------------------------------
1 | import { TaggedNostrEvent, u256 } from "@snort/system";
2 |
3 | export function getReplies(from: u256, chains?: Map>): Array {
4 | if (!from || !chains) {
5 | return [];
6 | }
7 | const replies = chains.get(from);
8 | return replies ? replies : [];
9 | }
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/ZapButton.css:
--------------------------------------------------------------------------------
1 | .zap-button {
2 | color: var(--bg-color);
3 | background-color: var(--highlight);
4 | padding: 4px 8px;
5 | border-radius: 16px;
6 | cursor: pointer;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Event/ZapGoal.css:
--------------------------------------------------------------------------------
1 | .zap-goal h1 {
2 | line-height: 1em;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Feed/LoadMore.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useInView } from "react-intersection-observer";
3 | import { FormattedMessage } from "react-intl";
4 |
5 | import messages from "../messages";
6 |
7 | export default function LoadMore({
8 | onLoadMore,
9 | shouldLoadMore,
10 | children,
11 | }: {
12 | onLoadMore: () => void;
13 | shouldLoadMore: boolean;
14 | children?: React.ReactNode;
15 | }) {
16 | const { ref, inView } = useInView({ rootMargin: "2000px" });
17 | const [tick, setTick] = useState(0);
18 |
19 | useEffect(() => {
20 | if (inView === true && shouldLoadMore === true) {
21 | onLoadMore();
22 | }
23 | }, [inView, shouldLoadMore, tick]);
24 |
25 | useEffect(() => {
26 | const t = setInterval(() => {
27 | setTick(x => (x += 1));
28 | }, 500);
29 | return () => clearInterval(t);
30 | }, []);
31 |
32 | return (
33 |
34 | {children ?? }
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Feed/RootTabs.css:
--------------------------------------------------------------------------------
1 | .root-type {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | flex-grow: 1;
6 | }
7 |
8 | .root-type > button {
9 | background: none;
10 | color: var(--font-color);
11 | font-size: 16px;
12 | padding: 10px 16px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | gap: 12px;
17 | border: none;
18 | box-shadow: none;
19 | }
20 |
21 | .root-type > button:hover {
22 | box-shadow: none !important;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Feed/Timeline.css:
--------------------------------------------------------------------------------
1 | .latest-notes {
2 | cursor: pointer;
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 6px 24px;
8 | gap: 8px;
9 | }
10 |
11 | .latest-notes-fixed {
12 | position: fixed;
13 | top: 50px;
14 | width: auto;
15 | z-index: 42;
16 | opacity: 0.9;
17 | box-shadow: 0px 0px 15px rgba(78, 0, 255, 0.6);
18 | color: white;
19 | background: var(--highlight);
20 | border-radius: 100px;
21 | border: none;
22 | }
23 |
24 | .latest-notes .pfp:not(:last-of-type) {
25 | margin: 0;
26 | margin-right: -26px;
27 | }
28 |
29 | .latest-notes .pfp:last-of-type {
30 | margin-right: -8px;
31 | }
32 |
33 | .latest-notes .pfp .avatar-wrapper .avatar {
34 | margin: 0;
35 | width: 32px;
36 | height: 32px;
37 | border: 2px solid white;
38 | }
39 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Icons/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler } from "react";
2 |
3 | import IconsSvg from "@/Components/Icons/icons.svg";
4 |
5 | export interface IconProps {
6 | name: string;
7 | size?: number;
8 | height?: number;
9 | className?: string;
10 | onClick?: MouseEventHandler;
11 | }
12 |
13 | const Icon = (props: IconProps) => {
14 | const size = props.size || 20;
15 | const href = `${IconsSvg}#` + props.name;
16 |
17 | return (
18 |
21 | );
22 | };
23 |
24 | export default Icon;
25 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Icons/Spinner.css:
--------------------------------------------------------------------------------
1 | .spinner_V8m1 {
2 | transform-origin: center;
3 | animation: spinner_zKoa 2s linear infinite;
4 | }
5 |
6 | .spinner_V8m1 circle {
7 | stroke-linecap: round;
8 | animation: spinner_YpZS 1.5s ease-in-out infinite;
9 | }
10 |
11 | @keyframes spinner_zKoa {
12 | 100% {
13 | transform: rotate(360deg);
14 | }
15 | }
16 |
17 | @keyframes spinner_YpZS {
18 | 0% {
19 | stroke-dasharray: 0 150;
20 | stroke-dashoffset: 0;
21 | }
22 |
23 | 47.5% {
24 | stroke-dasharray: 42 150;
25 | stroke-dashoffset: -16;
26 | }
27 |
28 | 95%,
29 | 100% {
30 | stroke-dasharray: 42 150;
31 | stroke-dashoffset: -59;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Icons/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import "./Spinner.css";
2 |
3 | const Spinner = (props: { width?: number; height?: number; className?: string }) => (
4 |
15 | );
16 |
17 | export default Spinner;
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Icons/Toggle.css:
--------------------------------------------------------------------------------
1 | svg#icon-toggle {
2 | cursor: pointer;
3 | }
4 | svg#icon-toggle #bg {
5 | fill: var(--gray);
6 | transition: fill 0.5s;
7 | }
8 | svg#icon-toggle #toggle {
9 | transform: translateX(0px);
10 | transition: transform 0.5s;
11 | }
12 | svg#icon-toggle.active #toggle {
13 | transform: translateX(12px);
14 | transition: transform 0.5s;
15 | }
16 | svg#icon-toggle.active #bg {
17 | fill: var(--success);
18 | transition: fill 0.5s;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/Components/IntlProvider/IntlProviderUtils.tsx:
--------------------------------------------------------------------------------
1 | export const DefaultLocale = "en-US";
2 |
3 | export const getLocale = () => {
4 | return (navigator.languages && navigator.languages[0]) ?? navigator.language ?? DefaultLocale;
5 | };
6 | export const getCurrency = () => {
7 | const locale = navigator.language || navigator.languages[0];
8 | const formatter = new Intl.NumberFormat(locale, {
9 | style: "currency",
10 | currency: "USD",
11 | currencyDisplay: "code",
12 | });
13 | return formatter.formatToParts(1.2345).find(a => a.type === "currency")?.value ?? "USD";
14 | };
15 | export const AllLanguageCodes = [
16 | "en",
17 | "ja",
18 | "es",
19 | "hu",
20 | "zh-CN",
21 | "zh-TW",
22 | "fr",
23 | "ar",
24 | "it",
25 | "id",
26 | "de",
27 | "ru",
28 | "sv",
29 | "hr",
30 | "ta-IN",
31 | "fa-IR",
32 | "th",
33 | "pt-BR",
34 | "sw",
35 | "nl",
36 | "fi",
37 | "ko",
38 | ];
39 |
--------------------------------------------------------------------------------
/packages/app/src/Components/IntlProvider/langStore.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalStore } from "@snort/shared";
2 |
3 | class LangStore extends ExternalStore {
4 | setLang(s: string) {
5 | localStorage.setItem("lang", s);
6 | this.notifyChange();
7 | }
8 |
9 | takeSnapshot() {
10 | return localStorage.getItem("lang");
11 | }
12 | }
13 |
14 | export const LangOverride = new LangStore();
15 |
--------------------------------------------------------------------------------
/packages/app/src/Components/IntlProvider/useLocale.tsx:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStore } from "react";
2 |
3 | import { getLocale } from "@/Components/IntlProvider/IntlProviderUtils";
4 | import { LangOverride } from "@/Components/IntlProvider/langStore";
5 | import usePreferences from "@/Hooks/usePreferences";
6 |
7 | export function useLocale() {
8 | const language = usePreferences(s => s.language);
9 | const loggedOutLang = useSyncExternalStore(
10 | c => LangOverride.hook(c),
11 | () => LangOverride.snapshot(),
12 | );
13 | const locale = language ?? loggedOutLang ?? getLocale();
14 | return {
15 | locale,
16 | lang: locale.toLowerCase().split(/[_-]+/)[0],
17 | setOverride: (s: string) => LangOverride.setLang(s),
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/Components/IrisAccount/AccountName.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | interface AccountNameProps {
5 | name?: string;
6 | link?: boolean;
7 | }
8 |
9 | export default function AccountName({ name = "", link = true }: AccountNameProps) {
10 | const navigate = useNavigate();
11 | return (
12 | <>
13 |
14 | : {name}
15 |
16 |
31 |
32 | : {name}@iris.to
33 |
34 | >
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/app/src/Components/LiveStream/nests-participants.tsx:
--------------------------------------------------------------------------------
1 | import { dedupe, unixNow } from "@snort/shared";
2 | import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
3 | import { useRequestBuilder } from "@snort/system-react";
4 | import { useMemo } from "react";
5 |
6 | import { AvatarGroup } from "../User/AvatarGroup";
7 |
8 | export function NestsParticipants({ ev }: { ev: TaggedNostrEvent }) {
9 | const link = NostrLink.fromEvent(ev);
10 | const sub = useMemo(() => {
11 | const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
12 | sub.withOptions({ leaveOpen: true });
13 | sub
14 | .withFilter()
15 | .replyToLink([link])
16 | .kinds([10_312 as EventKind])
17 | .since(unixNow() - 600);
18 | return sub;
19 | }, [link.tagKey]);
20 |
21 | const presense = useRequestBuilder(sub);
22 | const filteredPresence = presense.filter(ev => ev.created_at > unixNow() - 600);
23 | return a.pubkey)).slice(0, 5)} size={32} />;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Modal/Modal.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | width: 100vw;
3 | height: 100vh;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | background-color: var(--modal-bg-color);
8 | display: flex;
9 | justify-content: center;
10 | z-index: 42;
11 | overflow-y: auto;
12 | }
13 |
14 | .modal-body {
15 | background-color: var(--gray-superdark);
16 | padding: 24px 12px;
17 | border-radius: 16px;
18 | display: flex;
19 | flex-direction: column;
20 | margin-top: auto;
21 | margin-bottom: auto;
22 | --border-color: var(--gray);
23 | max-width: 100%;
24 | }
25 |
26 | @media (min-width: 600px) {
27 | .modal-body {
28 | padding: 24px;
29 | width: 600px;
30 | }
31 | }
32 | @media (max-width: 600px) {
33 | .modal-body {
34 | min-width: 100%;
35 | }
36 | }
37 |
38 | .modal-body button.secondary:hover {
39 | background-color: var(--gray);
40 | }
41 |
42 | .light .modal-body {
43 | background-color: #fff;
44 | }
45 |
46 | .modal.spotlight {
47 | color: #fff;
48 | }
49 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Offline.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import { FormattedMessage } from "react-intl";
3 |
4 | import Icon from "@/Components/Icons/Icon";
5 |
6 | import AsyncButton from "./Button/AsyncButton";
7 |
8 | export function Offline({ onRetry, className }: { onRetry?: () => void | Promise; className?: string }) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | {onRetry && (
16 |
17 |
18 |
19 | )}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/app/src/Components/PageSpinner.tsx:
--------------------------------------------------------------------------------
1 | import Spinner from "@/Components/Icons/Spinner";
2 |
3 | export default function PageSpinner() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/PinPrompt/PinPrompt.css:
--------------------------------------------------------------------------------
1 | .pin-box {
2 | border: 1px solid var(--border-color);
3 | padding: 12px 16px;
4 | font-size: 80px;
5 | height: 1em;
6 | border-radius: 12px;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Progress/Progress.css:
--------------------------------------------------------------------------------
1 | .progress {
2 | position: relative;
3 | height: 1em;
4 | border-radius: 4px;
5 | overflow: hidden;
6 | background-color: var(--gray);
7 | }
8 |
9 | .progress > div {
10 | position: absolute;
11 | background-color: var(--success);
12 | width: var(--progress);
13 | height: 100%;
14 | }
15 |
16 | .progress > span {
17 | position: absolute;
18 | width: 100%;
19 | height: 100%;
20 | text-align: center;
21 | font-size: small;
22 | line-height: 1em;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Progress/Progress.tsx:
--------------------------------------------------------------------------------
1 | import "./Progress.css";
2 |
3 | import { CSSProperties, ReactNode } from "react";
4 | import { FormattedNumber } from "react-intl";
5 |
6 | export default function Progress({ value, status }: { value: number; status?: ReactNode }) {
7 | const v = Math.max(0.01, Math.min(1, value));
8 | return (
9 |
10 |
16 |
17 | {status}
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Relay/paid.tsx:
--------------------------------------------------------------------------------
1 | import { RelayInfo } from "@snort/system";
2 | import classNames from "classnames";
3 | import { FormattedMessage } from "react-intl";
4 |
5 | export default function RelayPaymentLabel({ info }: { info: RelayInfo }) {
6 | const isPaid = info?.limitation?.payment_required ?? false;
7 | return (
8 |
13 | {isPaid && }
14 | {!isPaid && }
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Relay/software.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | export default function RelaySoftware({ software }: { software: string }) {
4 | if (software.includes("git")) {
5 | const u = new URL(software);
6 | return {u.pathname.split("/").at(-1)?.replace(".git", "")};
7 | }
8 | return software;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Relay/status-label.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectionType } from "@snort/system/dist/connection-pool";
2 | import classNames from "classnames";
3 | import { FormattedMessage } from "react-intl";
4 |
5 | export default function RelayStatusLabel({ conn }: { conn: ConnectionType }) {
6 | return (
7 |
8 |
13 | {conn.isOpen ?
:
}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Relay/uptime-label.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import { FormattedMessage } from "react-intl";
3 |
4 | export default function UptimeLabel({ avgPing }: { avgPing: number }) {
5 | const idealPing = 500;
6 | const badPing = idealPing * 2;
7 | return (
8 | badPing,
11 | "text-warning": avgPing > idealPing && avgPing < badPing,
12 | "text-success": avgPing < idealPing,
13 | })}
14 | title={`${avgPing.toFixed(0)} ms`}>
15 | {isNaN(avgPing) && }
16 | {avgPing > badPing && }
17 | {avgPing > idealPing && avgPing < badPing && }
18 | {avgPing < idealPing && }
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Review.tsx:
--------------------------------------------------------------------------------
1 | import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | export function ReviewSummary({ link }: { link: NostrLink }) {
6 | const sub = useMemo(() => {
7 | const rb = new RequestBuilder(`reviews:${link.id}`);
8 | rb.withFilter()
9 | .kinds([1986 as EventKind])
10 | .replyToLink([link]);
11 | return rb;
12 | }, [link.id]);
13 |
14 | const data = useRequestBuilder(sub);
15 | return {JSON.stringify(data, undefined, 2)}
;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Components/RightWidgets/base.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | import Icon from "../Icons/Icon";
4 |
5 | export interface BaseWidgetProps {
6 | title?: ReactNode;
7 | icon?: string;
8 | iconClassName?: string;
9 | children?: ReactNode;
10 | contextMenu?: ReactNode;
11 | }
12 | export function BaseWidget({ children, title, icon, iconClassName, contextMenu }: BaseWidgetProps) {
13 | return (
14 |
15 | {title && (
16 |
17 |
18 | {icon && (
19 |
20 |
21 |
22 | )}
23 |
{title}
24 |
25 | {contextMenu}
26 |
27 | )}
28 | {children}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Components/RightWidgets/index.tsx:
--------------------------------------------------------------------------------
1 | export enum RightColumnWidget {
2 | TaskList,
3 | TrendingNotes,
4 | TrendingPeople,
5 | TrendingHashtags,
6 | LatestArticls,
7 | LiveStreams,
8 | InviteFriends,
9 | }
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/ScrollToTop.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocation, useNavigationType } from "react-router-dom";
3 |
4 | export default function ScrollToTop() {
5 | const { pathname } = useLocation();
6 | const navigationType = useNavigationType();
7 |
8 | useEffect(() => {
9 | if (navigationType !== "POP") {
10 | window.scrollTo(0, 0);
11 | }
12 | // Only scrolls to top on PUSH or REPLACE, not on POP
13 | }, [pathname, navigationType]);
14 |
15 | return null;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Components/SearchBox/SearchBox.css:
--------------------------------------------------------------------------------
1 | .search {
2 | flex-grow: 1;
3 | display: flex;
4 | background: var(--gray-superdark);
5 | border-radius: 1000px;
6 | }
7 |
8 | .search input {
9 | border: none !important;
10 | border-radius: 0 !important;
11 | font-size: 15px;
12 | line-height: 21px;
13 | padding: 9px 16px;
14 | }
15 |
16 | .search > svg {
17 | margin: 9px 16px;
18 | }
19 |
20 | @media (max-width: 768px) {
21 | .search {
22 | padding: unset;
23 | background: unset;
24 | }
25 |
26 | .search input {
27 | display: none;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Tasks/DonateTask.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 | import { Link } from "react-router-dom";
3 |
4 | import { BaseUITask } from "@/Components/Tasks/index";
5 |
6 | export class DonateTask extends BaseUITask {
7 | id = "donate";
8 |
9 | check(): boolean {
10 | return !this.state.muted;
11 | }
12 |
13 | render() {
14 | return (
15 | <>
16 |
17 |
22 |
23 |
24 |
27 |
28 | >
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Tasks/FollowMorePeople.tsx:
--------------------------------------------------------------------------------
1 | import { CachedMetadata } from "@snort/system";
2 | import { FormattedMessage } from "react-intl";
3 | import { Link } from "react-router-dom";
4 |
5 | import { BaseUITask } from "@/Components/Tasks/index";
6 | import { LoginSession } from "@/Utils/Login";
7 |
8 | export class FollowMorePeopleTask extends BaseUITask {
9 | id = "follow-more-people";
10 |
11 | check(_meta: CachedMetadata, session: LoginSession): boolean {
12 | return !this.state.muted && (session.state.follows?.length ?? 0) < 10;
13 | }
14 |
15 | render() {
16 | return (
17 | <>
18 |
19 |
24 |
25 |
26 | ),
27 | }}
28 | />
29 |
30 | >
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Tasks/Nip5Task.tsx:
--------------------------------------------------------------------------------
1 | import { CachedMetadata } from "@snort/system";
2 | import { FormattedMessage } from "react-intl";
3 | import { Link } from "react-router-dom";
4 |
5 | import { BaseUITask } from "@/Components/Tasks/index";
6 |
7 | export class Nip5Task extends BaseUITask {
8 | id = "buy-nip5";
9 |
10 | check(user: CachedMetadata): boolean {
11 | return !this.state.muted && !user.nip05;
12 | }
13 |
14 | render(): JSX.Element {
15 | return (
16 |
17 |
23 |
24 |
25 | ),
26 | }}
27 | />
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Tasks/TaskList.css:
--------------------------------------------------------------------------------
1 | .task-list a {
2 | text-decoration: underline;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Text/DisableMedia.tsx:
--------------------------------------------------------------------------------
1 | const DisableMedia = ({ content }: { content: string }) => (
2 | e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
3 | {content}
4 |
5 | );
6 |
7 | export default DisableMedia;
8 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Text/HighlightedText.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const HighlightedText = ({ content, textToHighlight }: { content: string; textToHighlight: string }) => {
4 | const textToHighlightArray = textToHighlight.trim().toLowerCase().split(" ");
5 | const re = new RegExp(`(${textToHighlightArray.join("|")})`, "gi");
6 | const splittedContent = content.split(re);
7 |
8 | const fragments = splittedContent.map((part, index) => {
9 | if (textToHighlightArray.includes(part.toLowerCase())) {
10 | return (
11 |
12 | {part}
13 |
14 | );
15 | } else {
16 | return part;
17 | }
18 | });
19 |
20 | return <>{fragments}>;
21 | };
22 |
23 | export default HighlightedText;
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Text/const.ts:
--------------------------------------------------------------------------------
1 | export const baseImageWidth = 910;
2 | export const gridConfigMap = new Map([
3 | [1, [[4, 3]]],
4 | [
5 | 2,
6 | [
7 | [2, 2],
8 | [2, 2],
9 | ],
10 | ],
11 | [
12 | 3,
13 | [
14 | [2, 2],
15 | [2, 1],
16 | [2, 1],
17 | ],
18 | ],
19 | [
20 | 4,
21 | [
22 | [2, 1],
23 | [2, 1],
24 | [2, 1],
25 | [2, 1],
26 | ],
27 | ],
28 | [
29 | 5,
30 | [
31 | [2, 1],
32 | [2, 1],
33 | [2, 1],
34 | [1, 1],
35 | [1, 1],
36 | ],
37 | ],
38 | [
39 | 6,
40 | [
41 | [2, 2],
42 | [1, 1],
43 | [1, 1],
44 | [2, 2],
45 | [1, 1],
46 | [1, 1],
47 | ],
48 | ],
49 | ]);
50 | export const ROW_HEIGHT = 140;
51 | export const GRID_GAP = 2;
52 |
--------------------------------------------------------------------------------
/packages/app/src/Components/Toaster/Toaster.css:
--------------------------------------------------------------------------------
1 | .toaster {
2 | position: fixed;
3 | bottom: 2px;
4 | left: 2px;
5 | display: flex;
6 | flex-direction: column-reverse;
7 | z-index: 9999;
8 | gap: 4px;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/AvatarEditor.css:
--------------------------------------------------------------------------------
1 | .avatar .edit,
2 | .banner .edit {
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | width: 100%;
7 | height: 100%;
8 | background-color: var(--bg-color);
9 | cursor: pointer;
10 | opacity: 0;
11 | border-radius: 100%;
12 | }
13 |
14 | .avatar .edit.new {
15 | opacity: 0.5;
16 | }
17 |
18 | .avatar .edit:hover {
19 | opacity: 0.5;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/AvatarGroup.tsx:
--------------------------------------------------------------------------------
1 | import { HexKey } from "@snort/system";
2 |
3 | import ProfileImage from "@/Components/User/ProfileImage";
4 |
5 | export function AvatarGroup({ ids, onClick, size }: { ids: HexKey[]; onClick?: () => void; size?: number }) {
6 | return ids.map((a, index) => (
7 | 0 ? "-ml-4" : ""}`} key={a} style={{ zIndex: ids.length - index }}>
8 |
18 |
19 | ));
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/BadgeList.css:
--------------------------------------------------------------------------------
1 | .badge-list {
2 | margin-top: 4px;
3 | display: flex;
4 | align-items: center;
5 | }
6 |
7 | .badge-item {
8 | width: 32px;
9 | height: 32px;
10 | object-fit: contain;
11 | cursor: pointer;
12 | }
13 |
14 | .badge-item:not(:last-child) {
15 | margin-right: 8px;
16 | }
17 |
18 | .badge-info {
19 | margin-left: 12px;
20 | display: flex;
21 | flex-direction: column;
22 | }
23 |
24 | .badge-info p {
25 | margin: 0;
26 | }
27 |
28 | .badge-info h3 {
29 | margin: 0;
30 | }
31 |
32 | .badges-item {
33 | align-items: flex-start;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/DisplayName.css:
--------------------------------------------------------------------------------
1 | .placeholder {
2 | color: var(--gray-light);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/DisplayName.tsx:
--------------------------------------------------------------------------------
1 | import "./DisplayName.css";
2 |
3 | import { HexKey, UserMetadata } from "@snort/system";
4 | import { useUserProfile } from "@snort/system-react";
5 | import classNames from "classnames";
6 | import { useMemo } from "react";
7 |
8 | import { getDisplayNameOrPlaceHolder } from "@/Utils";
9 |
10 | interface DisplayNameProps {
11 | pubkey: HexKey;
12 | user?: UserMetadata | undefined;
13 | className?: string;
14 | }
15 |
16 | const DisplayName = ({ pubkey, user, className }: DisplayNameProps) => {
17 | const profile = useUserProfile(user ? undefined : pubkey) ?? user;
18 | const [name, isPlaceHolder] = useMemo(() => getDisplayNameOrPlaceHolder(profile, pubkey), [profile, pubkey]);
19 |
20 | return {name};
21 | };
22 |
23 | export default DisplayName;
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/Following.css:
--------------------------------------------------------------------------------
1 | span.following {
2 | padding: 2px 4px;
3 | border-radius: 4px;
4 | font-size: 11px;
5 | color: var(--font-secondary-color);
6 | background-color: var(--gray-superdark);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/Following.tsx:
--------------------------------------------------------------------------------
1 | import "./Following.css";
2 |
3 | import { FormattedMessage } from "react-intl";
4 |
5 | import Icon from "@/Components/Icons/Icon";
6 | import useFollowsControls from "@/Hooks/useFollowControls";
7 |
8 | export function FollowingMark({ pubkey }: { pubkey: string }) {
9 | const { isFollowing } = useFollowsControls();
10 | const doesFollow = isFollowing(pubkey);
11 | if (!doesFollow) return;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/FollowsYou.css:
--------------------------------------------------------------------------------
1 | .follows-you {
2 | color: var(--font-secondary-color);
3 | font-size: var(--font-size-tiny);
4 | margin-left: 0.2em;
5 | font-weight: normal;
6 | padding: 4px 6px;
7 | background: var(--bg-secondary);
8 | border-radius: 6px;
9 | line-height: 1;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/FollowsYou.tsx:
--------------------------------------------------------------------------------
1 | import "./FollowsYou.css";
2 |
3 | import { useIntl } from "react-intl";
4 |
5 | import messages from "../messages";
6 |
7 | export interface FollowsYouProps {
8 | followsMe: boolean;
9 | }
10 |
11 | export default function FollowsYou({ followsMe }: FollowsYouProps) {
12 | const { formatMessage } = useIntl();
13 | return followsMe ? {formatMessage(messages.FollowsYou)} : null;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/MuteButton.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 |
3 | import useModeration from "@/Hooks/useModeration";
4 |
5 | import AsyncButton from "../Button/AsyncButton";
6 |
7 | interface MuteButtonProps {
8 | pubkey: string;
9 | }
10 |
11 | const MuteButton = ({ pubkey }: MuteButtonProps) => {
12 | const { mute, unmute, isMuted } = useModeration();
13 | return isMuted(pubkey) ? (
14 | unmute(pubkey)}>
15 |
16 |
17 | ) : (
18 | mute(pubkey)}>
19 |
20 |
21 | );
22 | };
23 |
24 | export default MuteButton;
25 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/NoteToSelf.css:
--------------------------------------------------------------------------------
1 | .nts {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | .nts .avatar-wrapper {
7 | margin-right: 8px;
8 | }
9 |
10 | .nts .avatar {
11 | border-width: 1px;
12 | width: 48px;
13 | height: 48px;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | }
18 |
19 | .nts .avatar.clickable {
20 | cursor: pointer;
21 | }
22 |
23 | .nts a {
24 | text-decoration: none;
25 | }
26 |
27 | .nts .name {
28 | margin-top: -0.2em;
29 | display: flex;
30 | flex-direction: column;
31 | font-weight: bold;
32 | }
33 |
34 | .nts .nip05 {
35 | margin: 0;
36 | margin-top: -0.2em;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/NoteToSelf.tsx:
--------------------------------------------------------------------------------
1 | import "./NoteToSelf.css";
2 |
3 | import classNames from "classnames";
4 | import { FormattedMessage } from "react-intl";
5 |
6 | import Icon from "@/Components/Icons/Icon";
7 |
8 | import messages from "../messages";
9 |
10 | export interface NoteToSelfProps {
11 | className?: string;
12 | }
13 |
14 | function NoteLabel() {
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default function NoteToSelf({ className }: NoteToSelfProps) {
23 | return (
24 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/ProfileCard.css:
--------------------------------------------------------------------------------
1 | .profile-card {
2 | width: 360px;
3 | border-radius: 16px;
4 | background: var(--gray-superdark);
5 | box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
6 | }
7 |
8 | .profile-card > div {
9 | color: white;
10 | padding: 8px 12px;
11 | }
12 |
13 | .light .profile-card > div {
14 | color: black;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/ProfileLink.tsx:
--------------------------------------------------------------------------------
1 | import { CachedMetadata, UserMetadata } from "@snort/system";
2 | import { ReactNode } from "react";
3 | import { Link, LinkProps } from "react-router-dom";
4 |
5 | import { useProfileLink } from "@/Hooks/useProfileLink";
6 |
7 | export function ProfileLink({
8 | pubkey,
9 | user,
10 | explicitLink,
11 | children,
12 | ...others
13 | }: {
14 | pubkey: string;
15 | user?: UserMetadata | CachedMetadata;
16 | explicitLink?: string;
17 | children?: ReactNode;
18 | } & Omit) {
19 | const link = useProfileLink(pubkey, user);
20 | const oFiltered = others as Record;
21 | delete oFiltered["user"];
22 | delete oFiltered["link"];
23 | delete oFiltered["children"];
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/UserWebsiteLink.css:
--------------------------------------------------------------------------------
1 | .user-profile-link {
2 | display: flex;
3 | align-items: center;
4 | gap: 8px;
5 | }
6 |
7 | .user-profile-link a {
8 | text-decoration: none;
9 | }
10 |
11 | .user-profile-link a:hover {
12 | text-decoration: underline;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/UserWebsiteLink.tsx:
--------------------------------------------------------------------------------
1 | import "./UserWebsiteLink.css";
2 |
3 | import { CachedMetadata, UserMetadata } from "@snort/system";
4 |
5 | import Icon from "@/Components/Icons/Icon";
6 |
7 | export function UserWebsiteLink({ user }: { user?: CachedMetadata | UserMetadata }) {
8 | const website_url =
9 | user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
10 |
11 | function tryFormatWebsite(url: string) {
12 | try {
13 | const u = new URL(url);
14 | return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`;
15 | } catch {
16 | // ignore
17 | }
18 | return url;
19 | }
20 |
21 | if (user?.website) {
22 | return (
23 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Components/User/Username.tsx:
--------------------------------------------------------------------------------
1 | import { HexKey } from "@snort/system";
2 | import { useUserProfile } from "@snort/system-react";
3 |
4 | import DisplayName from "./DisplayName";
5 | import { ProfileLink } from "./ProfileLink";
6 |
7 | export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) {
8 | const user = useUserProfile(pubkey);
9 |
10 | return user ? (
11 |
12 |
13 |
14 | ) : null;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/app/src/Components/WarningNotice/WarningNotice.css:
--------------------------------------------------------------------------------
1 | .warning-notice {
2 | color: var(--font-tertiary-color);
3 | border: 1px solid var(--border-color);
4 | padding: 8px 16px;
5 | border-radius: 12px;
6 | display: flex;
7 | gap: 8px;
8 | }
9 |
10 | .warning-notice i {
11 | font-style: normal;
12 | color: var(--font-color);
13 | }
14 |
15 | .warning-notice > svg {
16 | color: var(--warning);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/WarningNotice/WarningNotice.tsx:
--------------------------------------------------------------------------------
1 | import "./WarningNotice.css";
2 |
3 | import Icon from "@/Components/Icons/Icon";
4 |
5 | export function WarningNotice({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
6 | return (
7 | {
10 | e.stopPropagation();
11 | onClick?.();
12 | }}>
13 |
14 |
{children}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Components/ZapModal/SuccessAction.tsx:
--------------------------------------------------------------------------------
1 | import { LNURLSuccessAction } from "@snort/shared";
2 | import React from "react";
3 | import { FormattedMessage } from "react-intl";
4 |
5 | import Icon from "@/Components/Icons/Icon";
6 |
7 | export function SuccessAction({ success }: { success: LNURLSuccessAction }) {
8 | return (
9 |
10 |
11 |
12 | {success?.description ?? }
13 |
14 | {success.url && (
15 |
16 |
17 | {success.url}
18 |
19 |
20 | )}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/Components/ZapModal/ZapType.tsx:
--------------------------------------------------------------------------------
1 | export enum ZapType {
2 | PublicZap = 1,
3 | AnonZap = 2,
4 | PrivateZap = 3,
5 | NonZap = 4,
6 | }
7 |
--------------------------------------------------------------------------------
/packages/app/src/Components/zap-amount.tsx:
--------------------------------------------------------------------------------
1 | import { formatShort } from "@/Utils/Number";
2 |
3 | import Icon from "./Icons/Icon";
4 |
5 | export default function ZapAmount({ n }: { n: number }) {
6 | return (
7 |
8 |
9 | {formatShort(n)}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/app/src/External/NostrBand.ts:
--------------------------------------------------------------------------------
1 | export default class NostrBandApi {
2 | readonly #url = "https://api.nostr.band";
3 | readonly #supportedLangs = ["en", "de", "ja", "zh", "th", "pt", "es", "fr"];
4 |
5 | trendingProfilesUrl() {
6 | return `${this.#url}/v0/trending/profiles`;
7 | }
8 |
9 | trendingNotesUrl(lang?: string) {
10 | return `${this.#url}/v0/trending/notes${lang && this.#supportedLangs.includes(lang) ? `?lang=${lang}` : ""}`;
11 | }
12 |
13 | suggestedFollowsUrl(pubkey: string) {
14 | return `${this.#url}/v0/suggested/profiles/${pubkey}`;
15 | }
16 |
17 | trendingHashtagsUrl(lang?: string) {
18 | return `${this.#url}/v0/trending/hashtags${lang && this.#supportedLangs.includes(lang) ? `?lang=${lang}` : ""}`;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/External/NostrServices.ts:
--------------------------------------------------------------------------------
1 | import { JsonApi } from ".";
2 |
3 | export interface LinkPreviewData {
4 | title?: string;
5 | description?: string;
6 | image?: string;
7 | og_tags?: Array<[name: string, value: string]>;
8 | }
9 |
10 | export class NostrServices extends JsonApi {
11 | constructor(readonly url: string) {
12 | super();
13 | url = url.endsWith("/") ? url.slice(0, -1) : url;
14 | }
15 |
16 | linkPreview(url: string) {
17 | return this.getJson(`/api/v1/preview?url=${encodeURIComponent(url)}`);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/External/SemisolDev.ts:
--------------------------------------------------------------------------------
1 | export default class SemisolDevApi {
2 | readonly #url = "https://api.semisol.dev";
3 |
4 | suggestedFollowsUrl(pubkey: string, follows: Array) {
5 | const query = new URLSearchParams({ pubkey, follows: JSON.stringify(follows) });
6 | return `${this.#url}/nosgraph/v1/recommend?${query.toString()}`;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/ArticlesFeed.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | import useFollowsControls from "@/Hooks/useFollowControls";
6 |
7 | export function useArticles() {
8 | const { followList } = useFollowsControls();
9 |
10 | const sub = useMemo(() => {
11 | const rb = new RequestBuilder("articles");
12 | if (followList.length > 0) {
13 | rb.withFilter().kinds([EventKind.LongFormTextNote]).authors(followList);
14 | }
15 | return rb;
16 | }, [followList]);
17 |
18 | return useRequestBuilder(sub);
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/FollowersFeed.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, HexKey, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | import useWoT from "@/Hooks/useWoT";
6 |
7 | export default function useFollowersFeed(pubkey?: HexKey) {
8 | const wot = useWoT();
9 | const sub = useMemo(() => {
10 | const b = new RequestBuilder(`followers`);
11 | if (pubkey) {
12 | b.withFilter().kinds([EventKind.ContactList]).tag("p", [pubkey]);
13 | }
14 | return b;
15 | }, [pubkey]);
16 |
17 | const followersFeed = useRequestBuilder(sub);
18 |
19 | const followers = useMemo(() => {
20 | const contactLists = followersFeed?.filter(
21 | a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey),
22 | );
23 | return wot.sortEvents(contactLists);
24 | }, [followersFeed, pubkey]);
25 |
26 | return followers;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/FollowsFeed.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, HexKey, RequestBuilder, TaggedNostrEvent } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | export default function useFollowsFeed(pubkey?: HexKey) {
6 | const sub = useMemo(() => {
7 | const b = new RequestBuilder(`follows:for`);
8 | if (pubkey) {
9 | b.withFilter().kinds([EventKind.ContactList]).authors([pubkey]);
10 | }
11 | return b;
12 | }, [pubkey]);
13 |
14 | const contactFeed = useRequestBuilder(sub);
15 | return useMemo(() => {
16 | return getFollowing(contactFeed ?? [], pubkey);
17 | }, [contactFeed, pubkey]);
18 | }
19 |
20 | export function getFollowing(notes: readonly TaggedNostrEvent[], pubkey?: HexKey) {
21 | const contactLists = notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
22 | const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
23 | return [...new Set(pTags?.flat())];
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/HashtagsFeed.ts:
--------------------------------------------------------------------------------
1 | import { removeUndefined, unixNow } from "@snort/shared";
2 | import { EventKind, RequestBuilder } from "@snort/system";
3 | import { useRequestBuilder } from "@snort/system-react";
4 | import { useMemo } from "react";
5 |
6 | import useLogin from "@/Hooks/useLogin";
7 | import { Hour } from "@/Utils/Const";
8 |
9 | export default function useHashtagsFeed() {
10 | const { hashtags } = useLogin(s => ({ hashtags: s.state.getList(EventKind.InterestsList) }));
11 | const sub = useMemo(() => {
12 | const rb = new RequestBuilder("hashtags-feed");
13 | rb.withFilter()
14 | .kinds([EventKind.TextNote, EventKind.LiveEvent, EventKind.LongFormTextNote, EventKind.Polls])
15 | .tag("t", removeUndefined(hashtags.map(a => a.toEventTag()?.[1])))
16 | .since(unixNow() - Hour * 6);
17 | return rb;
18 | }, [hashtags]);
19 |
20 | return {
21 | data: useRequestBuilder(sub),
22 | hashtags,
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/RelayState.ts:
--------------------------------------------------------------------------------
1 | import { SnortContext } from "@snort/system-react";
2 | import { useContext } from "react";
3 |
4 | export default function useRelayState(addr: string) {
5 | const system = useContext(SnortContext);
6 | const c = system.pool.getConnection(addr);
7 | return c;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/RelaysFeed.tsx:
--------------------------------------------------------------------------------
1 | import { EventKind, HexKey, parseRelayTags, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | export default function useRelaysFeed(pubkey?: HexKey) {
6 | const sub = useMemo(() => {
7 | const b = new RequestBuilder(`relays:${pubkey ?? ""}`);
8 | if (pubkey) {
9 | b.withFilter().authors([pubkey]).kinds([EventKind.Relays]);
10 | }
11 | return b;
12 | }, [pubkey]);
13 |
14 | const relays = useRequestBuilder(sub);
15 | return parseRelayTags(relays[0]?.tags.filter(a => a[0] === "r") ?? []);
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/StatusFeed.ts:
--------------------------------------------------------------------------------
1 | import { unixNow } from "@snort/shared";
2 | import { EventKind, RequestBuilder } from "@snort/system";
3 | import { useRequestBuilder } from "@snort/system-react";
4 | import { useMemo } from "react";
5 |
6 | import { findTag } from "@/Utils";
7 |
8 | export function useStatusFeed(id?: string, leaveOpen = false) {
9 | const sub = useMemo(() => {
10 | const rb = new RequestBuilder(`statud:${id}`);
11 | rb.withOptions({ leaveOpen });
12 | if (id) {
13 | rb.withFilter()
14 | .kinds([30315 as EventKind])
15 | .authors([id]);
16 | }
17 | return rb;
18 | }, [id]);
19 |
20 | const status = useRequestBuilder(sub);
21 |
22 | const statusFiltered = status.filter(a => {
23 | const exp = Number(findTag(a, "expiration"));
24 | return isNaN(exp) || exp >= unixNow();
25 | });
26 | const general = statusFiltered?.find(a => findTag(a, "d") === "general");
27 | const music = statusFiltered?.find(a => findTag(a, "d") === "music");
28 |
29 | return {
30 | general,
31 | music,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/WorkerRelayView.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | import useLogin from "@/Hooks/useLogin";
6 |
7 | export function useNotificationsView() {
8 | const publicKey = useLogin(s => s.publicKey);
9 | const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
10 | const req = useMemo(() => {
11 | const rb = new RequestBuilder("notifications");
12 | rb.withOptions({
13 | leaveOpen: true,
14 | });
15 | if (publicKey) {
16 | rb.withFilter().kinds(kinds).tag("p", [publicKey]).limit(100);
17 | }
18 | return rb;
19 | }, [publicKey]);
20 | return useRequestBuilder(req);
21 | }
22 |
--------------------------------------------------------------------------------
/packages/app/src/Feed/ZapsFeed.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, NostrLink, parseZap, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 | import { useMemo } from "react";
4 |
5 | export default function useZapsFeed(link?: NostrLink) {
6 | const sub = useMemo(() => {
7 | const b = new RequestBuilder(`zaps:${link?.encode()}`);
8 | if (link) {
9 | b.withFilter().kinds([EventKind.ZapReceipt]).replyToLink([link]);
10 | }
11 | return b;
12 | }, [link]);
13 |
14 | const zapsFeed = useRequestBuilder(sub);
15 |
16 | const zaps = useMemo(() => {
17 | if (zapsFeed) {
18 | const parsedZaps = zapsFeed.map(a => parseZap(a)).filter(z => z.valid);
19 | return parsedZaps.sort((a, b) => b.amount - a.amount);
20 | }
21 | return [];
22 | }, [zapsFeed]);
23 |
24 | return zaps;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useAppHandler.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
2 | import { useRequestBuilder } from "@snort/system-react";
3 |
4 | import useFollowsControls from "./useFollowControls";
5 |
6 | export default function useAppHandler(kind: EventKind) {
7 | const { followList } = useFollowsControls();
8 |
9 | const sub = new RequestBuilder(`app-handler:${kind}`);
10 | sub
11 | .withFilter()
12 | .kinds([31990 as EventKind])
13 | .tag("k", [kind.toString()]);
14 |
15 | const dataApps = useRequestBuilder(sub);
16 |
17 | const reccomendsSub = new RequestBuilder(`app-handler:${kind}:recommends`);
18 | if (dataApps.length > 0 && followList.length > 0) {
19 | reccomendsSub
20 | .withFilter()
21 | .kinds([31989 as EventKind])
22 | .replyToLink(dataApps.map(a => NostrLink.fromEvent(a)))
23 | .authors(followList);
24 | }
25 |
26 | const dataRecommends = useRequestBuilder(reccomendsSub);
27 | return {
28 | reccomends: dataRecommends,
29 | apps: dataApps,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useCommunityLeaders.tsx:
--------------------------------------------------------------------------------
1 | import { unwrap } from "@snort/shared";
2 | import { EventKind, parseNostrLink } from "@snort/system";
3 | import { useEffect, useSyncExternalStore } from "react";
4 |
5 | import { LeadersStore } from "@/Cache/CommunityLeadersStore";
6 |
7 | import { useLinkList } from "./useLists";
8 |
9 | export function useCommunityLeaders() {
10 | const link = CONFIG.communityLeaders ? parseNostrLink(unwrap(CONFIG.communityLeaders).list) : undefined;
11 |
12 | const list = useLinkList("leaders", rb => {
13 | if (link) {
14 | rb.withFilter().kinds([EventKind.FollowSet]).link(link);
15 | }
16 | });
17 |
18 | useEffect(() => {
19 | LeadersStore.setLeaders(list.map(a => a.id));
20 | }, [list]);
21 | }
22 |
23 | export function useCommunityLeader(pubkey?: string) {
24 | const store = useSyncExternalStore(
25 | c => LeadersStore.hook(c),
26 | () => LeadersStore.snapshot(),
27 | );
28 |
29 | return pubkey && store.includes(pubkey);
30 | }
31 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useCopy.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export const useCopy = (timeout = 2000) => {
4 | const [error, setError] = useState(false);
5 | const [copied, setCopied] = useState(false);
6 |
7 | const copy = async (text: string) => {
8 | setError(false);
9 | try {
10 | if (navigator.clipboard && window.isSecureContext) {
11 | await navigator.clipboard.writeText(text);
12 | } else {
13 | const textArea = document.createElement("textarea");
14 | textArea.value = text;
15 | textArea.style.position = "absolute";
16 | textArea.style.opacity = "0";
17 | document.body.appendChild(textArea);
18 | textArea.select();
19 | await document.execCommand("copy");
20 | textArea.remove();
21 | }
22 | setCopied(true);
23 | } catch (error) {
24 | setError(true);
25 | }
26 |
27 | setTimeout(() => setCopied(false), timeout);
28 | };
29 |
30 | return { error, copied, copy };
31 | };
32 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useDiscoverMediaServers.ts:
--------------------------------------------------------------------------------
1 | import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
2 | import { EventKind, RequestBuilder } from "@snort/system";
3 | import { useRequestBuilder } from "@snort/system-react";
4 | import { useMemo } from "react";
5 |
6 | export default function useDiscoverMediaServers() {
7 | const sub = useMemo(() => {
8 | const rb = new RequestBuilder("media-servers-all");
9 | rb.withFilter().kinds([EventKind.BlossomServerList]);
10 | return rb;
11 | }, []);
12 |
13 | const data = useRequestBuilder(sub);
14 |
15 | return data.reduce(
16 | (acc, v) => {
17 | const servers = removeUndefined(v.tags.filter(a => a[0] === "server").map(a => sanitizeRelayUrl(a[1])));
18 | for (const server of servers) {
19 | acc[server] ??= 0;
20 | acc[server]++;
21 | }
22 | return acc;
23 | },
24 | {} as Record,
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useEventPublisher.tsx:
--------------------------------------------------------------------------------
1 | import { SnortContext } from "@snort/system-react";
2 | import { useContext } from "react";
3 |
4 | import useLogin from "@/Hooks/useLogin";
5 | import { createPublisher, LoginStore, sessionNeedsPin } from "@/Utils/Login";
6 |
7 | export default function useEventPublisher() {
8 | const login = useLogin();
9 | const system = useContext(SnortContext);
10 |
11 | let existing = LoginStore.getPublisher(login.id);
12 |
13 | if (login.publicKey && !existing && !sessionNeedsPin(login)) {
14 | existing = createPublisher(login);
15 | if (existing) {
16 | LoginStore.setPublisher(login.id, existing);
17 | }
18 | }
19 | return {
20 | publisher: existing,
21 | system,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useHorizontalScroll.tsx:
--------------------------------------------------------------------------------
1 | import { LegacyRef, useEffect, useRef } from "react";
2 |
3 | function useHorizontalScroll() {
4 | const elRef = useRef();
5 | useEffect(() => {
6 | const el = elRef.current;
7 | if (el) {
8 | const onWheel = (ev: WheelEvent) => {
9 | if (ev.deltaY == 0) return;
10 | ev.preventDefault();
11 | el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" });
12 | };
13 | el.addEventListener("wheel", onWheel);
14 | return () => el.removeEventListener("wheel", onWheel);
15 | }
16 | }, []);
17 | return elRef as LegacyRef | undefined;
18 | }
19 |
20 | export default useHorizontalScroll;
21 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useLiveStreams.ts:
--------------------------------------------------------------------------------
1 | import { unixNow } from "@snort/shared";
2 | import { EventKind, RequestBuilder } from "@snort/system";
3 | import { useRequestBuilder } from "@snort/system-react";
4 | import { useMemo } from "react";
5 |
6 | import { findTag } from "@/Utils";
7 | import { Hour } from "@/Utils/Const";
8 |
9 | export default function useLiveStreams() {
10 | const sub = useMemo(() => {
11 | const rb = new RequestBuilder("streams");
12 | rb.withFilter()
13 | .kinds([EventKind.LiveEvent])
14 | .since(unixNow() - 4 * Hour);
15 | return rb;
16 | }, []);
17 |
18 | return useRequestBuilder(sub)
19 | .filter(a => {
20 | return findTag(a, "status") === "live";
21 | })
22 | .sort((a, b) => {
23 | const sA = Number(findTag(a, "starts"));
24 | const sB = Number(findTag(b, "starts"));
25 | return sA > sB ? -1 : 1;
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useLoading.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default function useLoading(fn: ((e: React.MouseEvent) => Promise | T) | undefined, disabled?: boolean) {
4 | const [loading, setLoading] = useState(false);
5 |
6 | async function handle(e: React.MouseEvent) {
7 | e.preventDefault();
8 | if (loading || disabled) return;
9 | setLoading(true);
10 | try {
11 | if (typeof fn === "function") {
12 | await fn(e);
13 | }
14 | } catch (e) {
15 | console.error(e);
16 | } finally {
17 | setLoading(false);
18 | }
19 | }
20 |
21 | return { handle, loading };
22 | }
23 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useLogin.tsx:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
2 |
3 | import { LoginSession, LoginStore } from "@/Utils/Login";
4 |
5 | export default function useLogin(selector?: (v: LoginSession) => T) {
6 | const defaultSelector = (v: LoginSession) => v as unknown as T;
7 |
8 | return useSyncExternalStoreWithSelector(
9 | s => LoginStore.hook(s),
10 | () => LoginStore.snapshot(),
11 | undefined,
12 | selector || defaultSelector,
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/usePageDimensions.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 |
3 | export default function usePageDimensions() {
4 | const ref = useRef(document.querySelector("#root"));
5 | const [dimensions, setDimensions] = useState({
6 | width: ref.current?.clientWidth ?? 0,
7 | height: ref.current?.clientHeight ?? 0,
8 | });
9 |
10 | useEffect(() => {
11 | if (ref.current && "ResizeObserver" in window) {
12 | const observer = new ResizeObserver(entries => {
13 | if (entries[0].target === ref.current) {
14 | const { width, height } = entries[0].contentRect;
15 | setDimensions({ width, height });
16 | }
17 | });
18 |
19 | observer.observe(ref.current);
20 |
21 | return () => observer.disconnect();
22 | }
23 | }, [ref]);
24 |
25 | return dimensions;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/usePreferences.ts:
--------------------------------------------------------------------------------
1 | import { DefaultPreferences, updateAppData, UserPreferences } from "@/Utils/Login";
2 |
3 | import useLogin from "./useLogin";
4 |
5 | export default function usePreferences(selector?: (v: UserPreferences) => T): T {
6 | const defaultSelector = (v: UserPreferences) => v as unknown as T;
7 | return useLogin(s => {
8 | const pref = s.state.appdata?.preferences ?? {
9 | ...DefaultPreferences,
10 | ...CONFIG.defaultPreferences,
11 | };
12 |
13 | return (selector || defaultSelector)(pref);
14 | });
15 | }
16 |
17 | export function useAllPreferences() {
18 | const { id, pref } = useLogin(s => {
19 | const pref = s.state.appdata?.preferences ?? {
20 | ...DefaultPreferences,
21 | ...CONFIG.defaultPreferences,
22 | };
23 |
24 | return {
25 | id: s.id,
26 | pref: pref,
27 | };
28 | });
29 | return {
30 | preferences: pref,
31 | update: async (data: UserPreferences) => {
32 | await updateAppData(id, d => {
33 | return { ...d, preferences: data };
34 | });
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useProfileLink.ts:
--------------------------------------------------------------------------------
1 | import { CachedMetadata, NostrLink, UserMetadata } from "@snort/system";
2 | import { SnortContext } from "@snort/system-react";
3 | import { useContext } from "react";
4 |
5 | import { randomSample } from "@/Utils";
6 |
7 | export function useProfileLink(pubkey?: string, user?: UserMetadata | CachedMetadata) {
8 | const system = useContext(SnortContext);
9 | if (!pubkey) return "#";
10 | const relays = system.relayCache
11 | .getFromCache(pubkey)
12 | ?.relays?.filter(a => a.settings.write)
13 | ?.map(a => a.url);
14 |
15 | if (
16 | user?.nip05 &&
17 | user.nip05.endsWith(`@${CONFIG.nip05Domain}`) &&
18 | (!("isNostrAddressValid" in user) || user.isNostrAddressValid)
19 | ) {
20 | const [username] = user.nip05.split("@");
21 | return `/${username}`;
22 | }
23 | const link = NostrLink.profile(pubkey, relays ? randomSample(relays, 3) : undefined);
24 | return `/${link.encode(CONFIG.profileLinkPrefix)}`;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useRelays.tsx:
--------------------------------------------------------------------------------
1 | import useLogin from "./useLogin";
2 |
3 | export default function useRelays() {
4 | const relays = useLogin(s => s.state.relays);
5 | return relays ? Object.fromEntries(relays.map(a => [a.url, a.settings])) : CONFIG.defaultRelays;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useTextTransformCache.tsx:
--------------------------------------------------------------------------------
1 | import { transformText } from "@snort/system";
2 |
3 | import { TextCache } from "@/Cache/TextCache";
4 |
5 | export function transformTextCached(id: string, content: string, tags: Array>) {
6 | if (content.length > 0) {
7 | const cached = TextCache.get(id);
8 | if (cached) return cached;
9 | const newCache = transformText(content, tags);
10 | TextCache.set(id, newCache);
11 | return newCache;
12 | }
13 | return [];
14 | }
15 |
16 | export function useTextTransformer(id: string, content: string, tags: Array>) {
17 | return transformTextCached(id, content, tags);
18 | }
19 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useTheme.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | import usePreferences from "./usePreferences";
4 |
5 | export function useTheme() {
6 | const theme = usePreferences(s => s.theme);
7 |
8 | function setTheme(theme: "light" | "dark") {
9 | const elm = document.documentElement;
10 | if (theme === "light" && !elm.classList.contains("light")) {
11 | elm.classList.add("light");
12 | } else if (theme === "dark" && elm.classList.contains("light")) {
13 | elm.classList.remove("light");
14 | }
15 | }
16 |
17 | useEffect(() => {
18 | const osTheme = window.matchMedia("(prefers-color-scheme: light)");
19 | setTheme(theme === "system" && osTheme.matches ? "light" : theme === "light" ? "light" : "dark");
20 |
21 | osTheme.onchange = e => {
22 | if (theme === "system") {
23 | setTheme(e.matches ? "light" : "dark");
24 | }
25 | };
26 | return () => {
27 | osTheme.onchange = null;
28 | };
29 | }, [theme]);
30 | }
31 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useTimelineChunks.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export interface WindowChunk {
4 | since: number;
5 | until: number;
6 | }
7 |
8 | export default function useTimelineChunks(opt: { window?: number; firstChunkSize?: number; now: number }) {
9 | const [windowSize] = useState(opt.window ?? 60 * 60 * 2);
10 | const [windows, setWindows] = useState(1);
11 |
12 | let offset = opt.now;
13 | const chunks: Array = [];
14 | for (let x = 0; x < windows; x++) {
15 | // offset from now going backwards in time
16 | const size = x === 0 && opt.firstChunkSize ? opt.firstChunkSize : windowSize;
17 | chunks.push({
18 | since: offset - size,
19 | until: offset,
20 | });
21 | offset -= size;
22 | }
23 |
24 | return {
25 | now: opt.now,
26 | chunks,
27 | showMore: () => {
28 | setWindows(s => s + 1);
29 | },
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useTimelineWindow.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default function useTimelineWindow(opt: { window?: number; now: number }) {
4 | const [window] = useState(opt.window ?? 60 * 60 * 2);
5 | const [until, setUntil] = useState(opt.now);
6 | const [since, setSince] = useState(opt.now - window);
7 |
8 | return {
9 | now: opt.now,
10 | since,
11 | until,
12 | setUntil,
13 | older: () => {
14 | setUntil(s => s - window);
15 | setSince(s => s - window);
16 | },
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/packages/app/src/Hooks/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useWindowSize() {
4 | const [dims, setDims] = useState({
5 | width: window.innerWidth,
6 | height: window.innerHeight,
7 | });
8 |
9 | useEffect(() => {
10 | const handler = () => {
11 | setDims({
12 | width: window.innerWidth,
13 | height: window.innerHeight,
14 | });
15 | };
16 | window.addEventListener("resize", handler);
17 | return () => window.removeEventListener("resize", handler);
18 | }, []);
19 | return dims;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/About.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 |
3 | import Changelog from "@/../CHANGELOG.md?raw";
4 | import { Markdown } from "@/Components/Event/Markdown";
5 |
6 | export function AboutPage() {
7 | const version = document.querySelector("meta[name='application-name']")?.getAttribute("content");
8 | return (
9 |
10 |
11 |
12 |
13 | Version: {version?.split(":")?.at(1) ?? "unknown version"}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Deck/Articles.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import Note, { NoteProps } from "@/Components/Event/EventComponent";
4 | import { useArticles } from "@/Feed/ArticlesFeed";
5 | import { DeckContext } from "@/Pages/Deck/DeckLayout";
6 |
7 | export default function Articles({ noteProps }: { noteProps?: Omit }) {
8 | const data = useArticles();
9 | const deck = useContext(DeckContext);
10 |
11 | return (
12 | <>
13 | {data.map(a => (
14 | {
23 | deck?.setArticle(ev);
24 | noteProps?.onClick?.(ev);
25 | }}
26 | />
27 | ))}
28 | >
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Deck/Deck.css:
--------------------------------------------------------------------------------
1 | .deck-layout {
2 | display: flex;
3 | height: 100vh;
4 | overflow-y: hidden;
5 | }
6 |
7 | .deck-layout .deck-cols {
8 | display: flex;
9 | height: 100vh;
10 | overflow-y: hidden;
11 | overflow-x: auto;
12 | }
13 |
14 | .deck-layout .deck-cols .deck-col-header {
15 | padding: 8px 16px;
16 | border: 1px solid var(--border-color);
17 | border-collapse: collapse;
18 | font-size: 20px;
19 | font-weight: 700;
20 | min-height: 40px;
21 | max-height: 40px;
22 | }
23 |
24 | .deck-layout .deck-cols .deck-col-header:not(:last-of-type) {
25 | border-right: 0;
26 | }
27 |
28 | .deck-layout .deck-cols > div {
29 | display: flex;
30 | flex-direction: column;
31 | height: 100vh;
32 | width: 550px;
33 | min-width: 550px;
34 | }
35 |
36 | .deck-layout .deck-cols > div > div:not(:first-of-type) {
37 | overflow-y: scroll;
38 | }
39 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/FreeNostrAddressPage.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 |
3 | import IrisAccount from "@/Components/IrisAccount/IrisAccount";
4 |
5 | import messages from "./messages";
6 |
7 | export default function FreeNostrAddressPage() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | -
21 |
22 |
23 | -
24 |
25 |
26 | -
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/HelpPage.tsx:
--------------------------------------------------------------------------------
1 | import { encodeTLVEntries, NostrPrefix, TLVEntryType } from "@snort/system";
2 | import { FormattedMessage } from "react-intl";
3 | import { Link } from "react-router-dom";
4 |
5 | import { bech32ToHex } from "@/Utils";
6 | import { KieranPubKey } from "@/Utils/Const";
7 |
8 | export default function HelpPage() {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 |
26 | Kieran
27 |
28 | ),
29 | }}
30 | />
31 |
32 | >
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Layout/HasNotificationsMarker.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 |
3 | import useLogin from "@/Hooks/useLogin";
4 |
5 | export function HasNotificationsMarker() {
6 | const readNotifications = useLogin(s => s.readNotifications);
7 | const latestNotification = 0; // TODO: get latest timestamp
8 | const hasNotifications = useMemo(() => latestNotification * 1000 > readNotifications, [readNotifications]);
9 |
10 | if (hasNotifications) {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Layout/Layout.css:
--------------------------------------------------------------------------------
1 | .has-unread {
2 | background: var(--highlight);
3 | border-radius: 100%;
4 | width: 9px;
5 | height: 9px;
6 | }
7 |
8 | .stalker {
9 | position: fixed;
10 | top: 0;
11 | width: 100vw;
12 | height: 100vh;
13 | box-shadow: 0px 0px 26px 0px rgba(139, 92, 246, 0.7) inset;
14 | pointer-events: none;
15 | }
16 |
17 | .stalker button {
18 | position: absolute;
19 | top: 50px;
20 | right: 50px;
21 | color: black;
22 | background-color: var(--btn-color);
23 | padding: 12px;
24 | pointer-events: all !important;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Messages/ChatParticipant.tsx:
--------------------------------------------------------------------------------
1 | import { CachedMetadata } from "@snort/system";
2 |
3 | import { ChatParticipant } from "@/chat";
4 | import NoteToSelf from "@/Components/User/NoteToSelf";
5 | import ProfileImage from "@/Components/User/ProfileImage";
6 | import useLogin from "@/Hooks/useLogin";
7 |
8 | export function ChatParticipantProfile({ participant }: { participant: ChatParticipant }) {
9 | const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
10 | if (participant.id === publicKey) {
11 | return ;
12 | }
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Messages/DM.css:
--------------------------------------------------------------------------------
1 | .dm-gradient {
2 | background: var(--dm-gradient);
3 | }
4 |
5 | .other {
6 | background: var(--gray-superdark);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Messages/UnreadCount.css:
--------------------------------------------------------------------------------
1 | .pill {
2 | color: var(--font-color);
3 | font-size: var(--font-size-small);
4 | display: inline-block;
5 | background-color: var(--gray-secondary);
6 | padding: 2px 8px;
7 | border-radius: 10px;
8 | user-select: none;
9 | margin: 2px 5px;
10 | }
11 |
12 | .pill.unread {
13 | background-color: var(--highlight);
14 | color: var(--font-color);
15 | }
16 |
17 | .pill:hover {
18 | cursor: pointer;
19 | }
20 |
21 | .light .pill.unread {
22 | color: white;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Messages/UnreadCount.tsx:
--------------------------------------------------------------------------------
1 | import "./UnreadCount.css";
2 |
3 | const UnreadCount = ({ unread }: { unread: number }) => {
4 | return 0 ? "unread" : ""}`}>{unread};
5 | };
6 |
7 | export default UnreadCount;
8 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Notifications/notificationContext.tsx:
--------------------------------------------------------------------------------
1 | import { EventKind, NostrLink, NostrPrefix } from "@snort/system";
2 | import { useEventFeed } from "@snort/system-react";
3 |
4 | import { LiveEvent } from "@/Components/LiveStream/LiveEvent";
5 | import Text from "@/Components/Text/Text";
6 | import ProfilePreview from "@/Components/User/ProfilePreview";
7 |
8 | export function NotificationContext({ link }: { link: NostrLink }) {
9 | const ev = useEventFeed(link);
10 | if (link.type === NostrPrefix.PublicKey) {
11 | return >} />;
12 | }
13 | if (!ev) return;
14 | if (ev.kind === EventKind.LiveEvent) {
15 | return ;
16 | }
17 | return (
18 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Profile/MusicStatus.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { ProxyImg } from "@/Components/ProxyImg";
4 | import { useStatusFeed } from "@/Feed/StatusFeed";
5 | import { findTag, unwrap } from "@/Utils";
6 |
7 | export const MusicStatus = ({ id }: { id: string }) => {
8 | const status = useStatusFeed(id, true);
9 |
10 | if (!status.music) return null;
11 |
12 | const link = findTag(status.music, "r");
13 | const cover = findTag(status.music, "cover");
14 |
15 | const content = (
16 |
17 | {cover &&
}
18 | 🎵 {unwrap(status.music).content}
19 |
20 | );
21 |
22 | return link ? (
23 |
24 | {content}
25 |
26 | ) : (
27 | content
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Profile/ProfileTabType.tsx:
--------------------------------------------------------------------------------
1 | export enum ProfileTabType {
2 | NOTES = 0,
3 | REACTIONS = 1,
4 | FOLLOWERS = 2,
5 | FOLLOWS = 3,
6 | ZAPS = 4,
7 | MUTED = 5,
8 | RELAYS = 7,
9 | BOOKMARKS = 8,
10 | }
11 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/ConversationsTab.tsx:
--------------------------------------------------------------------------------
1 | import TimelineFollows from "@/Components/Feed/TimelineFollows";
2 |
3 | export const ConversationsTab = () => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/DefaultTab.tsx:
--------------------------------------------------------------------------------
1 | import useLogin from "@/Hooks/useLogin";
2 | import usePreferences from "@/Hooks/usePreferences";
3 | import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
4 |
5 | export const DefaultTab = () => {
6 | const { publicKey } = useLogin(s => ({
7 | publicKey: s.publicKey,
8 | }));
9 | const defaultRootTab = usePreferences(s => s.defaultRootTab);
10 | const tab = publicKey ? defaultRootTab : `trending/notes`;
11 | const elm = RootTabRoutes.find(a => a.path === tab)?.element;
12 | return elm ?? RootTabRoutes.find(a => a.path === defaultRootTab)?.element;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/FollowedByFriendsTab.tsx:
--------------------------------------------------------------------------------
1 | import Timeline from "@/Components/Feed/Timeline";
2 | import { TimelineSubject } from "@/Feed/TimelineFeed";
3 | import useLogin from "@/Hooks/useLogin";
4 |
5 | export const FollowedByFriendsTab = () => {
6 | const { publicKey } = useLogin();
7 | const subject: TimelineSubject = {
8 | type: "global",
9 | items: [],
10 | discriminator: `followed-by-friends-${publicKey}`,
11 | streams: true,
12 | };
13 |
14 | return ;
15 | };
16 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/Media.tsx:
--------------------------------------------------------------------------------
1 | import { EventKind } from "@snort/system";
2 |
3 | import TimelineFollows from "@/Components/Feed/TimelineFollows";
4 | import { Day } from "@/Utils/Const";
5 |
6 | export default function MediaPosts() {
7 | return (
8 |
9 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/NotesTab.tsx:
--------------------------------------------------------------------------------
1 | import { NostrEvent, NostrLink } from "@snort/system";
2 | import { useContext, useMemo } from "react";
3 |
4 | import TimelineFollows from "@/Components/Feed/TimelineFollows";
5 | import { DeckContext } from "@/Pages/Deck/DeckLayout";
6 |
7 | export const NotesTab = () => {
8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
9 | const deckContext = useContext(DeckContext);
10 |
11 | const noteOnClick = useMemo(() => {
12 | if (deckContext) {
13 | return (ev: NostrEvent) => {
14 | deckContext.setThread(NostrLink.fromEvent(ev));
15 | };
16 | }
17 | return undefined;
18 | }, [deckContext]);
19 |
20 | return ;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/RootRoutes.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 | import { Outlet, RouteObject, useLocation } from "react-router-dom";
3 |
4 | import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
5 | import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
6 | import { getCurrentRefCode } from "@/Utils";
7 |
8 | const InviteModal = lazy(() => import("@/Components/Invite"));
9 | export default function RootPage() {
10 | const code = getCurrentRefCode();
11 | const location = useLocation();
12 | return (
13 | <>
14 | {(location.pathname === "/" || location.pathname === "/following") && }
15 |
16 |
17 |
18 | {code && }
19 | >
20 | );
21 | }
22 | export const RootRoutes = [
23 | {
24 | path: "/",
25 | element: ,
26 | children: RootTabRoutes,
27 | },
28 | ] as RouteObject[];
29 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/Root/TagsTab.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useParams } from "react-router-dom";
3 |
4 | import Timeline from "@/Components/Feed/Timeline";
5 | import { TimelineSubject } from "@/Feed/TimelineFeed";
6 |
7 | export const TagsTab = (params: { tag?: string }) => {
8 | const { tag } = useParams();
9 | const t = params.tag ?? tag ?? "";
10 | const subject: TimelineSubject = useMemo(
11 | () => ({
12 | type: "hashtag",
13 | items: [t],
14 | discriminator: `tags-${t}`,
15 | streams: true,
16 | }),
17 | [t],
18 | );
19 |
20 | return ;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/TopicsPage.tsx:
--------------------------------------------------------------------------------
1 | import { unwrap } from "@snort/shared";
2 | import { EventKind } from "@snort/system";
3 | import { useMemo } from "react";
4 |
5 | import Timeline from "@/Components/Feed/Timeline";
6 | import { TimelineSubject } from "@/Feed/TimelineFeed";
7 | import useLogin from "@/Hooks/useLogin";
8 |
9 | export function TopicsPage() {
10 | const { tags, pubKey } = useLogin(s => ({
11 | pubKey: s.publicKey,
12 | tags: s.state.getList(EventKind.InterestSet),
13 | }));
14 | const subject = useMemo(
15 | () =>
16 | ({
17 | type: "hashtag",
18 | items: tags.filter(a => a.toEventTag()?.[0] === "t").map(a => unwrap(a.toEventTag())[1]),
19 | discriminator: pubKey ?? "",
20 | }) as TimelineSubject,
21 | [tags, pubKey],
22 | );
23 |
24 | return ;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/ZapPool/ZapPool.css:
--------------------------------------------------------------------------------
1 | .zap-pool input[type="range"] {
2 | width: 200px;
3 | }
4 |
5 | .zap-pool h4 {
6 | margin: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/ZapPool/ZapPool.tsx:
--------------------------------------------------------------------------------
1 | import "./ZapPool.css";
2 |
3 | import { ZapPoolPageInner } from "@/Pages/ZapPool/ZapPoolPageInner";
4 | import { ZapPoolController } from "@/Utils/ZapPoolController";
5 |
6 | export default function ZapPoolPage() {
7 | if (!ZapPoolController) {
8 | return null;
9 | }
10 | return ;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/Keys.css:
--------------------------------------------------------------------------------
1 | .mnemonic-grid {
2 | display: grid;
3 | text-align: center;
4 | grid-template-columns: repeat(4, 1fr);
5 | gap: 8px;
6 | }
7 |
8 | .mnemonic-grid > div {
9 | border: 1px solid #222222;
10 | border-radius: 5px;
11 | overflow: hidden;
12 | user-select: none;
13 | }
14 |
15 | .mnemonic-grid .word > div:nth-of-type(1) {
16 | background-color: var(--gray);
17 | padding: 4px 8px;
18 | min-width: 2em;
19 | font-variant-numeric: ordinal;
20 | }
21 |
22 | .mnemonic-grid .word > div:nth-of-type(2) {
23 | flex-grow: 1;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/Preferences.css:
--------------------------------------------------------------------------------
1 | .preferences select {
2 | min-width: 100px;
3 | }
4 |
5 | .preferences h4 {
6 | margin: 0;
7 | font-size: 16px;
8 | font-weight: 600;
9 | line-height: 22px; /* 137.5% */
10 | }
11 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/SnortNostrAddressService.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 |
3 | import messages from "@/Pages/messages";
4 | import { ApiHost } from "@/Utils/Const";
5 |
6 | export const SnortNostrAddressService = {
7 | name: "Snort",
8 | service: `${ApiHost}/api/v1/n5sp`,
9 | link: "https://snort.social/",
10 | supportLink: "https://snort.social/help",
11 | about: ,
12 | };
13 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/handle/Manage.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate, useLocation } from "react-router-dom";
2 |
3 | import { ManageHandle } from "@/Utils/Nip05/SnortServiceProvider";
4 |
5 | import LNForwardAddress from "./LNAddress";
6 | import TransferHandle from "./TransferHandle";
7 |
8 | export default function ManageHandleIndex() {
9 | const location = useLocation();
10 | const handle = location.state as ManageHandle;
11 | if (!handle) {
12 | return ;
13 | }
14 | return (
15 | <>
16 |
17 | {handle.handle}@
18 |
19 | {handle.domain}
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/handle/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl";
2 | import { Outlet, RouteObject, useNavigate } from "react-router-dom";
3 |
4 | import ListHandles from "./ListHandles";
5 | import ManageHandleIndex from "./Manage";
6 |
7 | export default function ManageHandlePage() {
8 | const navigate = useNavigate();
9 |
10 | return (
11 | <>
12 | navigate("/settings/handle")} className="pointer">
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
20 | export const ManageHandleRoutes = [
21 | {
22 | path: "/settings/handle",
23 | element: ,
24 | children: [
25 | {
26 | path: "",
27 | element: ,
28 | },
29 | {
30 | path: "manage",
31 | element: ,
32 | },
33 | ],
34 | },
35 | ] as Array;
36 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/saveRelays.tsx:
--------------------------------------------------------------------------------
1 | import { EventPublisher, FullRelaySettings, RelaySettings, SystemInterface } from "@snort/system";
2 |
3 | import { Blasters } from "@/Utils/Const";
4 |
5 | export async function saveRelays(
6 | system: SystemInterface,
7 | publisher: EventPublisher | undefined,
8 | relays: Array | Record,
9 | ) {
10 | if (publisher) {
11 | const ev = await publisher.relayList(relays);
12 | system.BroadcastEvent(ev);
13 | Promise.all(Blasters.map(a => system.WriteOnceToRelay(a, ev)));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/settings/wallet/index.tsx:
--------------------------------------------------------------------------------
1 | import { RouteObject } from "react-router-dom";
2 |
3 | import WalletSettings from "../WalletSettings";
4 | import AlbyOAuth from "./Alby";
5 | import ConnectLNDHub from "./LNDHub";
6 | import ConnectNostrWallet from "./NWC";
7 |
8 | export const WalletSettingsRoutes = [
9 | {
10 | path: "/settings/wallet",
11 | element: ,
12 | },
13 | {
14 | path: "/settings/wallet/lndhub",
15 | element: ,
16 | },
17 | {
18 | path: "/settings/wallet/nwc",
19 | element: ,
20 | },
21 | {
22 | path: "/settings/wallet/alby",
23 | element: ,
24 | },
25 | ] as Array;
26 |
--------------------------------------------------------------------------------
/packages/app/src/Pages/subscribe/index.css:
--------------------------------------------------------------------------------
1 | .subscribe-page > div {
2 | margin: 5px;
3 | min-height: 400px;
4 | user-select: none;
5 | flex: 1;
6 | }
7 |
8 | .subscribe-page h2 {
9 | text-align: center;
10 | }
11 |
12 | .subscribe-page ul {
13 | padding-inline-start: 20px;
14 | }
15 |
16 | @media (max-width: 720px) {
17 | .subscribe-page {
18 | flex-direction: column;
19 | }
20 |
21 | .subscribe-page > div {
22 | flex: unset;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/Login/index.ts:
--------------------------------------------------------------------------------
1 | import { MultiAccountStore } from "./MultiAccountStore";
2 |
3 | export const LoginStore = new MultiAccountStore();
4 |
5 | export interface Nip7os {
6 | getPublicKey: () => string;
7 | signEvent: (ev: string) => string;
8 | saveKey: (key: string) => void;
9 | nip04_encrypt: (content: string, to: string) => string;
10 | nip04_decrypt: (content: string, from: string) => string;
11 | }
12 |
13 | declare global {
14 | interface Window {
15 | nostr_os?: Nip7os;
16 | }
17 | }
18 |
19 | export * from "./Functions";
20 | export * from "./LoginSession";
21 | export * from "./Preferences";
22 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/Nip05/Verifier.ts:
--------------------------------------------------------------------------------
1 | import { throwIfOffline } from "@snort/shared";
2 |
3 | interface NostrJson {
4 | names: Record;
5 | }
6 |
7 | export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000) {
8 | if (!name || !domain) {
9 | return undefined;
10 | }
11 | try {
12 | throwIfOffline();
13 | const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`, {
14 | signal: AbortSignal.timeout(timeout),
15 | });
16 | const data: NostrJson = await res.json();
17 | const match = Object.keys(data.names).find(n => {
18 | return n.toLowerCase() === name.toLowerCase();
19 | });
20 | return match ? data.names[match] : undefined;
21 | } catch {
22 | // ignored
23 | }
24 |
25 | return undefined;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/Number.ts:
--------------------------------------------------------------------------------
1 | const intl = new Intl.NumberFormat("en", {
2 | minimumFractionDigits: 0,
3 | maximumFractionDigits: 2,
4 | });
5 |
6 | export function formatShort(n: number) {
7 | if (n < 2e3) {
8 | return n;
9 | } else if (n < 1e6) {
10 | return `${intl.format(n / 1e3)}K`;
11 | } else if (n < 1e9) {
12 | return `${intl.format(n / 1e6)}M`;
13 | } else {
14 | return `${intl.format(n / 1e9)}G`;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/Thread/ChainKey.tsx:
--------------------------------------------------------------------------------
1 | import { EventKind, Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
2 |
3 | /**
4 | * Get the chain key as a reply event
5 | *
6 | * ie. Get the key for which this event is replying to
7 | */
8 | export function replyChainKey(ev: TaggedNostrEvent) {
9 | if (ev.kind !== EventKind.Comment) {
10 | const t = Nip10.parseThread(ev);
11 | const tag = t?.replyTo ?? t?.root;
12 | return tag?.tagKey;
13 | } else {
14 | const k = ev.tags.find(t => ["e", "a", "i"].includes(t[0]));
15 | return k?.[1];
16 | }
17 | }
18 |
19 | /**
20 | * Get the chain key of this event
21 | *
22 | * ie. Get the key which ties replies to this event
23 | */
24 | export function chainKey(ev: TaggedNostrEvent) {
25 | const link = NostrLink.fromEvent(ev);
26 | return link.tagKey;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/Thread/ThreadContext.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-debugger */
2 | import { TaggedNostrEvent } from "@snort/system";
3 | import { createContext } from "react";
4 |
5 | export interface ThreadContextState {
6 | current: string;
7 | root?: TaggedNostrEvent;
8 | chains: Map>;
9 | data: Array;
10 | mutedData: Array;
11 | setCurrent: (i: string) => void;
12 | }
13 |
14 | export const ThreadContext = createContext({} as ThreadContextState);
15 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/emoji-search.ts:
--------------------------------------------------------------------------------
1 | import { matchSorter } from "match-sorter";
2 |
3 | export default async function searchEmoji(key: string) {
4 | const emoji = await import("emojilib");
5 | /* build proper library with included name of the emoji */
6 | const library = Object.entries(emoji).map(([emoji, keywords]) => ({
7 | name: keywords[0],
8 | keywords,
9 | char: emoji,
10 | }));
11 | return matchSorter(library, key, { keys: ["keywords"] });
12 | }
13 |
--------------------------------------------------------------------------------
/packages/app/src/Utils/getEventMedia.ts:
--------------------------------------------------------------------------------
1 | import { EventKind, ParsedFragment, readNip94TagsFromIMeta, TaggedNostrEvent } from "@snort/system";
2 |
3 | import { transformTextCached } from "@/Hooks/useTextTransformCache";
4 |
5 | export default function getEventMedia(event: TaggedNostrEvent) {
6 | // emulate parsed media from imeta kinds
7 | const mediaKinds = [EventKind.Photo, EventKind.Video, EventKind.ShortVideo];
8 | if (mediaKinds.includes(event.kind)) {
9 | const meta = event.tags.filter(a => a[0] === "imeta").map(readNip94TagsFromIMeta);
10 | return meta.map(
11 | a =>
12 | ({
13 | type: "media",
14 | mimeType: a.mimeType,
15 | content: a.url,
16 | }) as ParsedFragment,
17 | );
18 | }
19 | const parsed = transformTextCached(event.id, event.content, event.tags);
20 | return parsed.filter(
21 | a => a.type === "media" && (a.mimeType?.startsWith("image/") || a.mimeType?.startsWith("video/")),
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2
--------------------------------------------------------------------------------
/packages/app/src/assets/img/cashu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/img/cashu.png
--------------------------------------------------------------------------------
/packages/app/src/assets/img/lnd-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/img/lnd-logo.png
--------------------------------------------------------------------------------
/packages/app/src/assets/img/nostrich.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/app/src/assets/img/nostrich.webp
--------------------------------------------------------------------------------
/packages/app/src/bench.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Snort Benchmarks
6 |
7 |
8 | Check console
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/app/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { TextDecoder, TextEncoder } from "util";
2 |
3 | Object.assign(global, { TextDecoder, TextEncoder });
4 |
--------------------------------------------------------------------------------
/packages/app/src/wdyr.ts:
--------------------------------------------------------------------------------
1 | import whyDidYouRender from "@welldone-software/why-did-you-render";
2 | import * as React from "react";
3 |
4 | if (import.meta.env.DEV) {
5 | whyDidYouRender(React, {
6 | trackAllPureComponents: true,
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/packages/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "jsx": "react-jsx",
7 | "moduleResolution": "Bundler",
8 | "sourceMap": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "strict": true,
11 | "skipLibCheck": true,
12 | "resolveJsonModule": true,
13 | "allowSyntheticDefaultImports": true,
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/bot/README.md:
--------------------------------------------------------------------------------
1 | # @snort/bot
2 |
3 | Simple live stream event chat bot (NIP-53)
4 |
5 | ## Example
6 |
7 | ```typescript
8 | import { parseNostrLink } from "@snort/system";
9 | import { SnortBot } from "../src/index";
10 |
11 | // listen to chat events on every NoGood live stream
12 | const noGoodLink = parseNostrLink("npub12hcytyr8fumy3axde8wgeced523gyp6v6zczqktwuqeaztfc2xzsz3rdp4");
13 |
14 | // Run a simple bot
15 | SnortBot.simple("example")
16 | .link(noGoodLink)
17 | .relay("wss://relay.damus.io")
18 | .relay("wss://nos.lol")
19 | .relay("wss://relay.nostr.band")
20 | .profile({
21 | name: "PingBot",
22 | picture: "https://nostr.download/572f5ff8286e8c719196f904fed24aef14586ec8181c14b09efa726682ef48ef",
23 | lud16: "kieran@zap.stream",
24 | about: "An example bot",
25 | })
26 | .command("!ping", h => {
27 | h.reply("PONG!");
28 | })
29 | .run();
30 | ```
31 |
--------------------------------------------------------------------------------
/packages/bot/example/simple.ts:
--------------------------------------------------------------------------------
1 | import { parseNostrLink } from "@snort/system";
2 | import { SnortBot } from "../src/index";
3 |
4 | const noGoodLink = parseNostrLink("npub12hcytyr8fumy3axde8wgeced523gyp6v6zczqktwuqeaztfc2xzsz3rdp4");
5 |
6 | SnortBot.simple("example")
7 | .link(noGoodLink)
8 | .relay("wss://relay.damus.io")
9 | .relay("wss://nos.lol")
10 | .relay("wss://relay.nostr.band")
11 | .profile({
12 | name: "PingBot",
13 | picture: "https://nostr.download/572f5ff8286e8c719196f904fed24aef14586ec8181c14b09efa726682ef48ef",
14 | lud16: "kieran@zap.stream",
15 | about: "An example bot",
16 | })
17 | .command("!ping", h => {
18 | h.reply("PONG!");
19 | })
20 | .run();
21 |
--------------------------------------------------------------------------------
/packages/bot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/bot",
3 | "version": "1.2.0",
4 | "description": "Simple bot framework",
5 | "type": "module",
6 | "module": "src/index.ts",
7 | "main": "dist/index.js",
8 | "types": "dist/index.d.ts",
9 | "repository": "https://git.v0l.io/Kieran/snort",
10 | "author": "Kieran",
11 | "license": "MIT",
12 | "scripts": {
13 | "build": "rm -rf dist && tsc"
14 | },
15 | "dependencies": {
16 | "@snort/system": "^1.5.2",
17 | "eventemitter3": "^5.0.1"
18 | },
19 | "devDependencies": {
20 | "@types/debug": "^4.1.8",
21 | "typescript": "^5.2.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/bot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts"],
17 | "exclude": ["src/**/*.test.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/bot/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/shared",
3 | "version": "1.0.17",
4 | "description": "Shared components for Snort",
5 | "type": "module",
6 | "module": "src/index.ts",
7 | "main": "dist/index.js",
8 | "types": "dist/index.d.ts",
9 | "repository": "https://git.v0l.io/Kieran/snort",
10 | "author": "Kieran",
11 | "license": "MIT",
12 | "scripts": {
13 | "build": "rm -rf dist && tsc"
14 | },
15 | "dependencies": {
16 | "@noble/curves": "^1.4.0",
17 | "@noble/hashes": "^1.4.0",
18 | "@scure/base": "^1.1.6",
19 | "debug": "^4.3.4",
20 | "eventemitter3": "^5.0.1",
21 | "light-bolt11-decoder": "^3.0.0"
22 | },
23 | "devDependencies": {
24 | "@types/debug": "^4.1.8",
25 | "typescript": "^5.2.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/shared/src/LRUSet.ts:
--------------------------------------------------------------------------------
1 | export default class LRUSet {
2 | private set = new Set();
3 | private limit: number;
4 |
5 | constructor(limit: number) {
6 | this.limit = limit;
7 | }
8 |
9 | add(item: T) {
10 | if (this.set.size >= this.limit) {
11 | this.set.delete(this.set.values().next().value);
12 | }
13 | this.set.add(item);
14 | }
15 |
16 | has(item: T) {
17 | return this.set.has(item);
18 | }
19 |
20 | values() {
21 | return this.set.values();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/shared/src/const.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Regex to match email address
3 | */
4 | export const EmailRegex =
5 | // eslint-disable-next-line no-useless-escape
6 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
7 |
8 | /**
9 | * Match any NIP-19 code
10 | */
11 | export const Bech32Regex = /(n(?:pub|profile|event|ote|addr|req|relay|chat)1[acdefghjklmnpqrstuvwxyz023456789]+)/;
12 |
--------------------------------------------------------------------------------
/packages/shared/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module "light-bolt11-decoder" {
2 | export function decode(pr?: string): ParsedInvoice;
3 |
4 | export interface ParsedInvoice {
5 | paymentRequest: string;
6 | sections: Section[];
7 | }
8 |
9 | export interface Section {
10 | name: string;
11 | value: string | Uint8Array | number | undefined;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/shared/src/external-store.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "eventemitter3";
2 |
3 | interface ExternalStoreEvents {
4 | change: () => void;
5 | }
6 |
7 | /**
8 | * Simple hookable store with manual change notifications
9 | */
10 | export abstract class ExternalStore extends EventEmitter {
11 | #snapshot: TSnapshot = {} as TSnapshot;
12 | #changed = true;
13 |
14 | hook(cb: () => void) {
15 | this.on("change", cb);
16 | return () => this.off("change", cb);
17 | }
18 |
19 | snapshot(p?: any) {
20 | if (this.#changed) {
21 | this.#snapshot = this.takeSnapshot(p);
22 | this.#changed = false;
23 | }
24 | return this.#snapshot;
25 | }
26 |
27 | protected notifyChange(sn?: TSnapshot) {
28 | this.#changed = true;
29 | this.emit("change");
30 | }
31 |
32 | abstract takeSnapshot(p?: any): TSnapshot;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./external-store";
2 | export * from "./lnurl";
3 | export * from "./utils";
4 | export * from "./work-queue";
5 | export * from "./feed-cache";
6 | export * from "./invoices";
7 | export * from "./dexie-like";
8 | export * from "./SortedMap/SortedMap";
9 | export * from "./const";
10 |
--------------------------------------------------------------------------------
/packages/shared/src/work-queue.ts:
--------------------------------------------------------------------------------
1 | export interface WorkQueueItem {
2 | next: () => Promise;
3 | resolve(v: unknown): void;
4 | reject(e: unknown): void;
5 | }
6 |
7 | export async function processWorkQueue(queue?: Array, queueDelay = 200) {
8 | while (queue && queue.length > 0) {
9 | const v = queue.shift();
10 | if (v) {
11 | try {
12 | const ret = await v.next();
13 | v.resolve(ret);
14 | } catch (e) {
15 | v.reject(e);
16 | }
17 | }
18 | }
19 | setTimeout(() => processWorkQueue(queue, queueDelay), queueDelay);
20 | }
21 |
22 | export const barrierQueue = async (queue: Array, then: () => Promise): Promise => {
23 | return new Promise((resolve, reject) => {
24 | queue.push({
25 | next: then,
26 | resolve,
27 | reject,
28 | });
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts"],
17 | "exclude": ["src/**/*.test.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/shared/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/system-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/system-react",
3 | "version": "1.6.2",
4 | "description": "React hooks for @snort/system",
5 | "main": "dist/index.js",
6 | "module": "src/index.ts",
7 | "types": "dist/index.d.ts",
8 | "repository": "https://git.v0l.io/Kieran/snort",
9 | "author": "Kieran",
10 | "license": "MIT",
11 | "scripts": {
12 | "build": "rm -rf dist && tsc"
13 | },
14 | "files": [
15 | "src",
16 | "dist"
17 | ],
18 | "dependencies": {
19 | "@snort/shared": "^1.0.17",
20 | "@snort/system": "^1.6.2",
21 | "react": "^18.2.0"
22 | },
23 | "devDependencies": {
24 | "@types/react": "^18.2.14",
25 | "typescript": "^5.2.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/system-react/src/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { NostrSystem, SystemInterface } from "@snort/system";
3 |
4 | export const SnortContext = createContext({} as SystemInterface);
5 |
--------------------------------------------------------------------------------
/packages/system-react/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./context";
2 |
3 | export * from "./useRequestBuilder";
4 | export * from "./useSystemState";
5 | export * from "./useUserProfile";
6 | export * from "./useUserSearch";
7 | export * from "./useEventReactions";
8 | export * from "./useReactions";
9 | export * from "./useEventFeed";
10 |
--------------------------------------------------------------------------------
/packages/system-react/src/useEventFeed.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { RequestBuilder, NostrLink } from "@snort/system";
3 | import { useRequestBuilder } from "./useRequestBuilder";
4 |
5 | export function useEventFeed(link: NostrLink) {
6 | const sub = useMemo(() => {
7 | const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`);
8 | b.withFilter().link(link);
9 | return b;
10 | }, [link]);
11 |
12 | return useRequestBuilder(sub).at(0);
13 | }
14 |
15 | export function useEventsFeed(id: string, links: Array) {
16 | const sub = useMemo(() => {
17 | const b = new RequestBuilder(`events:${id}`);
18 | links.forEach(v => b.withFilter().link(v));
19 | return b;
20 | }, [id, links]);
21 |
22 | return useRequestBuilder(sub);
23 | }
24 |
--------------------------------------------------------------------------------
/packages/system-react/src/useSystemState.tsx:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStore } from "react";
2 | import { SystemSnapshot } from "@snort/system";
3 | import { ExternalStore } from "@snort/shared";
4 |
5 | export function useSystemState(system: ExternalStore) {
6 | return useSyncExternalStore(
7 | cb => system.hook(cb),
8 | () => system.snapshot(),
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/packages/system-react/src/useUserProfile.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useSyncExternalStore } from "react";
2 | import { HexKey, CachedMetadata } from "@snort/system";
3 | import { SnortContext } from "./context";
4 |
5 | /**
6 | * Gets a profile from cache or requests it from the relays
7 | */
8 | export function useUserProfile(pubKey?: HexKey): CachedMetadata | undefined {
9 | const system = useContext(SnortContext);
10 | return useSyncExternalStore(
11 | h => {
12 | if (pubKey) {
13 | const handler = (keys: Array) => {
14 | if (keys.includes(pubKey)) {
15 | h();
16 | }
17 | };
18 | system.profileLoader.cache.on("change", handler);
19 | system.profileLoader.TrackKeys(pubKey);
20 |
21 | return () => {
22 | system.profileLoader.cache.off("change", handler);
23 | system.profileLoader.UntrackKeys(pubKey);
24 | };
25 | }
26 | return () => {
27 | // noop
28 | };
29 | },
30 | () => system.profileLoader.cache.getFromCache(pubKey),
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/system-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "jsx": "react-jsx",
10 | "strict": true,
11 | "declaration": true,
12 | "declarationMap": true,
13 | "inlineSourceMap": true,
14 | "outDir": "dist",
15 | "skipLibCheck": true
16 | },
17 | "include": ["./src/**/*.ts*"],
18 | "exclude": ["**/*.test.ts"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/system-react/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/system-svelte/README.md:
--------------------------------------------------------------------------------
1 | ## @snort/system-svelte
2 |
3 | Svelte hooks for @snort/system
4 |
--------------------------------------------------------------------------------
/packages/system-svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/system-svelte",
3 | "version": "1.0.0",
4 | "description": "Svelte functions for @snort/system",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "repository": "https://git.v0l.io/Kieran/snort",
9 | "author": "Kieran",
10 | "license": "MIT",
11 | "scripts": {
12 | "build": "rm -rf dist && tsc"
13 | },
14 | "files": [
15 | "src",
16 | "dist"
17 | ],
18 | "dependencies": {
19 | "@snort/shared": "^1.0.6",
20 | "@snort/system": "^1.0.21",
21 | "svelte": "^4.2.0"
22 | },
23 | "devDependencies": {
24 | "typescript": "^5.2.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/system-svelte/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./request-builder";
2 |
--------------------------------------------------------------------------------
/packages/system-svelte/src/request-builder.ts:
--------------------------------------------------------------------------------
1 | import { TaggedNostrEvent, type RequestBuilder, type SystemInterface } from "@snort/system";
2 | import { getContext } from "svelte";
3 |
4 | export function useRequestBuilder(rb: RequestBuilder) {
5 | const system = getContext("snort") as SystemInterface;
6 | return {
7 | subscribe: (set: (value: Array) => void) => {
8 | const q = system.Query(rb);
9 | const handle = () => {
10 | set(q.snapshot);
11 | };
12 | q.uncancel();
13 | q.on("event", handle);
14 | return () => {
15 | q.off("event", handle);
16 | q.cancel();
17 | };
18 | },
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/packages/system-svelte/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "inlineSourceMap": true,
12 | "outDir": "dist",
13 | "skipLibCheck": true
14 | },
15 | "include": ["./src/**/*.ts"],
16 | "exclude": ["**/*.test.ts"]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/system-svelte/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/system-wasm/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | target/
3 | *.txt
--------------------------------------------------------------------------------
/packages/system-wasm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "system-wasm"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 | [lib]
8 | crate-type = ["cdylib", "rlib"]
9 |
10 | [dependencies]
11 | console_error_panic_hook = "0.1.7"
12 | hex = { version = "0.4.3", features = [], default-features = false }
13 | itertools = "0.14.0"
14 | secp256k1 = { version = "0.30.0", features = ["global-context"] }
15 | serde = { version = "1.0.188", features = ["derive"], default-features = false }
16 | serde-wasm-bindgen = "0.6.5"
17 | serde_json = "1.0.105"
18 | sha256 = { version = "1.4.0", features = [], default-features = false }
19 | wasm-bindgen = "0.2.87"
20 |
21 | [dev-dependencies]
22 | rand = "0.8.5"
23 | wasm-bindgen-test = "0.3.37"
24 | serde_json = "1.0.105"
25 | criterion = { version = "0.5" }
26 |
27 | [[bench]]
28 | name = "basic"
29 | harness = false
30 |
31 | [profile.release]
32 | opt-level = 3
33 | lto = true
34 |
35 | [package.metadata.wasm-pack.profile.release]
36 | wasm-opt = ["-O3"]
37 |
--------------------------------------------------------------------------------
/packages/system-wasm/README.md:
--------------------------------------------------------------------------------
1 | # system-wasm
2 |
3 | ## Building
4 |
5 | ### Ubuntu/Debian
6 |
7 | ```bash
8 | sudo apt install clang
9 | cargo install wasm-pack
10 | yarn build
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/system-wasm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/system-wasm",
3 | "version": "1.0.5",
4 | "packageManager": "yarn@3.6.3",
5 | "author": "Kieran",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "wasm-pack build --release -t web -s snort && rm -f pkg/.gitignore"
9 | },
10 | "files": [
11 | "pkg/system_wasm_bg.wasm",
12 | "pkg/system_wasm.js",
13 | "pkg/system_wasm.d.ts"
14 | ],
15 | "module": "pkg/system_wasm.js",
16 | "types": "pkg/system_wasm.d.ts"
17 | }
18 |
--------------------------------------------------------------------------------
/packages/system-wasm/pkg/README.md:
--------------------------------------------------------------------------------
1 | # system-wasm
2 |
3 | ## Building
4 |
5 | ### Ubuntu/Debian
6 |
7 | ```bash
8 | sudo apt install clang
9 | cargo install wasm-pack
10 | yarn build
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/system-wasm/pkg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/system-wasm",
3 | "type": "module",
4 | "version": "0.1.0",
5 | "files": [
6 | "system_wasm_bg.wasm",
7 | "system_wasm.js",
8 | "system_wasm.d.ts"
9 | ],
10 | "main": "system_wasm.js",
11 | "types": "system_wasm.d.ts",
12 | "sideEffects": [
13 | "./snippets/*"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/packages/system-wasm/pkg/system_wasm_bg.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/system-wasm/pkg/system_wasm_bg.wasm
--------------------------------------------------------------------------------
/packages/system-wasm/system-query.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/system-wasm/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["pkg/system_wasm.d.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/system-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/system-web",
3 | "version": "1.2.11",
4 | "description": "Web based components @snort/system",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "repository": "https://git.v0l.io/Kieran/snort",
9 | "author": "Kieran",
10 | "license": "MIT",
11 | "scripts": {
12 | "build": "rm -rf dist && tsc"
13 | },
14 | "files": [
15 | "src",
16 | "dist"
17 | ],
18 | "dependencies": {
19 | "@snort/shared": "^1.0.14",
20 | "@snort/system": "^1.2.11",
21 | "dexie": "^3.2.4"
22 | },
23 | "devDependencies": {
24 | "typescript": "^5.2.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/system-web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "jsx": "react-jsx",
10 | "strict": true,
11 | "declaration": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts*"],
17 | "exclude": ["**/*.test.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/system-web/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/system/.npmignore:
--------------------------------------------------------------------------------
1 | tests/
2 | src/
3 | *.tgz
4 | jest.config.js
5 | worker.ts
6 | yarn*
--------------------------------------------------------------------------------
/packages/system/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | bail: true,
4 | preset: "ts-jest",
5 | testEnvironment: "jsdom",
6 | roots: ["src", "tests"],
7 | moduleDirectories: ["src", "node_modules"],
8 | setupFiles: ["./tests/setupTests.ts"],
9 | };
10 |
--------------------------------------------------------------------------------
/packages/system/src/cache-relay.ts:
--------------------------------------------------------------------------------
1 | import { OkResponse, ReqCommand, TaggedNostrEvent } from "./nostr";
2 |
3 | /**
4 | * A cache relay is an always available local (local network / browser worker) relay
5 | * Which should contain all of the content we're looking for and respond quickly.
6 | */
7 | export interface CacheRelay {
8 | /**
9 | * Write event to cache relay
10 | */
11 | event(ev: TaggedNostrEvent): Promise;
12 |
13 | /**
14 | * Read event from cache relay
15 | */
16 | query(req: ReqCommand): Promise>;
17 |
18 | /**
19 | * Delete events by filter
20 | */
21 | delete(req: ReqCommand): Promise>;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/system/src/cache/events.ts:
--------------------------------------------------------------------------------
1 | import { NostrEvent } from "../nostr";
2 | import { DexieTableLike, FeedCache } from "@snort/shared";
3 |
4 | export class EventsCache extends FeedCache {
5 | constructor(table?: DexieTableLike) {
6 | super("EventsCache", table);
7 | }
8 |
9 | key(of: NostrEvent): string {
10 | return of.id;
11 | }
12 |
13 | override async preload(): Promise {
14 | await super.preload();
15 | // load everything
16 | await this.buffer([...this.onTable]);
17 | }
18 |
19 | takeSnapshot(): Array {
20 | return [...this.cache.values()];
21 | }
22 |
23 | async search() {
24 | return >[];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/system/src/cache/relay-metric.ts:
--------------------------------------------------------------------------------
1 | import { RelayMetrics } from ".";
2 | import { DexieTableLike, FeedCache } from "@snort/shared";
3 |
4 | export class RelayMetricCache extends FeedCache {
5 | constructor(table?: DexieTableLike) {
6 | super("RelayMetrics", table);
7 | }
8 |
9 | key(of: RelayMetrics): string {
10 | return of.addr;
11 | }
12 |
13 | override async preload(): Promise {
14 | await super.preload();
15 | // load everything
16 | await this.buffer([...this.onTable]);
17 | }
18 |
19 | takeSnapshot(): Array {
20 | return [...this.cache.values()];
21 | }
22 |
23 | async search() {
24 | return >[];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/system/src/cache/user-follows-lists.ts:
--------------------------------------------------------------------------------
1 | import { UsersFollows } from ".";
2 | import { DexieTableLike, FeedCache } from "@snort/shared";
3 |
4 | export class UserFollowsCache extends FeedCache {
5 | constructor(table?: DexieTableLike) {
6 | super("UserFollowsCache", table);
7 | }
8 |
9 | key(of: UsersFollows): string {
10 | return of.pubkey;
11 | }
12 |
13 | override async preload(follows?: Array): Promise {
14 | await super.preload();
15 | if (follows) {
16 | await this.buffer(follows);
17 | }
18 | }
19 |
20 | newest(): number {
21 | let ret = 0;
22 | this.cache.forEach(v => (ret = v.created > ret ? v.created : ret));
23 | return ret;
24 | }
25 |
26 | takeSnapshot(): Array {
27 | return [...this.cache.values()];
28 | }
29 |
30 | async search() {
31 | return >[];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/system/src/cache/user-relays.ts:
--------------------------------------------------------------------------------
1 | import { UsersRelays } from ".";
2 | import { DexieTableLike, FeedCache } from "@snort/shared";
3 |
4 | export class UserRelaysCache extends FeedCache {
5 | constructor(table?: DexieTableLike) {
6 | super("UserRelays", table);
7 | }
8 |
9 | key(of: UsersRelays): string {
10 | return of.pubkey;
11 | }
12 |
13 | override async preload(follows?: Array): Promise {
14 | await super.preload();
15 | if (follows) {
16 | await this.buffer(follows);
17 | }
18 | }
19 |
20 | newest(): number {
21 | let ret = 0;
22 | this.cache.forEach(v => (ret = v.created > ret ? v.created : ret));
23 | return ret;
24 | }
25 |
26 | takeSnapshot(): Array {
27 | return [...this.cache.values()];
28 | }
29 |
30 | async search() {
31 | return >[];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/system/src/connection-stats.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Stats class for tracking metrics per connection
3 | */
4 | export class ConnectionStats {
5 | /**
6 | * Last n records of how long between REQ->EOSE
7 | */
8 | Latency: number[] = [];
9 |
10 | /**
11 | * Total number of REQ's sent on this connection
12 | */
13 | Subs: number = 0;
14 |
15 | /**
16 | * Count of REQ which took too long and where abandoned
17 | */
18 | SubsTimeout: number = 0;
19 |
20 | /**
21 | * Total number of EVENT messages received
22 | */
23 | EventsReceived: number = 0;
24 |
25 | /**
26 | * Total number of EVENT messages sent
27 | */
28 | EventsSent: number = 0;
29 |
30 | /**
31 | * Total number of times this connection was lost
32 | */
33 | Disconnects: number = 0;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/system/src/encryption/index.ts:
--------------------------------------------------------------------------------
1 | export const enum MessageEncryptorVersion {
2 | Nip4 = 0,
3 | Nip44 = 1,
4 | }
5 |
6 | export interface MessageEncryptor {
7 | encryptData(plaintext: string): Promise | string;
8 | decryptData(ciphertext: string): Promise | string;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/system/src/filter-cache-layer.ts:
--------------------------------------------------------------------------------
1 | import { BuiltRawReqFilter } from "./request-builder";
2 | import { NostrEvent } from "./nostr";
3 | import { Query } from "./query";
4 |
5 | export interface EventCache {
6 | bulkGet: (ids: Array) => Promise>;
7 | }
8 |
9 | export interface FilterCacheLayer {
10 | processFilter(q: Query, req: BuiltRawReqFilter): Promise;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/system/src/impl/nip44.ts:
--------------------------------------------------------------------------------
1 | import { nip44 } from "../encryption/nip44";
2 | import { MessageEncryptor } from "..";
3 |
4 | export class Nip44Encryptor implements MessageEncryptor {
5 | constructor(
6 | readonly privateKey: string,
7 | readonly publicKey: string,
8 | ) {}
9 |
10 | encryptData(plaintext: string) {
11 | const conversationKey = nip44.v2.getConversationKey(this.privateKey, this.publicKey);
12 | return nip44.v2.encrypt(plaintext, conversationKey);
13 | }
14 |
15 | decryptData(payload: string): string {
16 | const { version, ciphertext, nonce, mac } = nip44.utils.decodePayload(payload);
17 | if (version === 1) {
18 | const conversationKey = nip44.v1.getConversationKey(this.privateKey, this.publicKey);
19 | return nip44.v1.decrypt(ciphertext, nonce, conversationKey);
20 | }
21 | if (version === 2) {
22 | const conversationKey = nip44.v2.getConversationKey(this.privateKey, this.publicKey);
23 | return nip44.v2.decrypt(ciphertext, nonce, mac!, conversationKey);
24 | }
25 | throw new Error(`Unsupported payload version: ${version}`);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/system/src/nips.ts:
--------------------------------------------------------------------------------
1 | export enum Nips {
2 | Search = 50,
3 | GetMatchingEventIds = 114,
4 | }
5 |
--------------------------------------------------------------------------------
/packages/system/src/pow-worker.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { minePow, NostrPowEvent } from "./pow-util";
4 |
5 | export interface PowWorkerMessage {
6 | id: string;
7 | cmd: "req" | "rsp";
8 | event: NostrPowEvent;
9 | target: number;
10 | }
11 |
12 | globalThis.onmessage = ev => {
13 | const data = ev.data as PowWorkerMessage;
14 | if (data.cmd === "req") {
15 | queueMicrotask(() => {
16 | minePow(data.event, data.target);
17 | data.cmd = "rsp";
18 | globalThis.postMessage(data);
19 | });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/packages/system/src/profile-cache.ts:
--------------------------------------------------------------------------------
1 | import { unixNowMs } from "@snort/shared";
2 | import { EventKind, TaggedNostrEvent, RequestBuilder } from ".";
3 | import { ProfileCacheExpire } from "./const";
4 | import { mapEventToProfile, CachedMetadata } from "./cache";
5 | import { BackgroundLoader } from "./background-loader";
6 |
7 | export class ProfileLoaderService extends BackgroundLoader {
8 | override name(): string {
9 | return "ProfileLoaderService";
10 | }
11 |
12 | override onEvent(e: Readonly): CachedMetadata | undefined {
13 | return mapEventToProfile(e);
14 | }
15 |
16 | override getExpireCutoff(): number {
17 | return unixNowMs() - ProfileCacheExpire;
18 | }
19 |
20 | override buildSub(missing: string[]): RequestBuilder {
21 | const sub = new RequestBuilder(`profiles`);
22 | sub.withFilter().kinds([EventKind.SetMetadata]).authors(missing);
23 | return sub;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/system/src/query-optimizer/request-splitter.ts:
--------------------------------------------------------------------------------
1 | import { flatFilterEq } from "../utils";
2 | import { FlatReqFilter } from ".";
3 |
4 | export function diffFilters(prev: Array, next: Array, calcRemoved?: boolean) {
5 | const added = [];
6 | const removed = [];
7 |
8 | for (const n of next) {
9 | const px = prev.findIndex(a => flatFilterEq(a, n));
10 | if (px !== -1) {
11 | prev.splice(px, 1);
12 | } else {
13 | added.push(n);
14 | }
15 | }
16 | if (calcRemoved) {
17 | for (const p of prev) {
18 | const px = next.findIndex(a => flatFilterEq(a, p));
19 | if (px !== -1) {
20 | next.splice(px, 1);
21 | } else {
22 | removed.push(p);
23 | }
24 | }
25 | }
26 | const changed = added.length > 0 || removed.length > 0;
27 | return {
28 | added: changed ? added : [],
29 | removed: changed ? removed : [],
30 | changed,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/packages/system/src/relay-info.ts:
--------------------------------------------------------------------------------
1 | export interface RelayInfo {
2 | name?: string;
3 | description?: string;
4 | pubkey?: string;
5 | contact?: string;
6 | supported_nips?: number[];
7 | software?: string;
8 | version?: string;
9 | limitation?: {
10 | payment_required?: boolean;
11 | max_subscriptions?: number;
12 | max_filters?: number;
13 | max_event_tags?: number;
14 | auth_required?: boolean;
15 | write_restricted?: boolean;
16 | };
17 | relay_countries?: Array;
18 | language_tags?: Array;
19 | tags?: Array;
20 | posting_policy?: string;
21 | negentropy?: number;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/system/src/request-matcher.ts:
--------------------------------------------------------------------------------
1 | import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr";
2 |
3 | export function eventMatchesFilter(ev: NostrEvent, filter: ReqFilter) {
4 | if (filter.since && ev.created_at < filter.since) {
5 | return false;
6 | }
7 | if (filter.until && ev.created_at > filter.until) {
8 | return false;
9 | }
10 | if (!(filter.ids?.includes(ev.id) ?? true)) {
11 | return false;
12 | }
13 | if (!(filter.authors?.includes(ev.pubkey) ?? true)) {
14 | return false;
15 | }
16 | if (!(filter.kinds?.includes(ev.kind) ?? true)) {
17 | return false;
18 | }
19 | return true;
20 | }
21 |
22 | export function isRequestSatisfied(filter: ReqFilter, results: Array) {
23 | if (filter.ids && filter.ids.every(a => results.some(b => b.id === a))) {
24 | return true;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/system/src/request-trim.ts:
--------------------------------------------------------------------------------
1 | import { ReqFilter } from "./nostr";
2 |
3 | /**
4 | * Remove empty filters, filters which would result in no results
5 | */
6 | export function trimFilters(filters: Array) {
7 | const fNew = [];
8 | for (const f of filters) {
9 | const ent = Object.entries(f).filter(([, v]) => Array.isArray(v));
10 | if (ent.every(([, v]) => (v as Array).length > 0)) {
11 | fNew.push(f);
12 | }
13 | }
14 | return fNew;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/system/src/sync/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./safe-sync";
2 | export * from "./range-sync";
3 | export * from "./json-in-event-sync";
4 | export * from "./diff-sync";
5 |
--------------------------------------------------------------------------------
/packages/system/tests/negentropy.test.ts:
--------------------------------------------------------------------------------
1 | import { NegentropyStorageVector, VectorStorageItem } from "../src/negentropy/vector-storage";
2 |
3 | describe("negentropy", () => {
4 | it("should decodeBound", () => {});
5 | });
6 |
--------------------------------------------------------------------------------
/packages/system/tests/node.ts:
--------------------------------------------------------------------------------
1 | import { NostrSystem, SystemInterface } from "..";
2 |
3 | const Relay = "wss://relay.snort.social/";
4 |
5 | const system = new NostrSystem({}) as SystemInterface;
6 |
7 | async function test() {
8 | await system.ConnectToRelay(Relay, { read: true, write: true });
9 | setTimeout(() => {
10 | system.DisconnectRelay(Relay);
11 | }, 1000);
12 | }
13 |
14 | test().catch(console.error);
15 |
--------------------------------------------------------------------------------
/packages/system/tests/request-matcher.test.ts:
--------------------------------------------------------------------------------
1 | import { eventMatchesFilter } from "../src/request-matcher";
2 |
3 | describe("RequestMatcher", () => {
4 | it("should match simple filter", () => {
5 | const ev = {
6 | id: "test",
7 | kind: 1,
8 | pubkey: "pubkey",
9 | created_at: 99,
10 | tags: [],
11 | content: "test",
12 | sig: "",
13 | };
14 | const filter = {
15 | ids: ["test"],
16 | authors: ["pubkey", "other"],
17 | kinds: [1, 2, 3],
18 | since: 1,
19 | before: 100,
20 | };
21 | expect(eventMatchesFilter(ev, filter)).toBe(true);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/system/tests/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { TextEncoder, TextDecoder } from "util";
2 | import { Crypto } from "@peculiar/webcrypto";
3 |
4 | Object.assign(global, { TextDecoder, TextEncoder });
5 | Object.assign(globalThis.window.crypto, new Crypto());
6 |
--------------------------------------------------------------------------------
/packages/system/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts"],
17 | "exclude": ["**/*.test.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/system/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/wallet/README.md:
--------------------------------------------------------------------------------
1 | # wallet
2 |
--------------------------------------------------------------------------------
/packages/wallet/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/wallet",
3 | "version": "0.2.5",
4 | "description": "Snort wallet system package",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "module": "src/index.ts",
9 | "repository": "https://git.v0l.io/Kieran/snort",
10 | "author": "v0l",
11 | "license": "MIT",
12 | "scripts": {
13 | "build": "rm -rf dist && tsc",
14 | "test": "jest --runInBand"
15 | },
16 | "files": [
17 | "src",
18 | "dist"
19 | ],
20 | "packageManager": "yarn@4.1.1",
21 | "dependencies": {
22 | "@scure/base": "^1.1.6",
23 | "@snort/shared": "^1.0.17",
24 | "@snort/system": "^1.6.2",
25 | "debug": "^4.3.4",
26 | "eventemitter3": "^5.0.1"
27 | },
28 | "devDependencies": {
29 | "@types/debug": "^4.1.12",
30 | "@webbtc/webln-types": "^3.0.0",
31 | "typescript": "^5.4.5"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/wallet/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/wallet/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts"],
17 | "exclude": ["**/*.test.ts", "./src/Cashu.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/wallet/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/webrtc-server/README.md:
--------------------------------------------------------------------------------
1 | # webrtc-server
2 |
3 | ```
4 | yarn
5 | yarn start
6 | ```
7 |
8 | Websocket (socket.io) based signaling server for WebRTC.
9 |
--------------------------------------------------------------------------------
/packages/webrtc-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nostr-webrtc-server",
3 | "dependencies": {
4 | "ministun": "^1.0.6",
5 | "nostr-tools": "^2.0.2",
6 | "socket.io": "^4.7.2"
7 | },
8 | "scripts": {
9 | "start": "node index.js"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/worker-relay/example/basic.ts:
--------------------------------------------------------------------------------
1 | import { WorkerRelayInterface } from "@snort/worker-relay";
2 |
3 | // when using Vite import the worker script directly (for production)
4 | import WorkerVite from "@snort/worker-relay/src/worker?worker";
5 |
6 | // in dev mode import esm module, i have no idea why it has to work like this
7 | const workerScript = import.meta.env.DEV
8 | ? new URL("@snort/worker-relay/dist/esm/worker.mjs", import.meta.url)
9 | : new WorkerVite();
10 |
11 | const workerRelay = new WorkerRelayInterface(workerScript);
12 |
13 | // load sqlite database and run migrations
14 | await workerRelay.init({
15 | databasePath: "relay.db",
16 | insertBatchSize: 100,
17 | });
18 |
19 | // Query worker relay with regular nostr REQ command
20 | const results = await workerRelay.query(["REQ", "1", { kinds: [1], limit: 10 }]);
21 |
22 | // publish a new event to the relay
23 | const myEvent = {
24 | kind: 1,
25 | created_at: Math.floor(new Date().getTime() / 1000),
26 | content: "test",
27 | tags: [],
28 | };
29 | if (await workerRelay.event(myEvent)) {
30 | console.log("Success");
31 | }
32 |
--------------------------------------------------------------------------------
/packages/worker-relay/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@snort/worker-relay",
3 | "version": "1.3.1",
4 | "description": "A nostr relay in a service worker",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "type": "module",
8 | "module": "src/index.ts",
9 | "repository": "https://git.v0l.io/Kieran/snort",
10 | "author": "Kieran",
11 | "license": "MIT",
12 | "scripts": {
13 | "build": "rm -rf dist && tsc && yarn build:esm",
14 | "build:esm": "esbuild src/worker.ts --bundle --minify --sourcemap --outdir=dist/esm --format=esm --out-extension:.js=.mjs --loader:.wasm=copy"
15 | },
16 | "files": [
17 | "src",
18 | "dist"
19 | ],
20 | "dependencies": {
21 | "@sqlite.org/sqlite-wasm": "^3.46.1-build3",
22 | "eventemitter3": "^5.0.1",
23 | "uuid": "^9.0.1"
24 | },
25 | "devDependencies": {
26 | "@types/debug": "^4.1.12",
27 | "@types/sharedworker": "^0.0.112",
28 | "@types/uuid": "^9.0.7",
29 | "esbuild": "^0.20.1",
30 | "typescript": "^5.2.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.wasm" {
2 | const value: string;
3 | export default value;
4 | }
5 | declare module "*.wasm?url" {
6 | const value: string;
7 | export default value;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/debug.ts:
--------------------------------------------------------------------------------
1 | let debug = false;
2 | export function debugLog(scope: string, msg: string, ...args: Array) {
3 | if (!debug) return;
4 | console.log(scope, msg, ...args);
5 | }
6 |
7 | export function setLogging(v: boolean) {
8 | debug = v;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./interface";
2 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/queue.ts:
--------------------------------------------------------------------------------
1 | export interface WorkQueueItem {
2 | next: () => Promise;
3 | resolve(v: unknown): void;
4 | reject(e: unknown): void;
5 | }
6 |
7 | export async function processWorkQueue(queue?: Array) {
8 | while (queue && queue.length > 0) {
9 | const v = queue.shift();
10 | if (v) {
11 | try {
12 | const ret = await v.next();
13 | v.resolve(ret);
14 | } catch (e) {
15 | v.reject(e);
16 | }
17 | }
18 | }
19 | }
20 |
21 | export const barrierQueue = async (queue: Array, then: () => Promise): Promise => {
22 | return new Promise((resolve, reject) => {
23 | queue.push({
24 | next: then,
25 | resolve,
26 | reject,
27 | });
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/sqlite/fixers.ts:
--------------------------------------------------------------------------------
1 | import { SqliteRelay } from "./sqlite-relay";
2 |
3 | export async function runFixers(relay: SqliteRelay) {}
4 |
--------------------------------------------------------------------------------
/packages/worker-relay/src/sqlite/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/packages/worker-relay/src/sqlite/sqlite3.wasm
--------------------------------------------------------------------------------
/packages/worker-relay/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "esModuleInterop": true,
7 | "noImplicitOverride": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "inlineSourceMap": true,
13 | "outDir": "dist",
14 | "skipLibCheck": true
15 | },
16 | "include": ["./src/**/*.ts"],
17 | "files": ["./src/index.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/worker-relay/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src/index.ts"]
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | default-run = "app"
9 | edition = "2021"
10 | rust-version = "1.60"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "2.0.0-rc", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0"
19 | serde = { version = "1.0", features = ["derive"] }
20 | tauri = { version = "2.0.0-rc", features = [] }
21 |
22 | [features]
23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
24 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
25 | # DO NOT REMOVE!!
26 | custom-protocol = [ "tauri/custom-protocol" ]
27 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/migrated.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "migrated",
3 | "description": "permissions that were migrated from v1",
4 | "local": true,
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default"
10 | ]
11 | }
--------------------------------------------------------------------------------
/src-tauri/gen/schemas/capabilities.json:
--------------------------------------------------------------------------------
1 | {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default"]}}
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v0l/snort/2b48641db176012377e6a43d787e4d9a6be52f3f/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | use tauri::Manager;
5 |
6 | fn main() {
7 | tauri::Builder::default()
8 | .setup(|app| {
9 | #[cfg(debug_assertions)] // only include this code on debug builds
10 | {
11 | let window = app.get_webview_window("main").unwrap();
12 | window.open_devtools();
13 | window.close_devtools();
14 | }
15 | Ok(())
16 | })
17 | .run(tauri::generate_context!())
18 | .expect("error while running tauri application");
19 | }
20 |
--------------------------------------------------------------------------------
/zapstore.yaml:
--------------------------------------------------------------------------------
1 | snort:
2 | android:
3 | repository: https://github.com/v0l/snort
4 | name: Snort
5 | artifacts:
6 | - snort-arm64-v8a-v%v.apk
7 | - snort-armeabi-v7a-v%v.apk
8 | - snort-x86_64-v%v.apk
9 |
--------------------------------------------------------------------------------