├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .mirror
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .yarnrc
├── README.md
├── README_cn.md
├── assets
├── apps-button
│ ├── status_bar_button_exit.svg
│ ├── status_bar_button_fullscreen.svg
│ └── status_bar_button_min.svg
├── auth_default_read.svg
├── auth_default_write.svg
├── avatar
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 14.png
│ ├── 15.png
│ ├── 16.png
│ ├── 17.png
│ ├── 18.png
│ ├── 19.png
│ ├── 2.png
│ ├── 20.png
│ ├── 21.png
│ ├── 22.png
│ ├── 23.png
│ ├── 24.png
│ ├── 25.png
│ ├── 26.png
│ ├── 27.png
│ ├── 28.png
│ ├── 29.png
│ ├── 3.png
│ ├── 30.png
│ ├── 31.png
│ ├── 32.png
│ ├── 33.png
│ ├── 34.png
│ ├── 35.png
│ ├── 36.png
│ ├── 37.png
│ ├── 38.png
│ ├── 39.png
│ ├── 4.png
│ ├── 40.png
│ ├── 41.png
│ ├── 42.png
│ ├── 43.png
│ ├── 44.png
│ ├── 45.png
│ ├── 46.png
│ ├── 47.png
│ ├── 48.png
│ ├── 49.png
│ ├── 5.png
│ ├── 50.png
│ ├── 51.png
│ ├── 52.png
│ ├── 53.png
│ ├── 54.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
├── bond.svg
├── button_add_menu.svg
├── buyadrink.svg
├── buyadrink_white.svg
├── bx-link-external.svg
├── bx-trip.svg
├── bx-wallet.svg
├── check.svg
├── createSeed.svg
├── currency_icons
│ ├── BOX.png
│ ├── BTC.png
│ ├── CNB.png
│ ├── DOGE.png
│ ├── EOS.png
│ ├── ETH.png
│ ├── MOB.png
│ ├── PUSD.png
│ ├── RUM.png
│ ├── USDC.png
│ ├── USDT.png
│ └── XIN.png
├── default_avatar.png
├── entitlements.mac.plist
├── export.svg
├── fold.svg
├── fold_down.svg
├── fold_up.svg
├── font
│ └── VarelaRound-Regular.ttf
├── group_timeline.svg
├── icon.icns
├── icon.ico
├── icon.png
├── iconFilter.svg
├── iconReturn.svg
├── iconSwich.svg
├── icon_add_black.svg
├── icon_add_gray.svg
├── icon_add_seed.svg
├── icon_add_white.svg
├── icon_addanything.svg
├── icon_addseed.svg
├── icon_lab.svg
├── icon_search_all_seed.svg
├── icon_seednet_manage.svg
├── icon_wallet.svg
├── icon_wallet_2.svg
├── icon_wallet_3.svg
├── icons
│ └── pc_bar_icon.png
├── import.svg
├── joinSeed.svg
├── lang_local.svg
├── lang_local_2.svg
├── logo.png
├── logo_rumsystem.svg
├── logo_rumsystem_banner.svg
├── logo_rumsystem_banner_yellow.svg
├── migrateIn.svg
├── migrateOut.svg
├── post_ban.svg
├── reply.svg
├── rum_barrel_bg.png
├── rumsystem_text.svg
├── search_group.svg
├── seed.svg
├── status_bar_pixel_banner.svg
├── syncing.svg
├── template
│ ├── template_icon_notebook.svg
│ ├── template_icon_post.svg
│ ├── template_icon_sub.svg
│ └── template_icon_timeline.svg
├── template_icon_note.svg
├── template_icon_post.svg
├── template_icon_timeline.svg
├── unfollow.svg
├── unfollow_gray.svg
├── unlink_wallet.svg
└── wallet_gray.svg
├── build
├── index.html
├── svg-inline.ts
└── vite-svgr-plugin.ts
├── docs
├── i18n.md
├── i18n_cn.md
├── lint.md
├── lint_cn.md
├── test.md
└── test_cn.md
├── electron-builder.yml
├── index.html
├── main.js
├── package.json
├── postcss.config.js
├── scripts
├── CheckPortInUse.js
├── Notarize.js
└── clear_after_build.js
├── src
├── Updater.tsx
├── apis
│ ├── auth.ts
│ ├── client.ts
│ ├── content.ts
│ ├── group.ts
│ ├── keystore.ts
│ ├── metrics.ts
│ ├── mixin.ts
│ ├── mvm.ts
│ ├── network.ts
│ ├── node.ts
│ ├── pixabay.ts
│ ├── producer.ts
│ ├── psPing.ts
│ ├── pubkeyToAddr.ts
│ ├── trx.ts
│ └── user.ts
├── components
│ ├── Avatar.tsx
│ ├── BackToTop.tsx
│ ├── BottomLine.tsx
│ ├── BoxRadio.tsx
│ ├── Button.tsx
│ ├── ButtonProgress.tsx
│ ├── CommentReplyModal.tsx
│ ├── ConfirmDialog.tsx
│ ├── ContentSyncStatus.tsx
│ ├── Dialog.tsx
│ ├── DropdownMenu
│ │ └── index.tsx
│ ├── Editor.tsx
│ ├── EmojiPicker
│ │ ├── emoji.ts
│ │ ├── index.tsx
│ │ └── lang.ts
│ ├── GroupIcon.tsx
│ ├── GroupMenu
│ │ ├── AuthListModal.tsx
│ │ ├── InputPublisherModal.tsx
│ │ ├── MutedListModal.tsx
│ │ └── index.tsx
│ ├── ImageEditor
│ │ ├── ImageLibModal.tsx
│ │ ├── Menu.tsx
│ │ ├── PresetImagesModal.tsx
│ │ └── index.tsx
│ ├── Images.tsx
│ ├── Loading.tsx
│ ├── MiddleTruncate.tsx
│ ├── NotificationSlide.tsx
│ ├── Page.tsx
│ ├── PageLoading.tsx
│ ├── PasswordInput.tsx
│ ├── PreviewVersion.tsx
│ ├── SearchInput.tsx
│ ├── SnackBar.tsx
│ ├── TrxInfo.tsx
│ ├── TrxModal.tsx
│ ├── UserCard.tsx
│ ├── Video
│ │ ├── Plyr.tsx
│ │ ├── index.css
│ │ └── index.tsx
│ ├── index.ts
│ ├── mixinUIDSelector.tsx
│ └── profileSelector.tsx
├── hooks
│ ├── addGroups.tsx
│ ├── fetchGroups.tsx
│ ├── useAnchorClick.tsx
│ ├── useAppBadgeCount.tsx
│ ├── useAppLaunchTime.tsx
│ ├── useCanIPost.tsx
│ ├── useCheckPaidGroupAnounce.tsx
│ ├── useCheckPermission.tsx
│ ├── useCheckPrivatePermission.tsx
│ ├── useChecking
│ │ ├── index.tsx
│ │ └── useCheckingAnnouncedUsers.tsx
│ ├── useCleanLocalData.tsx
│ ├── useCloseNode.tsx
│ ├── useDatabase
│ │ ├── contentStatus.ts
│ │ ├── database.ts
│ │ ├── index.tsx
│ │ ├── migrations.ts
│ │ └── models
│ │ │ ├── comment.ts
│ │ │ ├── counter.ts
│ │ │ ├── emptyTrx.ts
│ │ │ ├── image.ts
│ │ │ ├── index.tsx
│ │ │ ├── likeStatus.ts
│ │ │ ├── notification.ts
│ │ │ ├── pendingTrx.ts
│ │ │ ├── posts.ts
│ │ │ ├── profile.ts
│ │ │ ├── relationSummaries.ts
│ │ │ ├── relations.ts
│ │ │ ├── summary.ts
│ │ │ ├── transfer.ts
│ │ │ ├── utils.ts
│ │ │ └── websiteMetadata.ts
│ ├── useDeletePost.tsx
│ ├── useExportToWindow.tsx
│ ├── useGroupChange.tsx
│ ├── useJoinGroup.tsx
│ ├── useLeaveGroup.tsx
│ ├── useParseMarkdown.tsx
│ ├── usePolling
│ │ ├── announcedProducers.tsx
│ │ ├── content
│ │ │ ├── ContentTaskManager
│ │ │ │ ├── fetchContent.ts
│ │ │ │ ├── handleContent
│ │ │ │ │ ├── handleComments.tsx
│ │ │ │ │ ├── handleCounters.tsx
│ │ │ │ │ ├── handleEmptyObjects.tsx
│ │ │ │ │ ├── handleImages.tsx
│ │ │ │ │ ├── handlePostDelete.tsx
│ │ │ │ │ ├── handlePosts.tsx
│ │ │ │ │ ├── handleProfiles.tsx
│ │ │ │ │ ├── handleRelations.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── EmptyContentManager
│ │ │ │ ├── index.ts
│ │ │ │ └── state.ts
│ │ │ ├── SocketManager.ts
│ │ │ └── index.ts
│ │ ├── groupConfig.tsx
│ │ ├── groups.tsx
│ │ ├── index.tsx
│ │ ├── myNodeInfo.tsx
│ │ ├── network.tsx
│ │ ├── token.tsx
│ │ └── transferTransactions.tsx
│ ├── usePrevious.tsx
│ ├── useQueryObjects.tsx
│ ├── useResetNode.tsx
│ ├── useScroll.tsx
│ ├── useSelectComment.tsx
│ ├── useSetupQuitHook.tsx
│ ├── useSubmitComment.tsx
│ ├── useSubmitCounter.tsx
│ ├── useSubmitImage.tsx
│ ├── useSubmitPost.tsx
│ ├── useSubmitProfile.tsx
│ ├── useSubmitRelation.tsx
│ ├── useSyncNotificationUnreadCount.tsx
│ └── useUpdatePermission.tsx
├── index.tsx
├── layouts
│ ├── App.tsx
│ ├── Content
│ │ ├── AnouncePaidGroupRequirement.tsx
│ │ ├── Header
│ │ │ ├── Notification
│ │ │ │ ├── CommentMessages.tsx
│ │ │ │ ├── ForwardMessages.tsx
│ │ │ │ ├── LikeMessages.tsx
│ │ │ │ ├── NotificationModal.tsx
│ │ │ │ ├── OtherMessages.tsx
│ │ │ │ ├── TransactionMessages.tsx
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── PaidRequirement.tsx
│ │ ├── Sidebar
│ │ │ ├── GroupItem.tsx
│ │ │ ├── GroupItems.tsx
│ │ │ ├── GroupPopup.tsx
│ │ │ ├── ListTypeSwitcher.tsx
│ │ │ ├── SidebarMenu.tsx
│ │ │ ├── Toolbar.tsx
│ │ │ ├── dndKitHooks
│ │ │ │ └── useCollisionDetectionStrategy.tsx
│ │ │ ├── index.tsx
│ │ │ └── sortableState.tsx
│ │ ├── Welcome.tsx
│ │ ├── index.tsx
│ │ ├── usePollingPaidGroupAnounce.tsx
│ │ └── usePollingPermission.tsx
│ ├── Init
│ │ ├── NodeType
│ │ │ └── index.tsx
│ │ ├── SelectApiConfigFromHistory
│ │ │ └── index.tsx
│ │ ├── SetExternalNode
│ │ │ └── index.tsx
│ │ ├── StartingTips
│ │ │ └── index.tsx
│ │ ├── StoragePath
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── Main
│ │ ├── Feed.tsx
│ │ ├── Forum
│ │ │ ├── Announcement.tsx
│ │ │ ├── Comment
│ │ │ │ ├── CommentItem.tsx
│ │ │ │ ├── Comments.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Feed.tsx
│ │ │ ├── MDEditor.tsx
│ │ │ ├── ObjectDetailModal.tsx
│ │ │ ├── ObjectItem.tsx
│ │ │ ├── ObjectToolbar.tsx
│ │ │ └── OpenObjectEditor
│ │ │ │ ├── icons.tsx
│ │ │ │ └── index.tsx
│ │ ├── Help.tsx
│ │ ├── MoreHistoricalObjectEntry.tsx
│ │ ├── Note
│ │ │ ├── Feed.tsx
│ │ │ ├── ObjectEditor.tsx
│ │ │ ├── ObjectItem.tsx
│ │ │ ├── OpenObjectDetail.tsx
│ │ │ └── OpenObjectEditor.tsx
│ │ ├── ObjectMenu.tsx
│ │ ├── Profile
│ │ │ ├── ProfileEditorModal.tsx
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ └── Timeline
│ │ │ ├── Comment
│ │ │ ├── CommentItem.tsx
│ │ │ ├── Comments.tsx
│ │ │ └── index.tsx
│ │ │ ├── ExternalLink.tsx
│ │ │ ├── Feed.tsx
│ │ │ ├── ForwardPost.tsx
│ │ │ ├── ObjectDetailModal.tsx
│ │ │ ├── ObjectEditorEntry.tsx
│ │ │ ├── ObjectItem.tsx
│ │ │ ├── ObjectItemBottom.tsx
│ │ │ ├── OpenObjectEditor.tsx
│ │ │ ├── PostAttachment.tsx
│ │ │ ├── PostQuote.tsx
│ │ │ └── helper.ts
│ ├── TitleBar
│ │ ├── index.sass
│ │ └── index.tsx
│ ├── index.tsx
│ └── modals
│ │ ├── MyNodeInfoModal.tsx
│ │ ├── NetworkInfoModal.tsx
│ │ └── NodeParamsModal.tsx
├── main.js
├── main
│ ├── appState.ts
│ ├── constants.ts
│ ├── index.ts
│ ├── lang.ts
│ ├── log.ts
│ ├── menu.ts
│ ├── processLock.ts
│ ├── quorum.ts
│ ├── test.ts
│ ├── tsconfig.json
│ ├── updater.ts
│ └── utils.ts
├── package.json
├── request.ts
├── standaloneModals
│ ├── about
│ │ └── index.tsx
│ ├── changeFontSize
│ │ └── index.tsx
│ ├── createGroup
│ │ ├── BottomBar.tsx
│ │ ├── StepBox.tsx
│ │ └── index.tsx
│ ├── editProfile
│ │ └── index.tsx
│ ├── exportKeyData
│ │ └── index.tsx
│ ├── getMixinUID
│ │ └── index.tsx
│ ├── groupInfo
│ │ └── index.tsx
│ ├── importKeyData
│ │ └── index.tsx
│ ├── index.ts
│ ├── initProfile
│ │ └── index.tsx
│ ├── inputPassword
│ │ └── index.tsx
│ ├── joinGroup
│ │ ├── JoinGroup.tsx
│ │ └── index.tsx
│ ├── languageSelect
│ │ └── index.tsx
│ ├── manageGroup
│ │ └── index.tsx
│ ├── migrate
│ │ └── index.tsx
│ ├── modal.ts
│ ├── myGroup
│ │ ├── filter.tsx
│ │ ├── index.tsx
│ │ └── order.tsx
│ ├── openBetaFeaturesModal
│ │ ├── index.tsx
│ │ └── openPsPingModal.tsx
│ ├── openPhotoSwipe.tsx
│ ├── openProducerModal
│ │ ├── AnnounceModal.tsx
│ │ ├── AnnouncedProducersModal.tsx
│ │ └── index.tsx
│ ├── pay.tsx
│ ├── shareGroup
│ │ └── index.tsx
│ ├── useMixinPayment
│ │ ├── index.tsx
│ │ └── utils.tsx
│ ├── view.tsx
│ └── wallet
│ │ ├── balance.tsx
│ │ ├── navbar.tsx
│ │ ├── openDepositModal.tsx
│ │ ├── openExchangeModal.tsx
│ │ ├── openMixinPayModal.tsx
│ │ ├── openTransactionsModal.tsx
│ │ ├── openTransferModal.tsx
│ │ ├── openWalletModal.tsx
│ │ ├── openWithdrawModal.tsx
│ │ ├── searcher.tsx
│ │ └── transactions.tsx
├── store
│ ├── activeGroup.ts
│ ├── apiConfigHistory.ts
│ ├── betaFeature.ts
│ ├── comment.ts
│ ├── confirmDialog.ts
│ ├── electronApiConfigHistoryStore.ts
│ ├── electronCurrentNodeStore.ts
│ ├── electronNodeStore.ts
│ ├── font.ts
│ ├── group.ts
│ ├── i18n
│ │ └── index.ts
│ ├── index.tsx
│ ├── latestStatus.ts
│ ├── modal.ts
│ ├── node.ts
│ ├── notification.ts
│ ├── notificationSlide.ts
│ ├── relation.ts
│ ├── selectors
│ │ ├── getLatestObject.ts
│ │ ├── group.ts
│ │ ├── useActiveGroup.ts
│ │ ├── useActiveGroupFollowingUserAddresses.ts
│ │ ├── useActiveGroupLatestStatus.ts
│ │ ├── useActiveGroupMutedUserAddress.ts
│ │ ├── useHasFrontHistoricalObject.ts
│ │ └── useIsCurrentGroupOwner.ts
│ ├── sidebar.ts
│ ├── snackbar.ts
│ └── types.ts
├── styles
│ ├── App.global.scss
│ ├── rendered-markdown.sass
│ ├── tailwind-base.sass
│ └── tailwind.sass
├── template.html
├── tests
│ ├── index.ts
│ ├── setup-global.ts
│ ├── setup.ts
│ ├── tests
│ │ ├── common
│ │ │ ├── createGroup.ts
│ │ │ ├── exitCurrentGroup.ts
│ │ │ ├── groupInfoModal.ts
│ │ │ ├── index.tsx
│ │ │ ├── myGroupModal.ts
│ │ │ ├── nodeAndNetwork.ts
│ │ │ └── shareGroup.ts
│ │ ├── noteGroup.ts
│ │ ├── postGroup.ts
│ │ └── timelineGroup.ts
│ └── types
│ │ └── puppeteer-core.d.ts
├── types
│ ├── assets.d.ts
│ ├── axios.d.ts
│ ├── expect-puppeteer.d.ts
│ ├── global.d.ts
│ ├── markdown-it-task-lists.d.ts
│ ├── process.d.ts
│ ├── styled-jsx.d.ts
│ └── wasm.d.ts
├── utils
│ ├── BFSReplace.ts
│ ├── PollingTask.ts
│ ├── ago.ts
│ ├── assets-custom-loader.js
│ ├── avatars.ts
│ ├── base64.ts
│ ├── cachePromise.ts
│ ├── constant.ts
│ ├── contentDetector.ts
│ ├── contract.ts
│ ├── decimal.ts
│ ├── digestMessage.ts
│ ├── domSelector.ts
│ ├── env.ts
│ ├── formatAmount.ts
│ ├── formatPath.ts
│ ├── getBase.ts
│ ├── getGroupIcon.ts
│ ├── getHotCount.ts
│ ├── getKeyName.ts
│ ├── getTimestampFromBlockTime.ts
│ ├── handleRumAppProtocol.ts
│ ├── highlightjs.ts
│ ├── index.ts
│ ├── inputFinanceAmount.ts
│ ├── inspect.ts
│ ├── isV2Seed.ts
│ ├── lang
│ │ ├── cn.ts
│ │ ├── en.ts
│ │ └── index.ts
│ ├── log.ts
│ ├── mainScrollView.ts
│ ├── markdown.ts
│ ├── mimeType.ts
│ ├── mixinOAuth.ts
│ ├── quorum
│ │ ├── index.ts
│ │ └── request.ts
│ ├── removeGroupData.ts
│ ├── replaceSeedAsButton.ts
│ ├── runLoading.ts
│ ├── schema.ts
│ ├── setClipboard.ts
│ ├── sleep.ts
│ ├── theme.tsx
│ └── urlify.ts
└── yarn.lock
├── tailwind.config.js
├── tsconfig-test.json
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | [production]
2 | chrome 96
3 |
4 | [wasm]
5 | chrome 57
6 | safari 11
7 | firefox 53
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | !/.erb
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 | .eslintcache
25 |
26 | # Dependency directory
27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
28 | node_modules
29 |
30 | # OSX
31 | .DS_Store
32 |
33 | # App packaged
34 | release
35 | src/*.main.prod.js
36 | src/main.prod.js
37 | src/main.prod.js.map
38 | src/renderer.prod.js
39 | src/renderer.prod.js.map
40 | src/style.css
41 | src/style.css.map
42 | dist
43 | src/main_dist
44 | dll
45 | # main.js
46 | main.js.map
47 |
48 | .idea
49 | npm-debug.log.*
50 | __snapshots__
51 |
52 | # Package.json
53 | package.json
54 | .travis.yml
55 | *.css.d.ts
56 | *.sass.d.ts
57 | *.scss.d.ts
58 |
59 | src/utils/quorum-wasm/wasm_exec.js
60 | src/tests/userData/**/*
61 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * -text
2 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build/Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | release:
10 | runs-on: ${{ matrix.os }}
11 |
12 | strategy:
13 | matrix:
14 | os: [macos-latest, ubuntu-latest, windows-latest]
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 | with:
20 | submodules: recursive
21 |
22 | - name: Install Node.js, NPM and Yarn
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 16
26 | cache: 'yarn'
27 |
28 | - name: Prepare for app notarization
29 | if: startsWith(matrix.os, 'macos')
30 | run: |
31 | mkdir -p ~/private_keys/
32 | echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
33 |
34 | - name: Build Electron app
35 | uses: samuelmeuli/action-electron-builder@v1
36 | with:
37 | github_token: ${{ secrets.github_token }}
38 | mac_certs: ${{ secrets.mac_certs }}
39 | mac_certs_password: ${{ secrets.mac_certs_password }}
40 | env:
41 | API_KEY_ID: ${{ secrets.api_key_id }}
42 | API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
43 |
44 | - name: Release
45 | uses: softprops/action-gh-release@v0.1.15
46 | if: startsWith(github.ref, 'refs/tags/v')
47 | with:
48 | files: |
49 | release/RUM-*.dmg
50 | release/RUM-*.zip
51 | release/RUM-*.exe
52 | release/RUM-*.AppImage
53 | release/latest.yml
54 | release/latest-mac.yml
55 | release/latest-linux.yml
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 | .eslintcache
25 |
26 | # Dependency directory
27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
28 | node_modules
29 |
30 | # OSX
31 | .DS_Store
32 |
33 | # App packaged
34 | release
35 | src/main.prod.js
36 | src/main.prod.js.map
37 | src/renderer.prod.js
38 | src/renderer.prod.js.map
39 | src/style.css
40 | src/style.css.map
41 | # src/main.js
42 | src/main.js.map
43 | dist
44 | /src/main_dist
45 | dll
46 |
47 | .idea
48 | npm-debug.log.*
49 | *.css.d.ts
50 | *.sass.d.ts
51 | *.scss.d.ts
52 |
53 | .env
54 |
55 | publish.sh
56 | /quorum_bin
57 | /.github_api_token
58 | quorum_data
59 | /src/tests/userdata/
60 | quorum
61 |
62 | # dev output
63 | /.erb/dev_dist/
64 |
--------------------------------------------------------------------------------
/.mirror:
--------------------------------------------------------------------------------
1 | {
2 | "hash": "e8ae79f618a33032f3f3fc6988d8f80fc1111bc0",
3 | "date": "2023-08-18T13:50:49+08:00",
4 | "mirrored": "2023-08-18T05:53:42.982Z"
5 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | **/*.ejs
4 | **/*.html
5 | **/*.js
6 | **/*.jsx
7 | **/*.ts
8 | **/*.tsx
9 | package.json
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 100,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "EditorConfig.EditorConfig",
5 | "msjsdiag.debugger-for-chrome"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Electron: Main",
6 | "type": "node",
7 | "request": "launch",
8 | "protocol": "inspector",
9 | "runtimeExecutable": "yarn",
10 | "runtimeArgs": ["start:main --inspect=5858 --remote-debugging-port=9223"],
11 | "preLaunchTask": "Start Webpack Dev"
12 | },
13 | {
14 | "name": "Electron: Renderer",
15 | "type": "chrome",
16 | "request": "attach",
17 | "port": 9223,
18 | "webRoot": "${workspaceFolder}",
19 | "timeout": 15000
20 | }
21 | ],
22 | "compounds": [
23 | {
24 | "name": "Electron: All",
25 | "configurations": ["Electron: Main", "Electron: Renderer"]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | ".babelrc": "jsonc",
4 | ".eslintrc": "jsonc",
5 | ".prettierrc": "jsonc",
6 | ".eslintignore": "ignore"
7 | },
8 |
9 | "javascript.validate.enable": false,
10 | "javascript.format.enable": false,
11 | "typescript.format.enable": false,
12 |
13 | "search.exclude": {
14 | ".git": true,
15 | ".eslintcache": true,
16 | "src/dist": true,
17 | "src/main.prod.js": true,
18 | "src/main.prod.js.map": true,
19 | "bower_components": true,
20 | "dll": true,
21 | "release": true,
22 | "node_modules": true,
23 | "npm-debug.log.*": true,
24 | "test/**/__snapshots__": true,
25 | "yarn.lock": true,
26 | "*.{css,sass,scss}.d.ts": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "label": "Start Webpack Dev",
7 | "script": "start:renderer",
8 | "options": {
9 | "cwd": "${workspaceFolder}"
10 | },
11 | "isBackground": true,
12 | "problemMatcher": {
13 | "owner": "custom",
14 | "pattern": {
15 | "regexp": "____________"
16 | },
17 | "background": {
18 | "activeOnStart": true,
19 | "beginsPattern": "Compiling\\.\\.\\.$",
20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$"
21 | }
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | network-timeout "300000"
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rum Desktop App
2 |
3 | [中文说明](README_cn.md)
4 |
5 | ## Requirements
6 | - node 14-lts or greater
7 | - go latest version
8 | - yarn `npm install yarn -g`
9 |
10 | ## Start
11 | Clone this repo and cd into it
12 | ```sh
13 | yarn install
14 | yarn start
15 | ```
16 |
17 | ## Docs
18 | [https://docs.prsdev.club/#/rum-app/](https://docs.prsdev.club/#/rum-app/)
19 |
20 | ## how i18n works
21 | [i18n.md](docs/i18n.md)
22 |
23 | ## Configure eslint
24 | [lint.md](docs/lint.md)
25 |
26 | ## Tests
27 | [test.md](docs/test.md)
28 |
--------------------------------------------------------------------------------
/README_cn.md:
--------------------------------------------------------------------------------
1 | # Rum Desktop App
2 |
3 | ## 环境要求
4 | - node 14-lts
5 | - go
6 | - yarn `npm install yarn -g`
7 |
8 | ## Start
9 | clone 后在目录下运行
10 | ```sh
11 | yarn install
12 | yarn start
13 | ```
14 |
15 | ## Docs
16 | [https://docs.prsdev.club/#/rum-app/](https://docs.prsdev.club/#/rum-app/)
17 |
18 | ## i18n 工作原理
19 | [i18n_cn.md](docs/i18n_cn.md)
20 |
21 | ## 配置 eslint
22 | [lint_cn.md](docs/lint_cn.md)
23 |
24 | ## 测试
25 | [test_cn.md](docs/test_cn.md)
26 |
--------------------------------------------------------------------------------
/assets/apps-button/status_bar_button_exit.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/apps-button/status_bar_button_fullscreen.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/apps-button/status_bar_button_min.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/avatar/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/1.png
--------------------------------------------------------------------------------
/assets/avatar/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/10.png
--------------------------------------------------------------------------------
/assets/avatar/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/11.png
--------------------------------------------------------------------------------
/assets/avatar/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/12.png
--------------------------------------------------------------------------------
/assets/avatar/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/13.png
--------------------------------------------------------------------------------
/assets/avatar/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/14.png
--------------------------------------------------------------------------------
/assets/avatar/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/15.png
--------------------------------------------------------------------------------
/assets/avatar/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/16.png
--------------------------------------------------------------------------------
/assets/avatar/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/17.png
--------------------------------------------------------------------------------
/assets/avatar/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/18.png
--------------------------------------------------------------------------------
/assets/avatar/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/19.png
--------------------------------------------------------------------------------
/assets/avatar/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/2.png
--------------------------------------------------------------------------------
/assets/avatar/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/20.png
--------------------------------------------------------------------------------
/assets/avatar/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/21.png
--------------------------------------------------------------------------------
/assets/avatar/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/22.png
--------------------------------------------------------------------------------
/assets/avatar/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/23.png
--------------------------------------------------------------------------------
/assets/avatar/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/24.png
--------------------------------------------------------------------------------
/assets/avatar/25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/25.png
--------------------------------------------------------------------------------
/assets/avatar/26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/26.png
--------------------------------------------------------------------------------
/assets/avatar/27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/27.png
--------------------------------------------------------------------------------
/assets/avatar/28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/28.png
--------------------------------------------------------------------------------
/assets/avatar/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/29.png
--------------------------------------------------------------------------------
/assets/avatar/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/3.png
--------------------------------------------------------------------------------
/assets/avatar/30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/30.png
--------------------------------------------------------------------------------
/assets/avatar/31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/31.png
--------------------------------------------------------------------------------
/assets/avatar/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/32.png
--------------------------------------------------------------------------------
/assets/avatar/33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/33.png
--------------------------------------------------------------------------------
/assets/avatar/34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/34.png
--------------------------------------------------------------------------------
/assets/avatar/35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/35.png
--------------------------------------------------------------------------------
/assets/avatar/36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/36.png
--------------------------------------------------------------------------------
/assets/avatar/37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/37.png
--------------------------------------------------------------------------------
/assets/avatar/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/38.png
--------------------------------------------------------------------------------
/assets/avatar/39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/39.png
--------------------------------------------------------------------------------
/assets/avatar/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/4.png
--------------------------------------------------------------------------------
/assets/avatar/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/40.png
--------------------------------------------------------------------------------
/assets/avatar/41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/41.png
--------------------------------------------------------------------------------
/assets/avatar/42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/42.png
--------------------------------------------------------------------------------
/assets/avatar/43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/43.png
--------------------------------------------------------------------------------
/assets/avatar/44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/44.png
--------------------------------------------------------------------------------
/assets/avatar/45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/45.png
--------------------------------------------------------------------------------
/assets/avatar/46.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/46.png
--------------------------------------------------------------------------------
/assets/avatar/47.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/47.png
--------------------------------------------------------------------------------
/assets/avatar/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/48.png
--------------------------------------------------------------------------------
/assets/avatar/49.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/49.png
--------------------------------------------------------------------------------
/assets/avatar/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/5.png
--------------------------------------------------------------------------------
/assets/avatar/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/50.png
--------------------------------------------------------------------------------
/assets/avatar/51.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/51.png
--------------------------------------------------------------------------------
/assets/avatar/52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/52.png
--------------------------------------------------------------------------------
/assets/avatar/53.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/53.png
--------------------------------------------------------------------------------
/assets/avatar/54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/54.png
--------------------------------------------------------------------------------
/assets/avatar/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/6.png
--------------------------------------------------------------------------------
/assets/avatar/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/7.png
--------------------------------------------------------------------------------
/assets/avatar/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/8.png
--------------------------------------------------------------------------------
/assets/avatar/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/avatar/9.png
--------------------------------------------------------------------------------
/assets/bond.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/button_add_menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/buyadrink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/buyadrink_white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/bx-link-external.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/bx-trip.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/bx-wallet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/createSeed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/assets/currency_icons/BOX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/BOX.png
--------------------------------------------------------------------------------
/assets/currency_icons/BTC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/BTC.png
--------------------------------------------------------------------------------
/assets/currency_icons/CNB.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/CNB.png
--------------------------------------------------------------------------------
/assets/currency_icons/DOGE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/DOGE.png
--------------------------------------------------------------------------------
/assets/currency_icons/EOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/EOS.png
--------------------------------------------------------------------------------
/assets/currency_icons/ETH.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/ETH.png
--------------------------------------------------------------------------------
/assets/currency_icons/MOB.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/MOB.png
--------------------------------------------------------------------------------
/assets/currency_icons/PUSD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/PUSD.png
--------------------------------------------------------------------------------
/assets/currency_icons/RUM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/RUM.png
--------------------------------------------------------------------------------
/assets/currency_icons/USDC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/USDC.png
--------------------------------------------------------------------------------
/assets/currency_icons/USDT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/USDT.png
--------------------------------------------------------------------------------
/assets/currency_icons/XIN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/currency_icons/XIN.png
--------------------------------------------------------------------------------
/assets/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/default_avatar.png
--------------------------------------------------------------------------------
/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/export.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/assets/fold.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/fold_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/fold_up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/font/VarelaRound-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/font/VarelaRound-Regular.ttf
--------------------------------------------------------------------------------
/assets/group_timeline.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/icon.png
--------------------------------------------------------------------------------
/assets/iconFilter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/iconReturn.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/iconSwich.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/assets/icon_add_black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/icon_add_gray.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/icon_add_seed.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon_add_white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/icon_addanything.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon_search_all_seed.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon_wallet.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon_wallet_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/assets/icon_wallet_3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/assets/icons/pc_bar_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/icons/pc_bar_icon.png
--------------------------------------------------------------------------------
/assets/import.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/assets/lang_local_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/logo.png
--------------------------------------------------------------------------------
/assets/migrateIn.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/migrateOut.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/post_ban.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/assets/reply.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/assets/rum_barrel_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/assets/rum_barrel_bg.png
--------------------------------------------------------------------------------
/assets/seed.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/syncing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/assets/template/template_icon_notebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/template/template_icon_post.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/template/template_icon_sub.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/template/template_icon_timeline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/unfollow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/assets/unfollow_gray.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rum
6 |
17 |
21 |
22 |
23 |
26 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/build/svg-inline.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs';
2 | import { Plugin } from 'vite';
3 |
4 | export const svgInline = (): Plugin => ({
5 | name: 'vite-svg-inline-plugin',
6 | enforce: 'pre',
7 | transform: async (_code, id) => {
8 | if (id.endsWith('.svg')) {
9 | try {
10 | const stat = await fs.stat(id);
11 | if (stat.size > 4096) {
12 | return;
13 | }
14 | const svgContent = (await fs.readFile(id, 'utf8')).toString();
15 | const svg = `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
16 |
17 | return `export default ${JSON.stringify(svg)}`;
18 | } catch (e) { }
19 | }
20 | return undefined;
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/docs/test.md:
--------------------------------------------------------------------------------
1 | ## Test
2 |
3 | To run test with a test build
4 | ```sh
5 | yarn build:test
6 | yarn test
7 | ```
8 |
9 | To run test with a dev server
10 | ```sh
11 | yarn start:renderer
12 | # start in another shell
13 | yarn test:dev
14 | ```
15 |
16 | To run specific tests
17 | ```sh
18 | # testname1 is a partial string of test file name
19 | yarn test testname1 [testname2] ...
20 | yarn test:dev testname1 [testname2] ...
21 | ```
22 |
23 | `setup.ts` includes a method for launching the electron app, userData will be saved to `tests/tests/userData`, and it will be cleared before each lauch to ensure a consistant enviroment.
24 |
25 | While running the tests, a env var will be set on main process and a global variable will be set on renderer procrss to distinguish the normal and test env. After open the app, it will create a internal mode node automatically with it's data saved in `tests/tests/userData/rum-user-data`.
26 |
27 | ## Writing tests
28 | Create a new file in `tests/tests/`, and `export default` the function you want to run during the test. The name of the test will be the filename.
29 |
--------------------------------------------------------------------------------
/docs/test_cn.md:
--------------------------------------------------------------------------------
1 | ## Test
2 |
3 | 构建测试版本并执行测试
4 | ```sh
5 | yarn build:test
6 | yarn test
7 | ```
8 |
9 | 启动 dev server 并执行测试
10 | ```sh
11 | yarn start:renderer
12 | # start in another shell
13 | yarn test
14 | ```
15 |
16 |
17 | 执行指定测试
18 | ```sh
19 | # testname1 是测试文件名称的一部分
20 | yarn test testname1 [testname2] ...
21 | yarn test:dev testname1 [testname2] ...
22 | ```
23 |
24 | `setup.ts` 包含了一个一键启动 electron 的函数,用户数据会保存到 `tests/tests/userData` 下,每次执行都会清空,生成新的用户数据。
25 |
26 | 执行测试时会设置一个环境变量和一个页面内全局变量用于区分测试环境,打开后应用会自动启动内置节点并把数据保存在 `tests/tests/userData/rum-user-data` 下。
27 |
28 | ## 写测试
29 | 在 `tests/tests/`下新建一个文件,将测试用例 `export default` 出来,测试时会自动执行。测试的名称为文件名。
30 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rum
6 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | require('ts-node').register({
4 | transpileOnly: true,
5 | project: path.join(__dirname, 'src/main/tsconfig.json'),
6 | });
7 |
8 | require('./src/main/index');
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = () => ({
2 | plugins: {
3 | tailwindcss: {},
4 | },
5 | });
6 |
--------------------------------------------------------------------------------
/scripts/CheckPortInUse.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const detectPort = require('detect-port');
3 |
4 | const port = process.env.PORT || '1212';
5 |
6 | detectPort(port, (err, availablePort) => {
7 | if (port !== String(availablePort)) {
8 | throw new Error(
9 | chalk.whiteBright.bgRed.bold(
10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn start`
11 | )
12 | );
13 | } else {
14 | process.exit(0);
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/scripts/clear_after_build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 | const util = require('util');
4 | const packageJson = require('../package.json');
5 |
6 | const removedFiles = [
7 | 'builder-debug.yml',
8 | 'builder-effective-config.yaml',
9 | 'win-ia32-unpacked',
10 | 'win-unpacked',
11 | 'linux-unpacked',
12 | 'latest-linux.yml',
13 | `RUM-${packageJson.version}.exe.blockmap`,
14 | `RUM-${packageJson.version}.dmg.blockmap`,
15 | ];
16 |
17 | (async () => {
18 | for (const file of removedFiles) {
19 | try {
20 | await fs.remove(path.join(__dirname, `../release/${file}`));
21 | } catch (err) {}
22 | }
23 | })();
24 |
--------------------------------------------------------------------------------
/src/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 | import type {
3 | TrxTypeUpper,
4 | AuthTypeLower,
5 | TrxTypeLower,
6 | } from 'rum-fullnode-sdk/dist/apis/auth';
7 |
8 | export type TrxType = TrxTypeUpper;
9 |
10 | export type AuthType = AuthTypeLower;
11 |
12 | export default {
13 | async getFollowingRule(groupId: string, trxType: TrxType) {
14 | return getClient().Auth.getAuthRule(groupId, trxType);
15 | },
16 |
17 | async updateFollowingRule(params: {
18 | group_id: string
19 | type: 'set_trx_auth_mode'
20 | config: {
21 | trx_type: TrxType
22 | trx_auth_mode: AuthTypeLower
23 | memo: string
24 | }
25 | }) {
26 | return getClient().Auth.updateChainConfig(params);
27 | },
28 |
29 | async updateAuthList(params: {
30 | group_id: string
31 | type: 'upd_alw_list' | 'upd_dny_list'
32 | config: {
33 | action: 'add' | 'remove'
34 | pubkey: string
35 | trx_type: TrxTypeLower[]
36 | memo: string
37 | }
38 | }) {
39 | return getClient().Auth.updateChainConfig(params);
40 | },
41 |
42 | async getAllowList(groupId: string) {
43 | return getClient().Auth.getAllowList(groupId);
44 | },
45 |
46 | async getDenyList(groupId: string) {
47 | return getClient().Auth.getDenyList(groupId);
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/src/apis/client.ts:
--------------------------------------------------------------------------------
1 | import { RumFullNodeClient } from 'rum-fullnode-sdk';
2 | import type { Store } from 'store/types';
3 |
4 |
5 | let base = '';
6 | let jwt = '';
7 |
8 | let client = RumFullNodeClient({
9 | baseURL: 'http://127.0.0.1:8000',
10 | jwt: 'eyJhbGciOiJI....',
11 | });
12 |
13 | export const getClient = () => {
14 | const { nodeStore } = (window as any).store as Store;
15 | const newBase = nodeStore.mode === 'EXTERNAL'
16 | ? nodeStore.apiConfig.origin
17 | : `http://127.0.0.1:${nodeStore.port}`;
18 | const newJwt = nodeStore.mode === 'EXTERNAL'
19 | ? nodeStore.apiConfig.jwt
20 | : '';
21 |
22 | if (newBase !== base || newJwt !== jwt) {
23 | client = RumFullNodeClient({
24 | baseURL: newBase,
25 | jwt: newJwt,
26 | });
27 | base = newBase;
28 | jwt = newJwt;
29 | }
30 | return client;
31 | };
32 |
--------------------------------------------------------------------------------
/src/apis/content.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 |
3 | export type { IContentItem } from 'rum-fullnode-sdk/dist/apis/content';
4 |
5 | export default {
6 | fetchContents(
7 | groupId: string,
8 | options: {
9 | num: number
10 | starttrx?: string
11 | reverse?: boolean
12 | includestarttrx?: boolean
13 | },
14 | ) {
15 | return getClient().Content.list(groupId, {
16 | num: options.num,
17 | start_trx: options.starttrx ?? '',
18 | reverse: options.reverse ?? false,
19 | include_start_trx: options.includestarttrx ?? false,
20 | });
21 | },
22 | postNote(content: unknown, groupId: string) {
23 | return getClient().Content.create(groupId, content as any);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/apis/keystore.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 | import getBase from 'utils/getBase';
3 |
4 | export default {
5 | signTx(payload: {
6 | keyname: string
7 | nonce: number
8 | to: string
9 | value: string
10 | gas_limit: number
11 | gas_price: string
12 | data: string
13 | chain_id: string
14 | }) {
15 | return request('/api/v1/keystore/signtx ', {
16 | method: 'POST',
17 | base: getBase(),
18 | body: payload,
19 | });
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/src/apis/metrics.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 | import getBase from 'utils/getBase';
3 |
4 | export interface MetricItem {
5 | name: string
6 | help: string
7 | type: string
8 | metrics: Array<{
9 | value?: string
10 | buckets?: Record
11 | count?: string
12 | sum?: string
13 | labels?: {
14 | action?: string
15 | version?: string
16 | }
17 | quantiles?: Record
18 | }>
19 | }
20 |
21 | export type IMetrics = Array;
22 |
23 | export default {
24 | fetchMetrics() {
25 | return request('/metrics', {
26 | headers: { 'Content-Type': 'application/json' },
27 | method: 'GET',
28 | base: getBase(),
29 | }) as Promise;
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/apis/mixin.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 |
3 | export const getAccessToken = (params: {
4 | client_id: string
5 | code: string
6 | code_verifier: string
7 | }) =>
8 | request('https://mixin-api.zeromesh.net/oauth/token', {
9 | method: 'POST',
10 | body: params,
11 | });
12 |
13 | export const getUserProfile = (accessToken: string) =>
14 | request('https://mixin-api.zeromesh.net/me', {
15 | method: 'GET',
16 | headers: { 'Authorization': `Bearer ${accessToken}` },
17 | });
18 |
19 | export const getPaymentStatus = (params: {
20 | amount: string
21 | asset_id: string
22 | counter_user_id: string
23 | trace_id: string
24 | }) =>
25 | request('https://mixin-api.zeromesh.net/payments', {
26 | method: 'POST',
27 | body: params,
28 | });
29 |
--------------------------------------------------------------------------------
/src/apis/network.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 | import type { INetwork } from 'rum-fullnode-sdk/dist/apis/network';
3 |
4 | export type { INetwork } from 'rum-fullnode-sdk/dist/apis/network';
5 |
6 | export type INetworkGroup = Exclude[number];
7 |
8 | export default {
9 | fetchNetwork() {
10 | return getClient().Network.get();
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/apis/node.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 |
3 | export default {
4 | fetchMyNodeInfo() {
5 | return getClient().Node.get();
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/apis/pixabay.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 | import qs from 'query-string';
3 |
4 | interface PixabayOptions {
5 | q?: string
6 | page?: number
7 | per_page?: number
8 | lang?: 'zh' | 'en'
9 | }
10 | export interface PixiabayRes {
11 | total: number
12 | totalHits: number
13 | hits: Array
14 | }
15 |
16 | export interface PixiabayImageItem {
17 | id: number
18 | pageURL: string
19 | type: Type
20 | tags: string
21 | previewURL: string
22 | previewWidth: number
23 | previewHeight: number
24 | webformatURL: string
25 | webformatWidth: number
26 | webformatHeight: number
27 | largeImageURL: string
28 | imageWidth: number
29 | imageHeight: number
30 | imageSize: number
31 | views: number
32 | downloads: number
33 | collections: number
34 | likes: number
35 | comments: number
36 | user_id: number
37 | user: string
38 | userImageURL: string
39 | }
40 |
41 | export enum Type {
42 | Photo = 'photo',
43 | }
44 |
45 |
46 | export default {
47 | search(options: PixabayOptions = {}) {
48 | return request(
49 | `/api/?key=13927481-1de5dcccace42d9447c90346f&safesearch=true&image_type=photo&${qs.stringify(
50 | options,
51 | )}`,
52 | {
53 | base: 'https://pixabay.com',
54 | minPendingDuration: 300,
55 | },
56 | ) as Promise;
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/src/apis/producer.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 |
3 | export type {
4 | IAnnouncedProducer,
5 | IApprovedProducer,
6 | } from 'rum-fullnode-sdk/dist/apis/producer';
7 |
8 | export default {
9 | announce(data: {
10 | group_id: string
11 | action: 'add' | 'remove'
12 | type: 'producer'
13 | memo: string
14 | }) {
15 | return getClient().Producer.announce(data);
16 | },
17 | fetchAnnouncedProducers(groupId: string) {
18 | return getClient().Producer.listAnnouncedProducers(groupId);
19 | },
20 | producer(data: {
21 | group_id: string
22 | action: 'add' | 'remove'
23 | producer_pubkey: string
24 | }) {
25 | return getClient().Producer.declare(data);
26 | },
27 | fetchApprovedProducers(groupId: string) {
28 | return getClient().Producer.listApprovedProducers(groupId);
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/src/apis/psPing.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 | import getBase from 'utils/getBase';
3 |
4 | export default {
5 | ping(peerId: string) {
6 | return request('/api/v1/psping', {
7 | method: 'POST',
8 | base: getBase(),
9 | body: {
10 | peer_id: peerId,
11 | },
12 | });
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/apis/pubkeyToAddr.ts:
--------------------------------------------------------------------------------
1 | import request from '../request';
2 | import getBase from 'utils/getBase';
3 |
4 | export default {
5 | get(pubkey: string) {
6 | return request('/api/v1/tools/pubkeytoaddr', {
7 | method: 'POST',
8 | base: getBase(),
9 | body: {
10 | encoded_pubkey: pubkey,
11 | },
12 | });
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/apis/trx.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 |
3 | export type { ITrx } from 'rum-fullnode-sdk/dist/apis/trx';
4 |
5 | export default {
6 | fetchTrx(GroupId: string, TrxId: string) {
7 | return getClient().Trx.get(GroupId, TrxId);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/apis/user.ts:
--------------------------------------------------------------------------------
1 | import { getClient } from './client';
2 |
3 | export default {
4 | announce(payload: {
5 | 'group_id': string
6 | 'action': 'add' | 'remove'
7 | 'type': 'user'
8 | 'memo': string
9 | }) {
10 | return getClient().User.announce(payload);
11 | },
12 |
13 | fetchAnnouncedUsers(groupId: string) {
14 | return getClient().User.listAnnouncedUsers(groupId);
15 | },
16 |
17 | declare(payload: {
18 | 'user_pubkey': string
19 | 'group_id': string
20 | 'action': 'add' | 'remove'
21 | }) {
22 | return getClient().User.declare(payload);
23 | },
24 |
25 | fetchUser(groupId: string, publisher: string) {
26 | return getClient().User.getAnnouncedUser(groupId, publisher);
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/BackToTop.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MdArrowUpward } from 'react-icons/md';
3 | import useScroll from 'hooks/useScroll';
4 | import { getPageElement } from 'utils/domSelector';
5 | import { observer, useLocalObservable } from 'mobx-react-lite';
6 |
7 | interface IProps {
8 | rootRef: React.RefObject
9 | }
10 |
11 | const BackToTop = (props: IProps) => {
12 | const element = props.rootRef.current;
13 |
14 | const back = () => {
15 | (element || getPageElement()).scroll(0, 0);
16 | };
17 |
18 | const scrollTop = useScroll({
19 | element,
20 | });
21 |
22 | if (scrollTop < window.innerHeight / 2) {
23 | return null;
24 | }
25 |
26 | return (
27 |
37 | );
38 | };
39 |
40 | export default observer((props: IProps) => {
41 | const state = useLocalObservable(() => ({
42 | pending: true,
43 | }));
44 |
45 | React.useEffect(() => {
46 | setTimeout(() => {
47 | state.pending = false;
48 | }, 500);
49 | }, [state]);
50 |
51 | if (state.pending) {
52 | return null;
53 | }
54 |
55 | return ;
56 | });
57 |
--------------------------------------------------------------------------------
/src/components/BottomLine.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
6 | ·
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/ButtonProgress.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CircularProgress } from '@mui/material';
3 | import { MdDone } from 'react-icons/md';
4 | import { observer, useLocalObservable } from 'mobx-react-lite';
5 | import { action } from 'mobx';
6 |
7 | interface IProps {
8 | size?: number
9 | color?: string
10 | isDoing: boolean
11 | isDone?: boolean
12 | noMargin?: boolean
13 | fixedDone?: boolean
14 | }
15 |
16 | const ButtonProgress = observer((props: IProps) => {
17 | const state = useLocalObservable(() => ({
18 | isShowDone: false,
19 | isShowDoneTimer: 0,
20 | }));
21 |
22 | React.useEffect(action(() => {
23 | if (props.isDone) {
24 | state.isShowDone = true;
25 | if (!props.fixedDone) {
26 | state.isShowDoneTimer = window.setTimeout(action(() => {
27 | state.isShowDone = false;
28 | }), 1500);
29 | }
30 | }
31 | }), [props.isDone]);
32 |
33 | React.useEffect(() => () => {
34 | window.clearTimeout(state.isShowDoneTimer);
35 | }, []);
36 |
37 | const { isDoing, color = 'text-white', size = 12 } = props;
38 | const { isShowDone } = state;
39 |
40 | if (isDoing) {
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 | if (isShowDone) {
48 | return (
49 |
52 |
53 |
54 | );
55 | }
56 | return null;
57 | });
58 |
59 | export default ButtonProgress;
60 |
--------------------------------------------------------------------------------
/src/components/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dialog, DialogProps } from '@mui/material';
3 | import { IoMdClose } from 'react-icons/io';
4 |
5 | interface IProps extends DialogProps {
6 | hideCloseButton?: boolean
7 | }
8 |
9 | export default (props: IProps) => {
10 | const { hideCloseButton, ...dialogProps } = props;
11 | return (
12 |
13 |
14 | {!hideCloseButton && (
15 |
20 |
21 |
22 | )}
23 | {props.children}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/EmojiPicker/lang.ts:
--------------------------------------------------------------------------------
1 | import { i18n } from 'store/i18n';
2 |
3 | export const lang = i18n.createLangLoader({
4 | cn: { content: {
5 | 'recent': '最近使用',
6 | 'people': '人物/表情',
7 | 'nature': '动物/自然',
8 | 'foods': '食物/饮料',
9 | 'activity': '活动',
10 | 'places': '旅游/地点',
11 | 'objects': '物体',
12 | 'symbols': '符号',
13 | 'flags': '旗帜',
14 | } },
15 | en: { content: {
16 | 'recent': 'Frequently Used',
17 | 'people': 'Smileys & People',
18 | 'nature': 'Animals & Nature',
19 | 'foods': 'Food & Drink',
20 | 'activity': 'Activities',
21 | 'places': 'Travel & Places',
22 | 'objects': 'Objects',
23 | 'symbols': 'Symbols',
24 | 'flags': 'Flags',
25 | } },
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/GroupIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import { useStore } from 'store';
4 | import { GROUP_CONFIG_KEY } from 'utils/constant';
5 |
6 | interface IProps {
7 | groupId?: string
8 | groupName?: string
9 | width: number
10 | height: number
11 | fontSize: number
12 | groupIcon?: string
13 | className?: string
14 | colorClassName?: string
15 | }
16 |
17 | export default observer((props: IProps) => {
18 | const { groupStore } = useStore();
19 | let groupName = '';
20 | let groupIcon = '';
21 | if (props.groupId) {
22 | const group = groupStore.map[props.groupId];
23 | groupName = group?.group_name || '';
24 | } else {
25 | groupName = props.groupName || '';
26 | }
27 |
28 | if (props.groupIcon) {
29 | groupIcon = props.groupIcon;
30 | } else if (props.groupId) {
31 | groupIcon = (groupStore.configMap.get(props.groupId)?.[GROUP_CONFIG_KEY.GROUP_ICON] ?? '') as string;
32 | }
33 |
34 | if (!groupIcon) {
35 | return (
36 |
45 | {groupName.trim().substring(0, 1)}
46 |
47 |
);
48 | }
49 |
50 | return ;
51 | });
52 |
--------------------------------------------------------------------------------
/src/components/Images.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Base64 from 'utils/base64';
3 | import classNames from 'classnames';
4 | import openPhotoSwipe from 'standaloneModals/openPhotoSwipe';
5 | import { IDBComment } from 'hooks/useDatabase/models/comment';
6 |
7 | export default (props: {
8 | images: Exclude
9 | className?: string
10 | }) => {
11 | const urls = props.images.map((image) => ('url' in image ? image.url : Base64.getUrl(image)));
12 | return (
13 |
14 | {urls.map((url: string, index: number) => (
15 |
{
18 | openPhotoSwipe({
19 | image: urls,
20 | index,
21 | });
22 | }}
23 | >
24 |
32 |
33 | ))}
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { CircularProgress } from '@mui/material';
3 | import { styled } from '@mui/material/styles';
4 |
5 | export default (props: { size?: number, color?: string }) => {
6 | const { size } = props;
7 |
8 | const Bottom = useMemo(() => styled(CircularProgress)({
9 | color: props.color,
10 | opacity: 0.3,
11 | }), [props.color]);
12 |
13 | const Top = useMemo(() => styled(CircularProgress)({
14 | color: props.color,
15 | animationDuration: '550ms',
16 | position: 'absolute',
17 | left: 0,
18 | }), [props.color]);
19 |
20 | return (
21 |
22 |
23 |
29 |
34 |
35 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/MiddleTruncate.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip } from '@mui/material';
3 | import copy from 'copy-to-clipboard';
4 | import { observer } from 'mobx-react-lite';
5 | import { useStore } from 'store';
6 | import { lang } from 'utils/lang';
7 |
8 | interface IProps {
9 | string: string
10 | length: number
11 | }
12 |
13 | export default observer((props: IProps) => {
14 | const { snackbarStore } = useStore();
15 | const { string, length } = props;
16 |
17 | if (!string) {
18 | return null;
19 | }
20 |
21 | return (
22 | {
23 | copy(string);
24 | snackbarStore.show({
25 | message: lang.copied,
26 | });
27 | }}
28 | >
29 |
36 | {`${string.slice(
37 | 0,
38 | length,
39 | )}......${string.slice(-length)}`}
40 |
41 |
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/Page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loading from 'components/Loading';
3 |
4 | interface IProps {
5 | title: string
6 | loading: boolean
7 | children?: React.ReactNode
8 | }
9 |
10 | export default (props: IProps) => (
11 |
12 |
13 | {props.loading && (
14 |
15 |
16 |
17 | )}
18 | {!props.loading && (
19 |
20 |
21 | {props.title}
22 |
23 |
24 |
{props.children}
25 |
26 |
27 | )}
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/PageLoading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import Loading from 'components/Loading';
4 | import { useStore } from 'store';
5 | import { Fade } from '@mui/material';
6 |
7 | export default observer(() => {
8 | const { modalStore } = useStore();
9 |
10 | if (!modalStore.pageLoading.open) {
11 | return null;
12 | }
13 |
14 | return (
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/PasswordInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer, useLocalObservable } from 'mobx-react-lite';
3 | import { TextField, TextFieldProps, IconButton, InputAdornment } from '@mui/material';
4 | import { MdVisibility, MdVisibilityOff } from 'react-icons/md';
5 |
6 | export default observer((props: TextFieldProps) => {
7 | const state = useLocalObservable(() => ({
8 | showPassword: false,
9 | }));
10 |
11 | return (
12 |
18 | {
21 | state.showPassword = !state.showPassword;
22 | }}
23 | size="large"
24 | >
25 | {state.showPassword ? : }
26 |
27 |
28 | ),
29 | }}
30 | />
31 | );
32 | });
33 |
--------------------------------------------------------------------------------
/src/components/PreviewVersion.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip } from '@mui/material';
3 |
4 | export default () => (
5 |
10 |
14 | 内测版
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/components/TrxInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer, useLocalObservable } from 'mobx-react-lite';
3 | import { ImInfo } from 'react-icons/im';
4 | import TrxModal from 'components/TrxModal';
5 |
6 | export default observer((props: { trxId: string }) => {
7 | const state = useLocalObservable(() => ({
8 | showTrxModal: false,
9 | }));
10 |
11 | const openTrxModal = () => {
12 | state.showTrxModal = true;
13 | };
14 |
15 | const closeTrxModal = () => {
16 | state.showTrxModal = false;
17 | };
18 |
19 | return (
20 |
33 | );
34 | });
35 |
--------------------------------------------------------------------------------
/src/components/Video/index.css:
--------------------------------------------------------------------------------
1 | .plyr.plyr--stopped .plyr__controls { display: none }
2 |
3 | .plyr-container video,
4 | .plyr-container.plyr__poster {
5 | border-radius: 12px;
6 | }
7 |
8 | .plyr-container.rect,
9 | .plyr-container.rect video,
10 | .plyr-container.rect .plyr__video-wrapper {
11 | width: 290px !important;
12 | height: 163px !important;
13 | }
14 |
15 | .plyr-container.rect-md,
16 | .plyr-container.rect-md video,
17 | .plyr-container.rect-md .plyr__video-wrapper {
18 | width: 460px !important;
19 | height: 258px !important;
20 | }
21 |
22 | .plyr-container.square,
23 | .plyr-container.square video,
24 | .plyr-container.square .plyr__video-wrapper {
25 | width: 290px !important;
26 | height: 290px !important;
27 | }
28 |
29 | .plyr-container.square-md,
30 | .plyr-container.square-md video,
31 | .plyr-container.square-md .plyr__video-wrapper {
32 | width: 460px !important;
33 | height: 460px !important;
34 | }
35 |
36 | .plyr--video,
37 | .plyr__video-wrapper {
38 | border-radius: 12px;
39 | }
40 |
41 | .plyr__control--overlaid {
42 | background: rgba(0,0,0,0.3) !important;
43 | border: 1px solid #fff;
44 | }
45 |
46 | .plyr--full-ui input[type=range] {
47 | color: white !important;
48 | }
49 |
50 | .plyr__control:hover {
51 | background: transparent !important;
52 | }
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Video';
2 | export { default as Avatar } from './Avatar';
3 | export { default as BottomLine } from './BottomLine';
4 | export { default as ContentSyncStatus } from './ContentSyncStatus';
5 | export { default as Dialog } from './Dialog';
6 | export { default as Editor } from './Editor';
7 | export { default as Images } from './Images';
8 | export { default as Loading } from './Loading';
9 | export { default as TrxInfo } from './TrxInfo';
10 | export { default as UserCard } from './UserCard';
11 |
--------------------------------------------------------------------------------
/src/hooks/addGroups.tsx:
--------------------------------------------------------------------------------
1 | import { store } from 'store';
2 | import { IGroup, GroupUpdatedStatus } from 'apis/group';
3 | import { differenceInSeconds, differenceInHours } from 'date-fns';
4 | import getTimestampFromBlockTime from 'utils/getTimestampFromBlockTime';
5 |
6 | const getUpdatedStatus = (latestUpdated: number) => {
7 | if (differenceInSeconds(Date.now(), new Date(latestUpdated)) < 60) {
8 | return GroupUpdatedStatus.ACTIVE;
9 | } if (differenceInHours(Date.now(), new Date(latestUpdated)) < 24) {
10 | return GroupUpdatedStatus.RECENTLY;
11 | }
12 | return GroupUpdatedStatus.SLEEPY;
13 | };
14 |
15 | export default (groups: IGroup[]) => {
16 | const { latestStatusStore, groupStore } = store;
17 | const derivedGroups = (groups ?? []).map((group) => {
18 | const latestStatus = latestStatusStore.map[group.group_id] || latestStatusStore.DEFAULT_LATEST_STATUS;
19 | group.updatedStatus = getUpdatedStatus(Math.max(latestStatus.lastUpdated || 0, getTimestampFromBlockTime(group.last_updated)));
20 | return group;
21 | });
22 | groupStore.addGroups(derivedGroups);
23 | };
24 |
--------------------------------------------------------------------------------
/src/hooks/fetchGroups.tsx:
--------------------------------------------------------------------------------
1 | import GroupApi from 'apis/group';
2 | import addGroups from 'hooks/addGroups';
3 |
4 | export default async () => {
5 | try {
6 | const { groups } = await GroupApi.fetchMyGroups();
7 | addGroups(groups ?? []);
8 | } catch (err) {
9 | console.error(err);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/hooks/useAnchorClick.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shell } from 'electron';
3 |
4 | export default () => {
5 | React.useEffect(() => {
6 | const body = document.querySelector('body')!;
7 | if (body) {
8 | body.onclick = (e) => {
9 | const target = e.target as HTMLElement;
10 | if (target && target.tagName === 'A') {
11 | e.preventDefault();
12 | const href = target.getAttribute('href');
13 | if (href && href.startsWith('http')) {
14 | shell.openExternal(href);
15 | }
16 | }
17 | };
18 | }
19 | }, []);
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/useAppBadgeCount.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import { sum } from 'lodash';
4 | import { ipcRenderer } from 'electron';
5 |
6 | export default () => {
7 | const { groupStore, latestStatusStore, nodeStore } = useStore();
8 | const { ids } = groupStore;
9 | const badgeCount = sum(
10 | ids.map(
11 | (groupId: string) => {
12 | const latestStatus = latestStatusStore.map[groupId] || latestStatusStore.DEFAULT_LATEST_STATUS;
13 | return latestStatus.unreadCount + sum(Object.values(latestStatus.notificationUnreadCountMap || {}));
14 | },
15 | ),
16 | );
17 |
18 | React.useEffect(() => {
19 | ipcRenderer.send('set-badge-count', nodeStore.connected ? badgeCount : 0);
20 | }, [badgeCount, nodeStore.connected]);
21 | };
22 |
--------------------------------------------------------------------------------
/src/hooks/useAppLaunchTime.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rumsystem/rum-app/d33578000c85f1af6bd7c704685dbc06eb3eae55/src/hooks/useAppLaunchTime.tsx
--------------------------------------------------------------------------------
/src/hooks/useCanIPost.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import useCheckPermission from 'hooks/useCheckPermission';
4 | import { lang } from 'utils/lang';
5 | import { GroupStatus } from 'apis/group';
6 |
7 | export default () => {
8 | const { snackbarStore, groupStore } = useStore();
9 | const checkPermission = useCheckPermission();
10 |
11 | return React.useCallback(async (groupId: string, options?: {
12 | ignoreGroupStatus: boolean
13 | }) => {
14 | const group = groupStore.map[groupId];
15 | if (!group) {
16 | snackbarStore.show({
17 | message: lang.notFound(lang.group),
18 | type: 'error',
19 | });
20 | throw new Error(lang.beBannedTip);
21 | }
22 |
23 | if (!options || !options.ignoreGroupStatus) {
24 | if (group.group_status === GroupStatus.SYNC_FAILED) {
25 | snackbarStore.show({
26 | message: lang.syncFailedTipForSubmit,
27 | type: 'error',
28 | });
29 | throw new Error(lang.syncFailedTipForSubmit);
30 | }
31 | }
32 |
33 | if (!await checkPermission(
34 | {
35 | groupId: group.group_id,
36 | publisher: group.user_pubkey,
37 | trxType: 'POST',
38 | },
39 | )) {
40 | snackbarStore.show({
41 | message: lang.beBannedTip,
42 | type: 'error',
43 | });
44 | throw new Error(lang.beBannedTip);
45 | }
46 | }, []);
47 | };
48 |
--------------------------------------------------------------------------------
/src/hooks/useCheckPaidGroupAnounce.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IGroup } from 'apis/group';
3 | import {
4 | isPublicGroup,
5 | isNoteGroup,
6 | } from 'store/selectors/group';
7 | import MVMApi, { ICoin } from 'apis/mvm';
8 | import { formatEther, Contract } from 'ethers';
9 | import * as ContractUtils from 'utils/contract';
10 |
11 | export default () => React.useCallback(async (group: IGroup) => {
12 | if (isPublicGroup(group)
13 | || isNoteGroup(group)
14 | ) {
15 | return true;
16 | }
17 | try {
18 | const res = await MVMApi.coins();
19 | const coins = Object.values(res.data).filter((coin) => coin.rumSymbol !== 'RUM') as ICoin[];
20 |
21 | const contract = new Contract(ContractUtils.PAID_GROUP_CONTRACT_ADDRESS, ContractUtils.PAID_GROUP_ABI, ContractUtils.provider);
22 | const groupDetail = await contract.getPrice(ContractUtils.uuidToBigInt(group.group_id));
23 |
24 | const amount = formatEther(groupDetail.amount);
25 | const rumSymbol = coins.find((coin) => coin.rumAddress === groupDetail.tokenAddr)?.rumSymbol || '';
26 |
27 | if (+amount > 0 && rumSymbol) {
28 | return true;
29 | }
30 | return false;
31 | } catch (_) {
32 | return false;
33 | }
34 | }, []);
35 |
--------------------------------------------------------------------------------
/src/hooks/useCheckPermission.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AuthApi, { TrxType } from 'apis/auth';
3 |
4 | interface IOptions {
5 | groupId: string
6 | publisher: string
7 | trxType: TrxType
8 | }
9 |
10 | export default () => React.useCallback(async ({
11 | groupId,
12 | publisher,
13 | trxType,
14 | }: IOptions) => {
15 | const followingRule = await AuthApi.getFollowingRule(groupId, trxType);
16 | if (followingRule.AuthType === 'FOLLOW_ALW_LIST') {
17 | const allowList = await AuthApi.getAllowList(groupId) || [];
18 | return allowList.some((item) => item.Pubkey === publisher);
19 | }
20 | if (followingRule.AuthType === 'FOLLOW_DNY_LIST') {
21 | const denyList = await AuthApi.getDenyList(groupId) || [];
22 | return !denyList.some((item) => item.Pubkey === publisher);
23 | }
24 | return true;
25 | }, []);
26 |
--------------------------------------------------------------------------------
/src/hooks/useCheckPrivatePermission.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IGroup } from 'apis/group';
3 | import UserApi from 'apis/user';
4 | import {
5 | isGroupOwner,
6 | isPublicGroup,
7 | isNoteGroup,
8 | } from 'store/selectors/group';
9 |
10 | export default () => React.useCallback(async (group: IGroup) => {
11 | if (isPublicGroup(group)
12 | || isGroupOwner(group)
13 | || isNoteGroup(group)
14 | ) {
15 | return true;
16 | }
17 | try {
18 | const ret = await UserApi.fetchUser(group.group_id, group.user_pubkey);
19 | return ret.Result === 'APPROVED';
20 | } catch (_) {
21 | return false;
22 | }
23 | }, []);
24 |
--------------------------------------------------------------------------------
/src/hooks/useChecking/index.tsx:
--------------------------------------------------------------------------------
1 | import useCheckingAnnouncedUsers from './useCheckingAnnouncedUsers';
2 |
3 | export default () => {
4 | const SECONDS = 1000;
5 |
6 | useCheckingAnnouncedUsers(20 * SECONDS);
7 | };
8 |
--------------------------------------------------------------------------------
/src/hooks/useCleanLocalData.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useDatabase from 'hooks/useDatabase';
3 | import sleep from 'utils/sleep';
4 | import { lang } from 'utils/lang';
5 | import { useStore } from 'store';
6 | import ElectronCurrentNodeStore from 'store/electronCurrentNodeStore';
7 | import electronApiConfigHistoryStore from 'store/electronApiConfigHistoryStore';
8 |
9 | export default () => {
10 | const { confirmDialogStore, nodeStore } = useStore();
11 | const database = useDatabase();
12 |
13 | return React.useCallback(() => {
14 | confirmDialogStore.show({
15 | content: lang.confirmToClearCacheData,
16 | okText: lang.yes,
17 | isDangerous: true,
18 | ok: async () => {
19 | if (confirmDialogStore.loading) {
20 | return;
21 | }
22 |
23 | confirmDialogStore.setLoading(true);
24 |
25 | if (process.env.NODE_ENV !== 'development') { await sleep(500); }
26 |
27 | nodeStore.setQuitting(true);
28 | nodeStore.setConnected(false);
29 |
30 | if (process.env.NODE_ENV !== 'development') { await sleep(500); }
31 |
32 | ElectronCurrentNodeStore.getStore().clear();
33 | electronApiConfigHistoryStore.getStore()?.clear();
34 |
35 | if (process.env.NODE_ENV !== 'development') { await sleep(500); }
36 |
37 | await database.delete();
38 |
39 | if (process.env.NODE_ENV !== 'development') { await sleep(500); }
40 | window.location.reload();
41 | },
42 | });
43 | }, [database]);
44 | };
45 |
--------------------------------------------------------------------------------
/src/hooks/useCloseNode.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import * as Quorum from 'utils/quorum';
4 |
5 | export default () => {
6 | const { nodeStore } = useStore();
7 |
8 | return React.useCallback(async () => {
9 | try {
10 | if (nodeStore.status.up) {
11 | nodeStore.setQuitting(true);
12 | nodeStore.setConnected(false);
13 | await Quorum.down();
14 | }
15 | } catch (err) {
16 | console.error(err);
17 | }
18 | }, []);
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/contentStatus.ts:
--------------------------------------------------------------------------------
1 | export enum ContentStatus {
2 | synced = 'synced',
3 | syncing = 'syncing',
4 | }
5 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/index.tsx:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie';
2 | import Database, { getDatabaseName } from './database';
3 |
4 | let database = null as Database | null;
5 |
6 | export default () => database!;
7 |
8 | export const init = async (nodePublickey: string) => {
9 | if (database) {
10 | return database;
11 | }
12 |
13 | database = new Database(nodePublickey);
14 | await database.open();
15 |
16 | return database;
17 | };
18 |
19 | export const exists = async (nodePublickey: string) => {
20 | const databaseName = getDatabaseName(nodePublickey);
21 | const exists = await Dexie.exists(databaseName);
22 | return exists;
23 | };
24 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/emptyTrx.ts:
--------------------------------------------------------------------------------
1 | import type Database from 'hooks/useDatabase/database';
2 |
3 | export interface IDBEmptyTrx {
4 | groupId: string
5 | trxId: string
6 | timestamp: number
7 | lastChecked: number
8 | }
9 |
10 | export enum Order {
11 | desc,
12 | hot,
13 | }
14 |
15 | export const DEFAULT_SUMMARY = {
16 | hotCount: 0,
17 | commentCount: 0,
18 | likeCount: 0,
19 | dislikeCount: 0,
20 | };
21 |
22 | export const get = async (db: Database, where: { groupId: string, trxId: string }) => {
23 | const item = await db.emptyTrx.where(where).first();
24 | return item;
25 | };
26 |
27 | export const getByGroupId = async (db: Database, groupId: string) => {
28 | const items = await db.emptyTrx.where({ groupId }).toArray();
29 | return items;
30 | };
31 |
32 | export const getAll = async (db: Database) => {
33 | const items = await db.emptyTrx.toArray();
34 | return items;
35 | };
36 |
37 | export const put = async (db: Database, trx: IDBEmptyTrx) => {
38 | await db.emptyTrx.put(trx);
39 | };
40 |
41 | export const bulkPut = async (db: Database, trxs: Array) => {
42 | if (!trxs.length) { return; }
43 | await db.emptyTrx.bulkPut(trxs);
44 | };
45 |
46 | export const del = async (db: Database, where: { groupId: string, trxId: string }) => {
47 | await db.emptyTrx.where(where).delete();
48 | };
49 |
50 | export const bulkDelete = async (db: Database, where: Array<{ groupId: string, trxId: string }>) => {
51 | if (!where.length) { return; }
52 | await db.emptyTrx
53 | .where('[groupId+trxId]')
54 | .anyOf(where.map((v) => [v.groupId, v.trxId]))
55 | .delete();
56 | };
57 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/index.tsx:
--------------------------------------------------------------------------------
1 | export * as CommentModel from 'hooks/useDatabase/models/comment';
2 | export * as CounterModel from 'hooks/useDatabase/models/counter';
3 | export * as EmptyTrxModel from 'hooks/useDatabase/models/emptyTrx';
4 | export * as ImageModel from 'hooks/useDatabase/models/image';
5 | export * as NotificationModel from 'hooks/useDatabase/models/notification';
6 | export * as PendingTrxModel from 'hooks/useDatabase/models/pendingTrx';
7 | export * as PostModel from 'hooks/useDatabase/models/posts';
8 | export * as ProfileModel from 'hooks/useDatabase/models/profile';
9 | export * as RelationModel from 'hooks/useDatabase/models/relations';
10 | export * as RelationSummaryModel from 'hooks/useDatabase/models/relationSummaries';
11 | export * as SummaryModel from 'hooks/useDatabase/models/summary';
12 | export * as TransferModel from 'hooks/useDatabase/models/transfer';
13 | export * as WebsiteMetadataModel from 'hooks/useDatabase/models/websiteMetadata';
14 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/likeStatus.ts:
--------------------------------------------------------------------------------
1 | import type Database from 'hooks/useDatabase/database';
2 | import { countBy } from 'lodash';
3 | import { IDBCounter } from './counter';
4 |
5 | export const bulkGetLikeStatus = async (
6 | db: Database,
7 | items: Array<{
8 | groupId: string
9 | publisher: string
10 | objectId: string
11 | }>,
12 | ) => {
13 | const counters = await db.counters
14 | .where('[groupId+publisher+objectId]')
15 | .anyOf(items.map((v) => [
16 | v.groupId,
17 | v.publisher,
18 | v.objectId,
19 | ]))
20 | .toArray();
21 | const countMap: Record<`${string}_${IDBCounter['type']}`, number> = countBy(
22 | counters,
23 | (counter) => `${counter.objectId}_${counter.type}`,
24 | );
25 | return items.map((v) => {
26 | const likeCount = countMap[`${v.objectId}_like`] || 0;
27 | const undoLikeCount = countMap[`${v.objectId}_undolike`] || 0;
28 |
29 | const dislikeCount = countMap[`${v.objectId}_dislike`] || 0;
30 | const undoDislikeCount = countMap[`${v.objectId}_undodislike`] || 0;
31 |
32 | return {
33 | likeCount: likeCount - undoLikeCount,
34 | dislikeCount: dislikeCount - undoDislikeCount,
35 | };
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/relations.ts:
--------------------------------------------------------------------------------
1 | import type Database from 'hooks/useDatabase/database';
2 | import { ContentStatus } from '../contentStatus';
3 |
4 | export interface IDBRelation {
5 | groupId: string
6 | trxId: string
7 | type: 'follow' | 'undofollow' | 'block' | 'undoblock'
8 | from: string
9 | to: string
10 | timestamp: number
11 | publisher: string
12 | status: ContentStatus
13 | }
14 |
15 | export const put = async (db: Database, item: IDBRelation) => {
16 | await db.relations.put(item);
17 | };
18 |
19 | export const bulkPut = async (db: Database, item: Array) => {
20 | await db.relations.bulkPut(item);
21 | };
22 |
23 | interface GetParam {
24 | groupId: string
25 | trxId: string
26 | }
27 |
28 | export const get = async (db: Database, data: GetParam) => {
29 | const item = await db.relations
30 | .where('[groupId+trxId]')
31 | .equals([data.groupId, data.trxId])
32 | .first();
33 | return item;
34 | };
35 |
36 | export const bulkGet = async (db: Database, data: Array) => {
37 | const items = await db.relations
38 | .where('[groupId+trxId]')
39 | .anyOf(data.map((v) => [v.groupId, v.trxId]))
40 | .toArray();
41 | return items;
42 | };
43 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/transfer.ts:
--------------------------------------------------------------------------------
1 | import type Database from 'hooks/useDatabase/database';
2 | import * as SummaryModel from 'hooks/useDatabase/models/summary';
3 | import { ITransaction } from 'apis/mvm';
4 |
5 | export const create = async (db: Database, transfer: ITransaction) => {
6 | await db.transfers.add(transfer);
7 | await syncCount(db, (transfer.uuid || '').slice(0, 36));
8 | };
9 |
10 | export const bulkCreate = async (db: Database, transfers: Array) => {
11 | await db.transfers.bulkAdd(transfers);
12 | const uuids = Array.from(new Set(transfers.map((transfer) => (transfer.uuid || '').slice(0, 36))));
13 | await Promise.all(uuids.map((uuid) => syncCount(db, uuid)));
14 | };
15 |
16 | const syncCount = async (db: Database, uuid: string) => {
17 | const count = await db.transfers.where('uuid').startsWith(uuid).count();
18 | await SummaryModel.createOrUpdate(db, {
19 | GroupId: '',
20 | ObjectId: uuid,
21 | ObjectType: SummaryModel.SummaryObjectType.transferCount,
22 | Count: count,
23 | });
24 | };
25 |
26 | export const getTransactions = async (
27 | db: Database,
28 | uuid: string,
29 | ) => {
30 | const transfers = await db.transfers.where('uuid').startsWith(uuid).toArray();
31 | return transfers || [];
32 | };
33 |
34 | export const getLastTransfer = async (
35 | db: Database,
36 | ) => {
37 | const transfer = await db.transfers.toCollection().last();
38 | if (!transfer) {
39 | return null;
40 | }
41 | return transfer;
42 | };
43 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/utils.ts:
--------------------------------------------------------------------------------
1 | import useDatabase from '..';
2 |
3 | export const getHotCount = (options: {
4 | likeCount: number
5 | dislikeCount: number
6 | commentCount: number
7 | }) => (options.likeCount - options.dislikeCount + options.commentCount * 0.4) * 10;
8 |
9 | export const removeGroupFromDatabase = async (groupId: string) => {
10 | const db = useDatabase();
11 | await db.transaction(
12 | 'rw',
13 | [
14 | db.posts,
15 | db.comments,
16 | db.counters,
17 | db.profiles,
18 | db.images,
19 | db.notifications,
20 | db.summary,
21 | db.relations,
22 | db.relationSummaries,
23 | db.pendingTrx,
24 | db.emptyTrx,
25 | ],
26 | async () => {
27 | await db.posts.where({ groupId }).delete();
28 | await db.comments.where({ groupId }).delete();
29 | await db.counters.where({ groupId }).delete();
30 | await db.profiles.where({ groupId }).delete();
31 | await db.images.where({ groupId }).delete();
32 | await db.notifications.where({ GroupId: groupId }).delete();
33 | await db.summary.where({ GroupId: groupId }).delete();
34 | await db.relations.where({ groupId }).delete();
35 | await db.relationSummaries.where({ groupId }).delete();
36 | await db.pendingTrx.where({ groupId }).delete();
37 | await db.emptyTrx.where({ groupId }).delete();
38 | },
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/hooks/useDatabase/models/websiteMetadata.ts:
--------------------------------------------------------------------------------
1 | import type Database from 'hooks/useDatabase/database';
2 |
3 | export interface IDBWebsiteMetadata {
4 | url: string
5 | title: string
6 | description: string
7 | site: string
8 | image: string
9 | favicon: string
10 | imageBase64: string
11 | timestamp: number
12 | }
13 |
14 | export const put = async (db: Database, item: IDBWebsiteMetadata) => {
15 | await db.websiteMetadata.put(item);
16 | };
17 |
18 | export const get = (db: Database, url: string) => db.websiteMetadata.where({ url }).first();
19 |
--------------------------------------------------------------------------------
/src/hooks/useExportToWindow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import useDatabase from 'hooks/useDatabase';
4 | import * as Quorum from 'utils/quorum';
5 |
6 | export default () => {
7 | const store = useStore();
8 | const database = useDatabase();
9 |
10 | React.useEffect(() => {
11 | (window as any).store = store;
12 | (window as any).database = database;
13 | (window as any).Quorum = Quorum;
14 | }, []);
15 | };
16 |
--------------------------------------------------------------------------------
/src/hooks/useGroupChange.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 |
4 | export default (changed: () => void) => {
5 | const { activeGroupStore } = useStore();
6 | const preGroupIdRef = React.useRef(activeGroupStore.id);
7 |
8 | React.useEffect(() => {
9 | if (preGroupIdRef.current && preGroupIdRef.current !== activeGroupStore.id) {
10 | changed();
11 | }
12 | preGroupIdRef.current = activeGroupStore.id;
13 | }, [activeGroupStore.id]);
14 |
15 | return null;
16 | };
17 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/content/ContentTaskManager/handleContent/handleEmptyObjects.tsx:
--------------------------------------------------------------------------------
1 | import type { IContentItem } from 'rum-fullnode-sdk/dist/apis/content';
2 | import { Store } from 'store';
3 | import Database from 'hooks/useDatabase/database';
4 | import * as EmptyTrxModel from 'hooks/useDatabase/models/emptyTrx';
5 | import { state } from 'hooks/usePolling/content/EmptyContentManager/state';
6 |
7 | interface IOptions {
8 | groupId: string
9 | objects: IContentItem[]
10 | store: Store
11 | database: Database
12 | }
13 |
14 | export default async (options: IOptions) => {
15 | const { database, groupId, objects } = options;
16 | if (objects.length === 0) { return; }
17 |
18 | try {
19 | await database.transaction('rw', [database.emptyTrx], async () => {
20 | const itemsToPut: Array = objects.map((v) => ({
21 | groupId,
22 | trxId: v.TrxId,
23 | lastChecked: Date.now(),
24 | timestamp: Number(v.TimeStamp.slice(0, -6)),
25 | }));
26 | await EmptyTrxModel.bulkPut(database, itemsToPut);
27 | itemsToPut.forEach((v) => {
28 | state.items.push(v);
29 | });
30 | });
31 | } catch (e) {
32 | console.error(e);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/content/EmptyContentManager/index.ts:
--------------------------------------------------------------------------------
1 | import useDatabase from 'hooks/useDatabase';
2 | import * as EmptyTrxModel from 'hooks/useDatabase/models/emptyTrx';
3 | import ContentApi from 'apis/content';
4 | import { state } from './state';
5 | import type { ContentTaskManager } from '../ContentTaskManager';
6 |
7 | export class EmptyContentManager {
8 | public constructor(private contentTaskManager: ContentTaskManager) {}
9 | public async init() {
10 | const items = await EmptyTrxModel.getAll(useDatabase());
11 | state.items = items;
12 | }
13 |
14 | public async handleNewTrx(items: Array<{ groupId: string, trxId: string }>) {
15 | for (const item of items) {
16 | const hasEmptyTrx = state.items.some((v) => v.groupId === item.groupId && v.trxId === item.trxId);
17 | if (!hasEmptyTrx) { continue; }
18 | const contents = await ContentApi.fetchContents(item.groupId, {
19 | num: 1,
20 | starttrx: item.trxId,
21 | includestarttrx: true,
22 | });
23 | const content = contents?.[0];
24 | if (!content) { continue; }
25 | if (content.TrxId !== item.trxId) { return; }
26 | await this.contentTaskManager.handleContent(item.groupId, [content]);
27 | const index = state.items.findIndex((v) => v.groupId === item.groupId && v.trxId === item.trxId);
28 | state.items.splice(index, 1);
29 | await EmptyTrxModel.del(useDatabase(), { groupId: item.groupId, trxId: item.trxId });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/content/EmptyContentManager/state.ts:
--------------------------------------------------------------------------------
1 | import * as EmptyTrxModel from 'hooks/useDatabase/models/emptyTrx';
2 |
3 | export const state = {
4 | items: [] as Array,
5 | };
6 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/content/index.ts:
--------------------------------------------------------------------------------
1 | import { ContentTaskManager } from './ContentTaskManager';
2 | import { EmptyContentManager } from './EmptyContentManager';
3 | import { SocketManager } from './SocketManager';
4 |
5 | export const contentTaskManager = new ContentTaskManager();
6 | export const emptyContentManager = new EmptyContentManager(contentTaskManager);
7 | export const socketManager = new SocketManager(contentTaskManager, emptyContentManager);
8 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/groups.tsx:
--------------------------------------------------------------------------------
1 | import sleep from 'utils/sleep';
2 | import { GroupStatus } from 'apis/group';
3 | import { store } from 'store';
4 | import fetchGroups from 'hooks/fetchGroups';
5 |
6 | const INACTIVE_ADDITIONAL_INTERVAL = 4000;
7 | const ACTIVE_ADDITIONAL_INTERVAL = 2000;
8 |
9 | export const groups = async () => {
10 | const { groupStore, activeGroupStore, nodeStore } = store;
11 |
12 | if (!nodeStore.quitting) {
13 | await fetchGroups();
14 | const busy = activeGroupStore.id
15 | && groupStore.map[activeGroupStore.id].group_status
16 | === GroupStatus.SYNCING;
17 | await sleep(busy ? ACTIVE_ADDITIONAL_INTERVAL : INACTIVE_ADDITIONAL_INTERVAL);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { myNodeInfo } from './myNodeInfo';
3 | import { network } from './network';
4 | import { groups } from './groups';
5 | import { contentTaskManager, socketManager, emptyContentManager } from './content';
6 | import { token } from './token';
7 | import { getAnouncedProducers } from './announcedProducers';
8 | import { getGroupConfig } from './groupConfig';
9 | import { transferTransactions } from './transferTransactions';
10 | import { PollingTask } from 'utils';
11 |
12 | export default () => {
13 | const SECONDS = 1000;
14 |
15 | const jobs = React.useMemo(() => ({
16 | myNodeInfo: new PollingTask({ task: myNodeInfo, interval: 4 * SECONDS }),
17 | network: new PollingTask({ task: network, interval: 4 * SECONDS }),
18 | groups: new PollingTask({ task: groups, interval: 2 * SECONDS }),
19 | content: contentTaskManager,
20 | token: new PollingTask({ task: token, interval: 5 * 60 * SECONDS }),
21 | announcedProducers: new PollingTask({ task: getAnouncedProducers(), interval: 60 * SECONDS }),
22 | groupConfig: new PollingTask({ task: getGroupConfig(), interval: 20 * SECONDS }),
23 | transferTransactions: new PollingTask({ task: transferTransactions, interval: 10 * SECONDS }),
24 | socket: socketManager,
25 | }), []);
26 |
27 | React.useEffect(() => {
28 | emptyContentManager.init();
29 | socketManager.start();
30 | contentTaskManager.start();
31 |
32 | return () => {
33 | Object.values(jobs).forEach((v) => {
34 | v.stop();
35 | });
36 | };
37 | }, []);
38 | };
39 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/network.tsx:
--------------------------------------------------------------------------------
1 | import sleep from 'utils/sleep';
2 | import NodeApi from 'apis/node';
3 | import NetworkApi from 'apis/network';
4 | import { store } from 'store';
5 |
6 | export const network = async () => {
7 | const { nodeStore } = store;
8 |
9 | while (!nodeStore.quitting) {
10 | await fetchNetwork();
11 | await sleep(4000);
12 | }
13 |
14 | async function fetchNetwork() {
15 | try {
16 | const [info, network] = await Promise.all([
17 | NodeApi.fetchMyNodeInfo(),
18 | NetworkApi.fetchNetwork(),
19 | ]);
20 |
21 | nodeStore.setInfo(info);
22 | nodeStore.setNetwork(network);
23 | } catch (err) {
24 | console.error(err);
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/hooks/usePolling/token.tsx:
--------------------------------------------------------------------------------
1 | import sleep from 'utils/sleep';
2 | import GroupApi from 'apis/group';
3 | import { store } from 'store';
4 |
5 | export const token = async () => {
6 | const { nodeStore, apiConfigHistoryStore } = store;
7 |
8 | if (nodeStore.mode !== 'EXTERNAL') {
9 | return;
10 | }
11 |
12 | if (!nodeStore.quitting) {
13 | await refreshToken();
14 | await sleep(1000);
15 | }
16 |
17 | async function refreshToken() {
18 | try {
19 | const { token } = await GroupApi.refreshToken();
20 | const apiConfig = {
21 | ...nodeStore.apiConfig,
22 | jwt: token,
23 | };
24 | nodeStore.setApiConfig(apiConfig);
25 | apiConfigHistoryStore.update(apiConfig);
26 | } catch (err) { }
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function usePrevious(value: T) {
4 | const ref = React.useRef();
5 | React.useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useResetNode.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import ElectronNodeStore from 'store/electronNodeStore';
4 |
5 | export default () => {
6 | const { nodeStore } = useStore();
7 |
8 | return React.useCallback(() => {
9 | nodeStore.reset();
10 | ElectronNodeStore.getStore()?.clear();
11 | }, []);
12 | };
13 |
--------------------------------------------------------------------------------
/src/hooks/useScroll.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getPageElement } from 'utils/domSelector';
3 |
4 | interface IProps {
5 | element?: HTMLElement | null
6 | threshold?: number
7 | callback?: (yes: boolean) => void
8 | }
9 |
10 | export default (props?: IProps) => {
11 | const [scrollTop, setScrollTop] = React.useState(0);
12 |
13 | React.useEffect(() => {
14 | const scrollElement = (props && props.element) || getPageElement();
15 | const callback = () => {
16 | if (props && props.callback && props.threshold) {
17 | props.callback(scrollElement.scrollTop >= props.threshold);
18 | }
19 | setScrollTop(scrollElement.scrollTop);
20 | };
21 | setScrollTop(scrollElement.scrollTop);
22 | scrollElement.addEventListener('scroll', callback);
23 |
24 | return () => {
25 | scrollElement.removeEventListener('scroll', callback);
26 | };
27 | }, [props]);
28 |
29 | return scrollTop;
30 | };
31 |
--------------------------------------------------------------------------------
/src/hooks/useSelectComment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import sleep from 'utils/sleep';
4 |
5 | interface IOptions {
6 | disabledHighlight?: boolean
7 | duration?: number
8 | scrollBlock?: 'center' | 'start' | 'end'
9 | inObjectDetailModal?: boolean
10 | }
11 |
12 | export default () => {
13 | const { commentStore } = useStore();
14 |
15 | return React.useCallback(
16 | (selectedCommentId: string, options: IOptions = {}) => {
17 | (async () => {
18 | const domElementId = `comment_${
19 | options.inObjectDetailModal ? 'in_object_detail_modal' : ''
20 | }_${selectedCommentId}`;
21 | const comment = document.querySelector(`#${domElementId}`);
22 | if (!comment) {
23 | console.error('selected comment not found');
24 | return;
25 | }
26 | comment.scrollIntoView({
27 | block: options.scrollBlock || 'center',
28 | behavior: 'smooth',
29 | });
30 | if (options.disabledHighlight) {
31 | return;
32 | }
33 | commentStore.setHighlightDomElementId(domElementId);
34 | await sleep(options.duration || 1500);
35 | commentStore.setHighlightDomElementId('');
36 | })();
37 | },
38 | [commentStore],
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/hooks/useSetupQuitHook.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import { ipcRenderer } from 'electron';
4 | import sleep from 'utils/sleep';
5 | import useCloseNode from 'hooks/useCloseNode';
6 | import { lang } from 'utils/lang';
7 | import { isGroupOwner } from 'store/selectors/group';
8 |
9 | export default () => {
10 | const { confirmDialogStore, groupStore } = useStore();
11 | const closeNode = useCloseNode();
12 |
13 | React.useEffect(() => {
14 | const beforeQuit = async () => {
15 | if (
16 | confirmDialogStore.open
17 | && confirmDialogStore.loading
18 | && confirmDialogStore.okText === lang.reload
19 | ) {
20 | confirmDialogStore.hide();
21 | } else {
22 | const ownerGroupCount = groupStore.groups.filter(
23 | (group) => isGroupOwner(group),
24 | ).length;
25 | const res = await ipcRenderer.invoke('message-box', {
26 | type: 'question',
27 | buttons: [lang.yes, lang.cancel],
28 | title: lang.exitNode,
29 | message: ownerGroupCount
30 | ? lang.exitConfirmTextWithGroupCount(ownerGroupCount)
31 | : lang.exitConfirmText,
32 | });
33 | if (res.response === 1) {
34 | return;
35 | }
36 | }
37 | ipcRenderer.send('disable-app-quit-prompt');
38 | await sleep(500);
39 | await closeNode();
40 | ipcRenderer.send('app-quit');
41 | };
42 | ipcRenderer.send('app-quit-prompt');
43 | ipcRenderer.on('app-before-quit', beforeQuit);
44 | return () => {
45 | ipcRenderer.off('app-before-quit', beforeQuit);
46 | };
47 | }, []);
48 | };
49 |
--------------------------------------------------------------------------------
/src/hooks/useSyncNotificationUnreadCount.tsx:
--------------------------------------------------------------------------------
1 | import Database from 'hooks/useDatabase/database';
2 | import { Store } from 'store';
3 | import * as NotificationModel from 'hooks/useDatabase/models/notification';
4 |
5 | export default (database: Database, store: Store) => {
6 | const { latestStatusStore } = store;
7 |
8 | return async (groupId: string) => {
9 | const unreadCountMap = await NotificationModel.getUnreadCountMap(
10 | database,
11 | { GroupId: groupId },
12 | );
13 | latestStatusStore.update(groupId, {
14 | notificationUnreadCountMap: unreadCountMap,
15 | });
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/useUpdatePermission.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AuthApi, { TrxType } from 'apis/auth';
3 | import type { TrxTypeLower } from 'rum-fullnode-sdk/dist/apis/auth';
4 |
5 | interface IOptions {
6 | groupId: string
7 | publisher: string
8 | trxType: TrxType
9 | action: 'allow' | 'deny'
10 | }
11 |
12 | export default () => React.useCallback(async ({
13 | groupId,
14 | publisher,
15 | trxType,
16 | action,
17 | }: IOptions) => {
18 | const followingRule = await AuthApi.getFollowingRule(groupId, trxType);
19 | await AuthApi.updateAuthList({
20 | group_id: groupId,
21 | type: followingRule.AuthType === 'FOLLOW_ALW_LIST' ? 'upd_alw_list' : 'upd_dny_list',
22 | config: {
23 | action: followingRule.AuthType === 'FOLLOW_ALW_LIST'
24 | ? action === 'allow' ? 'add' : 'remove'
25 | : action === 'allow' ? 'remove' : 'add',
26 | pubkey: publisher,
27 | trx_type: [trxType.toLowerCase() as TrxTypeLower],
28 | memo: '',
29 | },
30 | });
31 | return true;
32 | }, []);
33 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ipcRenderer } from 'electron';
3 | import { createRoot } from 'react-dom/client';
4 | import { configure } from 'mobx';
5 | import App from './layouts';
6 | import { initQuorum } from 'utils/quorum/request';
7 | import Log from 'utils/log';
8 | import './utils/highlightjs';
9 | import 'easymde/dist/easymde.min.css';
10 | import './styles/tailwind.sass';
11 | import css from './styles/tailwind-base.sass?inline';
12 | import './styles/App.global.scss';
13 | import './styles/rendered-markdown.sass';
14 |
15 | Log.setup();
16 | ipcRenderer.setMaxListeners(20);
17 |
18 | configure({
19 | enforceActions: 'never',
20 | computedRequiresReaction: false,
21 | reactionRequiresObservable: false,
22 | observableRequiresReaction: false,
23 | });
24 |
25 | const root = createRoot(document.getElementById('root')!);
26 | root.render( );
27 | initQuorum();
28 |
29 | const style = document.createElement('style');
30 | style.innerHTML = css;
31 | const commentNode = Array.from(document.head.childNodes)
32 | .filter((v) => v.nodeType === 8)
33 | .find((v) => v.textContent && v.textContent.includes('preflight-injection-point'))!;
34 |
35 | document.head.insertBefore(style, commentNode);
36 |
--------------------------------------------------------------------------------
/src/layouts/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import { useStore } from 'store';
5 |
6 | import { loadInspect } from 'utils/inspect';
7 | import { TitleBar } from './TitleBar';
8 | import { Init } from './Init';
9 | import Content from './Content';
10 |
11 | export default () => {
12 | const store = useStore();
13 | const [inited, setInited] = React.useState(false);
14 | const [show, setShow] = React.useState(false);
15 |
16 | React.useEffect(() => loadInspect(), []);
17 |
18 | return (
19 |
20 |
21 |
22 |
28 | {!inited && (
29 | {
31 | setInited(true);
32 | store.nodeStore.setConnected(true);
33 | }}
34 | onInitCheckDone={() => setShow(true)}
35 | />
36 | )}
37 | {inited && (
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/layouts/Content/Header/Notification/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { action } from 'mobx';
4 | import { observer, useLocalObservable } from 'mobx-react-lite';
5 | import { sum } from 'lodash';
6 | import { MdNotificationsNone } from 'react-icons/md';
7 | import { Badge } from '@mui/material';
8 | import useActiveGroupLatestStatus from 'store/selectors/useActiveGroupLatestStatus';
9 | import MessagesModal from './NotificationModal';
10 |
11 | interface Props {
12 | className?: string
13 | }
14 |
15 | export default observer((props: Props) => {
16 | const state = useLocalObservable(() => ({
17 | openMessageModal: false,
18 | }));
19 | const latestStatus = useActiveGroupLatestStatus();
20 |
21 | return (<>
22 | { state.openMessageModal = true; })}
32 | >
33 |
34 |
35 |
36 | { state.openMessageModal = false; }}
39 | />
40 | >);
41 | });
42 |
--------------------------------------------------------------------------------
/src/layouts/Content/Sidebar/ListTypeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { observer } from 'mobx-react-lite';
4 | import { lang } from 'utils/lang';
5 | import { BiGridAlt, BiListUl } from 'react-icons/bi';
6 |
7 | export enum ListType {
8 | text = 'text',
9 | icon = 'icon',
10 | }
11 |
12 | interface IProps {
13 | listType: ListType
14 | setListType: (type: ListType) => void
15 | }
16 |
17 | export default observer((props: IProps) => (
18 |
19 |
props.setListType(ListType.icon)}
25 | >
26 |
27 | {lang.sidebarIconStyleMode}
28 |
29 |
props.setListType(ListType.text)}
35 | >
36 |
37 | {lang.sidebarListStyleMode}
38 |
39 |
40 | ));
41 |
--------------------------------------------------------------------------------
/src/layouts/Content/Sidebar/sortableState.tsx:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 |
3 | const state = observable({
4 | disabled: false,
5 | });
6 |
7 | const disableSortable = action(() => {
8 | state.disabled = true;
9 | });
10 | const enableSortable = action(() => {
11 | state.disabled = false;
12 | });
13 |
14 |
15 | export const sortableState = {
16 | state,
17 | disableSortable,
18 | enableSortable,
19 | };
20 |
--------------------------------------------------------------------------------
/src/layouts/Content/Welcome.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import Button from 'components/Button';
4 | import { joinGroup } from 'standaloneModals/joinGroup';
5 | import { createGroup } from 'standaloneModals/createGroup';
6 | import { lang } from 'utils/lang';
7 |
8 | export default observer(() => (
9 |
10 |
{lang.welcomeToUseRum}
11 |
{lang.youCanTry}
12 |
13 |
createGroup()}
15 | data-test-id="welcome-page-create-group-button"
16 | >
17 | {lang.createGroup}
18 |
19 |
20 |
joinGroup()}
22 | outline
23 | >
24 | {lang.joinGroup}
25 |
26 |
27 |
28 | ));
29 |
--------------------------------------------------------------------------------
/src/layouts/Content/usePollingPaidGroupAnounce.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import useCheckPaidGroupAnounce from 'hooks/useCheckPaidGroupAnounce';
4 |
5 | export default () => {
6 | const { activeGroupStore, groupStore, snackbarStore } = useStore();
7 | const checkPaidGroupAnounce = useCheckPaidGroupAnounce();
8 |
9 | return React.useCallback(() => {
10 | const timer = setInterval(async () => {
11 | try {
12 | const group = groupStore.map[activeGroupStore.id];
13 | if (!group) {
14 | return;
15 | }
16 | const hasAnounce = await checkPaidGroupAnounce(group);
17 | if (hasAnounce) {
18 | activeGroupStore.setAnouncePaidGroupRequired(false);
19 | clearInterval(timer);
20 | snackbarStore.show({
21 | message: '您可以开始使用了',
22 | });
23 | }
24 | } catch (err) {
25 | console.log(err);
26 | }
27 | }, 2000);
28 | return timer;
29 | }, []);
30 | };
31 |
--------------------------------------------------------------------------------
/src/layouts/Content/usePollingPermission.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from 'store';
3 | import useCheckPrivatePermission from 'hooks/useCheckPrivatePermission';
4 |
5 | export default () => {
6 | const { activeGroupStore, groupStore, snackbarStore } = useStore();
7 | const checkPrivatePermission = useCheckPrivatePermission();
8 |
9 | return React.useCallback(() => {
10 | const timer = setInterval(async () => {
11 | try {
12 | const group = groupStore.map[activeGroupStore.id];
13 | if (!group) {
14 | return;
15 | }
16 | const hasPermission = await checkPrivatePermission(group);
17 | if (hasPermission) {
18 | activeGroupStore.setPaidRequired(false);
19 | clearInterval(timer);
20 | snackbarStore.show({
21 | message: '您可以开始使用了',
22 | });
23 | }
24 | } catch (err) {
25 | console.log(err);
26 | }
27 | }, 2000);
28 | return timer;
29 | }, []);
30 | };
31 |
--------------------------------------------------------------------------------
/src/layouts/Main/Forum/OpenObjectEditor/icons.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import {
4 | FaBold,
5 | FaItalic,
6 | FaHeading,
7 | FaQuoteLeft,
8 | FaListUl,
9 | FaListOl,
10 | FaImage,
11 | FaLink,
12 | FaEye,
13 | } from 'react-icons/fa';
14 |
15 | const div = document.createElement('div');
16 |
17 | export const iconMap = {} as Record;
18 |
19 | const icons = [
20 | 'bold',
21 | 'italic',
22 | 'heading',
23 | 'quote',
24 | 'ul',
25 | 'ol',
26 | 'image',
27 | 'link',
28 | 'preview',
29 | ] as const;
30 |
31 | const root = createRoot(div);
32 |
33 | const Component = () => {
34 | useEffect(() => {
35 | icons.forEach((v, i) => {
36 | iconMap[v] = div.children[i].outerHTML;
37 | });
38 | }, []);
39 | return (<>
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | >);
50 | };
51 |
52 | root.render( );
53 |
--------------------------------------------------------------------------------
/src/layouts/Main/Help.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BsQuestion } from 'react-icons/bs';
3 | import { shell } from 'electron';
4 |
5 | export default () => (
6 |
7 |
{
10 | shell.openExternal('https://docs.prsdev.club/#/rum-app/');
11 | }}
12 | >
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/layouts/Main/MoreHistoricalObjectEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Badge } from '@mui/material';
3 | import { FiChevronDown } from 'react-icons/fi';
4 |
5 | interface IProps {
6 | fetchUnreadObjects: () => void
7 | unreadCount: number
8 | }
9 |
10 | export default (props: IProps) => (
11 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/layouts/Main/Note/ObjectEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import useSubmitPost, { ISubmitObjectPayload } from 'hooks/useSubmitPost';
4 | import Editor from 'components/Editor';
5 | import { lang } from 'utils/lang';
6 |
7 | export default observer(() => {
8 | const submitPost = useSubmitPost();
9 |
10 | const submit = async (payload: ISubmitObjectPayload) => {
11 | try {
12 | await submitPost(payload);
13 | return true;
14 | } catch (_) {
15 | return false;
16 | }
17 | };
18 |
19 | return (
20 |
21 |
28 |
29 | );
30 | });
31 |
--------------------------------------------------------------------------------
/src/layouts/Main/Profile/index.scss:
--------------------------------------------------------------------------------
1 | .nickname-field {
2 | > div {
3 | border-color: #6f6f6f;
4 | }
5 | input {
6 | padding: 6px 32px 6px 6px !important;
7 | font-size: 16px;
8 | color: #333333;
9 | }
10 | .Mui-focused {
11 | input {
12 | color: #6f6f6f;
13 | }
14 | }
15 | .MuiOutlinedInput-notchedOutline {
16 | border-width: 1px !important;
17 | border-color: rgba(0, 0, 0, 0.23) !important;
18 | }
19 | }
20 |
21 | .save-nickname {
22 | width: 52px;
23 | height: 30px;
24 | background-color: #6f6f6f;
25 | border-radius: 40px;
26 | margin-left: 14px;
27 | color: #fff;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/src/layouts/Main/Timeline/ObjectEditorEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import { useStore } from 'store';
4 | import classNames from 'classnames';
5 | import { BsPencil } from 'react-icons/bs';
6 | import openObjectEditor from './OpenObjectEditor';
7 |
8 | export default observer(() => {
9 | const { sidebarStore } = useStore();
10 |
11 | return (
12 |
13 |
{
19 | openObjectEditor();
20 | }}
21 | data-test-id="timeline-open-editor-button"
22 | >
23 |
24 |
25 |
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/layouts/Main/Timeline/PostAttachment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import classNames from 'classnames';
4 | import { IDBPost } from 'hooks/useDatabase/models/posts';
5 | import { Video } from 'components/Video';
6 |
7 | interface IProps {
8 | className?: string
9 | attachment: IDBPost['attachment']
10 | }
11 |
12 | export const PostAttachment = observer((props: IProps) => {
13 | const video = props.attachment?.find((v) => v.type === 'Video');
14 |
15 | if (!video) { return null; }
16 |
17 | const poster = video.id
18 | ? `https://storage.googleapis.com/static.press.one/feed/videos/${video.id.replace(/\.mp4$/, '.jpg')}`
19 | : '';
20 | const url = video.id
21 | ? `https://storage.googleapis.com/static.press.one/feed/videos/${video.id}`
22 | : video.url!;
23 |
24 | return (
25 |
30 |
37 |
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/src/layouts/Main/Timeline/helper.ts:
--------------------------------------------------------------------------------
1 | import LinkifyIt from 'linkify-it';
2 |
3 | const linkifyIt = new LinkifyIt(
4 | {
5 | 'mailto:': { validate: () => 0 },
6 | 'ftp:': { validate: () => 0 },
7 | }, {
8 | fuzzyLink: false,
9 | fuzzyEmail: false,
10 | },
11 | );
12 |
13 | export interface MatchItemPost { type: 'post', url: string, id: string }
14 | export interface MatchItemLink { type: 'link', url: string }
15 | export type MatchItem = MatchItemPost | MatchItemLink;
16 |
17 | export const matchContent = (content: string, skipForward = false): { item: MatchItem | null, content: string } => {
18 | const match = linkifyIt.match(content);
19 | if (!match) { return { item: null, content }; }
20 | const lastItem = match.at(-1);
21 |
22 | if (!lastItem) {
23 | return { item: null, content };
24 | }
25 |
26 | let url: null | URL = null;
27 | try {
28 | url = new URL(lastItem.url);
29 | } catch (e) {
30 | return { item: null, content };
31 | }
32 |
33 | if (!['http:', 'https:'].includes(url.protocol)) {
34 | return { item: null, content };
35 | }
36 |
37 | if (!skipForward && url.hostname.endsWith('feed.base.one')) {
38 | const postMatch = /^\/\/?posts\/([a-zA-Z0-9-]+)\/?$/i.exec(url.pathname);
39 | if (postMatch) {
40 | return {
41 | item: {
42 | type: 'post',
43 | id: postMatch[1]!,
44 | url: lastItem.url,
45 | },
46 | content: content.replace(lastItem.url, '').trim(),
47 | };
48 | }
49 | }
50 |
51 | return {
52 | item: {
53 | type: 'link',
54 | url: lastItem.url,
55 | },
56 | content,
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/src/layouts/TitleBar/index.sass:
--------------------------------------------------------------------------------
1 | $height-title-bar: 40px
2 | $height-menu-bar: 40px
3 | $height-total: $height-title-bar + $height-title-bar
4 |
5 | .app-title-bar-placeholder
6 | height: $height-menu-bar
7 | box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16)
8 | position: relative
9 | z-index: 1
10 |
11 | .app-title-bar
12 | height: $height-title-bar
13 | -webkit-app-region: drag
14 | background-color: #6f6f6f
15 | background-size: auto 40px
16 | background-position: 148px 0
17 | background-repeat: no-repeat
18 | user-select: none
19 | z-index: 1000000000
20 |
21 | .app-logo
22 | $padding-left: 5px
23 | background-size: 148px auto
24 | background-repeat: no-repeat
25 | background-position: $padding-left -8px
26 | padding-left: $padding-left
27 | width: 148px + $padding-left
28 |
29 | .non-drag
30 | -webkit-app-region: none
31 |
32 | .apps-button-box
33 | height: $height-title-bar
34 |
35 | > div
36 | height: 40px
37 | width: 40px
38 |
39 | &.close-btn
40 | width: 48px
41 |
42 | .menu-bar
43 | height: $height-menu-bar
44 | // top: $height-title-bar
45 | top: 0
46 | z-index: 1
47 | user-select: none
48 |
49 | // .MuiDialog-root
50 | // top: $height-title-bar !important
51 |
--------------------------------------------------------------------------------
/src/layouts/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StoreProvider } from 'store';
3 |
4 | import { isProduction, isStaging } from 'utils/env';
5 | import { ThemeRoot } from 'utils/theme';
6 | import { preloadAvatars } from 'utils/avatars';
7 | import { handleRumAppProtocol } from 'utils/handleRumAppProtocol';
8 |
9 | import SnackBar from 'components/SnackBar';
10 | import NotificationSlide from 'components/NotificationSlide';
11 | import ConfirmDialog from 'components/ConfirmDialog';
12 | import PageLoading from 'components/PageLoading';
13 | import PreviewVersion from 'components/PreviewVersion';
14 | import { ModalView } from 'standaloneModals/view';
15 |
16 | import Updater from '../Updater';
17 | import MyNodeInfoModal from './modals/MyNodeInfoModal';
18 | import App from './App';
19 |
20 |
21 | export default () => {
22 | React.useEffect(() => {
23 | preloadAvatars();
24 | return handleRumAppProtocol();
25 | }, []);
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | {isProduction && !isStaging && }
34 |
35 | {isStaging && }
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | require('./main_dist');
2 |
--------------------------------------------------------------------------------
/src/main/appState.ts:
--------------------------------------------------------------------------------
1 | export const appState = {
2 | quitting: false,
3 | quitPrompt: false,
4 | };
5 |
--------------------------------------------------------------------------------
/src/main/constants.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | export const testUserDataPath = join(__dirname, '../../src/tests/userData');
4 | export const devRootPath = join(__dirname, '../..');
5 | export const win32Icon = join(__dirname, '../..', 'assets/icon.ico');
6 | export const othersIcon = join(__dirname, '../..', 'assets/icons/pc_bar_icon.png');
7 |
--------------------------------------------------------------------------------
/src/main/log.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { ipcMain } from 'electron';
3 | import log from 'electron-log';
4 |
5 | const filePath = log.transports.file.getFile().path;
6 |
7 | (async () => {
8 | try {
9 | const data = (await fs.promises.readFile(filePath)).toString();
10 | if (data.length > 2000) {
11 | log.transports.file.getFile().clear();
12 | }
13 | } catch (_e) {}
14 | })();
15 |
16 | // override console with electron-log
17 | Object.assign(console, log.functions);
18 |
19 | process.on('unhandledRejection', (reason) => {
20 | console.warn('unhandledRejection');
21 | console.warn(reason);
22 | });
23 |
24 | process.on('uncaughtException', (err) => {
25 | console.warn('uncaughtException');
26 | console.warn(err.message);
27 | console.warn(err.stack);
28 |
29 | process.exit(1);
30 | });
31 |
32 | ipcMain.on('get_main_log', async (event) => {
33 | const data = (await fs.promises.readFile(filePath)).toString();
34 | event.sender.send('response_main_log', {
35 | data: `${filePath}\n\n${data}`,
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/main/processLock.ts:
--------------------------------------------------------------------------------
1 | import { app } from 'electron';
2 |
3 | const args = app.isPackaged
4 | ? process.argv.slice(1)
5 | : process.argv.slice(2);
6 | const hasLock = app.requestSingleInstanceLock(args);
7 |
8 | if (!hasLock) {
9 | app.quit();
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/test.ts:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | import { testUserDataPath } from './constants';
3 |
4 | if (process.env.TEST_ENV) {
5 | electron.app.setPath('userData', testUserDataPath);
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "CommonJS",
5 | "lib": ["esnext"],
6 | "jsx": "preserve",
7 | "strict": true,
8 | "pretty": true,
9 | "sourceMap": false,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "moduleResolution": "node",
15 | "skipLibCheck": true,
16 | "esModuleInterop": true,
17 | "allowSyntheticDefaultImports": true,
18 | "resolveJsonModule": true,
19 | "outDir": "../main_dist",
20 | "paths": {
21 | },
22 | "types": []
23 | },
24 | "include": [
25 | "."
26 | ],
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/utils.ts:
--------------------------------------------------------------------------------
1 | export const sleep = (duration: number) =>
2 | new Promise((resolve) => {
3 | setTimeout(() => {
4 | resolve();
5 | }, duration);
6 | });
7 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "4.0.8",
3 | "name": "rum-app",
4 | "productName": "Rum",
5 | "description": "Rum Desktop App",
6 | "main": "main.js",
7 | "author": "rumsystem.net",
8 | "license": "MIT",
9 | "dependencies": {
10 | "@electron/remote": "^2.0.9",
11 | "@iarna/toml": "^2.2.5",
12 | "@noble/ed25519": "^1.7.3",
13 | "electron-dl": "^3.5.0",
14 | "electron-log": "^4.4.6",
15 | "electron-store": "^8.1.0",
16 | "electron-updater": "^4.6.5",
17 | "get-port": "^5.1.1",
18 | "jws": "^3.2.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/request.ts:
--------------------------------------------------------------------------------
1 | import sleep from 'utils/sleep';
2 |
3 | interface RequestOption extends Omit {
4 | base: string
5 | isTextResponse: boolean
6 | minPendingDuration: number
7 | body: unknown
8 | }
9 |
10 | export default async (url: string, options: Partial = {}) => {
11 | const hasEffectMethod = options.method === 'POST'
12 | || options.method === 'DELETE'
13 | || options.method === 'PUT';
14 | if (hasEffectMethod) {
15 | options.headers = { 'Content-Type': 'application/json' };
16 | options.body = JSON.stringify(options.body);
17 | }
18 | if (!options.base) {
19 | options.credentials = 'include';
20 | }
21 |
22 | const store = (window as any).store;
23 |
24 | if (store.nodeStore.mode === 'EXTERNAL') {
25 | options.headers = {
26 | ...options.headers,
27 | Authorization: `Bearer ${store.nodeStore.apiConfig.jwt}`,
28 | };
29 | }
30 | const result = await Promise.all([
31 | fetch(new Request((options.base || '') + url), options as RequestInit),
32 | sleep(options.minPendingDuration ? options.minPendingDuration : 0),
33 | ]);
34 | const res = result[0];
35 | let resData;
36 | if (options.isTextResponse) {
37 | resData = await res.text();
38 | } else {
39 | resData = await res.json();
40 | }
41 |
42 | if (res.ok) {
43 | return resData;
44 | }
45 | throw Object.assign(new Error(), {
46 | code: resData.code,
47 | status: res.status,
48 | message: resData.message || resData.error,
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/src/standaloneModals/createGroup/StepBox.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 |
4 | interface Props {
5 | className?: string
6 | total: number
7 | value: number
8 | }
9 |
10 | export const StepBox = (props: Props) => (
11 |
19 | {Array(props.total).fill(0).map((_, i) => (
20 |
21 |
= i && 'bg-black',
25 | props.value < i && 'bg-white',
26 | )}
27 | />
28 | {props.total - 1 !== i && (
29 |
30 | )}
31 |
32 | ))}
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/src/standaloneModals/index.ts:
--------------------------------------------------------------------------------
1 | export * from './joinGroup';
2 | export { about } from './about';
3 | export { changeFontSize } from './changeFontSize';
4 | export { createGroup } from './createGroup';
5 | export { default as editProfile } from './editProfile';
6 | export { exportKeyData } from './exportKeyData';
7 | export { default as getMixinUID } from './getMixinUID';
8 | export { groupInfo } from './groupInfo';
9 | export { importKeyData } from './importKeyData';
10 | export { default as inputPassword } from './inputPassword';
11 | export { languageSelect } from './languageSelect';
12 | export { manageGroup } from './manageGroup';
13 | export { migrate } from './migrate';
14 | export { myGroup } from './myGroup';
15 | export { default as openBetaFeaturesModal } from './openBetaFeaturesModal';
16 | export { default as openProducerModal } from './openProducerModal';
17 | export { shareGroup } from './shareGroup';
18 | export { default as useMixinPayment } from './useMixinPayment';
19 |
--------------------------------------------------------------------------------
/src/standaloneModals/joinGroup/index.tsx:
--------------------------------------------------------------------------------
1 | import { modalService } from '../modal';
2 | import { Props } from './JoinGroup';
3 |
4 | export const joinGroup = (seed?: Props['seed']) => {
5 | const item = modalService.createModal();
6 | item.addModal('joinGroup', {
7 | rs: () => setTimeout(item.destoryModal, 3000),
8 | seed,
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/src/standaloneModals/modal.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 |
3 | const state = observable({
4 | id: 0,
5 | componentMap: new Map
(),
10 | get components() {
11 | return Array.from(this.componentMap.values());
12 | },
13 | });
14 |
15 | const createModal = action(() => {
16 | state.id += 1;
17 | const id = state.id;
18 | return {
19 | addModal: action((name: string, props?: any) => {
20 | state.componentMap.set(id, {
21 | id,
22 | name,
23 | props,
24 | });
25 | }),
26 | destoryModal: action(() => {
27 | state.componentMap.delete(id);
28 | }),
29 | };
30 | });
31 |
32 | export const modalService = {
33 | state,
34 | createModal,
35 | };
36 |
--------------------------------------------------------------------------------
/src/standaloneModals/view.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react-lite';
3 | import { modalService } from './modal';
4 | import { JoinGroup } from './joinGroup/JoinGroup';
5 |
6 | export const ModalView = observer(() => (<>
7 | {modalService.state.components.map((v) => (
8 |
9 | {v.name === 'joinGroup' && ( )}
10 |
11 | ))}
12 | >));
13 |
--------------------------------------------------------------------------------
/src/standaloneModals/wallet/searcher.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import { lang } from 'utils/lang';
5 | import { TextField } from '@mui/material';
6 | import { IoSearch } from 'react-icons/io5';
7 |
8 | interface Props {
9 | width?: string
10 | keyword: string
11 | placeholder?: string
12 | onChange: (value: string) => void
13 | }
14 |
15 | export default (props: Props) => (
16 |
17 |
22 |
{
27 | props.onChange(e.target.value.trim().slice(0, 40));
28 | }}
29 | margin="none"
30 | variant="outlined"
31 | placeholder={props.placeholder || lang.search}
32 | />
33 |
34 |
37 |
38 |
39 |
52 |
53 | );
54 |
--------------------------------------------------------------------------------
/src/store/apiConfigHistory.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidV4 } from 'uuid';
2 | import ElectronApiConfigHistoryStore from 'store/electronApiConfigHistoryStore';
3 |
4 | export interface IApiConfig {
5 | origin: string
6 | jwt: string
7 | }
8 |
9 | const store = ElectronApiConfigHistoryStore.getStore();
10 |
11 | export interface IApiConfigHistoryItem extends IApiConfig {
12 | id: string
13 | }
14 |
15 | export function createApiConfigHistoryStore() {
16 | const apiConfigHistoryKey = 'apiConfigHistoryV2';
17 |
18 | return {
19 | apiConfigHistory: (store?.get(apiConfigHistoryKey) || []) as IApiConfigHistoryItem[],
20 |
21 | add(apiConfig: IApiConfig) {
22 | const exist = this.apiConfigHistory.find((a) => a.origin === apiConfig.origin);
23 | if (exist) {
24 | return;
25 | }
26 | this.apiConfigHistory.push({
27 | id: uuidV4(),
28 | ...apiConfig,
29 | });
30 | store?.set(apiConfigHistoryKey, this.apiConfigHistory);
31 | },
32 |
33 | update(apiConfig: IApiConfig) {
34 | this.apiConfigHistory = this.apiConfigHistory.map((_a) => {
35 | if (_a.origin === apiConfig.origin) {
36 | return {
37 | ..._a,
38 | ...apiConfig,
39 | };
40 | }
41 | return _a;
42 | });
43 | store?.set(apiConfigHistoryKey, this.apiConfigHistory);
44 | },
45 |
46 | remove(id: string) {
47 | this.apiConfigHistory = this.apiConfigHistory.filter((apiConfig) => apiConfig.id !== id);
48 | store?.set(apiConfigHistoryKey, this.apiConfigHistory);
49 | },
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/src/store/betaFeature.ts:
--------------------------------------------------------------------------------
1 | import ElectronCurrentNodeStore from 'store/electronCurrentNodeStore';
2 |
3 | const STORE_KEY = 'betaFeatures';
4 |
5 | type Item = 'PAID_GROUP' | 'RUM_EXCHANGE';
6 |
7 | export function createBetaFeatureStore() {
8 | return {
9 | betaFeatures: [] as Item[],
10 |
11 | init() {
12 | this.betaFeatures = (ElectronCurrentNodeStore.getStore().get(STORE_KEY) || ['PAID_GROUP']) as Item[];
13 | },
14 |
15 | add(item: Item) {
16 | this.betaFeatures.push(item);
17 | ElectronCurrentNodeStore.getStore().set(STORE_KEY, this.betaFeatures);
18 | },
19 |
20 | remove(item: Item) {
21 | this.betaFeatures = this.betaFeatures.filter((_item) => _item !== item);
22 | ElectronCurrentNodeStore.getStore().set(STORE_KEY, this.betaFeatures);
23 | },
24 |
25 | toggle(item: Item) {
26 | if (this.betaFeatures.includes(item)) {
27 | this.remove(item);
28 | } else {
29 | this.add(item);
30 | }
31 | },
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/store/electronApiConfigHistoryStore.ts:
--------------------------------------------------------------------------------
1 | import { isProduction, isStaging } from 'utils/env';
2 | import Store from 'electron-store';
3 |
4 | const ELECTRON_API_CONFIG_HISTORY_STORE_NAME = (isProduction ? `${isStaging ? 'staging_' : ''}api_config_history` : 'dev_api_config_history') + '_v1';
5 |
6 | export default {
7 | store: new Store({
8 | name: ELECTRON_API_CONFIG_HISTORY_STORE_NAME,
9 | }),
10 |
11 | getStore() {
12 | return this.store;
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/electronCurrentNodeStore.ts:
--------------------------------------------------------------------------------
1 | import { isProduction, isStaging } from 'utils/env';
2 | import Store from 'electron-store';
3 | import { digestMessage } from 'utils/digestMessage';
4 |
5 | const ELECTRON_STORE_NAME_PREFIX = isProduction ? `${isStaging ? 'staging_' : ''}` : 'dev_';
6 |
7 | export default {
8 | store: null as Store | null,
9 |
10 | getStore() {
11 | if (!this.store) {
12 | throw new Error('store is used before inited');
13 | }
14 | return this.store;
15 | },
16 |
17 | init(nodePublickey: string) {
18 | const storeName = `${ELECTRON_STORE_NAME_PREFIX}${digestMessage(nodePublickey)}`;
19 |
20 | this.store = new Store({ name: storeName });
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/store/electronNodeStore.ts:
--------------------------------------------------------------------------------
1 | import { isProduction, isStaging } from 'utils/env';
2 | import Store from 'electron-store';
3 |
4 | const ELECTRON_NODE_STORE_NAME = (isProduction ? `${isStaging ? 'staging_' : ''}node` : 'dev_node') + '_v1';
5 |
6 | export default {
7 | store: new Store({
8 | name: ELECTRON_NODE_STORE_NAME,
9 | }),
10 |
11 | getStore() {
12 | return this.store;
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/font.ts:
--------------------------------------------------------------------------------
1 | export function createFontStore() {
2 | return {
3 | fontSize: localStorage.getItem('font-size') || '14' as string,
4 |
5 | setFontSize(value: string) {
6 | if (['12', '14', '16', '18'].includes(value)) {
7 | this.fontSize = value;
8 | localStorage.setItem('font-size', value);
9 | }
10 | },
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/store/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { action, observable } from 'mobx';
3 |
4 | const STORAGE_KEY = 'I18N_CURRENT_LANG';
5 |
6 | const allLang = ['cn', 'en'] as const;
7 |
8 | export type AllLanguages = typeof allLang[number];
9 | type LangData = Record;
10 |
11 | const state = observable({
12 | lang: 'cn' as AllLanguages,
13 | });
14 |
15 | const createLangLoader = (langData: LangData) => {
16 | const langState = new Proxy({}, {
17 | get(_target, prop, _receiver) {
18 | const data = langData[state.lang];
19 | if (!data) {
20 | throw new Error(`${state.lang} language resource for this component is not defined.`);
21 | }
22 | return data.content[prop as keyof T];
23 | },
24 | });
25 |
26 | return langState as T;
27 | };
28 |
29 | const switchLang = action((lang: AllLanguages) => {
30 | state.lang = lang;
31 | localStorage.setItem(STORAGE_KEY, lang);
32 | ipcRenderer.send('change-language', lang);
33 | });
34 |
35 | const init = action(() => {
36 | let value = (localStorage.getItem(STORAGE_KEY) || 'cn') as AllLanguages;
37 | if (!allLang.includes(value)) {
38 | value = 'cn';
39 | }
40 | state.lang = value;
41 | ipcRenderer.send('change-language', value);
42 | });
43 |
44 | init();
45 |
46 | export const i18n = {
47 | state,
48 | createLangLoader,
49 | switchLang,
50 | };
51 |
--------------------------------------------------------------------------------
/src/store/notification.ts:
--------------------------------------------------------------------------------
1 | import type * as NotificationModel from 'hooks/useDatabase/models/notification';
2 | import { runInAction } from 'mobx';
3 |
4 | export function createNotificationStore() {
5 | return {
6 | idSet: new Set(),
7 |
8 | map: {} as Record,
9 |
10 | get notifications() {
11 | return Array.from(this.idSet).map((id) => this.map[id]);
12 | },
13 |
14 | addNotifications(
15 | notifications: NotificationModel.IDBNotification[],
16 | ) {
17 | runInAction(() => {
18 | for (const notification of notifications) {
19 | this.idSet.add(notification.Id || '');
20 | this.map[notification.Id || ''] = notification;
21 | }
22 | });
23 | },
24 |
25 | clear() {
26 | runInAction(() => {
27 | this.idSet.clear();
28 | this.map = {};
29 | });
30 | },
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/store/notificationSlide.ts:
--------------------------------------------------------------------------------
1 | interface ShowOptions {
2 | message?: string
3 | duration?: number
4 | type?: 'pending' | 'success' | 'failed'
5 | link?: {
6 | text: string
7 | url: string
8 | }
9 | }
10 |
11 | let timer: NodeJS.Timer;
12 |
13 | export function createNotificationSlideStore() {
14 | return {
15 | open: false,
16 | message: '',
17 | type: 'success',
18 | link: {
19 | text: '',
20 | url: '',
21 | },
22 | show(options?: ShowOptions) {
23 | clearTimeout(timer);
24 | const { message, duration = 3000, link, type = 'success' } = options ?? {};
25 | this.message = message ?? '';
26 | this.type = type ?? 'success';
27 | this.open = true;
28 | if (link) {
29 | this.link = link;
30 | }
31 | if (type === 'success') {
32 | timer = setTimeout(() => {
33 | this.close();
34 | }, duration);
35 | }
36 | },
37 | close() {
38 | this.open = false;
39 | },
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/store/relation.ts:
--------------------------------------------------------------------------------
1 | import * as RelationSummaryModel from 'hooks/useDatabase/models/relationSummaries';
2 |
3 | const getRelationSummaryKey = (v: RelationSummaryModel.IDBRelationSummary) => [
4 | v.groupId, v.from, v.to, v.type,
5 | ].join('-');
6 |
7 | export function createRelationStore() {
8 | return {
9 | relationMap: new Map(),
10 |
11 | get byGroupId() {
12 | return Array.from(this.relationMap.values()).reduce((p, c) => {
13 | const arr = p.get(c.groupId) ?? [];
14 | arr.push(c);
15 | p.set(c.groupId, arr);
16 | return p;
17 | }, new Map>());
18 | },
19 |
20 | addRelations(items: Array) {
21 | items.forEach((v) => {
22 | const key = getRelationSummaryKey(v);
23 | this.relationMap.set(key, v);
24 | });
25 | },
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/store/selectors/getLatestObject.ts:
--------------------------------------------------------------------------------
1 | import { Store } from 'store';
2 |
3 | export default (store: Store) => {
4 | const { activeGroupStore, latestStatusStore } = store;
5 | const latestStatus = latestStatusStore.map[activeGroupStore.id] || latestStatusStore.DEFAULT_LATEST_STATUS;
6 | return activeGroupStore.posts.find((o) => o.timestamp <= latestStatus.latestReadTimeStamp);
7 | };
8 |
--------------------------------------------------------------------------------
/src/store/selectors/group.ts:
--------------------------------------------------------------------------------
1 | import { IGroup } from 'apis/group';
2 | import { GROUP_TEMPLATE_TYPE } from 'utils/constant';
3 |
4 | export const isPublicGroup = (group: IGroup) => group.encryption_type.toLowerCase() === 'public';
5 |
6 | export const isPrivateGroup = (group: IGroup) => group.encryption_type.toLowerCase() === 'private';
7 |
8 | export const isTimelineGroup = (group: IGroup) => group.app_key === GROUP_TEMPLATE_TYPE.TIMELINE;
9 |
10 | export const isPostGroup = (group: IGroup) => group.app_key === GROUP_TEMPLATE_TYPE.POST;
11 |
12 | export const isNoteGroup = (group: IGroup) => group.app_key === GROUP_TEMPLATE_TYPE.NOTE;
13 |
14 | export const isCustomGroup = (group: IGroup) => group.app_key !== GROUP_TEMPLATE_TYPE.NOTE && group.app_key !== GROUP_TEMPLATE_TYPE.POST && group.app_key !== GROUP_TEMPLATE_TYPE.TIMELINE;
15 |
16 | export const isGroupOwner = (group: IGroup) => group.owner_pubkey === group.user_pubkey;
17 |
18 | export const getRole = (group: IGroup) => (isGroupOwner(group) ? 'owner' : 'user');
19 |
--------------------------------------------------------------------------------
/src/store/selectors/useActiveGroup.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 |
3 | export default () => {
4 | const { groupStore, activeGroupStore } = useStore();
5 | return groupStore.map[activeGroupStore.id] || {};
6 | };
7 |
--------------------------------------------------------------------------------
/src/store/selectors/useActiveGroupFollowingUserAddresses.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 |
3 | export default () => {
4 | const { activeGroupStore, relationStore, groupStore } = useStore();
5 | const group = groupStore.map[activeGroupStore.id];
6 | if (!group) { return []; }
7 | const relations = relationStore.byGroupId.get(group.group_id) ?? [];
8 | return relations
9 | .filter((v) => v.from === group.user_eth_addr && v.type === 'follow' && !!v.value)
10 | .map((v) => v.to);
11 | };
12 |
--------------------------------------------------------------------------------
/src/store/selectors/useActiveGroupLatestStatus.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 |
3 | export default () => {
4 | const { activeGroupStore, latestStatusStore } = useStore();
5 |
6 | return (
7 | latestStatusStore.map[activeGroupStore.id] || latestStatusStore.DEFAULT_LATEST_STATUS
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/store/selectors/useActiveGroupMutedUserAddress.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 |
3 | export default () => {
4 | const { activeGroupStore, relationStore, groupStore } = useStore();
5 | const group = groupStore.map[activeGroupStore.id];
6 | if (!group) { return []; }
7 | const relations = relationStore.byGroupId.get(group.group_id) ?? [];
8 | return relations
9 | .filter((v) => v.from === group.user_eth_addr && v.type === 'block' && !!v.value)
10 | .map((v) => v.to);
11 | };
12 |
--------------------------------------------------------------------------------
/src/store/selectors/useHasFrontHistoricalObject.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 |
3 | export default () => {
4 | const { activeGroupStore, latestStatusStore } = useStore();
5 | const latestStatus = latestStatusStore.map[activeGroupStore.id] || latestStatusStore.DEFAULT_LATEST_STATUS;
6 | return !!activeGroupStore.frontPost && activeGroupStore.frontPost.timestamp > latestStatus.latestPostTimeStamp;
7 | };
8 |
--------------------------------------------------------------------------------
/src/store/selectors/useIsCurrentGroupOwner.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'store';
2 | import { isGroupOwner } from 'store/selectors/group';
3 |
4 | export default () => {
5 | const { groupStore, activeGroupStore } = useStore();
6 | return isGroupOwner(groupStore.map[activeGroupStore.id] || {});
7 | };
8 |
--------------------------------------------------------------------------------
/src/store/snackbar.ts:
--------------------------------------------------------------------------------
1 | import sleep from 'utils/sleep';
2 |
3 | interface ShowOptions {
4 | message?: string
5 | delayDuration?: number
6 | duration?: number
7 | type?: 'error'
8 | meta?: any
9 | }
10 |
11 | export function createSnackbarStore() {
12 | return {
13 | open: false,
14 | message: '',
15 | type: 'default',
16 | useNotificationStyle: false,
17 | meta: {},
18 | show(options?: ShowOptions) {
19 | (async () => {
20 | await sleep(options?.delayDuration ?? 150);
21 | this.close();
22 | const { message, duration = 1500, type, meta = {} } = options ?? {};
23 | this.message = message ?? '';
24 | this.type = type || 'default';
25 | const autoHideDuration = type === 'error' && duration === 1500 ? 2000 : duration;
26 | this.open = true;
27 | this.meta = meta;
28 | await sleep(autoHideDuration);
29 | this.close();
30 | })();
31 | },
32 | close() {
33 | this.open = false;
34 | },
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/styles/tailwind-base.sass:
--------------------------------------------------------------------------------
1 | @tailwind base
2 |
--------------------------------------------------------------------------------
/src/styles/tailwind.sass:
--------------------------------------------------------------------------------
1 | @tailwind utilities
2 |
3 | @layer utilities
4 | .flex-col
5 | display: flex
6 | flex-direction: column
7 |
8 | .break-words
9 | word-break: break-word
10 |
11 | .flex-center
12 | justify-content: center
13 | align-items: center
14 |
15 | .truncate-2
16 | display: -webkit-box
17 | overflow: hidden
18 | -webkit-line-clamp: 2
19 | -webkit-box-orient: vertical
20 |
21 | .truncate-3
22 | display: -webkit-box
23 | overflow: hidden
24 | -webkit-line-clamp: 3
25 | -webkit-box-orient: vertical
26 |
27 | .truncate-4
28 | display: -webkit-box
29 | overflow: hidden
30 | -webkit-line-clamp: 4
31 | -webkit-box-orient: vertical
32 |
33 | .truncate-5
34 | display: -webkit-box
35 | overflow: hidden
36 | -webkit-line-clamp: 5
37 | -webkit-box-orient: vertical
38 |
39 | .font-default
40 | font-family: 'color-emoji', 'Nunito Sans', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', 'Source Han Sans SC', 'Source Han Sans', 'WenQuanYi Micro Hei', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif
41 |
--------------------------------------------------------------------------------
/src/tests/index.ts:
--------------------------------------------------------------------------------
1 | import './setup-global';
2 | import path from 'path';
3 | import fs from 'fs/promises';
4 |
5 | const testMain = async () => {
6 | const names = process.argv.slice(2);
7 | try {
8 | const testBasePath = path.join(__dirname, 'tests');
9 | const tests = await fs.readdir(testBasePath);
10 | const filteredTests = tests.filter((v) => {
11 | if (!/\.tsx?$/.test(v)) { return false; }
12 | if (!names.length) { return true; }
13 | return names.some((n) => v.includes(n));
14 | });
15 | for (const file of filteredTests) {
16 | const testName = file.replace(/\.tsx?$/, '');
17 | const filePath = path.join(testBasePath, file);
18 | const test = (await import(filePath)).default;
19 | console.log(`\nrunning test: ${testName}`);
20 | await test();
21 | }
22 | } catch (e) {
23 | console.error(e);
24 | process.exit(1);
25 | }
26 | };
27 |
28 | testMain();
29 |
--------------------------------------------------------------------------------
/src/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import electron from 'electron';
3 | import { rimraf } from 'rimraf';
4 | import puppeteer from 'puppeteer-core';
5 | import expect from 'expect-puppeteer';
6 | import sleep from 'utils/sleep';
7 |
8 | export const setup = async () => {
9 | try {
10 | await rimraf(path.join(__dirname, 'userData'));
11 | } catch (err) {
12 | console.error(err);
13 | process.exit(1);
14 | }
15 |
16 | const app = await puppeteer.launch({
17 | executablePath: electron as any,
18 | env: {
19 | TEST_ENV: process.env.TEST_ENV!,
20 | devtool: 'true',
21 | },
22 | args: ['.'],
23 | headless: false,
24 | });
25 | const pages = await app.pages();
26 | const [page] = pages;
27 | await page.setViewport({ width: 1260, height: 740 });
28 |
29 | await sleep(5000);
30 |
31 | await Promise.any([
32 | expect(page).toMatchElement('.sidebar', {
33 | timeout: 10000,
34 | }),
35 | expect(page).toMatchElement('[data-test-id="welcome-page"]', {
36 | timeout: 10000,
37 | }),
38 | ]).catch((e) => {
39 | console.log('failed to load app');
40 | throw e;
41 | });
42 |
43 | return {
44 | app,
45 | page,
46 | destroy: async () => {
47 | await page.close();
48 | await app.close();
49 | },
50 | } as const;
51 | };
52 |
--------------------------------------------------------------------------------
/src/tests/tests/common/createGroup.ts:
--------------------------------------------------------------------------------
1 | import { Page } from 'puppeteer-core';
2 | import sleep from 'utils/sleep';
3 | import { GROUP_TEMPLATE_TYPE } from 'utils/constant';
4 |
5 | export const createGroup = async (page: Page, groupName: string, groupType: GROUP_TEMPLATE_TYPE) => {
6 | Promise.any([
7 | (async () => {
8 | await page.clickByTestId('sidebar-plus-button');
9 | await page.clickByTestId('sidebar-menu-item-create-group');
10 | })(),
11 | (async () => {
12 | await page.clickByTestId('welcome-page-create-group-button');
13 | })(),
14 | ]).catch((e) => {
15 | console.log('can\'t find create group button');
16 | throw e;
17 | });
18 |
19 | await page.clickByTestId(`group-type-${groupType}`);
20 | await page.clickByTestId('create-group-modal-next-step');
21 | await sleep(100);
22 | if (groupType !== GROUP_TEMPLATE_TYPE.NOTE) {
23 | await page.clickByTestId('create-group-modal-next-step');
24 | }
25 | await page.fillByTestId('create-group-name-input input', groupName);
26 | await page.clickByTestId('create-group-modal-confirm');
27 | await page.clickByTestId('create-group-confirm-modal-confirm');
28 | await sleep(5000);
29 | };
30 |
--------------------------------------------------------------------------------
/src/tests/tests/common/exitCurrentGroup.ts:
--------------------------------------------------------------------------------
1 | import { Page } from 'puppeteer-core';
2 |
3 | export const exitCurrentGroup = async (page: Page) => {
4 | await page.clickByTestId('group-menu-button');
5 | await page.clickByTestId('group-menu-exit-group-button');
6 | await page.clickByTestId('exit-group-dialog-confirm-button');
7 | };
8 |
--------------------------------------------------------------------------------
/src/tests/tests/common/groupInfoModal.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect-puppeteer';
2 | import { Page } from 'puppeteer-core';
3 | import sleep from 'utils/sleep';
4 |
5 | export const groupInfoModal = async (page: Page) => {
6 | page.clickByTestId('header-group-name');
7 | await sleep(1000);
8 | const close = await expect(page).toMatchElement('.group-info-modal [data-test-id="dialog-close-button"]');
9 | await sleep(1000);
10 | await close.click();
11 | await sleep(1000);
12 | await expect(page).not.toMatchElement('.group-info-modal');
13 | };
14 |
--------------------------------------------------------------------------------
/src/tests/tests/common/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './createGroup';
2 | export * from './exitCurrentGroup';
3 | export * from './groupInfoModal';
4 | export * from './myGroupModal';
5 | export * from './nodeAndNetwork';
6 | export * from './shareGroup';
7 |
--------------------------------------------------------------------------------
/src/tests/tests/common/myGroupModal.ts:
--------------------------------------------------------------------------------
1 | import { Page } from 'puppeteer-core';
2 | import sleep from 'utils/sleep';
3 |
4 | export const myGroupModal = async (page: Page) => {
5 | await page.clickByTestId('sidebar-my-group-button');
6 | await sleep(1000);
7 | await page.matchByTestId('my-group-modal');
8 | await page.clickByTestId('my-group-modal-close');
9 | await sleep(1000);
10 | await page.notMatchByTestId('my-group-modal');
11 | };
12 |
--------------------------------------------------------------------------------
/src/tests/tests/common/nodeAndNetwork.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect-puppeteer';
2 | import { Page } from 'puppeteer-core';
3 | import sleep from 'utils/sleep';
4 |
5 | export const nodeAndNetowrk = async (page: Page) => {
6 | await page.clickByTestId('header-node-and-network');
7 | await sleep(500);
8 | await page.clickByTestId('node-and-network-node-params');
9 | await sleep(500);
10 | await expect(page).toClick('.node-params-modal [data-test-id="dialog-close-button"]');
11 | await sleep(500);
12 | await page.clickByTestId('node-and-network-network-status');
13 | await sleep(500);
14 | await expect(page).toClick('.network-info-modal [data-test-id="dialog-close-button"]');
15 | await sleep(500);
16 | await expect(page).toClick('.node-info-modal [data-test-id="dialog-close-button"]');
17 | await sleep(500);
18 | await expect(page).not.toMatchElement('.node-info-modal');
19 | };
20 |
--------------------------------------------------------------------------------
/src/tests/tests/common/shareGroup.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import { Page } from 'puppeteer-core';
3 | import sleep from 'utils/sleep';
4 |
5 | export const shareGroup = async (page: Page) => {
6 | await page.clickByTestId('header-share-group');
7 | const shareGroup = await page.matchByTestId('share-group-modal');
8 | await sleep(1000);
9 | const textarea = await shareGroup.matchByTestId('share-group-textarea');
10 | const content = await textarea.evaluate((e) => (e as HTMLTextAreaElement).value);
11 | expect(content).toBeTruthy();
12 | await shareGroup.clickByTestId('dialog-close-button');
13 | await sleep(1000);
14 | };
15 |
--------------------------------------------------------------------------------
/src/tests/tests/noteGroup.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect-puppeteer';
2 | import { format } from 'date-fns';
3 | import sleep from 'utils/sleep';
4 | import { GROUP_TEMPLATE_TYPE } from 'utils/constant';
5 | import { setup } from 'tests/setup';
6 | import { createGroup, exitCurrentGroup } from './common';
7 |
8 | export default async () => {
9 | const { page, destroy } = await setup();
10 | await createGroup(page, 'testgroup', GROUP_TEMPLATE_TYPE.NOTE);
11 | await page.matchByTestId('note-feed');
12 |
13 | const content = [
14 | format(new Date(), 'yyyy-MM-dd hh-mm:ss'),
15 | 'note-test-post',
16 | ].join('');
17 | await page.fillByTestId('note-editor textarea', content);
18 | await sleep(3000);
19 | await page.clickByTestId('editor-submit-button');
20 | await sleep(5000);
21 |
22 | await expect(page).toMatchElement('[data-test-id="note-object-item"] [data-test-id="synced-timeline-item-menu"]', {
23 | timeout: 30000,
24 | });
25 |
26 | await exitCurrentGroup(page);
27 |
28 | await expect(page).not.toMatchElement('.sidebar', {
29 | timeout: 10000,
30 | });
31 |
32 | await sleep(1000);
33 | await destroy();
34 | };
35 |
--------------------------------------------------------------------------------
/src/tests/tests/postGroup.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect-puppeteer';
2 | import { format } from 'date-fns';
3 | import sleep from 'utils/sleep';
4 | import { setup } from 'tests/setup';
5 | import { GROUP_TEMPLATE_TYPE } from 'utils/constant';
6 | import { createGroup, exitCurrentGroup } from './common';
7 |
8 | export default async () => {
9 | const { page, destroy } = await setup();
10 | await createGroup(page, 'testgroup', GROUP_TEMPLATE_TYPE.POST);
11 | await page.matchByTestId('post-feed');
12 |
13 | const content = [
14 | format(new Date(), 'yyyy-MM-dd hh-mm:ss'),
15 | 'forum-test-post',
16 | ].join('');
17 | await page.clickByTestId('forum-create-first-post-button');
18 | await sleep(1000);
19 | await page.fillByTestId('forum-post-title-input input', 'post title');
20 |
21 | await expect(page).toClick('.CodeMirror-line');
22 | page.keyboard.type(content);
23 | await sleep(2000);
24 |
25 | await page.clickByTestId('forum-post-submit-button');
26 | await sleep(5000);
27 |
28 | await expect(page).toMatchElement('[data-test-id="forum-object-item"] [data-test-id="synced-timeline-item-menu"]', {
29 | timeout: 30000,
30 | });
31 |
32 | await exitCurrentGroup(page);
33 |
34 | await expect(page).not.toMatchElement('.sidebar', {
35 | timeout: 10000,
36 | });
37 |
38 | await sleep(1000);
39 | await destroy();
40 | };
41 |
--------------------------------------------------------------------------------
/src/tests/types/puppeteer-core.d.ts:
--------------------------------------------------------------------------------
1 | import 'puppeteer-core';
2 | import { ExpectToClickOptions, ExpectTimingActions } from 'expect-puppeteer';
3 |
4 | declare module 'puppeteer-core' {
5 | interface CustomTestMethods {
6 | matchByTestId: (testId: string, options?: ExpectToClickOptions) => Promise
7 | notMatchByTestId: (testId: string, options?: ExpectToClickOptions) => Promise
8 | clickByTestId: (testId: string, options?: ExpectToClickOptions) => Promise
9 | /**
10 | * @param {string} [testId] - `"testId"` or `"testId subselector"`
11 | */
12 | fillByTestId: (testId: string, content: string, options?: ExpectTimingActions) => Promise
13 | }
14 | interface Page extends CustomTestMethods {}
15 | interface ElementHandle extends CustomTestMethods {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/assets.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: any;
3 | export default content;
4 | export const ReactComponent: React.ReactElement;
5 | }
6 |
7 | declare module '*.svg?react' {
8 | const content: React.FunctionComponent>;
9 | export default content;
10 | }
11 |
12 | declare module '*.png' {
13 | const content: any;
14 | export default content;
15 | }
16 |
17 | declare module '*.jpg' {
18 | const content: any;
19 | export default content;
20 | }
21 |
22 | declare module '*.sass?inline' {
23 | const content: string;
24 | export default content;
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/axios.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'axios/lib/adapters/http';
2 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | // const IS_E2E_TEST: string | undefined;
3 | }
4 |
5 | export {};
6 |
--------------------------------------------------------------------------------
/src/types/markdown-it-task-lists.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'markdown-it-task-lists';
2 |
--------------------------------------------------------------------------------
/src/types/process.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | interface ProcessEnv {
3 | TEST_ENV: string | undefined
4 | NODE_ENV: 'development' | 'production'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/styled-jsx.d.ts:
--------------------------------------------------------------------------------
1 | import 'react';
2 |
3 | declare module 'react' {
4 | interface StyleHTMLAttributes extends HTMLAttributes {
5 | jsx?: boolean
6 | global?: boolean
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/types/wasm.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.wasm'
2 |
--------------------------------------------------------------------------------
/src/utils/PollingTask.ts:
--------------------------------------------------------------------------------
1 | interface PollingTaskParams {
2 | interval: number
3 | task: () => unknown
4 | }
5 |
6 | export class PollingTask {
7 | private task: () => unknown;
8 | private interval: number;
9 | private stopFlag = false;
10 | private advancePromiseResolve: (v?: unknown) => void = () => {};
11 |
12 | public constructor(params: PollingTaskParams) {
13 | this.task = params.task;
14 | this.interval = params.interval;
15 | this.start();
16 | }
17 |
18 | public stop() {
19 | this.stopFlag = true;
20 | }
21 |
22 | public advance() {
23 | this.advancePromiseResolve?.();
24 | }
25 |
26 | public changeInterval(interval: number) {
27 | this.interval = interval;
28 | }
29 |
30 | private async start() {
31 | for (;;) {
32 | if (this.stopFlag) { break; }
33 | try {
34 | await this.task();
35 | } catch (e) {
36 | console.error(e);
37 | }
38 | if (this.stopFlag) { break; }
39 | await this.waitInterval();
40 | }
41 | }
42 |
43 | private async waitInterval() {
44 | await new Promise((rs) => {
45 | this.advancePromiseResolve = rs;
46 | setTimeout(rs, this.interval);
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/ago.ts:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns';
2 | import { lang } from 'utils/lang';
3 |
4 | export default (blockTimeStamp: number, options: { trimmed?: boolean } = {}) => {
5 | if (!blockTimeStamp) {
6 | return '';
7 | }
8 | const timestamp = blockTimeStamp > 10 ** 14
9 | ? blockTimeStamp / 1000000
10 | : blockTimeStamp;
11 | const time = new Date(timestamp);
12 | const now = new Date().getTime();
13 | const past = new Date(time).getTime();
14 | const diffValue = now - past;
15 | const minute = 1000 * 60;
16 | const hour = minute * 60;
17 | const day = hour * 24;
18 | const _week = diffValue / (7 * day);
19 | const _day = diffValue / day;
20 | const _hour = diffValue / hour;
21 | const _min = diffValue / minute;
22 | let result = '';
23 | const isLastYear = new Date().getFullYear() > time.getFullYear();
24 | const isDiffDay = new Date().getDate() !== time.getDate();
25 | if (isLastYear && _week >= 15) {
26 | result = format(time, options.trimmed ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm');
27 | } else if (_day >= 1 || isDiffDay) {
28 | result = format(time, options.trimmed ? 'MM-dd' : 'MM-dd HH:mm');
29 | } else if (_hour >= 4) {
30 | result = format(time, 'HH:mm');
31 | } else if (_hour >= 1) {
32 | result = Math.floor(_hour) + lang.hoursAgo;
33 | } else if (_min >= 1) {
34 | result = Math.floor(_min) + lang.minutesAgo;
35 | } else {
36 | result = lang.justNow;
37 | }
38 | return result;
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/assets-custom-loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rootPath = path.join(__dirname, '../..');
4 |
5 | module.exports = function assetsCustomLoader() {
6 | const filePath = this.resourcePath;
7 |
8 | if (process.env.NODE_ENV === 'development' || process.env.TEST_ENV) {
9 | const uri = `file://${filePath.replace(/\\/g, '/')}`;
10 | return `export default ${JSON.stringify(uri)};`;
11 | }
12 |
13 | const relativePath = filePath.replace(rootPath, '').replace(/\\/g, '/');
14 | return `export default \`file://\${process.resourcesPath.replace(/\\\\/g, '/')}${relativePath}\`;`;
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/cachePromise.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 返回原函数,但该函数在连续多次调用时,首次调用未resolve时,返回缓存的 promise
3 | */
4 | export const cachePromiseHof = ) => unknown>(fn: T) => {
5 | let promise: null | Promise = null;
6 |
7 | return ((...args) => {
8 | if (promise) {
9 | return promise;
10 | }
11 | promise = Promise.resolve(fn(...args));
12 | promise.finally(() => {
13 | promise = null;
14 | });
15 | return promise;
16 | }) as T;
17 | };
18 |
--------------------------------------------------------------------------------
/src/utils/decimal.ts:
--------------------------------------------------------------------------------
1 | export default (num: string, n: number) => num.slice(0, num.indexOf('.') + 1 + n).replace(/\.*0+$/, '');
2 |
--------------------------------------------------------------------------------
/src/utils/digestMessage.ts:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 |
3 | export const digestMessage = (message: string) => crypto.createHash('md5').update(message).digest('hex');
4 |
--------------------------------------------------------------------------------
/src/utils/domSelector.ts:
--------------------------------------------------------------------------------
1 | export const getPageElement = () =>
2 | document.querySelector('.layout-page')!;
3 |
--------------------------------------------------------------------------------
/src/utils/env.ts:
--------------------------------------------------------------------------------
1 | export const isDevelopment = process.env.NODE_ENV === 'development';
2 |
3 | export const isProduction = !isDevelopment;
4 |
5 | export const isStaging = process.env.BUILD_ENV === 'staging';
6 |
7 | export const isWindow = window.navigator.userAgent.includes('Windows NT');
8 |
--------------------------------------------------------------------------------
/src/utils/formatAmount.ts:
--------------------------------------------------------------------------------
1 | import decimal from './decimal';
2 |
3 | export default (num: string) => decimal(num, 8);
4 |
--------------------------------------------------------------------------------
/src/utils/formatPath.ts:
--------------------------------------------------------------------------------
1 | import { isWindow } from 'utils/env';
2 |
3 | export default (path: string, options: { truncateLength: number }) => {
4 | const _path = isWindow ? path.replaceAll('/', '\\') : path;
5 | return _path.length > options.truncateLength
6 | ? `...${_path.slice(-options.truncateLength)}`
7 | : _path;
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/getBase.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const { nodeStore } = (window as any).store;
3 | if (nodeStore.mode === 'EXTERNAL') {
4 | return nodeStore.apiConfig.origin;
5 | }
6 | return `http://127.0.0.1:${nodeStore.port}`;
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/getGroupIcon.ts:
--------------------------------------------------------------------------------
1 | import TimelineIcon from 'assets/template/template_icon_timeline.svg?react';
2 | import PostIcon from 'assets/template/template_icon_post.svg?react';
3 | import NotebookIcon from 'assets/template/template_icon_notebook.svg?react';
4 |
5 | import { GROUP_TEMPLATE_TYPE } from './constant';
6 |
7 | const groupIconMap = new Map>>([
8 | [GROUP_TEMPLATE_TYPE.TIMELINE, TimelineIcon],
9 | [GROUP_TEMPLATE_TYPE.POST, PostIcon],
10 | [GROUP_TEMPLATE_TYPE.NOTE, NotebookIcon],
11 | ]);
12 |
13 | export const getGroupIcon = (appKey: string) => {
14 | const GroupTypeIcon = groupIconMap.get(appKey) ?? TimelineIcon;
15 | return GroupTypeIcon;
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/getHotCount.ts:
--------------------------------------------------------------------------------
1 | interface HotCountParams {
2 | likeCount: number
3 | dislikeCount: number
4 | commentCount: number
5 | }
6 | export default (options: HotCountParams) => {
7 | const { likeCount, dislikeCount, commentCount } = options;
8 | return (likeCount - dislikeCount + commentCount * 0.4) * 10;
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/getKeyName.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs/promises';
2 | import path from 'path';
3 |
4 | export default async (folder: string, address: string) => {
5 | const ret = await fs.readdir(path.join(folder, 'keystore'));
6 | const signFilenames = ret.filter((item) => item.startsWith('sign_') && item !== 'sign_default');
7 | let keyName = '';
8 | for (const filename of signFilenames) {
9 | const content = await fs.readFile(path.join(folder, 'keystore', filename), 'utf8');
10 | if (content.includes(address.toLocaleLowerCase().replace('0x', ''))) {
11 | keyName = filename.replace('sign_', '');
12 | }
13 | }
14 | return keyName;
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/getTimestampFromBlockTime.ts:
--------------------------------------------------------------------------------
1 | export default (blockTime: number) => parseInt(`${blockTime / 1000000}`, 10);
2 |
--------------------------------------------------------------------------------
/src/utils/handleRumAppProtocol.ts:
--------------------------------------------------------------------------------
1 | import { Event, ipcRenderer } from 'electron';
2 | import qs from 'query-string';
3 | import { parse as parseUri } from 'uri-js';
4 | import { joinGroup } from 'standaloneModals/joinGroup';
5 |
6 | const actions: Record unknown> = {
7 | '/join-group': (params: any) => {
8 | const seed = params.seed;
9 | if (!seed) {
10 | return;
11 | }
12 | try {
13 | JSON.parse(seed);
14 | } catch (e) {
15 | return;
16 | }
17 | joinGroup(seed);
18 | },
19 | };
20 |
21 | export const handleRumAppProtocol = () => {
22 | const handler = (_e: Event, a: any) => {
23 | console.log(a);
24 | try {
25 | const uri = parseUri(a);
26 | const pathName = uri.path?.replace(/\/$/, '');
27 | if (!pathName) { return; }
28 | const query = uri.query ? qs.parse(uri.query) : null;
29 | actions[pathName]?.(query);
30 | } catch (e) {}
31 | };
32 | ipcRenderer.on('rum-app', handler);
33 | return () => {
34 | ipcRenderer.off('rum-app', handler);
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PollingTask';
2 | export * from './runLoading';
3 | export * from './lang';
4 | export { default as isV2Seed } from './isV2Seed';
5 | export { default as sleep } from './sleep';
6 | export { default as base64 } from './base64';
7 | export { default as ago } from './ago';
8 | export { replaceSeedAsButton } from './replaceSeedAsButton';
9 |
10 | export const notNullFilter = (v: T | undefined | null): v is T => v !== undefined && v !== null;
11 |
--------------------------------------------------------------------------------
/src/utils/inputFinanceAmount.ts:
--------------------------------------------------------------------------------
1 | export default (amount: string) => {
2 | if (amount === '') {
3 | return '';
4 | }
5 | const re = /^[0-9]+[.]?[0-9]{0,8}$/;
6 | if (re.test(amount)) {
7 | if (amount.includes('.')) {
8 | const [prefix, postfix] = amount.split('.');
9 | return `${parseInt(prefix, 10)}.${postfix}`;
10 | }
11 | return `${parseInt(amount, 10)}`;
12 | }
13 | return null;
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/inspect.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | export const loadInspect = () => {
4 | const keys = new Set();
5 |
6 | const handleKeyDown = (e: KeyboardEvent) => {
7 | keys.add(e.key.toLowerCase());
8 | check();
9 | };
10 | const handleKeyUp = (e: KeyboardEvent) => {
11 | keys.delete(e.key.toLowerCase());
12 | };
13 | const handleBlur = () => {
14 | keys.clear();
15 | };
16 | const check = () => {
17 | const conditions = [
18 | keys.size === 3,
19 | keys.has('control') || keys.has('command'),
20 | keys.has('shift'),
21 | keys.has('c'),
22 | ];
23 | if (conditions.every(Boolean)) {
24 | ipcRenderer.send('inspect-picker');
25 | }
26 | };
27 | window.addEventListener('keydown', handleKeyDown);
28 | window.addEventListener('keyup', handleKeyUp);
29 | window.addEventListener('blur', handleBlur);
30 |
31 | return () => {
32 | window.removeEventListener('keydown', handleKeyDown);
33 | window.removeEventListener('keyup', handleKeyUp);
34 | window.removeEventListener('blur', handleBlur);
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/isV2Seed.ts:
--------------------------------------------------------------------------------
1 | export default (seed: string) => seed.startsWith('rum://seed');
2 |
--------------------------------------------------------------------------------
/src/utils/lang/index.ts:
--------------------------------------------------------------------------------
1 | import { i18n } from 'store/i18n';
2 | import * as cn from './cn';
3 | import * as en from './en';
4 |
5 | export const lang = i18n.createLangLoader({
6 | cn,
7 | en,
8 | });
9 |
--------------------------------------------------------------------------------
/src/utils/mainScrollView.ts:
--------------------------------------------------------------------------------
1 | export const className = 'main-scroll-element';
2 |
3 | export const scrollToTop = () => {
4 | try {
5 | document.querySelector('.main-scroll-element')!.scroll(0, 0);
6 | } catch (_) {}
7 | };
8 |
9 | export const scrollTop = () => {
10 | try {
11 | return document.querySelector('.main-scroll-element')!.scrollTop || 0;
12 | } catch (_) {}
13 | return 0;
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/mimeType.ts:
--------------------------------------------------------------------------------
1 | const map: any = {
2 | gif: 'image/gif',
3 | htm: 'text/html',
4 | html: 'text/html',
5 | jpg: 'image/jpeg',
6 | jpeg: 'image/jpeg',
7 | jpe: 'image/jpeg',
8 | js: 'application/x-javascript',
9 | pdf: 'application/pdf',
10 | png: 'image/png',
11 | md: 'text/markdown',
12 | json: 'application/json',
13 | };
14 |
15 | export default {
16 | getByExt: (ext: string) => map[ext] || '',
17 | getByFilename: (filename: string) => map[(filename as any).split('.').pop()] || '',
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/mixinOAuth.ts:
--------------------------------------------------------------------------------
1 | import { createHash, randomBytes } from 'node:crypto';
2 |
3 | export const client_id = 'ef7ba9a7-c0ac-46a7-8ce3-717be19caf9c';
4 |
5 | const scope = 'PROFILE:READ';
6 |
7 | export const getVerifierAndChanllege = () => {
8 | const key = randomBytes(32);
9 | const verifier = key.toString('base64url');
10 | const hash = createHash('sha256');
11 | hash.update(key);
12 | const challenge = hash.digest('base64url');
13 | return { verifier, challenge };
14 | };
15 |
16 | export const getOAuthUrl = (challenge: string) => `https://mixin-www.zeromesh.net/oauth/authorize?client_id=${client_id}&scope=${scope}&response_type=code&code_challenge=${challenge}`;
17 |
--------------------------------------------------------------------------------
/src/utils/quorum/request.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | let id = 0;
4 |
5 | const callbackQueueMap = new Map unknown>();
6 |
7 | export interface QuorumIPCRequest {
8 | action: string
9 | param?: any
10 | id: number
11 | }
12 |
13 | export interface QuorumIPCResult {
14 | id: number
15 | data: T
16 | error: string | null
17 | }
18 |
19 | export const sendRequest = (
20 | param: Pick>,
21 | ) => {
22 | id += 1;
23 | const requestId = id;
24 | let resolve: (v: unknown) => unknown = () => {};
25 | const promise = new Promise((rs) => {
26 | resolve = rs;
27 | });
28 | callbackQueueMap.set(requestId, resolve);
29 | const data: QuorumIPCRequest = {
30 | ...param,
31 | id: requestId,
32 | };
33 | ipcRenderer.send('quorum', data);
34 | return promise as Promise>;
35 | };
36 |
37 | export const initQuorum = () => {
38 | ipcRenderer.on('quorum', (_event, args) => {
39 | const id = args.id;
40 | if (!id) {
41 | return;
42 | }
43 |
44 | const callback = callbackQueueMap.get(id);
45 | if (!callback) {
46 | return;
47 | }
48 |
49 | callback(args);
50 | callbackQueueMap.delete(id);
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/removeGroupData.ts:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie';
2 | import { find } from 'lodash';
3 | import Database from 'hooks/useDatabase/database';
4 |
5 | export default (dbs: Array, groupId: string) => {
6 | const dexiePromise: Array = [];
7 | dbs.forEach((db) => {
8 | db.tables.forEach((table) => {
9 | const index = find(table.schema.indexes, (item) => item.name === 'GroupId');
10 | if (index) {
11 | dexiePromise.push(table.where({
12 | GroupId: groupId,
13 | }).delete());
14 | }
15 | });
16 | });
17 | return Promise.all(dexiePromise);
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/runLoading.ts:
--------------------------------------------------------------------------------
1 | import { runInAction } from 'mobx';
2 |
3 | type SetLoading = (l: boolean) => unknown;
4 | type UnknownFunction = (...p: Array) => unknown;
5 | type RunLoading = (s: SetLoading, fn: T) => Promise>;
6 | /**
7 | * 立即执行异步函数 fn。
8 | * 执行前调用 setLoading(true),执行完毕调用 setLoading(false)
9 | */
10 | export const runLoading: RunLoading = async (setLoading, fn) => {
11 | runInAction(() => setLoading(true));
12 | try {
13 | const result = await fn();
14 | return result as ReturnType;
15 | } finally {
16 | runInAction(() => setLoading(false));
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/schema.ts:
--------------------------------------------------------------------------------
1 | const SCHEMA_PREFIX = 'rum://objects/';
2 |
3 | export default {
4 | getSchemaPrefix() {
5 | return SCHEMA_PREFIX;
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/setClipboard.ts:
--------------------------------------------------------------------------------
1 | import { clipboard } from 'electron';
2 |
3 | export const setClipboard = (text: string) => {
4 | clipboard.writeText(text);
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | export default (duration: number) =>
2 | new Promise((resolve: any) => {
3 | setTimeout(() => {
4 | resolve();
5 | }, duration);
6 | });
7 |
--------------------------------------------------------------------------------
/src/utils/urlify.ts:
--------------------------------------------------------------------------------
1 | export default (text: string) => {
2 | if (!text) {
3 | return text;
4 | }
5 | const urlRegex = /(https?:\/\/[^\s]+)/g;
6 | return text.replace(urlRegex, '$1 ');
7 | };
8 |
--------------------------------------------------------------------------------
/tsconfig-test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES2020",
5 | "module": "CommonJS",
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "lib": ["dom", "esnext"],
6 | "declaration": true,
7 | "declarationMap": true,
8 | "noEmit": true,
9 | "jsx": "preserve",
10 | "strict": true,
11 | "pretty": true,
12 | "sourceMap": true,
13 | /* Additional Checks */
14 | "skipLibCheck": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | /* Module Resolution Options */
20 | "moduleResolution": "node",
21 | "esModuleInterop": true,
22 | "allowSyntheticDefaultImports": true,
23 | "resolveJsonModule": true,
24 | "allowJs": true,
25 | "baseUrl": "src",
26 | "paths": {
27 | "assets/*": ["../assets/*"],
28 | "quorum_bin/*": ["../quorum_bin/*"]
29 | }
30 | },
31 | "include": [
32 | "src",
33 | "vite.config.ts"
34 | ],
35 | "exclude": [
36 | "test",
37 | "release",
38 | "src/main.prod.js",
39 | "src/renderer.prod.js",
40 | "src/dist",
41 | ".erb/dll"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------