├── .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 |
24 |
25 |
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 |
23 |
24 |
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 |
31 |
32 |
33 | 34 |
35 |
36 |
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 icon; 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 |
21 |
25 | 26 |
27 | 32 |
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 | 19 |
20 | 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 |
12 |
16 | 25 |
26 | 27 |
28 |
29 |
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 |
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 | --------------------------------------------------------------------------------