├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .storybook ├── addons.ts ├── config.tsx ├── preview-head.html ├── style.css └── webpack.config.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── deployment.extras.yml ├── deployment.template.yml ├── docker ├── Dockerfile ├── docker-compose.yml └── nginx │ ├── Dockerfile │ └── default.conf ├── export-env.js ├── fixes ├── fixed-polkadot-ext.js └── fixed-polkadot-react-qr.js ├── localhost.env ├── next-env.d.ts ├── next.config.js ├── package.json ├── patch-polkadot.sh ├── patch.sh ├── public ├── chrome.svg ├── favicon.ico ├── firefox.svg ├── fonts │ ├── Merriweather-Bold.ttf │ ├── NotoSerif-Bold.ttf │ └── PTSerif-Bold.ttf ├── index.html ├── manifest.json ├── subsocial-logo.svg ├── subsocial-sign.png ├── subsocial-sign.svg └── substrate.svg ├── run-dev.sh ├── src ├── components │ ├── activity │ │ ├── AccountActivity.tsx │ │ ├── FeedActivities.tsx │ │ ├── InnerActivities.tsx │ │ ├── MyFeed.tsx │ │ ├── MyNotifications.tsx │ │ ├── Notification.tsx │ │ ├── NotificationUtils.tsx │ │ ├── Notifications.tsx │ │ └── types.ts │ ├── api │ │ └── useSubsocialEffect.tsx │ ├── auth │ │ ├── AuthButtons.tsx │ │ ├── AuthContext.tsx │ │ ├── AuthorizationPanel.tsx │ │ ├── MyAccountContext.tsx │ │ ├── NotAuthorized.tsx │ │ ├── OnlySudo.tsx │ │ └── SignInModal.tsx │ ├── comments │ │ ├── CommentEditor.module.sass │ │ ├── CommentEditor.tsx │ │ ├── CommentTree.tsx │ │ ├── CommentsSection.tsx │ │ ├── CreateComment.tsx │ │ ├── UpdateComment.tsx │ │ ├── ViewComment.tsx │ │ ├── helpers │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ └── utils.ts │ ├── forms │ │ ├── AntForms.module.sass │ │ ├── AntForms.tsx │ │ ├── index.tsx │ │ └── messages.ts │ ├── lists │ │ ├── DataList.tsx │ │ ├── InfiniteList.tsx │ │ ├── PaginatedList.tsx │ │ └── utils.ts │ ├── main │ │ ├── HomePage.tsx │ │ ├── LatestPosts.tsx │ │ ├── LatestSpaces.tsx │ │ └── PageWrapper.tsx │ ├── onboarding │ │ ├── OnBoarding.tsx │ │ ├── OnBoardingCard.tsx │ │ ├── OnBoardingPage.tsx │ │ └── index.tsx │ ├── posts │ │ ├── EditPost.tsx │ │ ├── HiddenPostButton.tsx │ │ ├── NewPostButtonInTopMenu.module.sass │ │ ├── NewPostButtonInTopMenu.tsx │ │ ├── PostPreviewList.tsx │ │ ├── PostStats.tsx │ │ ├── PostValidation.ts │ │ ├── ShareModal │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ ├── ViewPostLink.tsx │ │ ├── share │ │ │ ├── ShareDropdown │ │ │ │ ├── index.module.sass │ │ │ │ └── index.tsx │ │ │ └── SpaceShareLink.tsx │ │ ├── slugify.ts │ │ └── view-post │ │ │ ├── DynamicPostPreview.tsx │ │ │ ├── PostPage.tsx │ │ │ ├── PostPreview.tsx │ │ │ ├── PostPreviewList.tsx │ │ │ ├── ViewRegularPreview.tsx │ │ │ ├── ViewSharedPreview.tsx │ │ │ ├── helpers.tsx │ │ │ └── index.tsx │ ├── profile-selector │ │ ├── AccountSelector.module.sass │ │ ├── AccountSelector.tsx │ │ ├── ActionMenu.tsx │ │ ├── MyAccountMenu.tsx │ │ └── MyAccountSection.tsx │ ├── profiles │ │ ├── AccountsListModal.module.sass │ │ ├── AccountsListModal.tsx │ │ ├── EditProfile.tsx │ │ ├── FollowingModal.tsx │ │ ├── ViewProfile.tsx │ │ ├── ViewProfileLink.tsx │ │ └── address-views │ │ │ ├── AuthorPreview.tsx │ │ │ ├── Avatar.tsx │ │ │ ├── InfoSection │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ │ ├── Name.tsx │ │ │ ├── ProfilePreview.tsx │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── Balance.tsx │ │ │ ├── NameDetails.tsx │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ └── withLoadedOwner.tsx │ ├── responsive │ │ ├── ResponsiveContext.tsx │ │ └── index.tsx │ ├── search │ │ ├── SearchInput.tsx │ │ └── SearchResults.tsx │ ├── settings │ │ ├── Settings.ts │ │ ├── defaults.ts │ │ ├── index.ts │ │ └── types.ts │ ├── sitemap │ │ └── index.ts │ ├── spaces │ │ ├── AboutSpace.tsx │ │ ├── AboutSpaceLink.tsx │ │ ├── AccountSpaces.tsx │ │ ├── EditSpace.tsx │ │ ├── EditTeamMember │ │ │ ├── index.module.scss.keep │ │ │ ├── index.tsx.keep │ │ │ └── validation.ts.keep │ │ ├── HiddenSpaceButton.tsx │ │ ├── ListAllSpaces.tsx │ │ ├── ListFollowingSpaces.tsx │ │ ├── NavValidation.ts │ │ ├── NavigationEditor.ignore │ │ ├── SocialLinks │ │ │ ├── NewSocialLinks.tsx │ │ │ ├── ViewSocialLinks.tsx │ │ │ └── utils.tsx │ │ ├── SpaceNav.tsx │ │ ├── SpaceStatsRow.tsx │ │ ├── SpacedSectionTitle.tsx │ │ ├── TransferSpaceOwnership.module.sass │ │ ├── TransferSpaceOwnership.tsx │ │ ├── ViewSpace.tsx │ │ ├── ViewSpaceById.tsx │ │ ├── ViewSpaceLink.tsx │ │ ├── ViewSpaceProps.ts │ │ ├── helpers │ │ │ ├── AllSpacesLink.tsx │ │ │ ├── CreatePostButton.tsx │ │ │ ├── CreateSpaceButton.tsx │ │ │ ├── DropdownMenu.tsx │ │ │ ├── EditMenuLink.tsx │ │ │ ├── PostPreviewsOnSpace.tsx │ │ │ ├── SpaceAvatar.tsx │ │ │ ├── common.tsx │ │ │ ├── index.tsx │ │ │ ├── useLoadUnlistedPostsByOwner.ts │ │ │ └── useLoadUnlistedSpace.tsx │ │ ├── withLoadSpaceDataById.tsx │ │ ├── withLoadSpaceFromUrl.tsx │ │ └── withSpaceIdFromUrl.tsx │ ├── substrate │ │ ├── KusamaContext │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── SubstrateContext.tsx │ │ ├── SubstrateTxButton.tsx │ │ ├── SubstrateWebConsole.tsx │ │ ├── TxDiv.tsx │ │ ├── hoc │ │ │ ├── api.tsx │ │ │ ├── call.tsx │ │ │ ├── calls.ts │ │ │ ├── index.ts │ │ │ ├── multi.ts │ │ │ └── types.ts │ │ ├── index.tsx │ │ ├── useSubstrate.tsx │ │ ├── useToggle.ts │ │ └── util │ │ │ ├── getTxParams.ts │ │ │ ├── index.ts │ │ │ ├── isEqual.ts │ │ │ ├── queryToProps.ts │ │ │ └── triggerChange.ts │ ├── types │ │ └── index.ts │ ├── uploader │ │ ├── index.module.sass │ │ └── index.tsx │ ├── urls │ │ ├── goToPage.ts │ │ ├── helpers.tsx │ │ ├── index.ts │ │ ├── social-share.ts │ │ └── subsocial.ts │ ├── utils │ │ ├── ButtonLink.tsx │ │ ├── DfAvatar.tsx │ │ ├── DfBgImg.tsx │ │ ├── DfMd.tsx │ │ ├── DfMdEditor │ │ │ ├── client.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── EditableTagGroup.ignore │ │ ├── EmptyList.tsx │ │ ├── EntityStatusPanels │ │ │ ├── EntityStatusPanel.tsx │ │ │ ├── HiddenEntityPanel.tsx │ │ │ ├── PendingSpaceOwnershipPanel.tsx │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ ├── Faucet │ │ │ ├── Step1.tsx │ │ │ ├── Step2.tsx │ │ │ ├── Step3.tsx │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ ├── FollowAccountButton.tsx │ │ ├── FollowSpaceButton.tsx │ │ ├── HiddenButton │ │ │ └── index.tsx │ │ ├── HtmlPage.tsx │ │ ├── IconWithLabel.tsx │ │ ├── IdentityIcon.tsx │ │ ├── ListsEditHistory.tsx │ │ ├── Message.tsx │ │ ├── MutedText.tsx │ │ ├── MyAccount.tsx │ │ ├── MyEntityLabel.tsx │ │ ├── NotifCounter.tsx.ignore │ │ ├── OffchainUtils.ts │ │ ├── Plularize.tsx │ │ ├── PrivacyPolicyLinks.tsx │ │ ├── ReorderNavTabs.tsx │ │ ├── Section.tsx │ │ ├── Segment.tsx │ │ ├── SelectSpacePreview.tsx │ │ ├── SideBarCollapsedContext.tsx │ │ ├── StorybookContext.tsx │ │ ├── SubTitle.tsx │ │ ├── SubsocialApiContext.tsx │ │ ├── SubsocialConnect.ts │ │ ├── Suspense.tsx │ │ ├── TxButton.tsx │ │ ├── ViewTags.tsx │ │ ├── WarningPanel.tsx │ │ ├── WhereAmIPanel │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ ├── content │ │ │ └── index.ts │ │ ├── env.ts │ │ ├── forms │ │ │ └── validation.ts │ │ ├── getIds.ts │ │ ├── index.tsx │ │ ├── md │ │ │ ├── SummarizeMd.tsx │ │ │ └── index.ts │ │ ├── next.ts │ │ └── types.ts │ └── voting │ │ ├── ListVoters.tsx │ │ └── VoterButtons.tsx ├── config │ ├── ListData.config.ts │ ├── Size.config.ts │ └── ValidationsConfig.ts ├── ipfs │ └── index.ts ├── layout │ ├── ClientLayout.tsx │ ├── MainPage.tsx │ ├── MySubscriptions.tsx.ignore │ ├── Navigation.tsx │ ├── SideMenu.tsx │ ├── SideMenuItems.tsx │ └── TopMenu.tsx ├── messages │ └── index.ts ├── pages │ ├── [spaceId] │ │ ├── [slug] │ │ │ ├── edit.tsx │ │ │ └── index.tsx │ │ ├── about.tsx │ │ ├── edit.tsx │ │ ├── index.tsx │ │ └── posts │ │ │ ├── index.tsx │ │ │ └── new.tsx │ ├── _app.js │ ├── accounts │ │ ├── [address] │ │ │ ├── following.tsx │ │ │ ├── index.tsx │ │ │ └── spaces.tsx │ │ ├── edit.tsx │ │ └── new.tsx │ ├── faucet.tsx │ ├── feed.tsx │ ├── index.tsx │ ├── legal │ │ ├── privacy.md │ │ ├── privacy.tsx │ │ ├── terms.md │ │ └── terms.tsx │ ├── notifications.tsx │ ├── robots.txt.tsx │ ├── search.tsx │ ├── sitemap │ │ ├── posts │ │ │ ├── index.xml.ts │ │ │ └── urlset.xml.ts │ │ ├── profiles │ │ │ ├── index.xml.ts │ │ │ └── urlset.xml.ts │ │ └── spaces │ │ │ ├── index.xml.ts │ │ │ └── urlset.xml.ts │ ├── spaces │ │ ├── index.tsx │ │ └── new.tsx │ └── sudo │ │ ├── forceTransfer.tsx │ │ └── index.tsx ├── redux │ ├── slices │ │ ├── postByIdSlice.ts │ │ └── replyIdsByPostIdSlice.ts │ ├── store.ts │ └── types.ts ├── storage │ ├── store.ts │ └── substrate.ts ├── stories │ ├── AccountSelector.stories.tsx │ ├── AddressComponents.stories.tsx │ ├── EditPost.stories.tsx │ ├── EditSpace.stories.tsx │ ├── HookFormsWithAntd.stories.tsx │ ├── ListSpaces.stories.tsx │ ├── Mobile.stories.tsx │ ├── Navigation.stories.tsx │ ├── Notifications.stories.tsx │ ├── OnBoarding.stories.tsx │ ├── SignInModal.stories.tsx │ ├── Team.stories.tsx │ ├── mobile.css │ ├── mockNextRouter.ts │ ├── mocks │ │ ├── AccountMocks.ts │ │ ├── NavTabsMocks.ts │ │ ├── PostMocks.ts │ │ ├── SocialProfileMocks.ts │ │ ├── SpaceMocks.ts │ │ └── TeamMocks.ts.keep │ └── withStorybookContext.tsx ├── styles │ ├── antd.css │ ├── bootstrap-utilities-4.3.1.css │ ├── components.scss │ ├── fonts.scss │ ├── github-markdown.css │ ├── subsocial-mobile.scss │ ├── subsocial-vars.scss │ ├── subsocial.scss │ └── utils.scss ├── types │ └── global.d.ts └── utils │ ├── hacks.ts │ ├── index.ts │ ├── md.ts │ ├── num.ts │ └── text.ts ├── subsocial-betanet.env ├── test ├── enzyme.js └── test.contract.wasm ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | .git/ 4 | .storybook/ 5 | .vscode/ 6 | **/stories/ 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style=space 4 | indent_size=2 5 | tab_width=2 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/.next/* 2 | **/node_modules/* 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const base = require('@subsocial/config/eslintrc') 3 | 4 | // add override for any (a metric ton of them, initial conversion) 5 | module.exports = { 6 | ...base, 7 | rules: { 8 | 'react/react-in-jsx-scope': 'off', 9 | '@typescript-eslint/explicit-module-boundary-types': 'off', 10 | 'semi': [ 'warn', 'never' ], 11 | 'react/prop-types': 'off', 12 | 'quotes': [ 'warn', 'single' ], 13 | 'array-bracket-spacing' : [ 'warn', 'always' ], 14 | 'no-multi-spaces': 'error', 15 | 'space-before-function-paren': [ 'warn', 'always' ], 16 | 'non-nullish value': 'off', 17 | 'react/display-name': 'off', 18 | '@typescript-eslint/ban-types': 'off', 19 | 'react-hooks/exhaustive-deps': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Code editors 2 | .idea 3 | .vscode 4 | *.code-workspace 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | public/env.js 65 | 66 | # next.js build output 67 | .next 68 | out 69 | 70 | -------------------------------------------------------------------------------- /.storybook/addons.ts: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-actions/register'; 3 | import '@storybook/addon-storysource/register'; 4 | import '@storybook/addon-viewport/register'; -------------------------------------------------------------------------------- /.storybook/config.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { configure, addDecorator } from '@storybook/react'; 3 | import '@storybook/addon-console'; 4 | // @ts-ignore 5 | import StoryRouter from 'storybook-react-router'; 6 | import { RouterContext } from 'next/dist/next-server/lib/router-context'; 7 | import { mockNextRouter } from '../src/stories/mockNextRouter'; 8 | 9 | import { withStorybookContext } from '../src/stories/withStorybookContext'; 10 | 11 | import '../src/components/utils/styles'; 12 | import { addParameters } from '@storybook/react'; 13 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; 14 | 15 | addParameters({ 16 | viewport: { 17 | viewports: INITIAL_VIEWPORTS, 18 | }, 19 | }); 20 | 21 | // Mock React router: 22 | addDecorator(StoryRouter()); 23 | 24 | // Mock Next.js router 25 | addDecorator(story => ( 26 | 27 | {story()} 28 | 29 | )) 30 | 31 | // Mock Substrate TxButton: 32 | addDecorator(withStorybookContext) 33 | 34 | addDecorator(story => ( 35 |
{sudo.toString()}
(Component: React.ComponentType) { 17 | return function (props: P) { 18 | const { postIds } = props 19 | const [ posts, setPosts ] = useState() 20 | const [ loaded, setLoaded ] = useState(false) 21 | 22 | useSubsocialEffect(({ subsocial }) => { 23 | const loadData = async () => { 24 | const extPostData = await subsocial.findPublicPostsWithAllDetails(postIds) 25 | extPostData && setPosts(extPostData) 26 | setLoaded(true) 27 | } 28 | 29 | loadData().catch(console.warn) 30 | }, [ false ]) 31 | 32 | return loaded && posts 33 | ? 34 | : 35 | } 36 | } 37 | 38 | const InnerPostPreviewList: React.FunctionComponent = ({ posts }) => 39 | <>{posts.map(x => )}> 40 | 41 | export const PostPreviewList = withLoadPostsWithSpaces(InnerPostPreviewList) 42 | -------------------------------------------------------------------------------- /src/components/posts/PostValidation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import { maxLenError, minLenError, urlValidation } from '../utils/forms/validation' 3 | import { pluralize } from '../utils/Plularize' 4 | 5 | const TITLE_MIN_LEN = 3 6 | const TITLE_MAX_LEN = 100 7 | 8 | const MAX_TAGS_PER_POST = 10 9 | 10 | const POST_MAX_LEN = 10000 11 | 12 | export const buildValidationSchema = () => Yup.object().shape({ 13 | title: Yup.string() 14 | // .required('Post title is required') 15 | .min(TITLE_MIN_LEN, minLenError('Post title', TITLE_MIN_LEN)) 16 | .max(TITLE_MAX_LEN, maxLenError('Post title', TITLE_MAX_LEN)), 17 | 18 | body: Yup.string() 19 | .required('Post body is required') 20 | // .min(p.minTextLen.toNumber(), minLenError('Post body', p.postMinLen)) 21 | .max(POST_MAX_LEN, maxLenError('Post body', POST_MAX_LEN)), 22 | 23 | image: urlValidation('Image'), 24 | 25 | tags: Yup.array() 26 | .max(MAX_TAGS_PER_POST, `Too many tags. You can use up to ${pluralize(MAX_TAGS_PER_POST, 'tag')} per post.`), 27 | 28 | canonical: urlValidation('Original post') 29 | }) 30 | 31 | export const buildSharePostValidationSchema = () => Yup.object().shape({ 32 | body: Yup.string() 33 | .max(POST_MAX_LEN, maxLenError('Post body', POST_MAX_LEN)) 34 | }) 35 | -------------------------------------------------------------------------------- /src/components/posts/ShareModal/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfShareModalBody 4 | \:global .DfSegment 5 | margin: 0 6 | 7 | .DfShareModal 8 | width: $max_width_content !important 9 | 10 | .DfShareModalSelector 11 | display: flex 12 | align-items: center 13 | 14 | .DfShareModalMdEditor 15 | .CodeMirror 16 | height: 5rem 17 | -------------------------------------------------------------------------------- /src/components/posts/ViewPostLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, HasDataForSlug, postUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | post: HasDataForSlug 8 | title?: string 9 | hint?: string 10 | className?: string 11 | } 12 | 13 | export const ViewPostLink = ({ 14 | space, 15 | post, 16 | title, 17 | hint, 18 | className 19 | }: Props) => { 20 | 21 | if (!space.id || !post.struct.id || !title) return null 22 | 23 | return ( 24 | 25 | {title} 26 | 27 | ) 28 | } 29 | 30 | export default ViewPostLink 31 | -------------------------------------------------------------------------------- /src/components/posts/share/ShareDropdown/index.module.sass: -------------------------------------------------------------------------------- 1 | .DfShareDropdown 2 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05) 3 | line-height: 2rem 4 | 5 | \:global .ant-menu-item 6 | padding: 0 1rem !important 7 | \:global .anticon 8 | margin-right: 0.25rem 9 | -------------------------------------------------------------------------------- /src/components/posts/share/SpaceShareLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { PostWithSomeDetails } from '@subsocial/types/dto' 3 | import { PostExtension } from '@subsocial/types/substrate/classes' 4 | import { EditOutlined } from '@ant-design/icons' 5 | import { ShareModal } from '../ShareModal' 6 | import { isRegularPost } from '../view-post' 7 | import { IconWithLabel } from '../../utils' 8 | import { useAuth } from '../../auth/AuthContext' 9 | 10 | type Props = { 11 | postDetails: PostWithSomeDetails 12 | title?: React.ReactNode 13 | preview?: boolean 14 | } 15 | 16 | export const SpaceShareLink = ({ 17 | postDetails: { 18 | post: { struct: { id, extension } }, 19 | ext 20 | } 21 | }: Props) => { 22 | 23 | const { openSignInModal, state: { completedSteps: { isSignedIn } } } = useAuth() 24 | const [ open, setOpen ] = useState() 25 | const postId = isRegularPost(extension as PostExtension) ? id : ext && ext.post.struct.id 26 | const title = 'Write a post' 27 | 28 | return <> 29 | isSignedIn ? setOpen(true) : openSignInModal('AuthRequired')} 32 | title={title} 33 | > 34 | } label={title} /> 35 | 36 | setOpen(false)} /> 37 | > 38 | } 39 | 40 | export default SpaceShareLink 41 | -------------------------------------------------------------------------------- /src/components/posts/slugify.ts: -------------------------------------------------------------------------------- 1 | import { PostContent } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import slugify from '@sindresorhus/slugify' 4 | import BN from 'bn.js' 5 | import { summarize } from 'src/utils' 6 | 7 | const MAX_SLUG_LENGTH = 60 8 | const SLUG_SEPARATOR = '-' 9 | 10 | export type HasTitleOrBody = Pick 11 | 12 | export const createPostSlug = (postId: BN, content?: HasTitleOrBody) => { 13 | let slug = postId.toString() 14 | 15 | if (content) { 16 | const { title, body } = content 17 | const titleOrBody = nonEmptyStr(title) ? title : body 18 | const summary = summarize(titleOrBody, { limit: MAX_SLUG_LENGTH, omission: '' }) 19 | const slugifiedSummary = slugify(summary, { separator: SLUG_SEPARATOR }) 20 | 21 | if (nonEmptyStr(slugifiedSummary)) { 22 | slug = slugifiedSummary + '-' + slug 23 | } 24 | } 25 | 26 | return slug 27 | } 28 | 29 | export const getPostIdFromSlug = (slug: string) => { 30 | try { 31 | const postId = slug.split(SLUG_SEPARATOR).pop() 32 | 33 | if (!postId) return undefined 34 | 35 | return new BN(postId) 36 | } catch { 37 | return undefined 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/posts/view-post/DynamicPostPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import { PostWithAllDetails } from '@subsocial/types/dto' 4 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 5 | import { InnerPreviewProps } from './ViewRegularPreview' 6 | import PostPreview, { BarePreviewProps } from './PostPreview' 7 | import { AnyPostId } from '@subsocial/types' 8 | 9 | const log = newLogger(DynamicPostPreview.name) 10 | 11 | export type DynamicPreviewProps = BarePreviewProps & { 12 | id: AnyPostId 13 | } 14 | 15 | export function DynamicPostPreview ({ id, withActions, replies, asRegularPost }: DynamicPreviewProps) { 16 | const [ postDetails, setPostStruct ] = useState() 17 | 18 | useSubsocialEffect(({ subsocial }) => { 19 | let isSubscribe = true 20 | 21 | const loadPost = async () => { 22 | const extPostData = id && await subsocial.findPostWithAllDetails(id) 23 | isSubscribe && setPostStruct(extPostData) 24 | } 25 | 26 | loadPost().catch(err => log.error(`Failed to load post data. ${err}`)) 27 | 28 | return () => { isSubscribe = false } 29 | }, [ false ]) 30 | 31 | if (!postDetails) return null 32 | 33 | const props = { 34 | postDetails: postDetails, 35 | space: postDetails.space, 36 | withActions: withActions, 37 | replies: replies, 38 | asRegularPost: asRegularPost 39 | } as InnerPreviewProps 40 | 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /src/components/posts/view-post/PostPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RegularPreview, SharedPreview, HiddenPostAlert } from '.' 3 | import { PostWithSomeDetails, PostWithAllDetails, SpaceData } from '@subsocial/types' 4 | import { PostExtension } from '@subsocial/types/substrate/classes' 5 | import { Segment } from 'src/components/utils/Segment' 6 | import { isSharedPost } from './helpers' 7 | 8 | export type BarePreviewProps = { 9 | withTags?: boolean, 10 | withActions?: boolean, 11 | replies?: PostWithAllDetails[], 12 | asRegularPost?: boolean 13 | } 14 | 15 | export type PreviewProps = BarePreviewProps & { 16 | postDetails: PostWithSomeDetails, 17 | space?: SpaceData 18 | } 19 | 20 | export function PostPreview (props: PreviewProps) { 21 | const { postDetails, space: externalSpace, asRegularPost } = props 22 | const { space: globalSpace, post: { struct } } = postDetails 23 | const { extension } = struct 24 | const space = externalSpace || globalSpace 25 | 26 | if (!space) return null 27 | 28 | return 29 | 30 | {asRegularPost || !isSharedPost(extension as PostExtension) 31 | ? 32 | : 33 | } 34 | 35 | } 36 | 37 | export default PostPreview -------------------------------------------------------------------------------- /src/components/posts/view-post/PostPreviewList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import BN from 'bn.js' 3 | import { Loading } from '../../utils' 4 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 5 | import { PostWithAllDetails } from '@subsocial/types' 6 | import PostPreview from './PostPreview' 7 | import DataList from 'src/components/lists/DataList' 8 | 9 | type OuterProps = { 10 | postIds: BN[] 11 | } 12 | 13 | type ResolvedProps = { 14 | posts: PostWithAllDetails[] 15 | } 16 | 17 | export function withLoadPostsWithSpaces (Component: React.ComponentType) { 18 | return function (props: P) { 19 | const { postIds } = props 20 | const [ posts, setPosts ] = useState() 21 | const [ loaded, setLoaded ] = useState(false) 22 | 23 | useSubsocialEffect(({ subsocial }) => { 24 | setLoaded(false) 25 | 26 | const loadData = async () => { 27 | const extPostData = await subsocial.findPublicPostsWithAllDetails(postIds) 28 | extPostData && setPosts(extPostData) 29 | setLoaded(true) 30 | } 31 | 32 | loadData().catch(console.warn) 33 | }, [ false ]) 34 | 35 | return loaded && posts 36 | ? 37 | : 38 | } 39 | } 40 | 41 | const InnerPostPreviewList: React.FunctionComponent = ({ posts }) => 42 | } /> 43 | 44 | export const PostPreviewList = withLoadPostsWithSpaces(InnerPostPreviewList) 45 | -------------------------------------------------------------------------------- /src/components/posts/view-post/ViewRegularPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { SpaceData } from '@subsocial/types/dto' 3 | import { CommentSection } from '../../comments/CommentsSection' 4 | import { InfoPostPreview, PostActionsPanel, PostNotFound } from './helpers' 5 | import { PreviewProps } from './PostPreview' 6 | import { isVisible } from 'src/components/utils' 7 | 8 | export type InnerPreviewProps = PreviewProps & { 9 | space: SpaceData 10 | } 11 | 12 | type ComponentType = React.FunctionComponent 13 | 14 | export const RegularPreview: ComponentType = (props) => { 15 | const { postDetails, space, replies, withTags, withActions } = props 16 | const extStruct = postDetails.ext?.post.struct 17 | const [ commentsSection, setCommentsSection ] = useState(false) 18 | 19 | return !extStruct || isVisible({ struct: extStruct, address: extStruct.owner }) 20 | ? <> 21 | 22 | {withActions && setCommentsSection(!commentsSection) } preview withBorder />} 23 | {commentsSection && } 24 | > 25 | : 26 | } 27 | -------------------------------------------------------------------------------- /src/components/posts/view-post/ViewSharedPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CommentSection } from '../../comments/CommentsSection' 3 | import { PostCreator, PostDropDownMenu, PostActionsPanel, SharePostContent } from './helpers' 4 | import { InnerPreviewProps } from '.' 5 | 6 | type ComponentType = React.FunctionComponent 7 | 8 | export const SharedPreview: ComponentType = (props) => { 9 | const { postDetails, space, withActions, replies } = props 10 | const [ commentsSection, setCommentsSection ] = useState(false) 11 | 12 | return <> 13 | 14 | 15 | 16 | 17 | 18 | {withActions && setCommentsSection(!commentsSection)} preview />} 19 | {commentsSection && } 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/posts/view-post/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './helpers' 2 | export * from './PostPage' 3 | export * from './ViewRegularPreview' 4 | export * from './ViewSharedPreview' 5 | -------------------------------------------------------------------------------- /src/components/profile-selector/AccountSelector.module.sass: -------------------------------------------------------------------------------- 1 | .DfAccountSelector 2 | overflow-y: auto 3 | 4 | .DfAccountPopup 5 | max-width: 368px 6 | position: fixed -------------------------------------------------------------------------------- /src/components/profile-selector/MyAccountSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SignOutButton } from 'src/components/auth/AuthButtons' 3 | import { AccountSelector } from './AccountSelector' 4 | import PrivacyPolicyLinks from '../utils/PrivacyPolicyLinks' 5 | import { Divider } from 'antd' 6 | import { ActionMenu } from './ActionMenu' 7 | 8 | export const MyAccountSection = () => { 9 | return 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | } 19 | 20 | export default MyAccountSection 21 | -------------------------------------------------------------------------------- /src/components/profiles/AccountsListModal.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .AccountsListModal 4 | margin-top: 3rem 5 | 6 | \:global 7 | 8 | .DfDataList 9 | margin-top: 0 10 | 11 | .header 12 | font-size: $space_large 13 | 14 | .ant-modal-body 15 | padding: 0 16 | 17 | .ant-list-lg .ant-list-item 18 | padding: $space_small $space_normal 19 | -------------------------------------------------------------------------------- /src/components/profiles/AccountsListModal.tsx: -------------------------------------------------------------------------------- 1 | import styles from './AccountsListModal.module.sass' 2 | 3 | import React from 'react' 4 | import { withCalls, withMulti, spaceFollowsQueryToProp, profileFollowsQueryToProp } from '../substrate' 5 | import { GenericAccountId as AccountId } from '@polkadot/types' 6 | import { Modal, Button } from 'antd' 7 | import { ProfilePreviewWithOwner } from './address-views' 8 | import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' 9 | import DataList from '../lists/DataList' 10 | 11 | type Props = { 12 | accounts?: AccountId[], 13 | accountsCount: number, 14 | title: string, 15 | open: boolean, 16 | close: () => void 17 | }; 18 | 19 | const InnerAccountsListModal = (props: Props) => { 20 | const { accounts, open, close, title } = props 21 | 22 | if (!accounts) return null 23 | 24 | return ( 25 | Close} 31 | > 32 | 35 | } 36 | noDataDesc='Nothing yet' 37 | /> 38 | 39 | ) 40 | } 41 | 42 | export const SpaceFollowersModal = withMulti( 43 | InnerAccountsListModal, 44 | withCalls( 45 | spaceFollowsQueryToProp('spaceFollowers', { paramName: 'id', propName: 'accounts' }) 46 | ) 47 | ) 48 | 49 | export const AccountFollowersModal = withMulti( 50 | InnerAccountsListModal, 51 | withCalls( 52 | profileFollowsQueryToProp('accountFollowers', { paramName: 'id', propName: 'accounts' }) 53 | ) 54 | ) 55 | 56 | export const AccountFollowingModal = withMulti( 57 | InnerAccountsListModal, 58 | withCalls( 59 | profileFollowsQueryToProp('accountsFollowedByAccount', { paramName: 'id', propName: 'accounts' }) 60 | ) 61 | ) 62 | -------------------------------------------------------------------------------- /src/components/profiles/FollowingModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { withCalls, withMulti, profileFollowsQueryToProp } from '../substrate' 4 | import { GenericAccountId as AccountId } from '@polkadot/types' 5 | import { Modal, Button } from 'antd' 6 | import { ProfilePreviewWithOwner } from './address-views' 7 | import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' 8 | 9 | type Props = { 10 | following?: AccountId[], 11 | followingCount: number 12 | }; 13 | 14 | const InnerFollowingModal = (props: Props) => { 15 | const { following, followingCount } = props 16 | const [ open, setOpen ] = useState(false) 17 | 18 | const renderFollowing = () => { 19 | return following && following.map((account) => 20 | 21 | 26 | 27 | ) 28 | } 29 | 30 | return ( 31 | <> 32 | setOpen(true)}>Following ({followingCount}) 33 | setOpen(false)}>Close} 39 | > 40 | {renderFollowing()} 41 | 42 | > 43 | ) 44 | } 45 | 46 | export const AccountFollowingModal = withMulti( 47 | InnerFollowingModal, 48 | withCalls( 49 | profileFollowsQueryToProp('accountsFollowedByAccount', { paramName: 'id', propName: 'following' }) 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /src/components/profiles/ViewProfileLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasAddressOrHandle, accountUrl } from '../urls' 4 | 5 | type Props = { 6 | account: HasAddressOrHandle 7 | title?: React.ReactNode 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const ViewProfileLink = ({ 13 | account, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | const { address } = account 19 | 20 | if (!address) return null 21 | 22 | return ( 23 | 24 | {title || address.toString()} 25 | 26 | ) 27 | } 28 | 29 | export default ViewProfileLink 30 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BaseAvatar, { BaseAvatarProps } from 'src/components/utils/DfAvatar' 3 | import { CopyAddress } from './utils' 4 | 5 | export const Avatar: React.FunctionComponent = (props) => { 6 | return 7 | } 8 | 9 | export default Avatar 10 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/InfoSection/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfInfoSection 4 | font-size: $font_small 5 | \:global .ant-descriptions-item 6 | padding: $space_mini 7 | \:global .ant-descriptions-item-label 8 | color: $color_muted 9 | \:global .descriptions-row > td 10 | padding-bottom: 0 11 | 12 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/InfoSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Descriptions as AntdDesc } from 'antd' 3 | import { useResponsiveSize } from 'src/components/responsive' 4 | import { BareProps } from 'src/components/utils/types' 5 | import Section from 'src/components/utils/Section' 6 | import styles from './index.module.sass' 7 | 8 | export type DescItem = { 9 | label?: React.ReactNode, 10 | value: React.ReactNode 11 | } 12 | 13 | type InfoPanelProps = BareProps & { 14 | title?: React.ReactNode 15 | items?: DescItem[], 16 | size?: 'middle' | 'small' | 'default', 17 | column?: number, 18 | layout?: 'vertical' | 'horizontal' 19 | } 20 | 21 | type DescriptionsProps = InfoPanelProps & { 22 | title: React.ReactNode 23 | level?: number, 24 | } 25 | 26 | export const InfoPanel = ({ title, size = 'small', layout, column = 2, items, ...bareProps }: InfoPanelProps) => { 27 | const { isMobile } = useResponsiveSize() 28 | 29 | return 36 | {items?.map(({ label, value }, key) => 37 | 41 | {value} 42 | )} 43 | 44 | } 45 | 46 | export const InfoSection = ({ title, level, className, style, ...props }: DescriptionsProps) => 52 | 53 | 54 | 55 | export default InfoSection 56 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/Name.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { toShortAddress } from 'src/components/utils' 3 | import { AddressProps } from './utils/types' 4 | import { ProfileData } from '@subsocial/types' 5 | import { withLoadedOwner } from './utils/withLoadedOwner' 6 | import ViewProfileLink from '../ViewProfileLink' 7 | import { useExtensionName } from './utils' 8 | import { MutedSpan } from 'src/components/utils/MutedText' 9 | 10 | type Props = AddressProps & { 11 | isShort?: boolean, 12 | asLink?: boolean, 13 | withShortAddress?: boolean, 14 | className?: string 15 | }; 16 | 17 | export const Name = ({ 18 | address, 19 | owner = {} as ProfileData, 20 | isShort = true, 21 | asLink = true, 22 | withShortAddress, 23 | className 24 | }: Props) => { 25 | 26 | const { content } = owner 27 | 28 | // TODO extract a function? (find similar copypasta in other files): 29 | const shortAddress = toShortAddress(address) 30 | const addressString = isShort ? shortAddress : address.toString() 31 | const name = content?.name || useExtensionName(address) 32 | const title = name 33 | ? 34 | {name} 35 | {withShortAddress && {shortAddress}} 36 | 37 | : addressString 38 | const nameClass = `ui--AddressComponents-address ${className}` 39 | 40 | return asLink 41 | ? 42 | : <>{title}> 43 | } 44 | 45 | export const NameWithOwner = withLoadedOwner(Name) 46 | 47 | export default Name 48 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../profile-selector/MyAccountMenu' 2 | export * from './AuthorPreview' 3 | export * from './ProfilePreview' 4 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { AnyAccountId } from '@subsocial/types' 3 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 4 | import { useSubstrateContext } from 'src/components/substrate' 5 | import { Copy } from 'src/components/urls/helpers' 6 | import Link from 'next/link' 7 | import { BareProps } from 'src/components/utils/types' 8 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 9 | import { accountUrl } from 'src/components/urls' 10 | 11 | export const useExtensionName = (address: AnyAccountId) => { 12 | const [ extensionName, setExtensionName ] = useState() 13 | const { keyring } = useSubstrateContext() 14 | 15 | useSubsocialEffect(() => { 16 | if (!keyring) return 17 | 18 | const name = keyring.getAccount(address)?.meta.name 19 | name && setExtensionName(name) 20 | }, [ keyring, address ]) 21 | 22 | return extensionName?.replace('(polkadot-js)', '').toUpperCase() 23 | } 24 | 25 | type ProfileLink = BareProps & { 26 | address: AnyAccountId, 27 | title?: string, 28 | onClick?: () => void 29 | } 30 | 31 | export const AccountSpacesLink = ({ address, title = 'Spaces', ...otherProps }: ProfileLink) => {title} 32 | 33 | export const EditProfileLink = ({ address, title = 'Edit my profile', onClick, ...props }: ProfileLink) => isMyAddress(address) 34 | ? 35 | {title} 36 | 37 | : null 38 | 39 | type CopyAddressProps = { 40 | address: AnyAccountId, 41 | message?: string, 42 | children?: React.ReactNode 43 | } 44 | 45 | export const CopyAddress = ({ address = '', message = 'Address copied', children = address }: CopyAddressProps) => 46 | {children} 47 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { ProfileData } from '@subsocial/types' 4 | 5 | export type AddressProps = { 6 | className?: string 7 | style?: React.CSSProperties 8 | address: AnyAccountId, 9 | owner?: ProfileData 10 | } 11 | 12 | export type ExtendedAddressProps = AddressProps & { 13 | children?: React.ReactNode, 14 | afterName?: JSX.Element 15 | details?: JSX.Element 16 | isPadded?: boolean, 17 | isShort?: boolean, 18 | size?: number, 19 | withFollowButton?: boolean, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/withLoadedOwner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 4 | import { ProfileData } from '@subsocial/types' 5 | import { ExtendedAddressProps } from './types' 6 | import { Loading } from '../../../utils' 7 | import { useMyAccount } from 'src/components/auth/MyAccountContext' 8 | 9 | const log = newLogger(withLoadedOwner.name) 10 | 11 | type Props = ExtendedAddressProps & { 12 | size?: number 13 | avatar?: string 14 | mini?: boolean 15 | }; 16 | 17 | export function withLoadedOwner (Component: React.ComponentType) { 18 | return function (props: P) { 19 | const { owner: initialOwner, address } = props as Props 20 | 21 | if (initialOwner) return 22 | 23 | const [ owner, setOwner ] = useState() 24 | const [ loaded, setLoaded ] = useState(true) 25 | 26 | useSubsocialEffect(({ subsocial }) => { 27 | if (!address) return 28 | 29 | setLoaded(false) 30 | let isSubscribe = true 31 | 32 | const loadContent = async () => { 33 | const owner = await subsocial.findProfile(address) 34 | isSubscribe && setOwner(owner) 35 | setLoaded(true) 36 | } 37 | 38 | loadContent().catch(err => 39 | log.error(`Failed to load profile data. ${err}`)) 40 | 41 | return () => { isSubscribe = false } 42 | }, [ address?.toString() ]) 43 | 44 | return loaded 45 | ? 46 | : 47 | } 48 | } 49 | 50 | export function withMyProfile (Component: React.ComponentType) { 51 | return function (props: any) { 52 | const { state: { account, address } } = useMyAccount() 53 | return address ? : null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/responsive/ResponsiveContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | import { useMediaQuery } from 'react-responsive' 3 | import { isMobileDevice } from 'src/config/Size.config' 4 | 5 | export type ResponsiveSizeState = { 6 | isMobile: boolean, 7 | isTablet: boolean, 8 | isDesktop: boolean, 9 | isNotMobile: boolean, 10 | isNotDesktop: boolean 11 | } 12 | 13 | const contextStub: ResponsiveSizeState = { 14 | isDesktop: true, 15 | isMobile: false, 16 | isNotMobile: false, 17 | isTablet: false, 18 | isNotDesktop: false 19 | } 20 | 21 | export const ResponsiveSizeContext = createContext(contextStub) 22 | 23 | export function ResponsiveSizeProvider (props: React.PropsWithChildren) { 24 | const value = { 25 | isDesktop: useMediaQuery({ minWidth: 992 }), 26 | isTablet: useMediaQuery({ minWidth: 768, maxWidth: 991 }), 27 | isMobile: useMediaQuery({ maxWidth: 767 }), 28 | isNotMobile: useMediaQuery({ minWidth: 768 }), 29 | isNotDesktop: useMediaQuery({ maxWidth: 991 }) 30 | } 31 | 32 | return 33 | {props.children} 34 | 35 | } 36 | 37 | export function useResponsiveSize () { 38 | return useContext(ResponsiveSizeContext) 39 | } 40 | 41 | export function useIsMobileWidthOrDevice () { 42 | const { isMobile } = useResponsiveSize() 43 | return isMobileDevice || isMobile 44 | } -------------------------------------------------------------------------------- /src/components/responsive/index.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsiveSize } from './ResponsiveContext' 2 | 3 | export * from './ResponsiveContext' 4 | 5 | type Props = { 6 | children?: React.ReactNode | null | JSX.Element 7 | } 8 | 9 | export const Desktop = ({ children }: Props) => { 10 | const { isDesktop } = useResponsiveSize() 11 | return isDesktop ? children : null 12 | } 13 | 14 | export const Tablet = ({ children }: Props) => { 15 | const { isTablet } = useResponsiveSize() 16 | return isTablet ? children : null 17 | } 18 | 19 | export const Mobile = ({ children }: Props) => { 20 | const { isMobile } = useResponsiveSize() 21 | return isMobile ? children : null 22 | } 23 | 24 | export const Default = ({ children }: Props) => { 25 | const { isNotMobile } = useResponsiveSize() 26 | return isNotMobile ? children : null 27 | } 28 | -------------------------------------------------------------------------------- /src/components/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Input } from 'antd' 3 | import { nonEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { isMobileDevice } from 'src/config/Size.config' 6 | 7 | const { Search } = Input 8 | 9 | const SearchInput = () => { 10 | const router = useRouter() 11 | const [ searchValue, setSearchValue ] = useState(router.query.q as string) 12 | const isSearchPage = router.pathname.includes('search') 13 | 14 | useEffect(() => { 15 | if (isSearchPage) return 16 | 17 | setSearchValue(undefined) 18 | }, [ isSearchPage ]) 19 | 20 | const onSearch = (value: string) => { 21 | const queryPath = { 22 | pathname: '/search', 23 | query: { 24 | ...router.query, 25 | q: value 26 | } 27 | } 28 | return nonEmptyStr(value) && router.replace(queryPath, queryPath) 29 | } 30 | 31 | const onChange = (value: string) => setSearchValue(value) 32 | 33 | return ( 34 | 35 | onChange(e.currentTarget.value)} 41 | /> 42 | 43 | ) 44 | } 45 | 46 | export default SearchInput 47 | -------------------------------------------------------------------------------- /src/components/settings/defaults.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { Options } from './types' 6 | 7 | const WSS_LOCALHOST = 'ws://127.0.0.1:9944/' 8 | 9 | const ENDPOINTS: Options = [ 10 | { text: 'Local Node (127.0.0.1:9944)', value: WSS_LOCALHOST } 11 | ] 12 | 13 | const LANGUAGE_DEFAULT = 'default' 14 | 15 | const CRYPTOS: Options = [ 16 | { text: 'Edwards (ed25519)', value: 'ed25519' }, 17 | { text: 'Schnorrkel (sr25519)', value: 'sr25519' } 18 | ] 19 | 20 | const LANGUAGES: Options = [ 21 | { value: LANGUAGE_DEFAULT, text: 'Default browser language (auto-detect)' } 22 | ] 23 | 24 | const UIMODES: Options = [ 25 | { value: 'full', text: 'Fully featured' }, 26 | { value: 'light', text: 'Basic features only' } 27 | ] 28 | 29 | const UITHEMES: Options = [ 30 | { value: 'substrate', text: 'Substrate' } 31 | ] 32 | 33 | const ENDPOINT_DEFAULT = WSS_LOCALHOST 34 | 35 | const UITHEME_DEFAULT = 'substrate' 36 | 37 | // tslint:disable-next-line 38 | const UIMODE_DEFAULT = typeof window !== 'undefined' 39 | ? 'light' 40 | : 'full' 41 | 42 | export { 43 | CRYPTOS, 44 | ENDPOINT_DEFAULT, 45 | ENDPOINTS, 46 | LANGUAGE_DEFAULT, 47 | LANGUAGES, 48 | UIMODE_DEFAULT, 49 | UIMODES, 50 | UITHEME_DEFAULT, 51 | UITHEMES 52 | } 53 | -------------------------------------------------------------------------------- /src/components/settings/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import settings, { Settings } from './Settings' 6 | 7 | export default settings 8 | 9 | export { 10 | Settings 11 | } 12 | -------------------------------------------------------------------------------- /src/components/settings/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export type Options = { 6 | disabled?: boolean, 7 | text: string, 8 | value: string 9 | }[]; 10 | 11 | export interface SettingsStruct { 12 | apiUrl: string; 13 | i18nLang: string; 14 | uiMode: string; 15 | uiTheme: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/spaces/AboutSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, aboutSpaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: string 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const AboutSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return ( 22 | 23 | {title} 24 | 25 | ) 26 | } 27 | 28 | export default AboutSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/index.module.scss.keep: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss'; 2 | 3 | .atm_switch_wrapper { 4 | display: flex; 5 | margin: $space_huge 0 $space_normal; 6 | } 7 | 8 | .atm_switch_label { 9 | margin-left: $space_normal; 10 | } 11 | 12 | .atm_submit_button span { 13 | color: #fff; 14 | } 15 | 16 | .atm_dates_wrapper { 17 | display: flex; 18 | } 19 | 20 | .atm_dates_wrapper .field { 21 | margin-right: $space_huge !important; 22 | } 23 | 24 | .atm_dates_wrapper input { 25 | padding: 1.286rem !important; 26 | } 27 | 28 | .atm_company_wrapper { 29 | position: relative; 30 | } 31 | 32 | .atm_prefix { 33 | left: $space_tiny; 34 | display: flex; 35 | position: absolute; 36 | top: $space_tiny; 37 | } 38 | 39 | .atm_prefix img { 40 | height: $space_huge; 41 | } 42 | 43 | .atm_company_autocomplete { 44 | background-color: #fff; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | 49 | .atm_company_autocomplete_item { 50 | cursor: pointer; 51 | } 52 | 53 | .atm_company_autocomplete_item img { 54 | height: $space_huge; 55 | } 56 | 57 | .atm_company_wrapper.with_prefix input { 58 | padding-left: 2.5rem !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/validation.ts.keep: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import moment from 'moment-timezone'; 3 | import { minLenError, maxLenError } from '../../utils/forms/validation'; 4 | 5 | const TITLE_MIN_LEN = 2; 6 | const TITLE_MAX_LEN = 50; 7 | 8 | const COMPANY_MIN_LEN = 2; 9 | const COMPANY_MAX_LEN = 50; 10 | 11 | const LOCATION_MIN_LEN = 2; 12 | const LOCATION_MAX_LEN = 100; 13 | 14 | const DESCRIPTION_MAX_LEN = 2000; 15 | 16 | export const buildValidationSchema = () => Yup.object().shape({ 17 | title: Yup.string() 18 | .required('Job title is required') 19 | .min(TITLE_MIN_LEN, minLenError('Job title', TITLE_MIN_LEN)) 20 | .max(TITLE_MAX_LEN, maxLenError('Job title', TITLE_MAX_LEN)), 21 | 22 | company: Yup.string() 23 | .required('Company name is required') 24 | .min(COMPANY_MIN_LEN, minLenError('Company name', COMPANY_MIN_LEN)) 25 | .max(COMPANY_MAX_LEN, maxLenError('Company name', COMPANY_MAX_LEN)), 26 | 27 | location: Yup.string() 28 | .min(LOCATION_MIN_LEN, minLenError('Location name', LOCATION_MIN_LEN)) 29 | .max(LOCATION_MAX_LEN, maxLenError('Location name', LOCATION_MAX_LEN)), 30 | 31 | startDate: Yup.object().test( 32 | 'startDate', 33 | 'Start date should not be in future', 34 | value => moment().diff(value, 'days') >= 0 35 | ), 36 | 37 | endDate: Yup.object().test( 38 | 'endDate', 39 | 'End date should not be in future', 40 | value => value ? moment().diff(value, 'days') >= 0 : true 41 | ), 42 | 43 | description: Yup.string() 44 | .max(DESCRIPTION_MAX_LEN, maxLenError('Description', DESCRIPTION_MAX_LEN)) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/spaces/HiddenSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space } from '@subsocial/types/substrate/interfaces' 3 | import HiddenButton from '../utils/HiddenButton' 4 | import { SpaceUpdate, OptionOptionText, OptionIpfsContent, OptionBool } from '@subsocial/types/substrate/classes' 5 | 6 | type HiddenSpaceButtonProps = { 7 | space: Space, 8 | asLink?: boolean 9 | }; 10 | 11 | export function HiddenSpaceButton (props: HiddenSpaceButtonProps) { 12 | const { space } = props 13 | const hidden = space.hidden.valueOf() 14 | 15 | const update = new SpaceUpdate({ 16 | handle: new OptionOptionText(), 17 | content: new OptionIpfsContent(), 18 | hidden: new OptionBool(!hidden) // TODO has no implementation on UI 19 | }) 20 | 21 | const newTxParams = () => { 22 | return [ space.id, update ] 23 | } 24 | 25 | return 26 | } 27 | 28 | export default HiddenSpaceButton 29 | -------------------------------------------------------------------------------- /src/components/spaces/NavValidation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import { minLenError, maxLenError } from '../utils/forms/validation' 3 | 4 | const TITLE_MIN_LEN = 2 5 | const TITLE_MAX_LEN = 50 6 | 7 | export const validationSchema = Yup.object().shape({ 8 | navTabs: Yup.array().of( 9 | Yup.object().shape({ 10 | title: Yup.string() 11 | .min(TITLE_MIN_LEN, minLenError('Tab title', TITLE_MIN_LEN)) 12 | .max(TITLE_MAX_LEN, maxLenError('Tab title', TITLE_MAX_LEN)) 13 | .required('Tab title is a required field') 14 | }) 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/spaces/SocialLinks/ViewSocialLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NamedLink } from '@subsocial/types' 3 | import { getLinkBrand, getLinkIcon } from './utils' 4 | import { MailOutlined } from '@ant-design/icons' 5 | import { isEmptyStr } from '@subsocial/utils' 6 | import { BareProps } from 'src/components/utils/types' 7 | 8 | type SocialLinkProps = BareProps & { 9 | link: string, 10 | label?: string 11 | } 12 | 13 | export const SocialLink = ({ link, label, className }: SocialLinkProps) => { 14 | if (isEmptyStr(link)) return null 15 | 16 | const brand = getLinkBrand(link) 17 | return 18 | {getLinkIcon(brand)} 19 | {label && <> 20 | {`${label} ${brand}`} 21 | >} 22 | 23 | } 24 | 25 | type SocialLinksProps = { 26 | links: string[] | NamedLink[] 27 | } 28 | 29 | export const ViewSocialLinks = ({ links }: SocialLinksProps) => { 30 | return <>{(links as string[]).map((link, i) => 31 | 32 | )}> 33 | } 34 | 35 | type ContactInfoProps = SocialLinksProps & { 36 | email: string 37 | } 38 | 39 | export const EmailLink = ({ link, label, className }: SocialLinkProps) => 40 | 41 | 42 | {label && {`${label} email`}} 43 | 44 | 45 | export const ContactInfo = ({ links, email }: ContactInfoProps) => { 46 | if (!links && !email) return null 47 | 48 | return 49 | {links && } 50 | {email && } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/components/spaces/SpacedSectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { useStorybookContext } from '../utils/StorybookContext' 4 | import { SpaceData } from '@subsocial/types' 5 | import { spaceUrl } from '../urls' 6 | 7 | type Props = { 8 | space?: SpaceData 9 | subtitle: React.ReactNode 10 | } 11 | 12 | export const SpacegedSectionTitle = ({ 13 | space, 14 | subtitle 15 | }: Props) => { 16 | const { isStorybook } = useStorybookContext() 17 | const name = space?.content?.name 18 | 19 | return <> 20 | {!isStorybook && space && name && <> 21 | 22 | {name} 23 | 24 | / 25 | >} 26 | {subtitle} 27 | > 28 | } 29 | 30 | export default SpacegedSectionTitle 31 | -------------------------------------------------------------------------------- /src/components/spaces/TransferSpaceOwnership.module.sass: -------------------------------------------------------------------------------- 1 | .TransferOwnershipForm 2 | margin: 0 3 | 4 | \:global .ant-form-item 5 | margin-bottom: 0 6 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceById.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ViewSpace } from './ViewSpace' 4 | import { useRouter } from 'next/router' 5 | import BN from 'bn.js' 6 | 7 | const Component = () => { 8 | const router = useRouter() 9 | const { spaceId } = router.query 10 | return spaceId 11 | ? 12 | : null 13 | } 14 | 15 | export default Component 16 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, spaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: React.ReactNode 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const ViewSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return 22 | 23 | {title} 24 | 25 | 26 | } 27 | 28 | export default ViewSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceProps.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { GenericAccountId as AccountId } from '@polkadot/types' 3 | import { SpaceData, PostWithSomeDetails, ProfileData } from '@subsocial/types/dto' 4 | import { PostId } from '@subsocial/types/substrate/interfaces' 5 | 6 | export type ViewSpaceProps = { 7 | nameOnly?: boolean 8 | miniPreview?: boolean 9 | preview?: boolean 10 | dropdownPreview?: boolean 11 | withLink?: boolean 12 | withFollowButton?: boolean 13 | withTags?: boolean 14 | withStats?: boolean 15 | id?: BN 16 | spaceData?: SpaceData 17 | owner?: ProfileData, 18 | postIds?: PostId[], 19 | posts?: PostWithSomeDetails[] 20 | followers?: AccountId[] 21 | imageSize?: number 22 | onClick?: () => void 23 | statusCode?: number 24 | } 25 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/AllSpacesLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | import { BareProps } from 'src/components/utils/types' 4 | 5 | type Props = BareProps & { 6 | title?: React.ReactNode 7 | } 8 | 9 | export const AllSpacesLink = ({ 10 | title = 'See all', 11 | ...otherProps 12 | }: Props) => 13 | 14 | {title} 19 | 20 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreatePostButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 5 | import ButtonLink from 'src/components/utils/ButtonLink' 6 | import { createNewPostLinkProps, isHiddenSpace, SpaceProps } from './common' 7 | 8 | type Props = SpaceProps & ButtonProps & { 9 | title?: React.ReactNode 10 | } 11 | 12 | export const CreatePostButton = (props: Props) => { 13 | const { space, title = 'Create post' } = props 14 | 15 | if (isHiddenSpace(space)) return null 16 | 17 | return isMyAddress(space.owner) 18 | ? } 22 | ghost 23 | {...createNewPostLinkProps(space)} 24 | > 25 | {' '}{title} 26 | 27 | : null 28 | } 29 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreateSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import ButtonLink from 'src/components/utils/ButtonLink' 5 | 6 | export const CreateSpaceButton = ({ 7 | children, 8 | type = 'primary', 9 | ghost = true, 10 | ...otherProps 11 | }: ButtonProps) => { 12 | const props = { type, ghost, ...otherProps } 13 | const newSpacePath = '/spaces/new' 14 | 15 | return 16 | {children || Create space} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from '@ant-design/icons' 2 | import { SpaceData } from '@subsocial/types/dto' 3 | import { Dropdown, Menu } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { editSpaceUrl } from 'src/components/urls' 7 | import { BareProps } from 'src/components/utils/types' 8 | import HiddenSpaceButton from '../HiddenSpaceButton' 9 | import { TransferOwnershipLink } from '../TransferSpaceOwnership' 10 | import { isHiddenSpace, createNewPostLinkProps, isMySpace } from './common' 11 | 12 | type Props = BareProps & { 13 | spaceData: SpaceData 14 | vertical?: boolean 15 | } 16 | 17 | export const DropdownMenu = (props: Props) => { 18 | const { spaceData: { struct }, vertical, style, className } = props 19 | const { id } = struct 20 | const spaceKey = `space-${id.toString()}` 21 | 22 | const buildMenu = () => 23 | 24 | 25 | 26 | Edit space 27 | 28 | 29 | {/* 30 | 31 | */} 32 | {isHiddenSpace(struct) 33 | ? null 34 | : 35 | 36 | Write post 37 | 38 | 39 | } 40 | 41 | 42 | 43 | { 44 | 45 | } 46 | 47 | 48 | return isMySpace(struct) 49 | ? 50 | 51 | 52 | : null 53 | } 54 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/EditMenuLink.tsx: -------------------------------------------------------------------------------- 1 | import { BareProps } from 'src/components/utils/types' 2 | import { SpaceProps } from './common' 3 | 4 | type Props = BareProps & SpaceProps & { 5 | withIcon?: boolean 6 | } 7 | 8 | export const EditMenuLink = ({ space: { id, owner }, withIcon }: Props) => /* isMyAddress(owner) 9 | ? 10 | 14 | 15 | {withIcon && } 16 | Edit menu 17 | 18 | 19 | 20 | : */ null 21 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/SpaceAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HasSpaceIdOrHandle } from 'src/components/urls' 3 | import BaseAvatar, { BaseAvatarProps } from 'src/components/utils/DfAvatar' 4 | import ViewSpaceLink from '../ViewSpaceLink' 5 | 6 | type Props = BaseAvatarProps & { 7 | space: HasSpaceIdOrHandle 8 | asLink?: boolean 9 | } 10 | 11 | export const SpaceAvatar = ({ asLink = true, ...props }: Props) => asLink 12 | ? } /> 13 | : 14 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/common.tsx: -------------------------------------------------------------------------------- 1 | import { isHidden } from '@subsocial/api/utils/visibility-filter' 2 | import { SpaceData } from '@subsocial/types' 3 | import { Space } from '@subsocial/types/substrate/interfaces' 4 | import { isDef } from '@subsocial/utils' 5 | import React from 'react' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | import { HasSpaceIdOrHandle, newPostUrl } from 'src/components/urls' 8 | import NoData from 'src/components/utils/EmptyList' 9 | import { EntityStatusProps, HiddenEntityPanel } from 'src/components/utils/EntityStatusPanels' 10 | import { isHidden as isMyAndHidden } from '../../utils' 11 | export type SpaceProps = { 12 | space: Space 13 | } 14 | 15 | export const isHiddenSpace = (space: Space) => 16 | isHidden(space) 17 | 18 | export const isUnlistedSpace = (spaceData?: SpaceData): spaceData is undefined => 19 | !spaceData || !spaceData?.struct || isMyAndHidden({ struct: spaceData.struct }) 20 | 21 | export const isMySpace = (space?: Space) => 22 | isDef(space) && isMyAddress(space.owner) 23 | 24 | export const createNewPostLinkProps = (space: HasSpaceIdOrHandle) => ({ 25 | href: '/[spaceId]/posts/new', 26 | as: newPostUrl(space) 27 | }) 28 | 29 | type StatusProps = EntityStatusProps & { 30 | space: Space 31 | } 32 | 33 | export const HiddenSpaceAlert = (props: StatusProps) => 34 | 35 | 36 | export const SpaceNotFound = () => 37 | 38 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './PostPreviewsOnSpace' 2 | export * from './AllSpacesLink' 3 | export * from './common' 4 | export * from './CreateSpaceButton' 5 | export * from './DropdownMenu' 6 | export * from './EditMenuLink' 7 | export * from './SpaceAvatar' 8 | export * from './useLoadUnlistedSpace' 9 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedPostsByOwner.ts: -------------------------------------------------------------------------------- 1 | import { PostWithSomeDetails } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { PostId } from '@subsocial/types/substrate/interfaces' 4 | import { useState } from 'react' 5 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | 8 | type Props = { 9 | owner: AnyAccountId 10 | postIds: PostId[] 11 | } 12 | 13 | export const useLoadUnlistedPostsByOwner = ({ owner, postIds }: Props) => { 14 | const isMySpaces = isMyAddress(owner) 15 | const [ myHiddenPosts, setMyHiddenPosts ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpaces) return setMyHiddenPosts([]) 19 | 20 | subsocial.findUnlistedPostsWithAllDetails(postIds) 21 | .then(setMyHiddenPosts) 22 | 23 | }, [ postIds.length, isMySpaces ]) 24 | 25 | return { 26 | isLoading: !myHiddenPosts, 27 | myHiddenPosts: myHiddenPosts || [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedSpace.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceData } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { isEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { useState } from 'react' 6 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 7 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 8 | import { getSpaceId } from 'src/components/substrate' 9 | 10 | export const useLoadUnlistedSpace = (address: AnyAccountId) => { 11 | const isMySpace = isMyAddress(address) 12 | const { query: { spaceId } } = useRouter() 13 | const idOrHandle = spaceId as string 14 | 15 | const [ myHiddenSpace, setMyHiddenSpace ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpace || isEmptyStr(idOrHandle)) return 19 | 20 | let isSubscribe = true 21 | 22 | const loadSpaceFromId = async () => { 23 | const id = await getSpaceId(idOrHandle, subsocial) 24 | const spaceData = id && await subsocial.findSpace({ id }) 25 | isSubscribe && spaceData && setMyHiddenSpace(spaceData) 26 | } 27 | 28 | loadSpaceFromId() 29 | 30 | return () => { isSubscribe = false } 31 | }, [ isMySpace ]) 32 | 33 | return { 34 | isLoading: !myHiddenSpace, 35 | myHiddenSpace 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceDataById.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import useSubsocialEffect from '../api/useSubsocialEffect' 3 | import { Loading } from '../utils' 4 | import NoData from '../utils/EmptyList' 5 | import { SpaceData, ProfileData } from '@subsocial/types/dto' 6 | import { ViewSpaceProps } from './ViewSpaceProps' 7 | 8 | type Props = ViewSpaceProps 9 | 10 | // TODO Copypasta. See withLoadSpaceFromUrl 11 | export const withLoadSpaceDataById = (Component: React.ComponentType) => { 12 | return (props: Props) => { 13 | const { id } = props 14 | 15 | if (!id) return Space id is undefined} /> 16 | 17 | const [ spaceData, setSpaceData ] = useState() 18 | const [ owner, setOwner ] = useState() 19 | 20 | useSubsocialEffect(({ subsocial }) => { 21 | const loadData = async () => { 22 | const spaceData = await subsocial.findSpace({ id }) 23 | if (spaceData) { 24 | setSpaceData(spaceData) 25 | const ownerId = spaceData.struct.owner 26 | const owner = await subsocial.findProfile(ownerId) 27 | setOwner(owner) 28 | } 29 | } 30 | loadData() 31 | }, [ false ]) 32 | 33 | return spaceData?.content 34 | ? 35 | : 36 | } 37 | } 38 | 39 | export default withLoadSpaceDataById 40 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { getSpaceId } from '../substrate' 4 | import { SpaceData } from '@subsocial/types' 5 | import useSubsocialEffect from '../api/useSubsocialEffect' 6 | import { Loading } from '../utils' 7 | import NoData from '../utils/EmptyList' 8 | import { isFunction } from '@polkadot/util' 9 | 10 | type CheckPermissionResult = { 11 | ok: boolean 12 | error: (space: SpaceData) => JSX.Element 13 | } 14 | 15 | export type CheckSpacePermissionFn = (space: SpaceData) => CheckPermissionResult 16 | 17 | type CheckSpacePermissionProps = { 18 | checkSpacePermission?: CheckSpacePermissionFn 19 | } 20 | 21 | export type CanHaveSpaceProps = { 22 | space?: SpaceData 23 | } 24 | 25 | export function withLoadSpaceFromUrl ( 26 | Component: React.ComponentType 27 | ) { 28 | return function (props: Props & CheckSpacePermissionProps): React.ReactElement { 29 | 30 | const { checkSpacePermission } = props 31 | const idOrHandle = useRouter().query.spaceId as string 32 | const [ isLoaded, setIsLoaded ] = useState(false) 33 | const [ loadedData, setLoadedData ] = useState({}) 34 | 35 | useSubsocialEffect(({ subsocial }) => { 36 | const load = async () => { 37 | const id = await getSpaceId(idOrHandle, subsocial) 38 | if (!id) return 39 | 40 | setIsLoaded(false) 41 | const space = await subsocial.findSpace({ id }) 42 | setLoadedData({ space }) 43 | setIsLoaded(true) 44 | } 45 | load() 46 | }, [ idOrHandle ]) 47 | 48 | if (!isLoaded) return 49 | 50 | const { space } = loadedData 51 | if (!space) return 52 | 53 | if (isFunction(checkSpacePermission)) { 54 | const { ok, error } = checkSpacePermission(space) 55 | if (!ok) return error(space) 56 | } 57 | 58 | return 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/spaces/withSpaceIdFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { SpaceId } from '@subsocial/types/substrate/interfaces' 4 | import BN from 'bn.js' 5 | import { getSpaceId } from '../substrate' 6 | import NoData from '../utils/EmptyList' 7 | 8 | export function withSpaceIdFromUrl 9 | (Component: React.ComponentType) { 10 | 11 | return function (props: Props) { 12 | const router = useRouter() 13 | const { spaceId } = router.query 14 | const idOrHandle = spaceId as string 15 | try { 16 | const [ id, setId ] = useState() 17 | 18 | useEffect(() => { 19 | const getId = async () => { 20 | const id = await getSpaceId(idOrHandle) 21 | id && setId(id) 22 | } 23 | getId().catch(err => console.error(err)) 24 | }, [ false ]) 25 | 26 | return !id ? null : 27 | } catch (err) { 28 | return 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/substrate/KusamaContext/index.module.scss: -------------------------------------------------------------------------------- 1 | .KusamaIdentitySection { 2 | margin-bottom: 1rem; 3 | 4 | .DfSectionOuter { 5 | margin-left: 0; 6 | max-width: initial; 7 | min-width: initial; 8 | width: 100%; 9 | .DfSection { 10 | margin-left: 0; 11 | width: 100%; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/substrate/SubstrateWebConsole.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import { useSubstrate } from './useSubstrate' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { Keyring } from '@polkadot/ui-keyring' 6 | 7 | const log = newLogger(SubstrateWebConsole.name) 8 | 9 | type WindowSubstrate = { 10 | api?: ApiPromise 11 | keyring?: Keyring 12 | util?: any 13 | crypto?: any 14 | } 15 | 16 | const getWindowSubstrate = (): WindowSubstrate => { 17 | let substrate = (window as any)?.substrate 18 | if (!substrate) { 19 | substrate = {} as WindowSubstrate 20 | (window as any).substrate = substrate 21 | } 22 | return substrate 23 | } 24 | 25 | /** This component will simply add Substrate utility functions to your web developer console. */ 26 | export function SubstrateWebConsole () { 27 | const { endpoint, api, apiState, keyring, keyringState } = useSubstrate() 28 | 29 | const addApi = () => { 30 | if (window && apiState === 'READY') { 31 | getWindowSubstrate().api = api 32 | log.info('Exported window.substrate.api') 33 | } 34 | } 35 | 36 | const addKeyring = () => { 37 | if (window && keyringState === 'READY') { 38 | getWindowSubstrate().keyring = keyring 39 | log.info('Exported window.substrate.keyring') 40 | } 41 | } 42 | 43 | const addUtilAndCrypto = () => { 44 | if (window) { 45 | const substrate = getWindowSubstrate() 46 | 47 | substrate.util = require('@polkadot/util') 48 | log.info('Exported window.substrate.util') 49 | 50 | substrate.crypto = require('@polkadot/util-crypto') 51 | log.info('Exported window.substrate.crypto') 52 | } 53 | } 54 | 55 | useEffect(() => { 56 | addApi() 57 | }, [ endpoint?.toString(), apiState ]) 58 | 59 | useEffect(() => { 60 | addKeyring() 61 | }, [ keyringState ]) 62 | 63 | useEffect(() => { 64 | addUtilAndCrypto() 65 | }, [ true ]) 66 | 67 | return null 68 | } 69 | 70 | export default SubstrateWebConsole 71 | -------------------------------------------------------------------------------- /src/components/substrate/TxDiv.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TxButtonProps } from './SubstrateTxButton' 3 | import TxButton from 'src/components/utils/TxButton' 4 | 5 | const Div: React.FunctionComponent = (props) => {props.children} 6 | 7 | export const TxDiv = ({ loading, withSpinner, ghost, ...divProps }: TxButtonProps) => 8 | 9 | export default React.memo(TxDiv) 10 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/api.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React, { useRef } from 'react' 6 | import { DefaultProps, ApiProps } from './types' 7 | import useSubstrate from '../useSubstrate' 8 | 9 | export default function withApi ( 10 | Inner: React.ComponentType, 11 | defaultProps: DefaultProps = {} 12 | ): React.ComponentType { 13 | return (props: any) => { 14 | const component = useRef() 15 | const { api } = useSubstrate() 16 | 17 | return !api ? null : 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { ApiProps, SubtractProps, Options } from './types' 6 | import React from 'react' 7 | import withCall from './call' 8 | 9 | type Call = string | [string, Options]; 10 | 11 | export default function withCalls (...calls: Call[]): (Component: React.ComponentType) => React.ComponentType> { 12 | return (Component: React.ComponentType): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
(Component: React.ComponentType) { 18 | return function (props: P) { 19 | const { postIds } = props 20 | const [ posts, setPosts ] = useState() 21 | const [ loaded, setLoaded ] = useState(false) 22 | 23 | useSubsocialEffect(({ subsocial }) => { 24 | setLoaded(false) 25 | 26 | const loadData = async () => { 27 | const extPostData = await subsocial.findPublicPostsWithAllDetails(postIds) 28 | extPostData && setPosts(extPostData) 29 | setLoaded(true) 30 | } 31 | 32 | loadData().catch(console.warn) 33 | }, [ false ]) 34 | 35 | return loaded && posts 36 | ? 37 | : 38 | } 39 | } 40 | 41 | const InnerPostPreviewList: React.FunctionComponent = ({ posts }) => 42 | } /> 43 | 44 | export const PostPreviewList = withLoadPostsWithSpaces(InnerPostPreviewList) 45 | -------------------------------------------------------------------------------- /src/components/posts/view-post/ViewRegularPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { SpaceData } from '@subsocial/types/dto' 3 | import { CommentSection } from '../../comments/CommentsSection' 4 | import { InfoPostPreview, PostActionsPanel, PostNotFound } from './helpers' 5 | import { PreviewProps } from './PostPreview' 6 | import { isVisible } from 'src/components/utils' 7 | 8 | export type InnerPreviewProps = PreviewProps & { 9 | space: SpaceData 10 | } 11 | 12 | type ComponentType = React.FunctionComponent 13 | 14 | export const RegularPreview: ComponentType = (props) => { 15 | const { postDetails, space, replies, withTags, withActions } = props 16 | const extStruct = postDetails.ext?.post.struct 17 | const [ commentsSection, setCommentsSection ] = useState(false) 18 | 19 | return !extStruct || isVisible({ struct: extStruct, address: extStruct.owner }) 20 | ? <> 21 | 22 | {withActions && setCommentsSection(!commentsSection) } preview withBorder />} 23 | {commentsSection && } 24 | > 25 | : 26 | } 27 | -------------------------------------------------------------------------------- /src/components/posts/view-post/ViewSharedPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CommentSection } from '../../comments/CommentsSection' 3 | import { PostCreator, PostDropDownMenu, PostActionsPanel, SharePostContent } from './helpers' 4 | import { InnerPreviewProps } from '.' 5 | 6 | type ComponentType = React.FunctionComponent 7 | 8 | export const SharedPreview: ComponentType = (props) => { 9 | const { postDetails, space, withActions, replies } = props 10 | const [ commentsSection, setCommentsSection ] = useState(false) 11 | 12 | return <> 13 | 14 | 15 | 16 | 17 | 18 | {withActions && setCommentsSection(!commentsSection)} preview />} 19 | {commentsSection && } 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/posts/view-post/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './helpers' 2 | export * from './PostPage' 3 | export * from './ViewRegularPreview' 4 | export * from './ViewSharedPreview' 5 | -------------------------------------------------------------------------------- /src/components/profile-selector/AccountSelector.module.sass: -------------------------------------------------------------------------------- 1 | .DfAccountSelector 2 | overflow-y: auto 3 | 4 | .DfAccountPopup 5 | max-width: 368px 6 | position: fixed -------------------------------------------------------------------------------- /src/components/profile-selector/MyAccountSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SignOutButton } from 'src/components/auth/AuthButtons' 3 | import { AccountSelector } from './AccountSelector' 4 | import PrivacyPolicyLinks from '../utils/PrivacyPolicyLinks' 5 | import { Divider } from 'antd' 6 | import { ActionMenu } from './ActionMenu' 7 | 8 | export const MyAccountSection = () => { 9 | return 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | } 19 | 20 | export default MyAccountSection 21 | -------------------------------------------------------------------------------- /src/components/profiles/AccountsListModal.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .AccountsListModal 4 | margin-top: 3rem 5 | 6 | \:global 7 | 8 | .DfDataList 9 | margin-top: 0 10 | 11 | .header 12 | font-size: $space_large 13 | 14 | .ant-modal-body 15 | padding: 0 16 | 17 | .ant-list-lg .ant-list-item 18 | padding: $space_small $space_normal 19 | -------------------------------------------------------------------------------- /src/components/profiles/AccountsListModal.tsx: -------------------------------------------------------------------------------- 1 | import styles from './AccountsListModal.module.sass' 2 | 3 | import React from 'react' 4 | import { withCalls, withMulti, spaceFollowsQueryToProp, profileFollowsQueryToProp } from '../substrate' 5 | import { GenericAccountId as AccountId } from '@polkadot/types' 6 | import { Modal, Button } from 'antd' 7 | import { ProfilePreviewWithOwner } from './address-views' 8 | import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' 9 | import DataList from '../lists/DataList' 10 | 11 | type Props = { 12 | accounts?: AccountId[], 13 | accountsCount: number, 14 | title: string, 15 | open: boolean, 16 | close: () => void 17 | }; 18 | 19 | const InnerAccountsListModal = (props: Props) => { 20 | const { accounts, open, close, title } = props 21 | 22 | if (!accounts) return null 23 | 24 | return ( 25 | Close} 31 | > 32 | 35 | } 36 | noDataDesc='Nothing yet' 37 | /> 38 | 39 | ) 40 | } 41 | 42 | export const SpaceFollowersModal = withMulti( 43 | InnerAccountsListModal, 44 | withCalls( 45 | spaceFollowsQueryToProp('spaceFollowers', { paramName: 'id', propName: 'accounts' }) 46 | ) 47 | ) 48 | 49 | export const AccountFollowersModal = withMulti( 50 | InnerAccountsListModal, 51 | withCalls( 52 | profileFollowsQueryToProp('accountFollowers', { paramName: 'id', propName: 'accounts' }) 53 | ) 54 | ) 55 | 56 | export const AccountFollowingModal = withMulti( 57 | InnerAccountsListModal, 58 | withCalls( 59 | profileFollowsQueryToProp('accountsFollowedByAccount', { paramName: 'id', propName: 'accounts' }) 60 | ) 61 | ) 62 | -------------------------------------------------------------------------------- /src/components/profiles/FollowingModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { withCalls, withMulti, profileFollowsQueryToProp } from '../substrate' 4 | import { GenericAccountId as AccountId } from '@polkadot/types' 5 | import { Modal, Button } from 'antd' 6 | import { ProfilePreviewWithOwner } from './address-views' 7 | import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' 8 | 9 | type Props = { 10 | following?: AccountId[], 11 | followingCount: number 12 | }; 13 | 14 | const InnerFollowingModal = (props: Props) => { 15 | const { following, followingCount } = props 16 | const [ open, setOpen ] = useState(false) 17 | 18 | const renderFollowing = () => { 19 | return following && following.map((account) => 20 | 21 | 26 | 27 | ) 28 | } 29 | 30 | return ( 31 | <> 32 | setOpen(true)}>Following ({followingCount}) 33 | setOpen(false)}>Close} 39 | > 40 | {renderFollowing()} 41 | 42 | > 43 | ) 44 | } 45 | 46 | export const AccountFollowingModal = withMulti( 47 | InnerFollowingModal, 48 | withCalls( 49 | profileFollowsQueryToProp('accountsFollowedByAccount', { paramName: 'id', propName: 'following' }) 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /src/components/profiles/ViewProfileLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasAddressOrHandle, accountUrl } from '../urls' 4 | 5 | type Props = { 6 | account: HasAddressOrHandle 7 | title?: React.ReactNode 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const ViewProfileLink = ({ 13 | account, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | const { address } = account 19 | 20 | if (!address) return null 21 | 22 | return ( 23 | 24 | {title || address.toString()} 25 | 26 | ) 27 | } 28 | 29 | export default ViewProfileLink 30 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BaseAvatar, { BaseAvatarProps } from 'src/components/utils/DfAvatar' 3 | import { CopyAddress } from './utils' 4 | 5 | export const Avatar: React.FunctionComponent = (props) => { 6 | return 7 | } 8 | 9 | export default Avatar 10 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/InfoSection/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfInfoSection 4 | font-size: $font_small 5 | \:global .ant-descriptions-item 6 | padding: $space_mini 7 | \:global .ant-descriptions-item-label 8 | color: $color_muted 9 | \:global .descriptions-row > td 10 | padding-bottom: 0 11 | 12 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/InfoSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Descriptions as AntdDesc } from 'antd' 3 | import { useResponsiveSize } from 'src/components/responsive' 4 | import { BareProps } from 'src/components/utils/types' 5 | import Section from 'src/components/utils/Section' 6 | import styles from './index.module.sass' 7 | 8 | export type DescItem = { 9 | label?: React.ReactNode, 10 | value: React.ReactNode 11 | } 12 | 13 | type InfoPanelProps = BareProps & { 14 | title?: React.ReactNode 15 | items?: DescItem[], 16 | size?: 'middle' | 'small' | 'default', 17 | column?: number, 18 | layout?: 'vertical' | 'horizontal' 19 | } 20 | 21 | type DescriptionsProps = InfoPanelProps & { 22 | title: React.ReactNode 23 | level?: number, 24 | } 25 | 26 | export const InfoPanel = ({ title, size = 'small', layout, column = 2, items, ...bareProps }: InfoPanelProps) => { 27 | const { isMobile } = useResponsiveSize() 28 | 29 | return 36 | {items?.map(({ label, value }, key) => 37 | 41 | {value} 42 | )} 43 | 44 | } 45 | 46 | export const InfoSection = ({ title, level, className, style, ...props }: DescriptionsProps) => 52 | 53 | 54 | 55 | export default InfoSection 56 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/Name.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { toShortAddress } from 'src/components/utils' 3 | import { AddressProps } from './utils/types' 4 | import { ProfileData } from '@subsocial/types' 5 | import { withLoadedOwner } from './utils/withLoadedOwner' 6 | import ViewProfileLink from '../ViewProfileLink' 7 | import { useExtensionName } from './utils' 8 | import { MutedSpan } from 'src/components/utils/MutedText' 9 | 10 | type Props = AddressProps & { 11 | isShort?: boolean, 12 | asLink?: boolean, 13 | withShortAddress?: boolean, 14 | className?: string 15 | }; 16 | 17 | export const Name = ({ 18 | address, 19 | owner = {} as ProfileData, 20 | isShort = true, 21 | asLink = true, 22 | withShortAddress, 23 | className 24 | }: Props) => { 25 | 26 | const { content } = owner 27 | 28 | // TODO extract a function? (find similar copypasta in other files): 29 | const shortAddress = toShortAddress(address) 30 | const addressString = isShort ? shortAddress : address.toString() 31 | const name = content?.name || useExtensionName(address) 32 | const title = name 33 | ? 34 | {name} 35 | {withShortAddress && {shortAddress}} 36 | 37 | : addressString 38 | const nameClass = `ui--AddressComponents-address ${className}` 39 | 40 | return asLink 41 | ? 42 | : <>{title}> 43 | } 44 | 45 | export const NameWithOwner = withLoadedOwner(Name) 46 | 47 | export default Name 48 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../profile-selector/MyAccountMenu' 2 | export * from './AuthorPreview' 3 | export * from './ProfilePreview' 4 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { AnyAccountId } from '@subsocial/types' 3 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 4 | import { useSubstrateContext } from 'src/components/substrate' 5 | import { Copy } from 'src/components/urls/helpers' 6 | import Link from 'next/link' 7 | import { BareProps } from 'src/components/utils/types' 8 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 9 | import { accountUrl } from 'src/components/urls' 10 | 11 | export const useExtensionName = (address: AnyAccountId) => { 12 | const [ extensionName, setExtensionName ] = useState() 13 | const { keyring } = useSubstrateContext() 14 | 15 | useSubsocialEffect(() => { 16 | if (!keyring) return 17 | 18 | const name = keyring.getAccount(address)?.meta.name 19 | name && setExtensionName(name) 20 | }, [ keyring, address ]) 21 | 22 | return extensionName?.replace('(polkadot-js)', '').toUpperCase() 23 | } 24 | 25 | type ProfileLink = BareProps & { 26 | address: AnyAccountId, 27 | title?: string, 28 | onClick?: () => void 29 | } 30 | 31 | export const AccountSpacesLink = ({ address, title = 'Spaces', ...otherProps }: ProfileLink) => {title} 32 | 33 | export const EditProfileLink = ({ address, title = 'Edit my profile', onClick, ...props }: ProfileLink) => isMyAddress(address) 34 | ? 35 | {title} 36 | 37 | : null 38 | 39 | type CopyAddressProps = { 40 | address: AnyAccountId, 41 | message?: string, 42 | children?: React.ReactNode 43 | } 44 | 45 | export const CopyAddress = ({ address = '', message = 'Address copied', children = address }: CopyAddressProps) => 46 | {children} 47 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { ProfileData } from '@subsocial/types' 4 | 5 | export type AddressProps = { 6 | className?: string 7 | style?: React.CSSProperties 8 | address: AnyAccountId, 9 | owner?: ProfileData 10 | } 11 | 12 | export type ExtendedAddressProps = AddressProps & { 13 | children?: React.ReactNode, 14 | afterName?: JSX.Element 15 | details?: JSX.Element 16 | isPadded?: boolean, 17 | isShort?: boolean, 18 | size?: number, 19 | withFollowButton?: boolean, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/profiles/address-views/utils/withLoadedOwner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 4 | import { ProfileData } from '@subsocial/types' 5 | import { ExtendedAddressProps } from './types' 6 | import { Loading } from '../../../utils' 7 | import { useMyAccount } from 'src/components/auth/MyAccountContext' 8 | 9 | const log = newLogger(withLoadedOwner.name) 10 | 11 | type Props = ExtendedAddressProps & { 12 | size?: number 13 | avatar?: string 14 | mini?: boolean 15 | }; 16 | 17 | export function withLoadedOwner (Component: React.ComponentType) { 18 | return function (props: P) { 19 | const { owner: initialOwner, address } = props as Props 20 | 21 | if (initialOwner) return 22 | 23 | const [ owner, setOwner ] = useState() 24 | const [ loaded, setLoaded ] = useState(true) 25 | 26 | useSubsocialEffect(({ subsocial }) => { 27 | if (!address) return 28 | 29 | setLoaded(false) 30 | let isSubscribe = true 31 | 32 | const loadContent = async () => { 33 | const owner = await subsocial.findProfile(address) 34 | isSubscribe && setOwner(owner) 35 | setLoaded(true) 36 | } 37 | 38 | loadContent().catch(err => 39 | log.error(`Failed to load profile data. ${err}`)) 40 | 41 | return () => { isSubscribe = false } 42 | }, [ address?.toString() ]) 43 | 44 | return loaded 45 | ? 46 | : 47 | } 48 | } 49 | 50 | export function withMyProfile (Component: React.ComponentType) { 51 | return function (props: any) { 52 | const { state: { account, address } } = useMyAccount() 53 | return address ? : null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/responsive/ResponsiveContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | import { useMediaQuery } from 'react-responsive' 3 | import { isMobileDevice } from 'src/config/Size.config' 4 | 5 | export type ResponsiveSizeState = { 6 | isMobile: boolean, 7 | isTablet: boolean, 8 | isDesktop: boolean, 9 | isNotMobile: boolean, 10 | isNotDesktop: boolean 11 | } 12 | 13 | const contextStub: ResponsiveSizeState = { 14 | isDesktop: true, 15 | isMobile: false, 16 | isNotMobile: false, 17 | isTablet: false, 18 | isNotDesktop: false 19 | } 20 | 21 | export const ResponsiveSizeContext = createContext(contextStub) 22 | 23 | export function ResponsiveSizeProvider (props: React.PropsWithChildren) { 24 | const value = { 25 | isDesktop: useMediaQuery({ minWidth: 992 }), 26 | isTablet: useMediaQuery({ minWidth: 768, maxWidth: 991 }), 27 | isMobile: useMediaQuery({ maxWidth: 767 }), 28 | isNotMobile: useMediaQuery({ minWidth: 768 }), 29 | isNotDesktop: useMediaQuery({ maxWidth: 991 }) 30 | } 31 | 32 | return 33 | {props.children} 34 | 35 | } 36 | 37 | export function useResponsiveSize () { 38 | return useContext(ResponsiveSizeContext) 39 | } 40 | 41 | export function useIsMobileWidthOrDevice () { 42 | const { isMobile } = useResponsiveSize() 43 | return isMobileDevice || isMobile 44 | } -------------------------------------------------------------------------------- /src/components/responsive/index.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsiveSize } from './ResponsiveContext' 2 | 3 | export * from './ResponsiveContext' 4 | 5 | type Props = { 6 | children?: React.ReactNode | null | JSX.Element 7 | } 8 | 9 | export const Desktop = ({ children }: Props) => { 10 | const { isDesktop } = useResponsiveSize() 11 | return isDesktop ? children : null 12 | } 13 | 14 | export const Tablet = ({ children }: Props) => { 15 | const { isTablet } = useResponsiveSize() 16 | return isTablet ? children : null 17 | } 18 | 19 | export const Mobile = ({ children }: Props) => { 20 | const { isMobile } = useResponsiveSize() 21 | return isMobile ? children : null 22 | } 23 | 24 | export const Default = ({ children }: Props) => { 25 | const { isNotMobile } = useResponsiveSize() 26 | return isNotMobile ? children : null 27 | } 28 | -------------------------------------------------------------------------------- /src/components/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Input } from 'antd' 3 | import { nonEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { isMobileDevice } from 'src/config/Size.config' 6 | 7 | const { Search } = Input 8 | 9 | const SearchInput = () => { 10 | const router = useRouter() 11 | const [ searchValue, setSearchValue ] = useState(router.query.q as string) 12 | const isSearchPage = router.pathname.includes('search') 13 | 14 | useEffect(() => { 15 | if (isSearchPage) return 16 | 17 | setSearchValue(undefined) 18 | }, [ isSearchPage ]) 19 | 20 | const onSearch = (value: string) => { 21 | const queryPath = { 22 | pathname: '/search', 23 | query: { 24 | ...router.query, 25 | q: value 26 | } 27 | } 28 | return nonEmptyStr(value) && router.replace(queryPath, queryPath) 29 | } 30 | 31 | const onChange = (value: string) => setSearchValue(value) 32 | 33 | return ( 34 | 35 | onChange(e.currentTarget.value)} 41 | /> 42 | 43 | ) 44 | } 45 | 46 | export default SearchInput 47 | -------------------------------------------------------------------------------- /src/components/settings/defaults.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { Options } from './types' 6 | 7 | const WSS_LOCALHOST = 'ws://127.0.0.1:9944/' 8 | 9 | const ENDPOINTS: Options = [ 10 | { text: 'Local Node (127.0.0.1:9944)', value: WSS_LOCALHOST } 11 | ] 12 | 13 | const LANGUAGE_DEFAULT = 'default' 14 | 15 | const CRYPTOS: Options = [ 16 | { text: 'Edwards (ed25519)', value: 'ed25519' }, 17 | { text: 'Schnorrkel (sr25519)', value: 'sr25519' } 18 | ] 19 | 20 | const LANGUAGES: Options = [ 21 | { value: LANGUAGE_DEFAULT, text: 'Default browser language (auto-detect)' } 22 | ] 23 | 24 | const UIMODES: Options = [ 25 | { value: 'full', text: 'Fully featured' }, 26 | { value: 'light', text: 'Basic features only' } 27 | ] 28 | 29 | const UITHEMES: Options = [ 30 | { value: 'substrate', text: 'Substrate' } 31 | ] 32 | 33 | const ENDPOINT_DEFAULT = WSS_LOCALHOST 34 | 35 | const UITHEME_DEFAULT = 'substrate' 36 | 37 | // tslint:disable-next-line 38 | const UIMODE_DEFAULT = typeof window !== 'undefined' 39 | ? 'light' 40 | : 'full' 41 | 42 | export { 43 | CRYPTOS, 44 | ENDPOINT_DEFAULT, 45 | ENDPOINTS, 46 | LANGUAGE_DEFAULT, 47 | LANGUAGES, 48 | UIMODE_DEFAULT, 49 | UIMODES, 50 | UITHEME_DEFAULT, 51 | UITHEMES 52 | } 53 | -------------------------------------------------------------------------------- /src/components/settings/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import settings, { Settings } from './Settings' 6 | 7 | export default settings 8 | 9 | export { 10 | Settings 11 | } 12 | -------------------------------------------------------------------------------- /src/components/settings/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export type Options = { 6 | disabled?: boolean, 7 | text: string, 8 | value: string 9 | }[]; 10 | 11 | export interface SettingsStruct { 12 | apiUrl: string; 13 | i18nLang: string; 14 | uiMode: string; 15 | uiTheme: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/spaces/AboutSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, aboutSpaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: string 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const AboutSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return ( 22 | 23 | {title} 24 | 25 | ) 26 | } 27 | 28 | export default AboutSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/index.module.scss.keep: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss'; 2 | 3 | .atm_switch_wrapper { 4 | display: flex; 5 | margin: $space_huge 0 $space_normal; 6 | } 7 | 8 | .atm_switch_label { 9 | margin-left: $space_normal; 10 | } 11 | 12 | .atm_submit_button span { 13 | color: #fff; 14 | } 15 | 16 | .atm_dates_wrapper { 17 | display: flex; 18 | } 19 | 20 | .atm_dates_wrapper .field { 21 | margin-right: $space_huge !important; 22 | } 23 | 24 | .atm_dates_wrapper input { 25 | padding: 1.286rem !important; 26 | } 27 | 28 | .atm_company_wrapper { 29 | position: relative; 30 | } 31 | 32 | .atm_prefix { 33 | left: $space_tiny; 34 | display: flex; 35 | position: absolute; 36 | top: $space_tiny; 37 | } 38 | 39 | .atm_prefix img { 40 | height: $space_huge; 41 | } 42 | 43 | .atm_company_autocomplete { 44 | background-color: #fff; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | 49 | .atm_company_autocomplete_item { 50 | cursor: pointer; 51 | } 52 | 53 | .atm_company_autocomplete_item img { 54 | height: $space_huge; 55 | } 56 | 57 | .atm_company_wrapper.with_prefix input { 58 | padding-left: 2.5rem !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/validation.ts.keep: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import moment from 'moment-timezone'; 3 | import { minLenError, maxLenError } from '../../utils/forms/validation'; 4 | 5 | const TITLE_MIN_LEN = 2; 6 | const TITLE_MAX_LEN = 50; 7 | 8 | const COMPANY_MIN_LEN = 2; 9 | const COMPANY_MAX_LEN = 50; 10 | 11 | const LOCATION_MIN_LEN = 2; 12 | const LOCATION_MAX_LEN = 100; 13 | 14 | const DESCRIPTION_MAX_LEN = 2000; 15 | 16 | export const buildValidationSchema = () => Yup.object().shape({ 17 | title: Yup.string() 18 | .required('Job title is required') 19 | .min(TITLE_MIN_LEN, minLenError('Job title', TITLE_MIN_LEN)) 20 | .max(TITLE_MAX_LEN, maxLenError('Job title', TITLE_MAX_LEN)), 21 | 22 | company: Yup.string() 23 | .required('Company name is required') 24 | .min(COMPANY_MIN_LEN, minLenError('Company name', COMPANY_MIN_LEN)) 25 | .max(COMPANY_MAX_LEN, maxLenError('Company name', COMPANY_MAX_LEN)), 26 | 27 | location: Yup.string() 28 | .min(LOCATION_MIN_LEN, minLenError('Location name', LOCATION_MIN_LEN)) 29 | .max(LOCATION_MAX_LEN, maxLenError('Location name', LOCATION_MAX_LEN)), 30 | 31 | startDate: Yup.object().test( 32 | 'startDate', 33 | 'Start date should not be in future', 34 | value => moment().diff(value, 'days') >= 0 35 | ), 36 | 37 | endDate: Yup.object().test( 38 | 'endDate', 39 | 'End date should not be in future', 40 | value => value ? moment().diff(value, 'days') >= 0 : true 41 | ), 42 | 43 | description: Yup.string() 44 | .max(DESCRIPTION_MAX_LEN, maxLenError('Description', DESCRIPTION_MAX_LEN)) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/spaces/HiddenSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space } from '@subsocial/types/substrate/interfaces' 3 | import HiddenButton from '../utils/HiddenButton' 4 | import { SpaceUpdate, OptionOptionText, OptionIpfsContent, OptionBool } from '@subsocial/types/substrate/classes' 5 | 6 | type HiddenSpaceButtonProps = { 7 | space: Space, 8 | asLink?: boolean 9 | }; 10 | 11 | export function HiddenSpaceButton (props: HiddenSpaceButtonProps) { 12 | const { space } = props 13 | const hidden = space.hidden.valueOf() 14 | 15 | const update = new SpaceUpdate({ 16 | handle: new OptionOptionText(), 17 | content: new OptionIpfsContent(), 18 | hidden: new OptionBool(!hidden) // TODO has no implementation on UI 19 | }) 20 | 21 | const newTxParams = () => { 22 | return [ space.id, update ] 23 | } 24 | 25 | return 26 | } 27 | 28 | export default HiddenSpaceButton 29 | -------------------------------------------------------------------------------- /src/components/spaces/NavValidation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import { minLenError, maxLenError } from '../utils/forms/validation' 3 | 4 | const TITLE_MIN_LEN = 2 5 | const TITLE_MAX_LEN = 50 6 | 7 | export const validationSchema = Yup.object().shape({ 8 | navTabs: Yup.array().of( 9 | Yup.object().shape({ 10 | title: Yup.string() 11 | .min(TITLE_MIN_LEN, minLenError('Tab title', TITLE_MIN_LEN)) 12 | .max(TITLE_MAX_LEN, maxLenError('Tab title', TITLE_MAX_LEN)) 13 | .required('Tab title is a required field') 14 | }) 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/spaces/SocialLinks/ViewSocialLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NamedLink } from '@subsocial/types' 3 | import { getLinkBrand, getLinkIcon } from './utils' 4 | import { MailOutlined } from '@ant-design/icons' 5 | import { isEmptyStr } from '@subsocial/utils' 6 | import { BareProps } from 'src/components/utils/types' 7 | 8 | type SocialLinkProps = BareProps & { 9 | link: string, 10 | label?: string 11 | } 12 | 13 | export const SocialLink = ({ link, label, className }: SocialLinkProps) => { 14 | if (isEmptyStr(link)) return null 15 | 16 | const brand = getLinkBrand(link) 17 | return 18 | {getLinkIcon(brand)} 19 | {label && <> 20 | {`${label} ${brand}`} 21 | >} 22 | 23 | } 24 | 25 | type SocialLinksProps = { 26 | links: string[] | NamedLink[] 27 | } 28 | 29 | export const ViewSocialLinks = ({ links }: SocialLinksProps) => { 30 | return <>{(links as string[]).map((link, i) => 31 | 32 | )}> 33 | } 34 | 35 | type ContactInfoProps = SocialLinksProps & { 36 | email: string 37 | } 38 | 39 | export const EmailLink = ({ link, label, className }: SocialLinkProps) => 40 | 41 | 42 | {label && {`${label} email`}} 43 | 44 | 45 | export const ContactInfo = ({ links, email }: ContactInfoProps) => { 46 | if (!links && !email) return null 47 | 48 | return 49 | {links && } 50 | {email && } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/components/spaces/SpacedSectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { useStorybookContext } from '../utils/StorybookContext' 4 | import { SpaceData } from '@subsocial/types' 5 | import { spaceUrl } from '../urls' 6 | 7 | type Props = { 8 | space?: SpaceData 9 | subtitle: React.ReactNode 10 | } 11 | 12 | export const SpacegedSectionTitle = ({ 13 | space, 14 | subtitle 15 | }: Props) => { 16 | const { isStorybook } = useStorybookContext() 17 | const name = space?.content?.name 18 | 19 | return <> 20 | {!isStorybook && space && name && <> 21 | 22 | {name} 23 | 24 | / 25 | >} 26 | {subtitle} 27 | > 28 | } 29 | 30 | export default SpacegedSectionTitle 31 | -------------------------------------------------------------------------------- /src/components/spaces/TransferSpaceOwnership.module.sass: -------------------------------------------------------------------------------- 1 | .TransferOwnershipForm 2 | margin: 0 3 | 4 | \:global .ant-form-item 5 | margin-bottom: 0 6 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceById.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ViewSpace } from './ViewSpace' 4 | import { useRouter } from 'next/router' 5 | import BN from 'bn.js' 6 | 7 | const Component = () => { 8 | const router = useRouter() 9 | const { spaceId } = router.query 10 | return spaceId 11 | ? 12 | : null 13 | } 14 | 15 | export default Component 16 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, spaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: React.ReactNode 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const ViewSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return 22 | 23 | {title} 24 | 25 | 26 | } 27 | 28 | export default ViewSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceProps.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { GenericAccountId as AccountId } from '@polkadot/types' 3 | import { SpaceData, PostWithSomeDetails, ProfileData } from '@subsocial/types/dto' 4 | import { PostId } from '@subsocial/types/substrate/interfaces' 5 | 6 | export type ViewSpaceProps = { 7 | nameOnly?: boolean 8 | miniPreview?: boolean 9 | preview?: boolean 10 | dropdownPreview?: boolean 11 | withLink?: boolean 12 | withFollowButton?: boolean 13 | withTags?: boolean 14 | withStats?: boolean 15 | id?: BN 16 | spaceData?: SpaceData 17 | owner?: ProfileData, 18 | postIds?: PostId[], 19 | posts?: PostWithSomeDetails[] 20 | followers?: AccountId[] 21 | imageSize?: number 22 | onClick?: () => void 23 | statusCode?: number 24 | } 25 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/AllSpacesLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | import { BareProps } from 'src/components/utils/types' 4 | 5 | type Props = BareProps & { 6 | title?: React.ReactNode 7 | } 8 | 9 | export const AllSpacesLink = ({ 10 | title = 'See all', 11 | ...otherProps 12 | }: Props) => 13 | 14 | {title} 19 | 20 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreatePostButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 5 | import ButtonLink from 'src/components/utils/ButtonLink' 6 | import { createNewPostLinkProps, isHiddenSpace, SpaceProps } from './common' 7 | 8 | type Props = SpaceProps & ButtonProps & { 9 | title?: React.ReactNode 10 | } 11 | 12 | export const CreatePostButton = (props: Props) => { 13 | const { space, title = 'Create post' } = props 14 | 15 | if (isHiddenSpace(space)) return null 16 | 17 | return isMyAddress(space.owner) 18 | ? } 22 | ghost 23 | {...createNewPostLinkProps(space)} 24 | > 25 | {' '}{title} 26 | 27 | : null 28 | } 29 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreateSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import ButtonLink from 'src/components/utils/ButtonLink' 5 | 6 | export const CreateSpaceButton = ({ 7 | children, 8 | type = 'primary', 9 | ghost = true, 10 | ...otherProps 11 | }: ButtonProps) => { 12 | const props = { type, ghost, ...otherProps } 13 | const newSpacePath = '/spaces/new' 14 | 15 | return 16 | {children || Create space} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from '@ant-design/icons' 2 | import { SpaceData } from '@subsocial/types/dto' 3 | import { Dropdown, Menu } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { editSpaceUrl } from 'src/components/urls' 7 | import { BareProps } from 'src/components/utils/types' 8 | import HiddenSpaceButton from '../HiddenSpaceButton' 9 | import { TransferOwnershipLink } from '../TransferSpaceOwnership' 10 | import { isHiddenSpace, createNewPostLinkProps, isMySpace } from './common' 11 | 12 | type Props = BareProps & { 13 | spaceData: SpaceData 14 | vertical?: boolean 15 | } 16 | 17 | export const DropdownMenu = (props: Props) => { 18 | const { spaceData: { struct }, vertical, style, className } = props 19 | const { id } = struct 20 | const spaceKey = `space-${id.toString()}` 21 | 22 | const buildMenu = () => 23 | 24 | 25 | 26 | Edit space 27 | 28 | 29 | {/* 30 | 31 | */} 32 | {isHiddenSpace(struct) 33 | ? null 34 | : 35 | 36 | Write post 37 | 38 | 39 | } 40 | 41 | 42 | 43 | { 44 | 45 | } 46 | 47 | 48 | return isMySpace(struct) 49 | ? 50 | 51 | 52 | : null 53 | } 54 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/EditMenuLink.tsx: -------------------------------------------------------------------------------- 1 | import { BareProps } from 'src/components/utils/types' 2 | import { SpaceProps } from './common' 3 | 4 | type Props = BareProps & SpaceProps & { 5 | withIcon?: boolean 6 | } 7 | 8 | export const EditMenuLink = ({ space: { id, owner }, withIcon }: Props) => /* isMyAddress(owner) 9 | ? 10 | 14 | 15 | {withIcon && } 16 | Edit menu 17 | 18 | 19 | 20 | : */ null 21 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/SpaceAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HasSpaceIdOrHandle } from 'src/components/urls' 3 | import BaseAvatar, { BaseAvatarProps } from 'src/components/utils/DfAvatar' 4 | import ViewSpaceLink from '../ViewSpaceLink' 5 | 6 | type Props = BaseAvatarProps & { 7 | space: HasSpaceIdOrHandle 8 | asLink?: boolean 9 | } 10 | 11 | export const SpaceAvatar = ({ asLink = true, ...props }: Props) => asLink 12 | ? } /> 13 | : 14 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/common.tsx: -------------------------------------------------------------------------------- 1 | import { isHidden } from '@subsocial/api/utils/visibility-filter' 2 | import { SpaceData } from '@subsocial/types' 3 | import { Space } from '@subsocial/types/substrate/interfaces' 4 | import { isDef } from '@subsocial/utils' 5 | import React from 'react' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | import { HasSpaceIdOrHandle, newPostUrl } from 'src/components/urls' 8 | import NoData from 'src/components/utils/EmptyList' 9 | import { EntityStatusProps, HiddenEntityPanel } from 'src/components/utils/EntityStatusPanels' 10 | import { isHidden as isMyAndHidden } from '../../utils' 11 | export type SpaceProps = { 12 | space: Space 13 | } 14 | 15 | export const isHiddenSpace = (space: Space) => 16 | isHidden(space) 17 | 18 | export const isUnlistedSpace = (spaceData?: SpaceData): spaceData is undefined => 19 | !spaceData || !spaceData?.struct || isMyAndHidden({ struct: spaceData.struct }) 20 | 21 | export const isMySpace = (space?: Space) => 22 | isDef(space) && isMyAddress(space.owner) 23 | 24 | export const createNewPostLinkProps = (space: HasSpaceIdOrHandle) => ({ 25 | href: '/[spaceId]/posts/new', 26 | as: newPostUrl(space) 27 | }) 28 | 29 | type StatusProps = EntityStatusProps & { 30 | space: Space 31 | } 32 | 33 | export const HiddenSpaceAlert = (props: StatusProps) => 34 | 35 | 36 | export const SpaceNotFound = () => 37 | 38 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './PostPreviewsOnSpace' 2 | export * from './AllSpacesLink' 3 | export * from './common' 4 | export * from './CreateSpaceButton' 5 | export * from './DropdownMenu' 6 | export * from './EditMenuLink' 7 | export * from './SpaceAvatar' 8 | export * from './useLoadUnlistedSpace' 9 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedPostsByOwner.ts: -------------------------------------------------------------------------------- 1 | import { PostWithSomeDetails } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { PostId } from '@subsocial/types/substrate/interfaces' 4 | import { useState } from 'react' 5 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | 8 | type Props = { 9 | owner: AnyAccountId 10 | postIds: PostId[] 11 | } 12 | 13 | export const useLoadUnlistedPostsByOwner = ({ owner, postIds }: Props) => { 14 | const isMySpaces = isMyAddress(owner) 15 | const [ myHiddenPosts, setMyHiddenPosts ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpaces) return setMyHiddenPosts([]) 19 | 20 | subsocial.findUnlistedPostsWithAllDetails(postIds) 21 | .then(setMyHiddenPosts) 22 | 23 | }, [ postIds.length, isMySpaces ]) 24 | 25 | return { 26 | isLoading: !myHiddenPosts, 27 | myHiddenPosts: myHiddenPosts || [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedSpace.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceData } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { isEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { useState } from 'react' 6 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 7 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 8 | import { getSpaceId } from 'src/components/substrate' 9 | 10 | export const useLoadUnlistedSpace = (address: AnyAccountId) => { 11 | const isMySpace = isMyAddress(address) 12 | const { query: { spaceId } } = useRouter() 13 | const idOrHandle = spaceId as string 14 | 15 | const [ myHiddenSpace, setMyHiddenSpace ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpace || isEmptyStr(idOrHandle)) return 19 | 20 | let isSubscribe = true 21 | 22 | const loadSpaceFromId = async () => { 23 | const id = await getSpaceId(idOrHandle, subsocial) 24 | const spaceData = id && await subsocial.findSpace({ id }) 25 | isSubscribe && spaceData && setMyHiddenSpace(spaceData) 26 | } 27 | 28 | loadSpaceFromId() 29 | 30 | return () => { isSubscribe = false } 31 | }, [ isMySpace ]) 32 | 33 | return { 34 | isLoading: !myHiddenSpace, 35 | myHiddenSpace 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceDataById.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import useSubsocialEffect from '../api/useSubsocialEffect' 3 | import { Loading } from '../utils' 4 | import NoData from '../utils/EmptyList' 5 | import { SpaceData, ProfileData } from '@subsocial/types/dto' 6 | import { ViewSpaceProps } from './ViewSpaceProps' 7 | 8 | type Props = ViewSpaceProps 9 | 10 | // TODO Copypasta. See withLoadSpaceFromUrl 11 | export const withLoadSpaceDataById = (Component: React.ComponentType) => { 12 | return (props: Props) => { 13 | const { id } = props 14 | 15 | if (!id) return Space id is undefined} /> 16 | 17 | const [ spaceData, setSpaceData ] = useState() 18 | const [ owner, setOwner ] = useState() 19 | 20 | useSubsocialEffect(({ subsocial }) => { 21 | const loadData = async () => { 22 | const spaceData = await subsocial.findSpace({ id }) 23 | if (spaceData) { 24 | setSpaceData(spaceData) 25 | const ownerId = spaceData.struct.owner 26 | const owner = await subsocial.findProfile(ownerId) 27 | setOwner(owner) 28 | } 29 | } 30 | loadData() 31 | }, [ false ]) 32 | 33 | return spaceData?.content 34 | ? 35 | : 36 | } 37 | } 38 | 39 | export default withLoadSpaceDataById 40 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { getSpaceId } from '../substrate' 4 | import { SpaceData } from '@subsocial/types' 5 | import useSubsocialEffect from '../api/useSubsocialEffect' 6 | import { Loading } from '../utils' 7 | import NoData from '../utils/EmptyList' 8 | import { isFunction } from '@polkadot/util' 9 | 10 | type CheckPermissionResult = { 11 | ok: boolean 12 | error: (space: SpaceData) => JSX.Element 13 | } 14 | 15 | export type CheckSpacePermissionFn = (space: SpaceData) => CheckPermissionResult 16 | 17 | type CheckSpacePermissionProps = { 18 | checkSpacePermission?: CheckSpacePermissionFn 19 | } 20 | 21 | export type CanHaveSpaceProps = { 22 | space?: SpaceData 23 | } 24 | 25 | export function withLoadSpaceFromUrl ( 26 | Component: React.ComponentType 27 | ) { 28 | return function (props: Props & CheckSpacePermissionProps): React.ReactElement { 29 | 30 | const { checkSpacePermission } = props 31 | const idOrHandle = useRouter().query.spaceId as string 32 | const [ isLoaded, setIsLoaded ] = useState(false) 33 | const [ loadedData, setLoadedData ] = useState({}) 34 | 35 | useSubsocialEffect(({ subsocial }) => { 36 | const load = async () => { 37 | const id = await getSpaceId(idOrHandle, subsocial) 38 | if (!id) return 39 | 40 | setIsLoaded(false) 41 | const space = await subsocial.findSpace({ id }) 42 | setLoadedData({ space }) 43 | setIsLoaded(true) 44 | } 45 | load() 46 | }, [ idOrHandle ]) 47 | 48 | if (!isLoaded) return 49 | 50 | const { space } = loadedData 51 | if (!space) return 52 | 53 | if (isFunction(checkSpacePermission)) { 54 | const { ok, error } = checkSpacePermission(space) 55 | if (!ok) return error(space) 56 | } 57 | 58 | return 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/spaces/withSpaceIdFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { SpaceId } from '@subsocial/types/substrate/interfaces' 4 | import BN from 'bn.js' 5 | import { getSpaceId } from '../substrate' 6 | import NoData from '../utils/EmptyList' 7 | 8 | export function withSpaceIdFromUrl 9 | (Component: React.ComponentType) { 10 | 11 | return function (props: Props) { 12 | const router = useRouter() 13 | const { spaceId } = router.query 14 | const idOrHandle = spaceId as string 15 | try { 16 | const [ id, setId ] = useState() 17 | 18 | useEffect(() => { 19 | const getId = async () => { 20 | const id = await getSpaceId(idOrHandle) 21 | id && setId(id) 22 | } 23 | getId().catch(err => console.error(err)) 24 | }, [ false ]) 25 | 26 | return !id ? null : 27 | } catch (err) { 28 | return 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/substrate/KusamaContext/index.module.scss: -------------------------------------------------------------------------------- 1 | .KusamaIdentitySection { 2 | margin-bottom: 1rem; 3 | 4 | .DfSectionOuter { 5 | margin-left: 0; 6 | max-width: initial; 7 | min-width: initial; 8 | width: 100%; 9 | .DfSection { 10 | margin-left: 0; 11 | width: 100%; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/substrate/SubstrateWebConsole.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import { useSubstrate } from './useSubstrate' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { Keyring } from '@polkadot/ui-keyring' 6 | 7 | const log = newLogger(SubstrateWebConsole.name) 8 | 9 | type WindowSubstrate = { 10 | api?: ApiPromise 11 | keyring?: Keyring 12 | util?: any 13 | crypto?: any 14 | } 15 | 16 | const getWindowSubstrate = (): WindowSubstrate => { 17 | let substrate = (window as any)?.substrate 18 | if (!substrate) { 19 | substrate = {} as WindowSubstrate 20 | (window as any).substrate = substrate 21 | } 22 | return substrate 23 | } 24 | 25 | /** This component will simply add Substrate utility functions to your web developer console. */ 26 | export function SubstrateWebConsole () { 27 | const { endpoint, api, apiState, keyring, keyringState } = useSubstrate() 28 | 29 | const addApi = () => { 30 | if (window && apiState === 'READY') { 31 | getWindowSubstrate().api = api 32 | log.info('Exported window.substrate.api') 33 | } 34 | } 35 | 36 | const addKeyring = () => { 37 | if (window && keyringState === 'READY') { 38 | getWindowSubstrate().keyring = keyring 39 | log.info('Exported window.substrate.keyring') 40 | } 41 | } 42 | 43 | const addUtilAndCrypto = () => { 44 | if (window) { 45 | const substrate = getWindowSubstrate() 46 | 47 | substrate.util = require('@polkadot/util') 48 | log.info('Exported window.substrate.util') 49 | 50 | substrate.crypto = require('@polkadot/util-crypto') 51 | log.info('Exported window.substrate.crypto') 52 | } 53 | } 54 | 55 | useEffect(() => { 56 | addApi() 57 | }, [ endpoint?.toString(), apiState ]) 58 | 59 | useEffect(() => { 60 | addKeyring() 61 | }, [ keyringState ]) 62 | 63 | useEffect(() => { 64 | addUtilAndCrypto() 65 | }, [ true ]) 66 | 67 | return null 68 | } 69 | 70 | export default SubstrateWebConsole 71 | -------------------------------------------------------------------------------- /src/components/substrate/TxDiv.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TxButtonProps } from './SubstrateTxButton' 3 | import TxButton from 'src/components/utils/TxButton' 4 | 5 | const Div: React.FunctionComponent = (props) => {props.children} 6 | 7 | export const TxDiv = ({ loading, withSpinner, ghost, ...divProps }: TxButtonProps) => 8 | 9 | export default React.memo(TxDiv) 10 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/api.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React, { useRef } from 'react' 6 | import { DefaultProps, ApiProps } from './types' 7 | import useSubstrate from '../useSubstrate' 8 | 9 | export default function withApi ( 10 | Inner: React.ComponentType, 11 | defaultProps: DefaultProps = {} 12 | ): React.ComponentType { 13 | return (props: any) => { 14 | const component = useRef() 15 | const { api } = useSubstrate() 16 | 17 | return !api ? null : 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { ApiProps, SubtractProps, Options } from './types' 6 | import React from 'react' 7 | import withCall from './call' 8 | 9 | type Call = string | [string, Options]; 10 | 11 | export default function withCalls (...calls: Call[]): (Component: React.ComponentType) => React.ComponentType> { 12 | return (Component: React.ComponentType): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
{shortAddress}
(Component: React.ComponentType) { 18 | return function (props: P) { 19 | const { owner: initialOwner, address } = props as Props 20 | 21 | if (initialOwner) return 22 | 23 | const [ owner, setOwner ] = useState() 24 | const [ loaded, setLoaded ] = useState(true) 25 | 26 | useSubsocialEffect(({ subsocial }) => { 27 | if (!address) return 28 | 29 | setLoaded(false) 30 | let isSubscribe = true 31 | 32 | const loadContent = async () => { 33 | const owner = await subsocial.findProfile(address) 34 | isSubscribe && setOwner(owner) 35 | setLoaded(true) 36 | } 37 | 38 | loadContent().catch(err => 39 | log.error(`Failed to load profile data. ${err}`)) 40 | 41 | return () => { isSubscribe = false } 42 | }, [ address?.toString() ]) 43 | 44 | return loaded 45 | ? 46 | : 47 | } 48 | } 49 | 50 | export function withMyProfile (Component: React.ComponentType) { 51 | return function (props: any) { 52 | const { state: { account, address } } = useMyAccount() 53 | return address ? : null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/responsive/ResponsiveContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | import { useMediaQuery } from 'react-responsive' 3 | import { isMobileDevice } from 'src/config/Size.config' 4 | 5 | export type ResponsiveSizeState = { 6 | isMobile: boolean, 7 | isTablet: boolean, 8 | isDesktop: boolean, 9 | isNotMobile: boolean, 10 | isNotDesktop: boolean 11 | } 12 | 13 | const contextStub: ResponsiveSizeState = { 14 | isDesktop: true, 15 | isMobile: false, 16 | isNotMobile: false, 17 | isTablet: false, 18 | isNotDesktop: false 19 | } 20 | 21 | export const ResponsiveSizeContext = createContext(contextStub) 22 | 23 | export function ResponsiveSizeProvider (props: React.PropsWithChildren) { 24 | const value = { 25 | isDesktop: useMediaQuery({ minWidth: 992 }), 26 | isTablet: useMediaQuery({ minWidth: 768, maxWidth: 991 }), 27 | isMobile: useMediaQuery({ maxWidth: 767 }), 28 | isNotMobile: useMediaQuery({ minWidth: 768 }), 29 | isNotDesktop: useMediaQuery({ maxWidth: 991 }) 30 | } 31 | 32 | return 33 | {props.children} 34 | 35 | } 36 | 37 | export function useResponsiveSize () { 38 | return useContext(ResponsiveSizeContext) 39 | } 40 | 41 | export function useIsMobileWidthOrDevice () { 42 | const { isMobile } = useResponsiveSize() 43 | return isMobileDevice || isMobile 44 | } -------------------------------------------------------------------------------- /src/components/responsive/index.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsiveSize } from './ResponsiveContext' 2 | 3 | export * from './ResponsiveContext' 4 | 5 | type Props = { 6 | children?: React.ReactNode | null | JSX.Element 7 | } 8 | 9 | export const Desktop = ({ children }: Props) => { 10 | const { isDesktop } = useResponsiveSize() 11 | return isDesktop ? children : null 12 | } 13 | 14 | export const Tablet = ({ children }: Props) => { 15 | const { isTablet } = useResponsiveSize() 16 | return isTablet ? children : null 17 | } 18 | 19 | export const Mobile = ({ children }: Props) => { 20 | const { isMobile } = useResponsiveSize() 21 | return isMobile ? children : null 22 | } 23 | 24 | export const Default = ({ children }: Props) => { 25 | const { isNotMobile } = useResponsiveSize() 26 | return isNotMobile ? children : null 27 | } 28 | -------------------------------------------------------------------------------- /src/components/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Input } from 'antd' 3 | import { nonEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { isMobileDevice } from 'src/config/Size.config' 6 | 7 | const { Search } = Input 8 | 9 | const SearchInput = () => { 10 | const router = useRouter() 11 | const [ searchValue, setSearchValue ] = useState(router.query.q as string) 12 | const isSearchPage = router.pathname.includes('search') 13 | 14 | useEffect(() => { 15 | if (isSearchPage) return 16 | 17 | setSearchValue(undefined) 18 | }, [ isSearchPage ]) 19 | 20 | const onSearch = (value: string) => { 21 | const queryPath = { 22 | pathname: '/search', 23 | query: { 24 | ...router.query, 25 | q: value 26 | } 27 | } 28 | return nonEmptyStr(value) && router.replace(queryPath, queryPath) 29 | } 30 | 31 | const onChange = (value: string) => setSearchValue(value) 32 | 33 | return ( 34 | 35 | onChange(e.currentTarget.value)} 41 | /> 42 | 43 | ) 44 | } 45 | 46 | export default SearchInput 47 | -------------------------------------------------------------------------------- /src/components/settings/defaults.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { Options } from './types' 6 | 7 | const WSS_LOCALHOST = 'ws://127.0.0.1:9944/' 8 | 9 | const ENDPOINTS: Options = [ 10 | { text: 'Local Node (127.0.0.1:9944)', value: WSS_LOCALHOST } 11 | ] 12 | 13 | const LANGUAGE_DEFAULT = 'default' 14 | 15 | const CRYPTOS: Options = [ 16 | { text: 'Edwards (ed25519)', value: 'ed25519' }, 17 | { text: 'Schnorrkel (sr25519)', value: 'sr25519' } 18 | ] 19 | 20 | const LANGUAGES: Options = [ 21 | { value: LANGUAGE_DEFAULT, text: 'Default browser language (auto-detect)' } 22 | ] 23 | 24 | const UIMODES: Options = [ 25 | { value: 'full', text: 'Fully featured' }, 26 | { value: 'light', text: 'Basic features only' } 27 | ] 28 | 29 | const UITHEMES: Options = [ 30 | { value: 'substrate', text: 'Substrate' } 31 | ] 32 | 33 | const ENDPOINT_DEFAULT = WSS_LOCALHOST 34 | 35 | const UITHEME_DEFAULT = 'substrate' 36 | 37 | // tslint:disable-next-line 38 | const UIMODE_DEFAULT = typeof window !== 'undefined' 39 | ? 'light' 40 | : 'full' 41 | 42 | export { 43 | CRYPTOS, 44 | ENDPOINT_DEFAULT, 45 | ENDPOINTS, 46 | LANGUAGE_DEFAULT, 47 | LANGUAGES, 48 | UIMODE_DEFAULT, 49 | UIMODES, 50 | UITHEME_DEFAULT, 51 | UITHEMES 52 | } 53 | -------------------------------------------------------------------------------- /src/components/settings/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import settings, { Settings } from './Settings' 6 | 7 | export default settings 8 | 9 | export { 10 | Settings 11 | } 12 | -------------------------------------------------------------------------------- /src/components/settings/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 @polkadot/ui-settings authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export type Options = { 6 | disabled?: boolean, 7 | text: string, 8 | value: string 9 | }[]; 10 | 11 | export interface SettingsStruct { 12 | apiUrl: string; 13 | i18nLang: string; 14 | uiMode: string; 15 | uiTheme: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/spaces/AboutSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, aboutSpaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: string 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const AboutSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return ( 22 | 23 | {title} 24 | 25 | ) 26 | } 27 | 28 | export default AboutSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/index.module.scss.keep: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss'; 2 | 3 | .atm_switch_wrapper { 4 | display: flex; 5 | margin: $space_huge 0 $space_normal; 6 | } 7 | 8 | .atm_switch_label { 9 | margin-left: $space_normal; 10 | } 11 | 12 | .atm_submit_button span { 13 | color: #fff; 14 | } 15 | 16 | .atm_dates_wrapper { 17 | display: flex; 18 | } 19 | 20 | .atm_dates_wrapper .field { 21 | margin-right: $space_huge !important; 22 | } 23 | 24 | .atm_dates_wrapper input { 25 | padding: 1.286rem !important; 26 | } 27 | 28 | .atm_company_wrapper { 29 | position: relative; 30 | } 31 | 32 | .atm_prefix { 33 | left: $space_tiny; 34 | display: flex; 35 | position: absolute; 36 | top: $space_tiny; 37 | } 38 | 39 | .atm_prefix img { 40 | height: $space_huge; 41 | } 42 | 43 | .atm_company_autocomplete { 44 | background-color: #fff; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | 49 | .atm_company_autocomplete_item { 50 | cursor: pointer; 51 | } 52 | 53 | .atm_company_autocomplete_item img { 54 | height: $space_huge; 55 | } 56 | 57 | .atm_company_wrapper.with_prefix input { 58 | padding-left: 2.5rem !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/spaces/EditTeamMember/validation.ts.keep: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import moment from 'moment-timezone'; 3 | import { minLenError, maxLenError } from '../../utils/forms/validation'; 4 | 5 | const TITLE_MIN_LEN = 2; 6 | const TITLE_MAX_LEN = 50; 7 | 8 | const COMPANY_MIN_LEN = 2; 9 | const COMPANY_MAX_LEN = 50; 10 | 11 | const LOCATION_MIN_LEN = 2; 12 | const LOCATION_MAX_LEN = 100; 13 | 14 | const DESCRIPTION_MAX_LEN = 2000; 15 | 16 | export const buildValidationSchema = () => Yup.object().shape({ 17 | title: Yup.string() 18 | .required('Job title is required') 19 | .min(TITLE_MIN_LEN, minLenError('Job title', TITLE_MIN_LEN)) 20 | .max(TITLE_MAX_LEN, maxLenError('Job title', TITLE_MAX_LEN)), 21 | 22 | company: Yup.string() 23 | .required('Company name is required') 24 | .min(COMPANY_MIN_LEN, minLenError('Company name', COMPANY_MIN_LEN)) 25 | .max(COMPANY_MAX_LEN, maxLenError('Company name', COMPANY_MAX_LEN)), 26 | 27 | location: Yup.string() 28 | .min(LOCATION_MIN_LEN, minLenError('Location name', LOCATION_MIN_LEN)) 29 | .max(LOCATION_MAX_LEN, maxLenError('Location name', LOCATION_MAX_LEN)), 30 | 31 | startDate: Yup.object().test( 32 | 'startDate', 33 | 'Start date should not be in future', 34 | value => moment().diff(value, 'days') >= 0 35 | ), 36 | 37 | endDate: Yup.object().test( 38 | 'endDate', 39 | 'End date should not be in future', 40 | value => value ? moment().diff(value, 'days') >= 0 : true 41 | ), 42 | 43 | description: Yup.string() 44 | .max(DESCRIPTION_MAX_LEN, maxLenError('Description', DESCRIPTION_MAX_LEN)) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/spaces/HiddenSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space } from '@subsocial/types/substrate/interfaces' 3 | import HiddenButton from '../utils/HiddenButton' 4 | import { SpaceUpdate, OptionOptionText, OptionIpfsContent, OptionBool } from '@subsocial/types/substrate/classes' 5 | 6 | type HiddenSpaceButtonProps = { 7 | space: Space, 8 | asLink?: boolean 9 | }; 10 | 11 | export function HiddenSpaceButton (props: HiddenSpaceButtonProps) { 12 | const { space } = props 13 | const hidden = space.hidden.valueOf() 14 | 15 | const update = new SpaceUpdate({ 16 | handle: new OptionOptionText(), 17 | content: new OptionIpfsContent(), 18 | hidden: new OptionBool(!hidden) // TODO has no implementation on UI 19 | }) 20 | 21 | const newTxParams = () => { 22 | return [ space.id, update ] 23 | } 24 | 25 | return 26 | } 27 | 28 | export default HiddenSpaceButton 29 | -------------------------------------------------------------------------------- /src/components/spaces/NavValidation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import { minLenError, maxLenError } from '../utils/forms/validation' 3 | 4 | const TITLE_MIN_LEN = 2 5 | const TITLE_MAX_LEN = 50 6 | 7 | export const validationSchema = Yup.object().shape({ 8 | navTabs: Yup.array().of( 9 | Yup.object().shape({ 10 | title: Yup.string() 11 | .min(TITLE_MIN_LEN, minLenError('Tab title', TITLE_MIN_LEN)) 12 | .max(TITLE_MAX_LEN, maxLenError('Tab title', TITLE_MAX_LEN)) 13 | .required('Tab title is a required field') 14 | }) 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/spaces/SocialLinks/ViewSocialLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NamedLink } from '@subsocial/types' 3 | import { getLinkBrand, getLinkIcon } from './utils' 4 | import { MailOutlined } from '@ant-design/icons' 5 | import { isEmptyStr } from '@subsocial/utils' 6 | import { BareProps } from 'src/components/utils/types' 7 | 8 | type SocialLinkProps = BareProps & { 9 | link: string, 10 | label?: string 11 | } 12 | 13 | export const SocialLink = ({ link, label, className }: SocialLinkProps) => { 14 | if (isEmptyStr(link)) return null 15 | 16 | const brand = getLinkBrand(link) 17 | return 18 | {getLinkIcon(brand)} 19 | {label && <> 20 | {`${label} ${brand}`} 21 | >} 22 | 23 | } 24 | 25 | type SocialLinksProps = { 26 | links: string[] | NamedLink[] 27 | } 28 | 29 | export const ViewSocialLinks = ({ links }: SocialLinksProps) => { 30 | return <>{(links as string[]).map((link, i) => 31 | 32 | )}> 33 | } 34 | 35 | type ContactInfoProps = SocialLinksProps & { 36 | email: string 37 | } 38 | 39 | export const EmailLink = ({ link, label, className }: SocialLinkProps) => 40 | 41 | 42 | {label && {`${label} email`}} 43 | 44 | 45 | export const ContactInfo = ({ links, email }: ContactInfoProps) => { 46 | if (!links && !email) return null 47 | 48 | return 49 | {links && } 50 | {email && } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/components/spaces/SpacedSectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { useStorybookContext } from '../utils/StorybookContext' 4 | import { SpaceData } from '@subsocial/types' 5 | import { spaceUrl } from '../urls' 6 | 7 | type Props = { 8 | space?: SpaceData 9 | subtitle: React.ReactNode 10 | } 11 | 12 | export const SpacegedSectionTitle = ({ 13 | space, 14 | subtitle 15 | }: Props) => { 16 | const { isStorybook } = useStorybookContext() 17 | const name = space?.content?.name 18 | 19 | return <> 20 | {!isStorybook && space && name && <> 21 | 22 | {name} 23 | 24 | / 25 | >} 26 | {subtitle} 27 | > 28 | } 29 | 30 | export default SpacegedSectionTitle 31 | -------------------------------------------------------------------------------- /src/components/spaces/TransferSpaceOwnership.module.sass: -------------------------------------------------------------------------------- 1 | .TransferOwnershipForm 2 | margin: 0 3 | 4 | \:global .ant-form-item 5 | margin-bottom: 0 6 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceById.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ViewSpace } from './ViewSpace' 4 | import { useRouter } from 'next/router' 5 | import BN from 'bn.js' 6 | 7 | const Component = () => { 8 | const router = useRouter() 9 | const { spaceId } = router.query 10 | return spaceId 11 | ? 12 | : null 13 | } 14 | 15 | export default Component 16 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { HasSpaceIdOrHandle, spaceUrl } from '../urls' 4 | 5 | type Props = { 6 | space: HasSpaceIdOrHandle 7 | title?: React.ReactNode 8 | hint?: string 9 | className?: string 10 | } 11 | 12 | export const ViewSpaceLink = ({ 13 | space, 14 | title, 15 | hint, 16 | className 17 | }: Props) => { 18 | 19 | if (!space.id || !title) return null 20 | 21 | return 22 | 23 | {title} 24 | 25 | 26 | } 27 | 28 | export default ViewSpaceLink 29 | -------------------------------------------------------------------------------- /src/components/spaces/ViewSpaceProps.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { GenericAccountId as AccountId } from '@polkadot/types' 3 | import { SpaceData, PostWithSomeDetails, ProfileData } from '@subsocial/types/dto' 4 | import { PostId } from '@subsocial/types/substrate/interfaces' 5 | 6 | export type ViewSpaceProps = { 7 | nameOnly?: boolean 8 | miniPreview?: boolean 9 | preview?: boolean 10 | dropdownPreview?: boolean 11 | withLink?: boolean 12 | withFollowButton?: boolean 13 | withTags?: boolean 14 | withStats?: boolean 15 | id?: BN 16 | spaceData?: SpaceData 17 | owner?: ProfileData, 18 | postIds?: PostId[], 19 | posts?: PostWithSomeDetails[] 20 | followers?: AccountId[] 21 | imageSize?: number 22 | onClick?: () => void 23 | statusCode?: number 24 | } 25 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/AllSpacesLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | import { BareProps } from 'src/components/utils/types' 4 | 5 | type Props = BareProps & { 6 | title?: React.ReactNode 7 | } 8 | 9 | export const AllSpacesLink = ({ 10 | title = 'See all', 11 | ...otherProps 12 | }: Props) => 13 | 14 | {title} 19 | 20 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreatePostButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 5 | import ButtonLink from 'src/components/utils/ButtonLink' 6 | import { createNewPostLinkProps, isHiddenSpace, SpaceProps } from './common' 7 | 8 | type Props = SpaceProps & ButtonProps & { 9 | title?: React.ReactNode 10 | } 11 | 12 | export const CreatePostButton = (props: Props) => { 13 | const { space, title = 'Create post' } = props 14 | 15 | if (isHiddenSpace(space)) return null 16 | 17 | return isMyAddress(space.owner) 18 | ? } 22 | ghost 23 | {...createNewPostLinkProps(space)} 24 | > 25 | {' '}{title} 26 | 27 | : null 28 | } 29 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/CreateSpaceButton.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { ButtonProps } from 'antd/lib/button' 3 | import React from 'react' 4 | import ButtonLink from 'src/components/utils/ButtonLink' 5 | 6 | export const CreateSpaceButton = ({ 7 | children, 8 | type = 'primary', 9 | ghost = true, 10 | ...otherProps 11 | }: ButtonProps) => { 12 | const props = { type, ghost, ...otherProps } 13 | const newSpacePath = '/spaces/new' 14 | 15 | return 16 | {children || Create space} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from '@ant-design/icons' 2 | import { SpaceData } from '@subsocial/types/dto' 3 | import { Dropdown, Menu } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { editSpaceUrl } from 'src/components/urls' 7 | import { BareProps } from 'src/components/utils/types' 8 | import HiddenSpaceButton from '../HiddenSpaceButton' 9 | import { TransferOwnershipLink } from '../TransferSpaceOwnership' 10 | import { isHiddenSpace, createNewPostLinkProps, isMySpace } from './common' 11 | 12 | type Props = BareProps & { 13 | spaceData: SpaceData 14 | vertical?: boolean 15 | } 16 | 17 | export const DropdownMenu = (props: Props) => { 18 | const { spaceData: { struct }, vertical, style, className } = props 19 | const { id } = struct 20 | const spaceKey = `space-${id.toString()}` 21 | 22 | const buildMenu = () => 23 | 24 | 25 | 26 | Edit space 27 | 28 | 29 | {/* 30 | 31 | */} 32 | {isHiddenSpace(struct) 33 | ? null 34 | : 35 | 36 | Write post 37 | 38 | 39 | } 40 | 41 | 42 | 43 | { 44 | 45 | } 46 | 47 | 48 | return isMySpace(struct) 49 | ? 50 | 51 | 52 | : null 53 | } 54 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/EditMenuLink.tsx: -------------------------------------------------------------------------------- 1 | import { BareProps } from 'src/components/utils/types' 2 | import { SpaceProps } from './common' 3 | 4 | type Props = BareProps & SpaceProps & { 5 | withIcon?: boolean 6 | } 7 | 8 | export const EditMenuLink = ({ space: { id, owner }, withIcon }: Props) => /* isMyAddress(owner) 9 | ? 10 | 14 | 15 | {withIcon && } 16 | Edit menu 17 | 18 | 19 | 20 | : */ null 21 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/SpaceAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HasSpaceIdOrHandle } from 'src/components/urls' 3 | import BaseAvatar, { BaseAvatarProps } from 'src/components/utils/DfAvatar' 4 | import ViewSpaceLink from '../ViewSpaceLink' 5 | 6 | type Props = BaseAvatarProps & { 7 | space: HasSpaceIdOrHandle 8 | asLink?: boolean 9 | } 10 | 11 | export const SpaceAvatar = ({ asLink = true, ...props }: Props) => asLink 12 | ? } /> 13 | : 14 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/common.tsx: -------------------------------------------------------------------------------- 1 | import { isHidden } from '@subsocial/api/utils/visibility-filter' 2 | import { SpaceData } from '@subsocial/types' 3 | import { Space } from '@subsocial/types/substrate/interfaces' 4 | import { isDef } from '@subsocial/utils' 5 | import React from 'react' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | import { HasSpaceIdOrHandle, newPostUrl } from 'src/components/urls' 8 | import NoData from 'src/components/utils/EmptyList' 9 | import { EntityStatusProps, HiddenEntityPanel } from 'src/components/utils/EntityStatusPanels' 10 | import { isHidden as isMyAndHidden } from '../../utils' 11 | export type SpaceProps = { 12 | space: Space 13 | } 14 | 15 | export const isHiddenSpace = (space: Space) => 16 | isHidden(space) 17 | 18 | export const isUnlistedSpace = (spaceData?: SpaceData): spaceData is undefined => 19 | !spaceData || !spaceData?.struct || isMyAndHidden({ struct: spaceData.struct }) 20 | 21 | export const isMySpace = (space?: Space) => 22 | isDef(space) && isMyAddress(space.owner) 23 | 24 | export const createNewPostLinkProps = (space: HasSpaceIdOrHandle) => ({ 25 | href: '/[spaceId]/posts/new', 26 | as: newPostUrl(space) 27 | }) 28 | 29 | type StatusProps = EntityStatusProps & { 30 | space: Space 31 | } 32 | 33 | export const HiddenSpaceAlert = (props: StatusProps) => 34 | 35 | 36 | export const SpaceNotFound = () => 37 | 38 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './PostPreviewsOnSpace' 2 | export * from './AllSpacesLink' 3 | export * from './common' 4 | export * from './CreateSpaceButton' 5 | export * from './DropdownMenu' 6 | export * from './EditMenuLink' 7 | export * from './SpaceAvatar' 8 | export * from './useLoadUnlistedSpace' 9 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedPostsByOwner.ts: -------------------------------------------------------------------------------- 1 | import { PostWithSomeDetails } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { PostId } from '@subsocial/types/substrate/interfaces' 4 | import { useState } from 'react' 5 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 6 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 7 | 8 | type Props = { 9 | owner: AnyAccountId 10 | postIds: PostId[] 11 | } 12 | 13 | export const useLoadUnlistedPostsByOwner = ({ owner, postIds }: Props) => { 14 | const isMySpaces = isMyAddress(owner) 15 | const [ myHiddenPosts, setMyHiddenPosts ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpaces) return setMyHiddenPosts([]) 19 | 20 | subsocial.findUnlistedPostsWithAllDetails(postIds) 21 | .then(setMyHiddenPosts) 22 | 23 | }, [ postIds.length, isMySpaces ]) 24 | 25 | return { 26 | isLoading: !myHiddenPosts, 27 | myHiddenPosts: myHiddenPosts || [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/spaces/helpers/useLoadUnlistedSpace.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceData } from '@subsocial/types/dto' 2 | import { AnyAccountId } from '@subsocial/types/substrate' 3 | import { isEmptyStr } from '@subsocial/utils' 4 | import { useRouter } from 'next/router' 5 | import { useState } from 'react' 6 | import useSubsocialEffect from 'src/components/api/useSubsocialEffect' 7 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 8 | import { getSpaceId } from 'src/components/substrate' 9 | 10 | export const useLoadUnlistedSpace = (address: AnyAccountId) => { 11 | const isMySpace = isMyAddress(address) 12 | const { query: { spaceId } } = useRouter() 13 | const idOrHandle = spaceId as string 14 | 15 | const [ myHiddenSpace, setMyHiddenSpace ] = useState() 16 | 17 | useSubsocialEffect(({ subsocial }) => { 18 | if (!isMySpace || isEmptyStr(idOrHandle)) return 19 | 20 | let isSubscribe = true 21 | 22 | const loadSpaceFromId = async () => { 23 | const id = await getSpaceId(idOrHandle, subsocial) 24 | const spaceData = id && await subsocial.findSpace({ id }) 25 | isSubscribe && spaceData && setMyHiddenSpace(spaceData) 26 | } 27 | 28 | loadSpaceFromId() 29 | 30 | return () => { isSubscribe = false } 31 | }, [ isMySpace ]) 32 | 33 | return { 34 | isLoading: !myHiddenSpace, 35 | myHiddenSpace 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceDataById.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import useSubsocialEffect from '../api/useSubsocialEffect' 3 | import { Loading } from '../utils' 4 | import NoData from '../utils/EmptyList' 5 | import { SpaceData, ProfileData } from '@subsocial/types/dto' 6 | import { ViewSpaceProps } from './ViewSpaceProps' 7 | 8 | type Props = ViewSpaceProps 9 | 10 | // TODO Copypasta. See withLoadSpaceFromUrl 11 | export const withLoadSpaceDataById = (Component: React.ComponentType) => { 12 | return (props: Props) => { 13 | const { id } = props 14 | 15 | if (!id) return Space id is undefined} /> 16 | 17 | const [ spaceData, setSpaceData ] = useState() 18 | const [ owner, setOwner ] = useState() 19 | 20 | useSubsocialEffect(({ subsocial }) => { 21 | const loadData = async () => { 22 | const spaceData = await subsocial.findSpace({ id }) 23 | if (spaceData) { 24 | setSpaceData(spaceData) 25 | const ownerId = spaceData.struct.owner 26 | const owner = await subsocial.findProfile(ownerId) 27 | setOwner(owner) 28 | } 29 | } 30 | loadData() 31 | }, [ false ]) 32 | 33 | return spaceData?.content 34 | ? 35 | : 36 | } 37 | } 38 | 39 | export default withLoadSpaceDataById 40 | -------------------------------------------------------------------------------- /src/components/spaces/withLoadSpaceFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { getSpaceId } from '../substrate' 4 | import { SpaceData } from '@subsocial/types' 5 | import useSubsocialEffect from '../api/useSubsocialEffect' 6 | import { Loading } from '../utils' 7 | import NoData from '../utils/EmptyList' 8 | import { isFunction } from '@polkadot/util' 9 | 10 | type CheckPermissionResult = { 11 | ok: boolean 12 | error: (space: SpaceData) => JSX.Element 13 | } 14 | 15 | export type CheckSpacePermissionFn = (space: SpaceData) => CheckPermissionResult 16 | 17 | type CheckSpacePermissionProps = { 18 | checkSpacePermission?: CheckSpacePermissionFn 19 | } 20 | 21 | export type CanHaveSpaceProps = { 22 | space?: SpaceData 23 | } 24 | 25 | export function withLoadSpaceFromUrl ( 26 | Component: React.ComponentType 27 | ) { 28 | return function (props: Props & CheckSpacePermissionProps): React.ReactElement { 29 | 30 | const { checkSpacePermission } = props 31 | const idOrHandle = useRouter().query.spaceId as string 32 | const [ isLoaded, setIsLoaded ] = useState(false) 33 | const [ loadedData, setLoadedData ] = useState({}) 34 | 35 | useSubsocialEffect(({ subsocial }) => { 36 | const load = async () => { 37 | const id = await getSpaceId(idOrHandle, subsocial) 38 | if (!id) return 39 | 40 | setIsLoaded(false) 41 | const space = await subsocial.findSpace({ id }) 42 | setLoadedData({ space }) 43 | setIsLoaded(true) 44 | } 45 | load() 46 | }, [ idOrHandle ]) 47 | 48 | if (!isLoaded) return 49 | 50 | const { space } = loadedData 51 | if (!space) return 52 | 53 | if (isFunction(checkSpacePermission)) { 54 | const { ok, error } = checkSpacePermission(space) 55 | if (!ok) return error(space) 56 | } 57 | 58 | return 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/spaces/withSpaceIdFromUrl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { SpaceId } from '@subsocial/types/substrate/interfaces' 4 | import BN from 'bn.js' 5 | import { getSpaceId } from '../substrate' 6 | import NoData from '../utils/EmptyList' 7 | 8 | export function withSpaceIdFromUrl 9 | (Component: React.ComponentType) { 10 | 11 | return function (props: Props) { 12 | const router = useRouter() 13 | const { spaceId } = router.query 14 | const idOrHandle = spaceId as string 15 | try { 16 | const [ id, setId ] = useState() 17 | 18 | useEffect(() => { 19 | const getId = async () => { 20 | const id = await getSpaceId(idOrHandle) 21 | id && setId(id) 22 | } 23 | getId().catch(err => console.error(err)) 24 | }, [ false ]) 25 | 26 | return !id ? null : 27 | } catch (err) { 28 | return 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/substrate/KusamaContext/index.module.scss: -------------------------------------------------------------------------------- 1 | .KusamaIdentitySection { 2 | margin-bottom: 1rem; 3 | 4 | .DfSectionOuter { 5 | margin-left: 0; 6 | max-width: initial; 7 | min-width: initial; 8 | width: 100%; 9 | .DfSection { 10 | margin-left: 0; 11 | width: 100%; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/substrate/SubstrateWebConsole.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { newLogger } from '@subsocial/utils' 3 | import { useSubstrate } from './useSubstrate' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { Keyring } from '@polkadot/ui-keyring' 6 | 7 | const log = newLogger(SubstrateWebConsole.name) 8 | 9 | type WindowSubstrate = { 10 | api?: ApiPromise 11 | keyring?: Keyring 12 | util?: any 13 | crypto?: any 14 | } 15 | 16 | const getWindowSubstrate = (): WindowSubstrate => { 17 | let substrate = (window as any)?.substrate 18 | if (!substrate) { 19 | substrate = {} as WindowSubstrate 20 | (window as any).substrate = substrate 21 | } 22 | return substrate 23 | } 24 | 25 | /** This component will simply add Substrate utility functions to your web developer console. */ 26 | export function SubstrateWebConsole () { 27 | const { endpoint, api, apiState, keyring, keyringState } = useSubstrate() 28 | 29 | const addApi = () => { 30 | if (window && apiState === 'READY') { 31 | getWindowSubstrate().api = api 32 | log.info('Exported window.substrate.api') 33 | } 34 | } 35 | 36 | const addKeyring = () => { 37 | if (window && keyringState === 'READY') { 38 | getWindowSubstrate().keyring = keyring 39 | log.info('Exported window.substrate.keyring') 40 | } 41 | } 42 | 43 | const addUtilAndCrypto = () => { 44 | if (window) { 45 | const substrate = getWindowSubstrate() 46 | 47 | substrate.util = require('@polkadot/util') 48 | log.info('Exported window.substrate.util') 49 | 50 | substrate.crypto = require('@polkadot/util-crypto') 51 | log.info('Exported window.substrate.crypto') 52 | } 53 | } 54 | 55 | useEffect(() => { 56 | addApi() 57 | }, [ endpoint?.toString(), apiState ]) 58 | 59 | useEffect(() => { 60 | addKeyring() 61 | }, [ keyringState ]) 62 | 63 | useEffect(() => { 64 | addUtilAndCrypto() 65 | }, [ true ]) 66 | 67 | return null 68 | } 69 | 70 | export default SubstrateWebConsole 71 | -------------------------------------------------------------------------------- /src/components/substrate/TxDiv.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TxButtonProps } from './SubstrateTxButton' 3 | import TxButton from 'src/components/utils/TxButton' 4 | 5 | const Div: React.FunctionComponent = (props) => {props.children} 6 | 7 | export const TxDiv = ({ loading, withSpinner, ghost, ...divProps }: TxButtonProps) => 8 | 9 | export default React.memo(TxDiv) 10 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/api.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React, { useRef } from 'react' 6 | import { DefaultProps, ApiProps } from './types' 7 | import useSubstrate from '../useSubstrate' 8 | 9 | export default function withApi ( 10 | Inner: React.ComponentType, 11 | defaultProps: DefaultProps = {} 12 | ): React.ComponentType { 13 | return (props: any) => { 14 | const component = useRef() 15 | const { api } = useSubstrate() 16 | 17 | return !api ? null : 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { ApiProps, SubtractProps, Options } from './types' 6 | import React from 'react' 7 | import withCall from './call' 8 | 9 | type Call = string | [string, Options]; 10 | 11 | export default function withCalls (...calls: Call[]): (Component: React.ComponentType) => React.ComponentType> { 12 | return (Component: React.ComponentType): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
( 10 | Inner: React.ComponentType
, 11 | defaultProps: DefaultProps = {} 12 | ): React.ComponentType { 13 | return (props: any) => { 14 | const component = useRef() 15 | const { api } = useSubstrate() 16 | 17 | return !api ? null : 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { ApiProps, SubtractProps, Options } from './types' 6 | import React from 'react' 7 | import withCall from './call' 8 | 9 | type Call = string | [string, Options]; 10 | 11 | export default function withCalls (...calls: Call[]): (Component: React.ComponentType) => React.ComponentType> { 12 | return (Component: React.ComponentType): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
(...calls: Call[]): (Component: React.ComponentType
) => React.ComponentType> { 12 | return (Component: React.ComponentType): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
): React.ComponentType => { 13 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 14 | // after something can use the value of the preceding version 15 | return calls 16 | .reverse() 17 | .reduce((Component, call): React.ComponentType => { 18 | return Array.isArray(call) 19 | ? withCall(...call)(Component as any) 20 | : withCall(call)(Component as any) 21 | }, Component) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | export { default as withApi } from './api' 6 | export { default as withCall } from './call' 7 | export { default as withCalls } from './calls' 8 | export { default as withMulti } from './multi' 9 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/multi.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | 7 | type HOC = (Component: React.ComponentType) => React.ComponentType; 8 | 9 | export default function withMulti (Component: React.ComponentType, ...hocs: HOC[]): React.ComponentType { 10 | // NOTE: Order is reversed so it makes sense in the props, i.e. component 11 | // after something can use the value of the preceding version 12 | return hocs 13 | .reverse() 14 | .reduce((Component, hoc): React.ComponentType => 15 | hoc(Component), Component 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/substrate/hoc/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import React from 'react' 6 | import { ApiPromise } from '@polkadot/api' 7 | 8 | export interface OnChangeCbObs { 9 | next: (value?: any) => any; 10 | } 11 | 12 | export type OnChangeCbFn = (value?: any) => any; 13 | export type OnChangeCb = OnChangeCbObs | OnChangeCbFn; 14 | 15 | export type Transform = (value: any, index: number) => any; 16 | 17 | export interface DefaultProps { 18 | callOnResult?: OnChangeCb; 19 | [index: string]: any; 20 | } 21 | 22 | export interface Options { 23 | at?: Uint8Array | string; 24 | atProp?: string; 25 | callOnResult?: OnChangeCb; 26 | fallbacks?: string[]; 27 | isMulti?: boolean; 28 | params?: any[]; 29 | paramName?: string; 30 | paramPick?: (props: any) => any; 31 | paramValid?: boolean; 32 | propName?: string; 33 | skipIf?: (props: any) => boolean; 34 | transform?: Transform; 35 | withIndicator?: boolean; 36 | } 37 | 38 | export type RenderFn = (value?: any) => React.ReactNode; 39 | 40 | export type StorageTransform = (input: any, index: number) => any | null; 41 | 42 | export type HOC = (Component: React.ComponentType, defaultProps?: DefaultProps, render?: RenderFn) => React.ComponentType; 43 | 44 | export interface ApiMethod { 45 | name: string; 46 | section?: string; 47 | } 48 | 49 | export type ComponentRenderer = (render: RenderFn, defaultProps?: DefaultProps) => React.ComponentType; 50 | 51 | export type OmitProps = Pick>; 52 | export type SubtractProps = OmitProps; 53 | 54 | export type ApiProps = { 55 | api: ApiPromise 56 | } 57 | 58 | export interface CallState { 59 | callResult?: any; 60 | callUpdated?: boolean; 61 | callUpdatedAt?: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/substrate/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SubstrateContext' 2 | export * from './useSubstrate' 3 | export * from './SubstrateWebConsole' 4 | export * from './hoc' 5 | export * from './util' 6 | -------------------------------------------------------------------------------- /src/components/substrate/useSubstrate.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { SubstrateContext, State, Dispatch } from './SubstrateContext' 3 | 4 | export const useSubstrate = (): State & { dispatch: Dispatch } => { 5 | const [ state, dispatch ] = useContext(SubstrateContext) 6 | return { ...state, dispatch } 7 | } 8 | 9 | export default useSubstrate 10 | -------------------------------------------------------------------------------- /src/components/substrate/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-hooks authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { useCallback, useState } from 'react' 6 | 7 | // Simple wrapper for a true/false toggle 8 | export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] { 9 | const [ isActive, setActive ] = useState(defaultValue) 10 | const toggleActive = useCallback( 11 | (): void => setActive((isActive: boolean) => !isActive), 12 | [] 13 | ) 14 | 15 | return [ isActive, toggleActive, setActive ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/substrate/util/getTxParams.ts: -------------------------------------------------------------------------------- 1 | import { newLogger } from '@subsocial/utils' 2 | import { CommonContent } from '@subsocial/types' 3 | import { IpfsCid } from '@subsocial/types/substrate/interfaces' 4 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 5 | 6 | const log = newLogger('BuildTxParams') 7 | 8 | // TODO rename setIpfsCid -> setIpfsCid 9 | type Params = { 10 | ipfs: SubsocialIpfsApi 11 | json: C 12 | setIpfsCid: (cid: IpfsCid) => void 13 | buildTxParamsCallback: (cid: IpfsCid) => any[] 14 | } 15 | 16 | // TODO rename to: pinToIpfsAndBuildTxParams() 17 | export const getTxParams = async ({ 18 | ipfs, 19 | json, 20 | setIpfsCid, 21 | buildTxParamsCallback 22 | }: Params) => { 23 | try { 24 | const cid = await ipfs.saveContent(json) 25 | if (cid) { 26 | setIpfsCid(cid) 27 | return buildTxParamsCallback(cid) 28 | } else { 29 | log.error('Save to IPFS returned an undefined CID') 30 | } 31 | } catch (err) { 32 | log.error(`Failed to build tx params. ${err}`) 33 | } 34 | return [] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/substrate/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { newLogger } from '@subsocial/utils' 6 | 7 | function flatten (key: string | null, value: any): any { 8 | if (!value) { 9 | return value 10 | } 11 | 12 | if (value.$$typeof) { 13 | return '' 14 | } 15 | 16 | if (Array.isArray(value)) { 17 | return value.map((item): any => 18 | flatten(null, item) 19 | ) 20 | } 21 | 22 | return value 23 | } 24 | 25 | const log = newLogger(isEqual.name) 26 | 27 | export function isEqual (a?: T, b?: T, debug = false): boolean { 28 | const jsonA = JSON.stringify({ test: a }, flatten) 29 | const jsonB = JSON.stringify({ test: b }, flatten) 30 | 31 | if (debug) { 32 | log.debug('jsonA', jsonA, 'jsonB', jsonB) 33 | } 34 | 35 | return jsonA === jsonB 36 | } 37 | -------------------------------------------------------------------------------- /src/components/substrate/util/queryToProps.ts: -------------------------------------------------------------------------------- 1 | import { Options as QueryOptions } from '../hoc/types' 2 | import { PalletName } from '@subsocial/types' 3 | 4 | /** Example of apiQuery: 'query.councilElection.round' */ 5 | export function queryToProp ( 6 | apiQuery: string, 7 | paramNameOrOpts?: string | QueryOptions 8 | ): [ string, QueryOptions ] { 9 | let paramName: string | undefined 10 | let propName: string | undefined 11 | 12 | if (typeof paramNameOrOpts === 'string') { 13 | paramName = paramNameOrOpts 14 | } else if (paramNameOrOpts) { 15 | paramName = paramNameOrOpts.paramName 16 | propName = paramNameOrOpts.propName 17 | } 18 | 19 | // If prop name is still undefined, derive it from the name of storage item: 20 | if (!propName) { 21 | propName = apiQuery.split('.').slice(-1)[0] 22 | } 23 | 24 | return [ apiQuery, { paramName, propName } ] 25 | } 26 | 27 | const palletQueryToProp = (pallet: PalletName, storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 28 | return queryToProp(`query.${pallet}.${storageItem}`, paramNameOrOpts) 29 | } 30 | 31 | export const postsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 32 | return palletQueryToProp('posts', storageItem, paramNameOrOpts) 33 | } 34 | 35 | export const spacesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 36 | return palletQueryToProp('spaces', storageItem, paramNameOrOpts) 37 | } 38 | 39 | export const spaceFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 40 | return palletQueryToProp('spaceFollows', storageItem, paramNameOrOpts) 41 | } 42 | 43 | export const profilesQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 44 | return palletQueryToProp('profiles', storageItem, paramNameOrOpts) 45 | } 46 | 47 | export const profileFollowsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 48 | return palletQueryToProp('profileFollows', storageItem, paramNameOrOpts) 49 | } 50 | 51 | export const reactionsQueryToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => { 52 | return palletQueryToProp('reactions', storageItem, paramNameOrOpts) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/substrate/util/triggerChange.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-api authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { OnChangeCb } from '../hoc/types' 6 | 7 | import { isFunction, isObservable } from '@polkadot/util' 8 | 9 | export function triggerChange (value?: any, ...callOnResult: (OnChangeCb | undefined)[]): void { 10 | if (!callOnResult || !callOnResult.length) { 11 | return 12 | } 13 | 14 | callOnResult.forEach((callOnResult): void => { 15 | if (isObservable(callOnResult)) { 16 | callOnResult.next(value) 17 | } else if (isFunction(callOnResult)) { 18 | callOnResult(value) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/types/index.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@subsocial/types/substrate/preparedTypes' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | const log = newLogger('SubsocialTypes') 6 | 7 | export const registerSubsocialTypes = (): void => { 8 | try { 9 | registry.register(types) 10 | log.info('Succesfully registered custom types of Subsocial modules') 11 | } catch (err) { 12 | log.error('Failed to register custom types of Subsocial modules:', err) 13 | } 14 | } 15 | 16 | export default registerSubsocialTypes 17 | -------------------------------------------------------------------------------- /src/components/uploader/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfUploadAvatar 4 | display: flex 5 | justify-content: flex-start 6 | \:global .ant-upload-select-picture-card 7 | border-radius: 100% 8 | margin: 0 9 | 10 | .DfUploadCover 11 | \:global .ant-upload-select-picture-card 12 | width: 100% 13 | height: 80px 14 | 15 | .DfRemoveIcon 16 | color: #ea2828 17 | cursor: pointer 18 | 19 | .DfRemoveCover 20 | @extend .DfRemoveIcon 21 | margin-left: -2.5rem 22 | margin-top: .5rem 23 | width: 32px 24 | height: 32px 25 | justify-content: center 26 | display: flex 27 | align-items: center 28 | background-color: #00000088 29 | border-radius: 50% 30 | 31 | -------------------------------------------------------------------------------- /src/components/urls/goToPage.ts: -------------------------------------------------------------------------------- 1 | import { AnySpaceId } from '@subsocial/types' 2 | import { newLogger } from '@subsocial/utils' 3 | import Router from 'next/router' 4 | import { HasSpaceIdOrHandle } from '.' 5 | import { createNewPostLinkProps } from '../spaces/helpers' 6 | 7 | const log = newLogger('Go to page') 8 | 9 | export function goToSpacePage (spaceId: AnySpaceId) { 10 | Router.push('/[spaceId]', `/${spaceId.toString()}`) 11 | .catch(err => log.error('Failed to redirect to "View Space" page:', err)) 12 | } 13 | 14 | export function goToNewPostPage (space: HasSpaceIdOrHandle) { 15 | const { href, as } = createNewPostLinkProps(space) 16 | Router.push(href, as) 17 | .catch(err => log.error('Failed to redirect to "New Post" page:', err)) 18 | } -------------------------------------------------------------------------------- /src/components/urls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './social-share' 2 | export * from './subsocial' 3 | -------------------------------------------------------------------------------- /src/components/urls/social-share.ts: -------------------------------------------------------------------------------- 1 | const SUBSOCIAL_TAG = 'subsocial' 2 | 3 | // TODO should we use fullUrl() here? 4 | const subsocialUrl = (url: string) => `${window.location.origin}${url}` 5 | 6 | export const twitterShareUrl = 7 | ( 8 | url: string, 9 | text?: string 10 | ) => { 11 | const textVal = text ? `text=${text}` : '' 12 | 13 | return `https://twitter.com/intent/tweet?${textVal}&url=${subsocialUrl(url)}&hashtags=${SUBSOCIAL_TAG}&original_referer=${url}` 14 | } 15 | 16 | export const linkedInShareUrl = 17 | ( 18 | url: string, 19 | title?: string, 20 | summary?: string 21 | ) => { 22 | const titleVal = title ? `title=${title}` : '' 23 | const summaryVal = summary ? `summary=${summary}` : '' 24 | 25 | return `https://www.linkedin.com/shareArticle?mini=true&url=${subsocialUrl(url)}&${titleVal}&${summaryVal}` 26 | } 27 | 28 | export const facebookShareUrl = (url: string) => 29 | `https://www.facebook.com/sharer/sharer.php?u=${subsocialUrl(url)}` 30 | 31 | export const redditShareUrl = 32 | ( 33 | url: string, 34 | title?: string 35 | ) => { 36 | const titleVal = title ? `title=${title}` : '' 37 | 38 | return `http://www.reddit.com/submit?url=${subsocialUrl(url)}&${titleVal}` 39 | } 40 | 41 | export const copyUrl = (url: string) => subsocialUrl(url) 42 | -------------------------------------------------------------------------------- /src/components/utils/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonProps } from 'antd/lib/button' 3 | import Link from 'next/link' 4 | 5 | type ButtonLinkProps = ButtonProps & { 6 | href: string, 7 | as?: string, 8 | target?: string 9 | } 10 | 11 | export const ButtonLink = ({ as, href, target, children, ...buttonProps }: ButtonLinkProps) => 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | export default ButtonLink 21 | -------------------------------------------------------------------------------- /src/components/utils/DfAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | import { DfBgImg } from 'src/components/utils/DfBgImg' 4 | import IdentityIcon from 'src/components/utils/IdentityIcon' 5 | import { AnyAccountId } from '@subsocial/types/substrate' 6 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 7 | 8 | export type BaseAvatarProps = { 9 | size?: number, 10 | style?: CSSProperties, 11 | avatar?: string 12 | address: AnyAccountId, 13 | } 14 | 15 | export const BaseAvatar = ({ size = DEFAULT_AVATAR_SIZE, avatar, style, address }: BaseAvatarProps) => { 16 | const icon = nonEmptyStr(avatar) 17 | ? 18 | : 23 | 24 | if (!icon) return null 25 | 26 | return icon 27 | } 28 | 29 | export default BaseAvatar 30 | -------------------------------------------------------------------------------- /src/components/utils/DfBgImg.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { resolveIpfsUrl } from 'src/ipfs' 3 | import Link, { LinkProps } from 'next/link' 4 | 5 | export type BgImgProps = { 6 | src: string, 7 | size?: number | string, 8 | height?: number | string, 9 | width?: number | string, 10 | rounded?: boolean, 11 | className?: string, 12 | style?: CSSProperties 13 | }; 14 | 15 | export function DfBgImg (props: BgImgProps) { 16 | const { src, size, height = size, width = size, rounded = false, className, style } = props 17 | 18 | const fullClass = 'DfBgImg ' + className 19 | 20 | const fullStyle = Object.assign({ 21 | backgroundImage: `url(${resolveIpfsUrl(src)})`, 22 | width: width, 23 | height: height, 24 | minWidth: width, 25 | minHeight: height, 26 | borderRadius: rounded && '50%' 27 | }, style) 28 | 29 | return 30 | } 31 | 32 | type DfBgImageLinkProps = BgImgProps & LinkProps 33 | 34 | export const DfBgImageLink = ({ href, as, ...props }: DfBgImageLinkProps) => 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/utils/DfMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | interface Props { 5 | source?: string 6 | className?: string 7 | } 8 | 9 | export const DfMd = ({ source, className = '' }: Props) => 10 | 15 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import SimpleMDE from 'easymde' 3 | import SimpleMDEReact from 'react-simplemde-editor' 4 | import { AutoSaveId, MdEditorProps } from './types' 5 | import store from 'store' 6 | import { nonEmptyStr } from '@subsocial/utils' 7 | 8 | const getStoreKey = (id: AutoSaveId) => `smde_${id}` 9 | 10 | /** Get auto saved content for MD editor from the browser's local storage. */ 11 | const getAutoSavedContent = (id?: AutoSaveId): string | undefined => { 12 | return id ? store.get(getStoreKey(id)) : undefined 13 | } 14 | 15 | export const clearAutoSavedContent = (id: AutoSaveId) => 16 | store.remove(getStoreKey(id)) 17 | 18 | const AUTO_SAVE_INTERVAL_MILLIS = 5000 19 | 20 | const MdEditor = ({ 21 | className, 22 | options = {}, 23 | events = {}, 24 | onChange = () => void(0), 25 | value, 26 | autoSaveId, 27 | autoSaveIntervalMillis = AUTO_SAVE_INTERVAL_MILLIS, 28 | ...otherProps 29 | }: MdEditorProps) => { 30 | const { toolbar = true, ...otherOptions } = options 31 | 32 | const autosavedContent = getAutoSavedContent(autoSaveId) 33 | 34 | const classToolbar = !toolbar && 'hideToolbar' 35 | 36 | const autosave = autoSaveId 37 | ? { 38 | enabled: true, 39 | uniqueId: autoSaveId, 40 | delay: autoSaveIntervalMillis 41 | } 42 | : undefined 43 | 44 | const newOptions: SimpleMDE.Options = { 45 | previewClass: 'markdown-body', 46 | autosave, 47 | ...otherOptions 48 | } 49 | 50 | useEffect(() => { 51 | if (autosave && nonEmptyStr(autosavedContent)) { 52 | // Need to trigger onChange event to notify a wrapping Ant D. form 53 | // that this editor received a value from local storage. 54 | onChange(autosavedContent) 55 | } 56 | }, []) 57 | 58 | return 66 | } 67 | 68 | export default MdEditor 69 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'antd' 3 | import { MdEditorProps } from './types' 4 | import { isClientSide } from '..' 5 | import ClientMdEditor from './client' 6 | 7 | const TextAreaStub = (props: Omit) => 8 | 9 | 10 | /** 11 | * MdEditor is based on CodeMirror that is a large dependency: 55 KB (gzipped). 12 | * Do not use MdEditor on server side, becasue we don't need it there. 13 | * That's why we import editor dynamically only on the client side. 14 | */ 15 | function Inner (props: MdEditorProps) { 16 | return isClientSide() 17 | ? 18 | : 19 | } 20 | 21 | export default Inner 22 | -------------------------------------------------------------------------------- /src/components/utils/DfMdEditor/types.ts: -------------------------------------------------------------------------------- 1 | import { SimpleMDEEditorProps } from 'react-simplemde-editor' 2 | 3 | export type AutoSaveId = 'space' | 'post' | 'profile' 4 | 5 | export type MdEditorProps = Omit & { 6 | onChange?: (value: string) => any | void 7 | autoSaveId?: AutoSaveId 8 | autoSaveIntervalMillis?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/components/utils/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty } from 'antd' 3 | import { MutedSpan } from './MutedText' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | image?: string 7 | description?: React.ReactNode 8 | }> 9 | 10 | export const NoData = (props: Props) => 11 | {props.description} 16 | } 17 | > 18 | {props.children} 19 | 20 | 21 | export default NoData 22 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/EntityStatusPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WarningPanel, { WarningPanelProps } from '../WarningPanel' 3 | import styles from './index.module.sass' 4 | 5 | export type EntityStatusProps = Partial 6 | 7 | export const EntityStatusPanel = ({ 8 | desc, 9 | actions, 10 | preview = false, 11 | centered = false, 12 | withIcon = true, 13 | className, 14 | style 15 | }: EntityStatusProps) => { 16 | 17 | const alertCss = preview 18 | ? styles.DfEntityStatusInPreview 19 | : styles.DfEntityStatusOnPage 20 | 21 | return 29 | } 30 | 31 | type EntityStatusGroupProps = React.PropsWithChildren<{}> 32 | 33 | export const EntityStatusGroup = ({ children }: EntityStatusGroupProps) => 34 | children 35 | ? 36 | {children} 37 | 38 | : null 39 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/HiddenEntityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Post, Space } from '@subsocial/types/substrate/interfaces' 2 | import React from 'react' 3 | import { isMyAddress } from 'src/components/auth/MyAccountContext' 4 | import HiddenPostButton from 'src/components/posts/HiddenPostButton' 5 | import HiddenSpaceButton from 'src/components/spaces/HiddenSpaceButton' 6 | import { EntityStatusPanel, EntityStatusProps } from './EntityStatusPanel' 7 | 8 | type Props = EntityStatusProps & { 9 | type: 'space' | 'post' | 'comment' 10 | struct: Space | Post 11 | } 12 | 13 | export const HiddenEntityPanel = ({ 14 | type, 15 | struct, 16 | ...otherProps 17 | }: Props) => { 18 | 19 | // If entity is not hidden or it's not my entity 20 | if (!struct.hidden.valueOf() || !isMyAddress(struct.owner)) return null 21 | 22 | const HiddenButton = () => type === 'space' 23 | ? 24 | : 25 | 26 | return ]} 29 | {...otherProps} 30 | /> 31 | } 32 | 33 | export default HiddenEntityPanel 34 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_padding: $space_normal 4 | $_border: 1px solid $color_warn_border 5 | 6 | .DfEntityStatus 7 | display: block 8 | margin-bottom: $space_normal 9 | 10 | \:global .ant-btn 11 | background-color: transparent 12 | margin-left: $space_tiny 13 | 14 | .DfEntityStatusOnPage 15 | padding-top: $_padding 16 | padding-bottom: $_padding 17 | border: $_border 18 | border-radius: $border_radius_normal 19 | 20 | \:global .ant-alert-icon 21 | margin-top: $space_tiny 22 | 23 | .RadiusesForPreview 24 | border-top-left-radius: $border_radius_normal 25 | border-top-right-radius: $border_radius_normal 26 | 27 | .SpacingForPreview 28 | margin: -$space_normal 29 | margin-bottom: $space_normal 30 | 31 | .DfEntityStatusInPreview 32 | @extend .SpacingForPreview 33 | @extend .RadiusesForPreview 34 | border-bottom: $_border 35 | 36 | .DfEntityStatusGroup 37 | @extend .SpacingForPreview 38 | 39 | .DfEntityStatusInPreview 40 | margin: 0 41 | border-radius: 0 42 | border-bottom: $_border 43 | 44 | &:first-child 45 | @extend .RadiusesForPreview 46 | -------------------------------------------------------------------------------- /src/components/utils/EntityStatusPanels/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EntityStatusPanel' 2 | export * from './HiddenEntityPanel' 3 | export * from './PendingSpaceOwnershipPanel' 4 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step1ButtonName = 'Great, I\'m with you. Next' 4 | 5 | export const Step1Content = React.memo(() => <> 6 | 7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 | 12 | 13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 | 17 | >) 18 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/Step3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Step3ButtonName = 'Proceed to faucet on Telegram' 4 | 5 | export const Step3Content = React.memo(() => <> 6 | 7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 | 12 | >) 13 | -------------------------------------------------------------------------------- /src/components/utils/Faucet/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .Faucet 4 | li 5 | margin-top: $space_normal 6 | -------------------------------------------------------------------------------- /src/components/utils/HiddenButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space, Post } from '@subsocial/types/substrate/interfaces' 3 | import { TxCallback } from 'src/components/substrate/SubstrateTxButton' 4 | import { TxDiv } from 'src/components/substrate/TxDiv' 5 | import TxButton from 'src/components/utils/TxButton' 6 | import Router from 'next/router' 7 | 8 | export type FSetVisible = (visible: boolean) => void 9 | 10 | type HiddenButtonProps = { 11 | struct: Space | Post, 12 | newTxParams: () => any[] 13 | type: 'post' | 'space' | 'comment', 14 | setVisibility?: FSetVisible 15 | label?: string, 16 | asLink?: boolean 17 | } 18 | 19 | export function HiddenButton (props: HiddenButtonProps) { 20 | const { struct, newTxParams, label, type, asLink, setVisibility } = props 21 | const hidden = struct.hidden.valueOf() 22 | 23 | const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' 24 | 25 | const onTxSuccess: TxCallback = () => { 26 | setVisibility && setVisibility(!hidden) 27 | Router.reload() 28 | } 29 | 30 | const TxAction = asLink ? TxDiv : TxButton 31 | 32 | return 44 | } 45 | 46 | export default HiddenButton 47 | -------------------------------------------------------------------------------- /src/components/utils/HtmlPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from 'src/components/main/PageWrapper' 3 | 4 | type Props = { 5 | title: string 6 | html: string 7 | } 8 | 9 | /** Use this component carefully and not to oftern, because it allows to inject a dangerous HTML. */ 10 | export const HtmlPage = ({ title, html }: Props) => 11 | 12 | 13 | 14 | 15 | export default HtmlPage 16 | -------------------------------------------------------------------------------- /src/components/utils/IconWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BN from 'bn.js' 3 | import { gtZero } from '.' 4 | 5 | type IconWithTitleProps = { 6 | icon: JSX.Element, 7 | count?: BN, 8 | label?: string 9 | } 10 | 11 | export const IconWithLabel = ({ icon, label, count = new BN(0) }: IconWithTitleProps) => { 12 | const countStr = gtZero(count) ? count.toString() : undefined 13 | const text = label 14 | ? label + (countStr ? ` (${countStr})` : '') 15 | : countStr 16 | 17 | return <> 18 | {icon} 19 | {text && {text}} 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /src/components/utils/IdentityIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2020 @polkadot/react-components authors & contributors 2 | // This software may be modified and distributed under the terms 3 | // of the Apache-2.0 license. See the LICENSE file for details. 4 | 5 | import { IdentityProps as Props } from '@polkadot/react-identicon/types' 6 | 7 | import React from 'react' 8 | import BaseIdentityIcon from '@polkadot/react-identicon' 9 | import Avatar from 'antd/lib/avatar/avatar' 10 | import { DEFAULT_AVATAR_SIZE } from 'src/config/Size.config' 11 | 12 | export function getIdentityTheme (): 'substrate' { 13 | return 'substrate' 14 | } 15 | 16 | export function IdentityIcon ({ prefix, theme, value, size = DEFAULT_AVATAR_SIZE, ...props }: Props): React.ReactElement { 17 | const address = value?.toString() || '' 18 | const thisTheme = theme || getIdentityTheme() 19 | 20 | return ( 21 | } 30 | size={size} 31 | className='DfIdentityIcon' 32 | {...props} 33 | /> 34 | ) 35 | } 36 | 37 | export default IdentityIcon 38 | -------------------------------------------------------------------------------- /src/components/utils/MutedText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = React.PropsWithChildren<{ 4 | smaller?: boolean 5 | className?: string 6 | style?: React.CSSProperties 7 | onClick?: React.MouseEventHandler 8 | }>; 9 | 10 | function getClassNames (props: Props): string { 11 | const { smaller = false, className } = props 12 | return `MutedText grey text ${smaller ? 'smaller' : ''} ${className}` 13 | } 14 | 15 | export const MutedSpan = (props: Props) => { 16 | const { style, onClick, children } = props 17 | return {children} 18 | } 19 | 20 | export const MutedDiv = (props: Props) => { 21 | const { style, onClick, children } = props 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/utils/MyAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withMulti } from '../substrate' 3 | import { useMyAddress } from '../auth/MyAccountContext' 4 | 5 | export type MyAddressProps = { 6 | address?: string 7 | }; 8 | 9 | export type MyAccountProps = MyAddressProps; 10 | 11 | function withMyAddress (Component: React.ComponentType) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount = (Component: React.ComponentType) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 | 5 | Privacy Policy 6 | {' · '} 7 | Terms of Use 8 | 9 | ) 10 | 11 | export default PrivacyPolicyLinks 12 | -------------------------------------------------------------------------------- /src/components/utils/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from 'src/components/utils/types' 3 | 4 | type Props = React.PropsWithChildren 10 | 11 | export const Section = ({ title, level = 2, className, id, children }: Props) => { 12 | 13 | const renderTitle = () => { 14 | if (!title) return null 15 | 16 | const className = 'DfSection-title' 17 | return React.createElement( 18 | `h${level}`, 19 | { className }, 20 | title 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {renderTitle()} 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Section 35 | -------------------------------------------------------------------------------- /src/components/utils/Segment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BareProps } from './types' 3 | 4 | export const Segment: React.FunctionComponent = 5 | ({ children, style, className }) => 6 | 10 | {children} 11 | 12 | 13 | export default Segment 14 | -------------------------------------------------------------------------------- /src/components/utils/StorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from 'react' 2 | 3 | type Storybook = { 4 | isStorybook: boolean 5 | } 6 | 7 | export const StorybookContext = createContext({ isStorybook: false }) 8 | 9 | export const useStorybookContext = () => 10 | useContext(StorybookContext) 11 | 12 | export const StorybookProvider = (props: React.PropsWithChildren<{}>) => { 13 | return 14 | {props.children} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/SubTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const SubTitle = ({ title, className }: Props) => ( 9 | {title} 10 | ) 11 | 12 | export default SubTitle 13 | -------------------------------------------------------------------------------- /src/components/utils/SubsocialConnect.ts: -------------------------------------------------------------------------------- 1 | import { api as apiFromContext } from '../substrate' 2 | import { Api as SubstrateApi } from '@subsocial/api/substrateConnect' 3 | import { offchainUrl, substrateUrl, ipfsNodeUrl, dagHttpMethod } from './env' 4 | import { ApiPromise } from '@polkadot/api' 5 | import { newLogger } from '@subsocial/utils' 6 | import { SubsocialApi } from '@subsocial/api/subsocial' 7 | 8 | const log = newLogger('SubsocialConnect') 9 | 10 | let subsocial!: SubsocialApi 11 | let isLoadingSubsocial = false 12 | 13 | export const newSubsocialApi = (substrateApi: ApiPromise) => { 14 | return new SubsocialApi({ substrateApi, ipfsNodeUrl, offchainUrl, useServer: { 15 | httpRequestMethod: dagHttpMethod as any 16 | }}) 17 | } 18 | 19 | export const getSubsocialApi = async () => { 20 | if (!subsocial && !isLoadingSubsocial) { 21 | isLoadingSubsocial = true 22 | const api = await getSubstrateApi() 23 | subsocial = newSubsocialApi(api) 24 | isLoadingSubsocial = false 25 | } 26 | return subsocial 27 | } 28 | 29 | let api: ApiPromise 30 | let isLoadingSubstrate = false 31 | 32 | const getSubstrateApi = async () => { 33 | if (apiFromContext) { 34 | log.debug('Get Substrate API from context') 35 | return apiFromContext.isReady 36 | } 37 | 38 | if (!api && !isLoadingSubstrate) { 39 | isLoadingSubstrate = true 40 | log.debug('Get Substrate API as Api.connect()') 41 | api = await SubstrateApi.connect(substrateUrl) 42 | isLoadingSubstrate = false 43 | } 44 | 45 | return api 46 | } 47 | -------------------------------------------------------------------------------- /src/components/utils/Suspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export default Suspense 4 | -------------------------------------------------------------------------------- /src/components/utils/TxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AntdButton from 'antd/lib/button' 3 | import { newLogger } from '@subsocial/utils' 4 | 5 | import { isClientSide } from '.' 6 | import { useStorybookContext } from './StorybookContext' 7 | import { useMyAddress } from '../auth/MyAccountContext' 8 | import SubstrateTxButton, { TxButtonProps } from '../substrate/SubstrateTxButton' 9 | 10 | const log = newLogger('TxButton') 11 | 12 | const mockSendTx = () => { 13 | const msg = 'Cannot send a Substrate tx in a mock mode (e.g. in Stoorybook)' 14 | if (isClientSide()) { 15 | window.alert(`WARN: ${msg}`) 16 | } else { 17 | log.warn(msg) 18 | } 19 | } 20 | 21 | function ResolvedTxButton (props: TxButtonProps) { 22 | const { isStorybook = false } = useStorybookContext() 23 | const myAddress = useMyAddress() 24 | 25 | return isStorybook 26 | ? 27 | : 28 | } 29 | 30 | // TODO use React.memo() ?? 31 | export default ResolvedTxButton 32 | -------------------------------------------------------------------------------- /src/components/utils/ViewTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmptyArray, isEmptyStr, nonEmptyStr } from '@subsocial/utils' 2 | import { TagOutlined } from '@ant-design/icons' 3 | import { Tag } from 'antd' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import { BaseProps } from '@polkadot/react-identicon/types' 7 | 8 | type ViewTagProps = { 9 | tag?: string 10 | } 11 | 12 | const ViewTag = React.memo(({ tag }: ViewTagProps) => { 13 | const searchLink = `/search?tags=${tag}` 14 | 15 | return isEmptyStr(tag) 16 | ? null 17 | : 18 | 19 | {tag} 20 | 21 | 22 | }) 23 | 24 | type ViewTagsProps = BaseProps & { 25 | tags?: string[] 26 | } 27 | 28 | export const ViewTags = React.memo(({ 29 | tags = [], 30 | className = '', 31 | ...props 32 | }: ViewTagsProps) => 33 | isEmptyArray(tags) 34 | ? null 35 | : 36 | {tags.filter(nonEmptyStr).map((tag, i) => )} 37 | 38 | ) 39 | 40 | export default ViewTags 41 | -------------------------------------------------------------------------------- /src/components/utils/WarningPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'antd' 3 | import { BareProps } from './types' 4 | 5 | export type WarningPanelProps = BareProps & { 6 | desc: React.ReactNode, 7 | actions?: React.ReactNode[] 8 | preview?: boolean, 9 | withIcon?: boolean, 10 | centered?: boolean, 11 | closable?: boolean 12 | } 13 | 14 | export const WarningPanel = ({ 15 | desc, 16 | actions, 17 | centered, 18 | closable, 19 | withIcon = false, 20 | className, 21 | style 22 | }: WarningPanelProps) => 27 | {desc} 28 | {actions} 29 | 30 | } 31 | banner 32 | showIcon={withIcon} 33 | closable={closable} 34 | type='warning' 35 | /> 36 | 37 | export default WarningPanel 38 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | $_border: 1px solid $color_warn_border 4 | $_height: 40px 5 | 6 | .DfActionButton 7 | margin-left: $space_normal 8 | color: $color_volcano !important 9 | border-color: $color_volcano !important 10 | 11 | .Wrapper 12 | margin-top: 3rem 13 | 14 | @media (max-width: 767px) 15 | .Wrapper 16 | height: $_height 17 | margin-top: 2rem 18 | 19 | .DfWhereAmIPanel 20 | z-index: 1000 21 | position: fixed 22 | bottom: 0px 23 | left: 0px 24 | width: 100% 25 | height: $_height 26 | border-top: $_border 27 | -------------------------------------------------------------------------------- /src/components/utils/WhereAmIPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | import React from 'react' 3 | import { didSignIn } from 'src/components/auth/MyAccountContext' 4 | import { useResponsiveSize } from 'src/components/responsive' 5 | import { isBot, isServerSide } from '..' 6 | import { landingPageUrl } from '../env' 7 | import WarningPanel from '../WarningPanel' 8 | import styles from './index.module.sass' 9 | 10 | const LearnMoreButton = React.memo(() => 11 | 18 | Learn more 19 | 20 | ) 21 | 22 | const InnerPanel = React.memo(() => { 23 | const { isMobile } = useResponsiveSize() 24 | 25 | const msg = isMobile 26 | ? 'You are on Subsocial' 27 | : 'You are on Subsocial – a social networking protocol on Polkadot & IPFS' 28 | 29 | return 30 | ]} 34 | closable 35 | centered 36 | /> 37 | 38 | }) 39 | 40 | export const WhereAmIPanel = () => { 41 | const doNotShow = isServerSide() || didSignIn() || isBot() 42 | return doNotShow ? null : 43 | } 44 | -------------------------------------------------------------------------------- /src/components/utils/content/index.ts: -------------------------------------------------------------------------------- 1 | import { SpaceContent, PostContent, NamedLink } from '@subsocial/types' 2 | import { nonEmptyStr } from '@subsocial/utils' 3 | 4 | export const getNonEmptySpaceContent = (content: SpaceContent): SpaceContent => { 5 | const { tags, links, ...rest } = content 6 | return { 7 | tags: getNonEmptyStrings(tags), 8 | links: getNonEmptyLinks(links) as [], 9 | ...rest 10 | } 11 | } 12 | 13 | export const getNonEmptyPostContent = (content: PostContent): PostContent => { 14 | const { tags, ...rest } = content 15 | return { 16 | tags: getNonEmptyStrings(tags), 17 | ...rest 18 | } 19 | } 20 | 21 | const getNonEmptyStrings = (inputArr: string[] = []): string[] => { 22 | const res: string[] = [] 23 | inputArr.forEach(x => { 24 | if (nonEmptyStr(x)) { 25 | res.push(x.trim()) 26 | } 27 | }) 28 | return res 29 | } 30 | 31 | type Link = string | NamedLink 32 | 33 | const getNonEmptyLinks = (inputArr: Link[] = []): Link[] => { 34 | const res: Link[] = [] 35 | inputArr.forEach(x => { 36 | if (nonEmptyStr(x)) { 37 | res.push(x.trim()) 38 | } else if (typeof x === 'object' && nonEmptyStr(x.url)) { 39 | const { name } = x 40 | res.push({ 41 | name: nonEmptyStr(name) ? name.trim() : name, 42 | url: x.url.trim() 43 | }) 44 | } 45 | }) 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /src/components/utils/forms/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import BN from 'bn.js' 3 | import { pluralize } from '../Plularize' 4 | 5 | export function minLenError (fieldName: string, minLen: number | BN): string { 6 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 7 | } 8 | 9 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 10 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 11 | } 12 | 13 | const URL_MAX_LEN = 2000 14 | 15 | export function urlValidation (urlName: string) { 16 | return Yup.string() 17 | .url(`${urlName} must be a valid URL.`) 18 | .max(URL_MAX_LEN, `${urlName} URL is too long. Maximum length is ${URL_MAX_LEN} chars.`) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/md/SummarizeMd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isEmptyStr } from '@subsocial/utils' 3 | import { mdToText, summarize } from 'src/utils' 4 | import { useIsMobileWidthOrDevice } from 'src/components/responsive' 5 | 6 | const MOBILE_SUMMARY_LEN = 120 7 | const DESKTOP_SUMMARY_LEN = 220 8 | 9 | type Props = { 10 | md?: string 11 | limit?: number 12 | more?: JSX.Element 13 | } 14 | 15 | export const SummarizeMd = ({ md, limit: initialLimit, more }: Props) => { 16 | const isMobile = useIsMobileWidthOrDevice() 17 | 18 | if (isEmptyStr(md)) return null 19 | 20 | const limit = initialLimit 21 | ? initialLimit 22 | : (isMobile 23 | ? MOBILE_SUMMARY_LEN 24 | : DESKTOP_SUMMARY_LEN 25 | ) 26 | 27 | const getSummary = (s?: string) => !s ? '' : summarize(s, { limit }) 28 | 29 | const text = mdToText(md)?.trim() || '' 30 | const summary = getSummary(text) 31 | const showMore = text.length > summary.length 32 | 33 | if (isEmptyStr(summary)) return null 34 | 35 | return ( 36 | 37 | {summary} 38 | {showMore && {' '}{more}} 39 | 40 | ) 41 | } 42 | 43 | export default SummarizeMd 44 | -------------------------------------------------------------------------------- /src/components/utils/md/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SummarizeMd' 2 | -------------------------------------------------------------------------------- /src/components/utils/next.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | 3 | export const return404 = ({ res }: NextPageContext) => { 4 | if (res) { 5 | res.statusCode = 404 6 | } 7 | return { statusCode: 404 } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export type FVoid = () => void 4 | 5 | export interface BareProps { 6 | className?: string 7 | style?: CSSProperties 8 | } 9 | -------------------------------------------------------------------------------- /src/config/ListData.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FIRST_PAGE = 1 2 | export const DEFAULT_PAGE_SIZE = 20 3 | export const MAX_PAGE_SIZE = 100 4 | export const PAGE_SIZE_OPTIONS = [ 10, 20, 50, 100 ] 5 | -------------------------------------------------------------------------------- /src/config/Size.config.ts: -------------------------------------------------------------------------------- 1 | import { isMobile as isBaseMobile, isTablet, isBrowser as isBaseBrowser } from 'react-device-detect' 2 | 3 | export const isMobileDevice = isBaseMobile || isTablet 4 | export const isBrowser = isBaseBrowser 5 | 6 | export const DEFAULT_AVATAR_SIZE = isMobileDevice ? 30 : 36 7 | export const LARGE_AVATAR_SIZE = isMobileDevice ? 60 : 64 8 | -------------------------------------------------------------------------------- /src/config/ValidationsConfig.ts: -------------------------------------------------------------------------------- 1 | export const NAME_MIN_LEN = 3 2 | export const NAME_MAX_LEN = 100 3 | 4 | export const DESC_MAX_LEN = 20_000 5 | 6 | export const MIN_HANDLE_LEN = 5 7 | export const MAX_HANDLE_LEN = 50 8 | -------------------------------------------------------------------------------- /src/ipfs/index.ts: -------------------------------------------------------------------------------- 1 | import { ipfsNodeUrl } from 'src/components/utils/env' 2 | import CID from 'cids' 3 | 4 | const getPath = (cid: string) => `ipfs/${cid}` 5 | 6 | export const resolveIpfsUrl = (cid: string) => { 7 | try { 8 | return CID.isCID(new CID(cid)) ? `${ipfsNodeUrl}/${getPath(cid)}` : cid 9 | } catch (err) { 10 | return cid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubsocialApiProvider } from '../components/utils/SubsocialApiContext' 3 | import { MyAccountProvider } from '../components/auth/MyAccountContext' 4 | import { Navigation } from './Navigation' 5 | import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' 6 | import { AuthProvider } from '../components/auth/AuthContext' 7 | import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' 8 | import { ResponsiveSizeProvider } from 'src/components/responsive' 9 | // import { KusamaProvider } from 'src/components/substrate/KusamaContext'; 10 | 11 | const ClientLayout: React.FunctionComponent = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | {/* */} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | {/* */} 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default ClientLayout 35 | -------------------------------------------------------------------------------- /src/layout/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { registerSubsocialTypes } from '../components/types' 3 | import ClientLayout from './ClientLayout' 4 | import { WhereAmIPanel } from 'src/components/utils/WhereAmIPanel' 5 | 6 | const Page: React.FunctionComponent = ({ children }) => <> 7 | {children} 8 | 9 | > 10 | 11 | const NextLayout: React.FunctionComponent = (props) => { 12 | registerSubsocialTypes() 13 | 14 | return 15 | 16 | 17 | } 18 | 19 | export default NextLayout 20 | -------------------------------------------------------------------------------- /src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useMemo } from 'react' 2 | import { Layout, Drawer } from 'antd' 3 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 4 | 5 | import dynamic from 'next/dynamic' 6 | import { useRouter } from 'next/router' 7 | 8 | const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) 9 | const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) 10 | 11 | const { Header, Sider, Content } = Layout 12 | 13 | interface Props { 14 | children: React.ReactNode; 15 | } 16 | 17 | const HomeNav = () => { 18 | const { state: { collapsed } } = useSidebarCollapsed() 19 | 20 | return 27 | 28 | 29 | } 30 | 31 | const DefaultNav: FunctionComponent = () => { 32 | const { state: { collapsed }, hide } = useSidebarCollapsed() 33 | const { asPath } = useRouter() 34 | 35 | useEffect(() => hide(), [ asPath ]) 36 | 37 | return 47 | 48 | 49 | } 50 | 51 | export const Navigation = (props: Props): JSX.Element => { 52 | const { children } = props 53 | const { state: { asDrawer } } = useSidebarCollapsed() 54 | 55 | const content = useMemo(() => 56 | {children}, 57 | [ children ] 58 | ) 59 | 60 | return 61 | 62 | 63 | 64 | 65 | {asDrawer ? : } 66 | {content} 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/layout/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { CloseCircleOutlined, SearchOutlined, MenuOutlined } from '@ant-design/icons' 3 | import { Button } from 'antd' 4 | import SearchInput from '../components/search/SearchInput' 5 | import { useSidebarCollapsed } from '../components/utils/SideBarCollapsedContext' 6 | import AuthorizationPanel from '../components/auth/AuthorizationPanel' 7 | import Link from 'next/link' 8 | import { useResponsiveSize } from 'src/components/responsive' 9 | import { SignInMobileStub } from 'src/components/auth/AuthButtons' 10 | import { isMobileDevice } from 'src/config/Size.config' 11 | import { uiShowSearch } from 'src/components/utils/env' 12 | 13 | const InnerMenu = () => { 14 | const { toggle } = useSidebarCollapsed() 15 | const { isNotMobile, isMobile } = useResponsiveSize() 16 | const [ show, setShow ] = useState(false) 17 | 18 | const logoImg = '/subsocial-logo.svg' 19 | 20 | return isMobile && show 21 | ? 22 | 23 | setShow(false)} /> 24 | 25 | : 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {isNotMobile && uiShowSearch && } 37 | 38 | {isMobile && uiShowSearch && 39 | setShow(true)} /> 40 | } 41 | {isMobileDevice 42 | ? 43 | : 44 | } 45 | 46 | 47 | } 48 | 49 | export default InnerMenu 50 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | imageShouldBeLessThanTwoMB: 'Image should be less than 2 MB', 3 | notifications: { 4 | AccountFollowed: 'followed your account', 5 | SpaceFollowed: 'followed your space', 6 | SpaceCreated: 'created a new space', 7 | CommentCreated: 'commented on your post', 8 | CommentReplyCreated: 'replied to your comment on', 9 | PostShared: 'shared your post', 10 | CommentShared: 'shared your comment on', 11 | PostReactionCreated: 'reacted to your post', 12 | CommentReactionCreated: 'reacted to your comment on', 13 | }, 14 | activities: { 15 | AccountFollowed: 'followed the account', 16 | SpaceFollowed: 'followed the space', 17 | SpaceCreated: 'created the space', 18 | PostCreated: 'created the post', 19 | PostSharing: 'shared the post', 20 | PostShared: 'shared the post', 21 | CommentCreated: 'commented on the post', 22 | CommentShared: 'shared a comment on', 23 | CommentReplyCreated: 'replied to a comment on', 24 | PostReactionCreated: 'reacted to the post', 25 | CommentReactionCreated: 'reacted to a comment on', 26 | }, 27 | connectingToNetwork: 'Connecting to the network...' 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.EditPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import PostPage from '../../../components/posts/view-post/PostPage' 2 | 3 | export default PostPage 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/about.tsx: -------------------------------------------------------------------------------- 1 | import AboutSpace from '../../components/spaces/AboutSpace' 2 | 3 | export default AboutSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditSpace = dynamic(() => import('../../components/spaces/EditSpace').then((mod: any) => mod.EditSpace), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewSpace from '../../../components/spaces/ViewSpace' 2 | 3 | export default ViewSpace 4 | -------------------------------------------------------------------------------- /src/pages/[spaceId]/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewPost = dynamic(() => import('../../../components/posts/EditPost').then((mod: any) => mod.NewPost), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | // TODO remove global import of all AntD CSS, use modular LESS loading instead. 2 | // See .babelrc options: https://github.com/ant-design/babel-plugin-import#usage 3 | import 'src/styles/antd.css' 4 | 5 | import 'src/styles/bootstrap-utilities-4.3.1.css' 6 | import 'src/styles/components.scss' 7 | import 'src/styles/github-markdown.css' 8 | import 'easymde/dist/easymde.min.css' 9 | 10 | // Subsocial custom styles: 11 | import 'src/styles/subsocial.scss' 12 | import 'src/styles/utils.scss' 13 | import 'src/styles/subsocial-mobile.scss' 14 | 15 | import React from 'react' 16 | import App from 'next/app' 17 | import Head from 'next/head' 18 | import MainPage from '../layout/MainPage' 19 | import { Provider } from 'react-redux' 20 | import store from 'src/redux/store' 21 | 22 | import dayjs from 'dayjs' 23 | import relativeTime from 'dayjs/plugin/relativeTime' 24 | import localizedFormat from 'dayjs/plugin/localizedFormat' 25 | dayjs.extend(relativeTime) 26 | dayjs.extend(localizedFormat) 27 | 28 | function MyApp (props) { 29 | const { Component, pageProps } = props 30 | return ( 31 | <> 32 | 33 | 34 | {/* 35 | See how to work with custom fonts in Next.js: 36 | https://codeconqueror.com/blog/using-google-fonts-with-next-js 37 | */} 38 | {/* */} 39 | {/* */} 40 | {/* */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | MyApp.getInitialProps = async (appContext) => { 52 | // calls page's `getInitialProps` and fills `appProps.pageProps` 53 | const appProps = await App.getInitialProps(appContext) 54 | 55 | return { ...appProps } 56 | } 57 | 58 | export default MyApp 59 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/following.tsx: -------------------------------------------------------------------------------- 1 | import { ListFollowingSpacesPage } from '../../../components/spaces/ListFollowingSpaces' 2 | 3 | export default ListFollowingSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewProfile from '../../../components/profiles/ViewProfile' 2 | 3 | export default ViewProfile 4 | -------------------------------------------------------------------------------- /src/pages/accounts/[address]/spaces.tsx: -------------------------------------------------------------------------------- 1 | import AccountSpacesPage from '../../../components/spaces/AccountSpaces' 2 | 3 | export default AccountSpacesPage 4 | -------------------------------------------------------------------------------- /src/pages/accounts/edit.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const EditProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.EditProfile), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/accounts/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewProfile = dynamic(() => import('../../components/profiles/EditProfile').then((mod: any) => mod.NewProfile), { ssr: false }) 3 | 4 | export default NewProfile 5 | -------------------------------------------------------------------------------- /src/pages/faucet.tsx: -------------------------------------------------------------------------------- 1 | import { PageContent } from 'src/components/main/PageWrapper' 2 | import { Section } from 'src/components/utils/Section' 3 | 4 | // Deprecated: Old Telegram faucet. 5 | // export const page = () => 6 | 7 | const title = 'Subsocial Token Faucet (SMN)' 8 | 9 | export const page = () => ( 10 | 16 | 17 | ⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it. 18 | 19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 | 25 | Sorry for the inconvenience 🙏. 26 | 27 | 28 | ) 29 | 30 | export default page 31 | -------------------------------------------------------------------------------- /src/pages/feed.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowFeed } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyFeed = dynamic(() => import('../components/activity/MyFeed'), { ssr: false }) 6 | 7 | export const Page: NextPage<{}> = () => 8 | 9 | export default uiShowFeed ? Page : PageNotFound 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomePage from '../components/main/HomePage' 2 | 3 | export default HomePage 4 | -------------------------------------------------------------------------------- /src/pages/legal/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './privacy.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/legal/terms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HtmlPage from 'src/components/utils/HtmlPage' 3 | import html from './terms.md' 4 | 5 | export default React.memo(() => ) 6 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { NextPage } from 'next' 3 | import { uiShowNotifications } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | const MyNotifications: NextPage<{}> = dynamic(() => import('../components/activity/MyNotifications'), { ssr: false }) 6 | 7 | export default uiShowNotifications ? MyNotifications : PageNotFound 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import React from 'react' 3 | import { appBaseUrl } from 'src/components/utils/env' 4 | 5 | const createRobotsTxt = () => ` 6 | User-agent: * 7 | Disallow: /_next/static/ 8 | Disallow: /*/new$ 9 | Disallow: /*/*/new$ 10 | Disallow: /*/edit$ 11 | Disallow: /*/*/edit$ 12 | Disallow: /sudo 13 | Disallow: /feed 14 | Disallow: /notifications 15 | Disallow: /search 16 | 17 | Sitemap: ${appBaseUrl}/sitemap/profiles/index.xml 18 | Sitemap: ${appBaseUrl}/sitemap/spaces/index.xml 19 | Sitemap: ${appBaseUrl}/sitemap/posts/index.xml 20 | ` 21 | 22 | class Robots extends React.Component { 23 | public static async getInitialProps ({ res }: NextPageContext) { 24 | if (res) { 25 | res.setHeader('Content-Type', 'text/plain') 26 | res.write(createRobotsTxt()) 27 | res.end() 28 | } 29 | } 30 | } 31 | 32 | export default Robots 33 | -------------------------------------------------------------------------------- /src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | import SearchResults from '../components/search/SearchResults' 3 | import { uiShowSearch } from 'src/components/utils/env' 4 | import { PageNotFound } from 'src/components/utils' 5 | 6 | export default uiShowSearch ? SearchResults : PageNotFound 7 | -------------------------------------------------------------------------------- /src/pages/sitemap/posts/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default PostsSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/posts/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { PostsUrlSet } from 'src/components/sitemap' 2 | 3 | export default PostsUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default ProfilesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/profiles/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { ProfilesUrlSet } from 'src/components/sitemap' 2 | 3 | export default ProfilesUrlSet -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/index.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesSitemapIndex } from 'src/components/sitemap' 2 | 3 | export default SpacesSitemapIndex -------------------------------------------------------------------------------- /src/pages/sitemap/spaces/urlset.xml.ts: -------------------------------------------------------------------------------- 1 | import { SpacesUrlSet } from 'src/components/sitemap' 2 | 3 | export default SpacesUrlSet -------------------------------------------------------------------------------- /src/pages/spaces/index.tsx: -------------------------------------------------------------------------------- 1 | import ListAllSpaces from '../../components/spaces/ListAllSpaces' 2 | 3 | export default ListAllSpaces 4 | -------------------------------------------------------------------------------- /src/pages/spaces/new.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | const NewSpace = dynamic(() => import('../../components/spaces/EditSpace'), { ssr: false }) 3 | 4 | export const page = () => 5 | export default page 6 | -------------------------------------------------------------------------------- /src/pages/sudo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { PageContent } from 'src/components/main/PageWrapper' 4 | 5 | const TITLE = 'Sudo' 6 | 7 | const SudoPage = () => 8 | 9 | 10 | 11 | forceTransfer 12 | 13 | 14 | 15 | 16 | 17 | export default SudoPage -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | getDefaultMiddleware 4 | } from '@reduxjs/toolkit' 5 | import replyIdsByPostIdReducer from './slices/replyIdsByPostIdSlice' 6 | import postByIdReducer from './slices/postByIdSlice' 7 | 8 | export default configureStore({ 9 | reducer: { 10 | replyIdsByPostId: replyIdsByPostIdReducer, 11 | postById: postByIdReducer 12 | }, 13 | middleware: getDefaultMiddleware({ 14 | serializableCheck: false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/redux/types.ts: -------------------------------------------------------------------------------- 1 | import { CommentsState } from './slices/replyIdsByPostIdSlice' 2 | import { PostState } from './slices/postByIdSlice' 3 | import { PostWithAllDetails, PostWithSomeDetails } from '@subsocial/types' 4 | 5 | export type Store = { 6 | replyIdsByPostId: CommentsState 7 | postById: PostState 8 | } 9 | 10 | export type PostsStoreType = PostWithAllDetails | PostWithSomeDetails | (PostWithAllDetails | PostWithSomeDetails)[] 11 | -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage' 2 | 3 | export const newStore = (storeName: string) => 4 | localForage.createInstance({ 5 | name: 'SubsocialDB', 6 | storeName 7 | }) 8 | -------------------------------------------------------------------------------- /src/stories/AccountSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 3 | import { AddressPopup } from '../components/profiles/address-views' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { AccountSelectorView } from '../components/profile-selector/AccountSelector' 6 | 7 | export default { 8 | title: 'Auth | AccountSelector' 9 | } 10 | 11 | export const _AddressPopup = () => ( 12 | 13 | ) 14 | 15 | export const _AccountSelector = () => { 16 | const profilesByAddressMap = new Map() 17 | const aliceAddress = mockAccountAlice.toString() 18 | profilesByAddressMap.set(aliceAddress, mockProfileDataAlice) 19 | 20 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/AddressComponents.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AuthorPreview, ProfilePreview, AddressPopup } from '../components/profiles/address-views' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | 6 | export default { 7 | title: 'Profiles | Previews' 8 | } 9 | 10 | export const _AuthorPreview = () => 11 | {new Date().toLocaleString()}>}/> 12 | 13 | export const _ProfilePreview = () => 14 | 15 | 16 | export const _ProfilePreviewMini = () => 17 | 18 | 19 | export const __AddressPopup = () => 20 | 21 | -------------------------------------------------------------------------------- /src/stories/EditPost.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { InnerEditPost } from '../components/posts/EditPost' 3 | import { mockSpaceId } from './mocks/SpaceMocks' 4 | import { mockPostJson, mockPostStruct, mockPostValidation } from './mocks/PostMocks' 5 | 6 | export default { 7 | title: 'Posts | Edit' 8 | } 9 | 10 | export const _NewPost = () => 11 | 12 | 13 | export const _EditPost = () => 14 | 15 | -------------------------------------------------------------------------------- /src/stories/EditSpace.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditForm } from '../components/spaces/EditSpace' 3 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson, mockSpaceValidation } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | Edit' 7 | } 8 | 9 | export const _NewSpace = () => 10 | 11 | 12 | export const _EditSpace = () => 13 | 14 | -------------------------------------------------------------------------------- /src/stories/HookFormsWithAntd.stories.tsx: -------------------------------------------------------------------------------- 1 | // import { Form } from '@ant-design/compatible'; 2 | // import '@ant-design/compatible/assets/index.css'; 3 | // import { Input, Button } from 'antd'; 4 | // import React from 'react'; 5 | // import { useForm, Controller } from 'react-hook-form'; 6 | // import * as Yup from 'yup'; 7 | 8 | // const buildValidationSchema = () => Yup.object().shape({ 9 | 10 | // send: Yup.string() 11 | // .required('Send is required') 12 | // .min(5, 'Min length is 5') 13 | // }) 14 | 15 | // export default { 16 | // title: 'Form | SimpleLogin' 17 | // } 18 | 19 | // const NormalLoginForm = () => { 20 | 21 | // const { control, errors, watch, handleSubmit } = useForm({ 22 | // validationSchema: buildValidationSchema() 23 | // }) 24 | 25 | // const handle = (data) => { 26 | // console.log(data) 27 | // } 28 | 29 | // return ( 30 | // 31 | // 35 | // } 37 | // name='send' 38 | // control={control} 39 | // /> 40 | // 41 | // 42 | // Submit 43 | // 44 | // 45 | // ); 46 | // } 47 | 48 | // export const _WrappedNormalLoginForm = NormalLoginForm; 49 | -------------------------------------------------------------------------------- /src/stories/ListSpaces.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 4 | 5 | export default { 6 | title: 'Spaces | List' 7 | } 8 | 9 | export const _NoSpacePreviews = () => 10 | 11 | 12 | export const _ListOneSpacePreview = () => 13 | 14 | 15 | export const _ListManySpacePreviews = () => 16 | 17 | -------------------------------------------------------------------------------- /src/stories/Mobile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs' 2 | import './mobile.css' 3 | 4 | export default { 5 | title: 'Mobile', 6 | decorators: [ withKnobs ] 7 | } 8 | -------------------------------------------------------------------------------- /src/stories/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SpaceNav, SpaceNavProps } from '../components/spaces/SpaceNav' 3 | import { NavigationEditor } from '../components/spaces/NavigationEditor' 4 | import { mockSpaceId, mockSpaceStruct, mockSpaceJson } from './mocks/SpaceMocks' 5 | import { mockAccountAlice } from './mocks/AccountMocks' 6 | import { mockNavTabs } from './mocks/NavTabsMocks' 7 | 8 | export default { 9 | title: 'Spaces | Navigation' 10 | } 11 | 12 | const { name, desc, image } = mockSpaceJson 13 | 14 | const commonNavProps: SpaceNavProps = { 15 | spaceId: mockSpaceId, 16 | creator: mockAccountAlice, 17 | name: name, 18 | desc: desc, 19 | image: image, 20 | followingCount: 123, 21 | followersCount: 45678 22 | } 23 | 24 | export const _EmptyNavigation = () => 25 | 26 | 27 | export const _NavigationWithTabs = () => 28 | 29 | 30 | export const _EditNavigation = () => 31 | 32 | -------------------------------------------------------------------------------- /src/stories/Notifications.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Notification } from '../components/activity/Notification' 3 | import { mockProfileDataAlice } from './mocks/SocialProfileMocks' 4 | import { mockAccountAlice } from './mocks/AccountMocks' 5 | import { mockSpaceDataAlice } from './mocks/SpaceMocks' 6 | import { ViewSpace } from '../components/spaces/ViewSpace' 7 | 8 | export default { 9 | title: 'Activity | Notifications' 10 | } 11 | 12 | export const _MyNotifications = () => 13 | and 1 people use here notification >}/> 14 | -------------------------------------------------------------------------------- /src/stories/OnBoarding.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageContent } from '../components/main/PageWrapper' 3 | import { MockAuthProvider, StepsEnum } from '../components/auth/AuthContext' 4 | import { OnBoardingCard } from '../components/onboarding' 5 | 6 | export default { 7 | title: 'Auth | OnBoarding' 8 | } 9 | 10 | export const _OnBoaringCardDisable = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | 18 | export const _OnBoaringCardSignIn = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export const _OnBoaringCardGetTokents = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | export const _OnBoaringCardCreateSpace = () => ( 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/stories/SignInModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LatestSpaces } from '../components/main/LatestSpaces' 3 | import { PageContent } from '../components/main/PageWrapper' 4 | import { mockSpaceDataAlice, mockSpaceDataBob } from './mocks/SpaceMocks' 5 | import { MockAuthProvider, StepsEnum, ModalKind } from '../components/auth/AuthContext' 6 | import SignInModal from '../components/auth/SignInModal' 7 | 8 | export default { 9 | title: 'Auth | SignInModal' 10 | } 11 | 12 | type Props = { 13 | kind: ModalKind 14 | } 15 | 16 | const MockSignInModal = ({ kind }: Props) => ( 17 | console.log('Mock hide')} kind={kind} /> 18 | ) 19 | 20 | export const _WaitSecSignIn = () => ( 21 | 22 | 23 | 24 | ) 25 | 26 | export const _WaitSecGetTokens = () => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const _SignIn = () => ( 33 | 34 | 35 | 36 | ) 37 | 38 | export const _SwitchAccount = () => ( 39 | 40 | 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /src/stories/Team.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditTeamMember } from '../components/spaces/EditTeamMember' 3 | import { suggestedCompanies, suggestedEmployerTypes } from './mocks/TeamMocks' 4 | 5 | export default { 6 | title: 'Spaces | Team' 7 | } 8 | 9 | export const _EditTeamMember = () => { 10 | const props = { 11 | suggestedEmployerTypes, 12 | suggestedCompanies 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/mobile.css: -------------------------------------------------------------------------------- 1 | .my-drawer { 2 | position: relative; 3 | overflow: auto; 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | .my-drawer .am-drawer-sidebar { 7 | background-color: #fff; 8 | overflow: auto; 9 | -webkit-overflow-scrolling: touch; 10 | } 11 | .my-drawer .am-drawer-sidebar .am-list { 12 | width: 300px; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/stories/mockNextRouter.ts: -------------------------------------------------------------------------------- 1 | import Router, { Router as RouterClass } from 'next/router' 2 | import { UrlObject } from 'url' 3 | 4 | type Url = UrlObject | string 5 | 6 | type PrefetchOptions = { 7 | priority?: boolean; 8 | } 9 | 10 | const newPromise = (res: T): Promise => 11 | new Promise(resolve => resolve(res)) 12 | 13 | export const mockNextRouter: RouterClass = { 14 | push: (url: Url, as?: Url, options?: {}) => newPromise(false), 15 | replace: (url: Url, as?: Url, options?: {}) => newPromise(false), 16 | prefetch: (url: string, asPath?: string, options?: PrefetchOptions) => newPromise(void (0)), 17 | query: {} 18 | } as RouterClass 19 | 20 | Router.router = mockNextRouter 21 | -------------------------------------------------------------------------------- /src/stories/mocks/AccountMocks.ts: -------------------------------------------------------------------------------- 1 | import { GenericAccountId as AccountId } from '@polkadot/types' 2 | import { registry } from '@subsocial/types/substrate/registry' 3 | 4 | export const mockAccountAlice = new AccountId(registry, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') 5 | 6 | export const mockAccountBob = new AccountId(registry, '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty') 7 | -------------------------------------------------------------------------------- /src/stories/mocks/NavTabsMocks.ts: -------------------------------------------------------------------------------- 1 | import { NavTab } from '@subsocial/types/offchain' 2 | 3 | export const mockNavTabs: NavTab[] = [ 4 | { 5 | id: 1, 6 | hidden: false, 7 | title: 'Posts by tags', 8 | type: 'by-tag', 9 | description: '', 10 | content: { 11 | data: [ 'crypto', 'coin' ] 12 | } 13 | }, { 14 | id: 2, 15 | hidden: true, 16 | title: 'Search Internet', 17 | type: 'url', 18 | description: 'DuckDuckGo is an internet search engine that emphasizes protecting searchers privacy and avoiding the filter bubble of personalized search results.', 19 | content: { 20 | data: 'https://duckduckgo.com/' 21 | } 22 | }, { 23 | id: 3, 24 | hidden: false, 25 | title: 'Wikipedia', 26 | type: 'url', 27 | description: 'Wikipedia is a multilingual online encyclopedia created and maintained as an open collaboration project by a community of volunteer editors using a wiki-based editing system.', 28 | content: { 29 | data: 'https://www.wikipedia.org/' 30 | } 31 | }, { 32 | id: 4, 33 | hidden: false, 34 | title: 'Example Site', 35 | type: 'url', 36 | description: '', 37 | content: { 38 | data: 'example.com' 39 | } 40 | }, { 41 | id: 5, 42 | hidden: false, 43 | title: 'Q & A', 44 | type: 'by-tag', 45 | description: '', 46 | content: { 47 | data: [ 'question', 'answer', 'help', 'qna' ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/stories/mocks/PostMocks.ts: -------------------------------------------------------------------------------- 1 | import { mockSpaceId } from './SpaceMocks' 2 | import U32 from '@polkadot/types/primitive/U32' 3 | import { registry } from '@subsocial/types/substrate/registry' 4 | import { SpaceId, Post } from '@subsocial/types/substrate/interfaces' 5 | import { PostContent } from '@subsocial/types/offchain' 6 | import BN from 'bn.js' 7 | import { mockAccountAlice } from './AccountMocks' 8 | 9 | let _id = 200 10 | const nextId = (): SpaceId => new BN(++_id) as SpaceId 11 | 12 | export const mockPostId = nextId() 13 | 14 | export const mockPostValidation = { 15 | postMaxLen: new U32(registry, 2000) 16 | } 17 | 18 | export const mockPostStruct = { 19 | id: new BN(34), 20 | created: { 21 | account: mockAccountAlice, 22 | time: new Date().getSeconds() 23 | }, 24 | space_id: mockSpaceId 25 | } as unknown as Post 26 | 27 | export const mockPostJson: PostContent = { 28 | title: 'Example post', 29 | body: 'The most interesting content ever.', 30 | image: '', 31 | tags: [ 'bitcoin', 'ethereum', 'polkadot' ], 32 | canonical: 'http://example.com' 33 | } 34 | -------------------------------------------------------------------------------- /src/stories/mocks/TeamMocks.ts.keep: -------------------------------------------------------------------------------- 1 | import { Company } from '../../components/spaces/EditTeamMember'; 2 | 3 | export const suggestedEmployerTypes = [ 4 | 'Full-time', 5 | 'Part-time', 6 | 'Self-employed', 7 | 'Freelance', 8 | 'Contract', 9 | 'Internship', 10 | 'Apprenticeship' 11 | ] 12 | 13 | export const suggestedCompanies: Company[] = [{ 14 | id: 1, 15 | name: 'Web3 Foundation', 16 | img: 'https://storage.googleapis.com/job-listing-logos/2ae39131-4f27-4944-b9f2-cd7a2e4e2bef.png' 17 | }] 18 | -------------------------------------------------------------------------------- /src/stories/withStorybookContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StorybookProvider } from '../components/utils/StorybookContext' 3 | 4 | export const withStorybookContext = (storyFn: () => React.ReactElement) => 5 | {storyFn()} 6 | -------------------------------------------------------------------------------- /src/styles/components.scss: -------------------------------------------------------------------------------- 1 | /* Copyright 2017-2019 @polkadot/ui-app authors & contributors 2 | /* This software may be modified and distributed under the terms 3 | /* of the Apache-2.0 license. See the LICENSE file for details. */ 4 | 5 | .ui--AddressComponents { 6 | display: inline-block; 7 | padding: 0 0.25rem 0 0; 8 | } 9 | 10 | .ui--AddressComponents.padded { 11 | display: inline-block; 12 | padding: 0.25rem 0 0 0; 13 | } 14 | 15 | .ui--AddressComponents.summary { 16 | position: relative; 17 | top: -0.2rem; 18 | } 19 | 20 | .ui--AddressComponents-info div { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .ui--AddressComponents-address { 26 | width: 100%; 27 | text-align: left; 28 | &.activity { 29 | width: initial; 30 | font-weight: bold; 31 | } 32 | &.withAddr { 33 | font-family: monospace; 34 | } 35 | 36 | &.withName { 37 | text-transform: uppercase; 38 | } 39 | } 40 | 41 | .ui--AddressComponents .ui--IdentityIcon { 42 | margin-left: 1rem; 43 | margin-right: 0.5rem; 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | // See how to work with custom fonts in Next.js: 2 | // https://codeconqueror.com/blog/using-google-fonts-with-next-js 3 | 4 | // @font-face { 5 | // font-family: 'PT Serif'; 6 | // src: url('/fonts/PTSerif-Bold.ttf'); 7 | // } 8 | 9 | // @font-face { 10 | // font-family: 'Noto Serif'; 11 | // src: url('/fonts/NotoSerif-Bold.ttf'); 12 | // } 13 | 14 | // @font-face { 15 | // font-family: 'Merriweather'; 16 | // src: url('/fonts/Merriweather-Bold.ttf'); 17 | // } -------------------------------------------------------------------------------- /src/styles/subsocial-vars.scss: -------------------------------------------------------------------------------- 1 | /*-------------Font Size-----------*/ 2 | 3 | $font_tiny: .75rem; 4 | $font_small: .875rem; 5 | $font_normal: 1rem; 6 | $font_large: 1.35rem; 7 | $font_big: 1.5rem; 8 | $font_huge: 2rem; 9 | 10 | /*----------Space Size-------------*/ 11 | 12 | $space_mini: .25rem; 13 | $space_tiny: .5rem; 14 | $space_small: .75rem; 15 | $space_normal: 1rem; 16 | $space_large: 1.25rem; 17 | $space_big: 1.5rem; 18 | $space_huge: 2rem; 19 | 20 | /*-------------Colors----------*/ 21 | 22 | $color_page_bg: #fafafa; 23 | $color_font_normal: #222222; 24 | $color_muted: #888; 25 | $color_link: #bd018b; 26 | $color_secondary: #595959; 27 | $color_light_border: #ddd; 28 | $color_warn_border: #f3e8ac; 29 | $color_volcano: #fa541c; 30 | $color_hover_selectable_bg: #fff0f6; 31 | 32 | /*------------- Shadow --------*/ 33 | 34 | $shadow: 0 0 5px 2px #eeecec !important; 35 | 36 | /*------------- Misc ----------*/ 37 | 38 | /* 64px is a height of Ant Design toolbar */ 39 | $height_top_menu: 64px; 40 | $border_radius_normal: 4px; 41 | $width_panel: 300px; 42 | $min_width_content: 428px; 43 | $max_width_content: calc(680px + #{$space_normal} * 2); 44 | 45 | // $font_family_title: 'PT Serif', Georgia, serif; 46 | // $font_family_title: 'Noto Serif', Georgia, serif; 47 | // $font_family_title: 'Merriweather', Georgia, serif; 48 | -------------------------------------------------------------------------------- /src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @import './subsocial-vars.scss'; 2 | 3 | .flipH { 4 | display: inline-block; 5 | transform: scale(-1, 1) !important; 6 | -moz-transform: scale(-1, 1) !important; 7 | -webkit-transform: scale(-1, 1) !important; 8 | -o-transform: scale(-1, 1) !important; 9 | -ms-transform: scale(-1, 1) !important; 10 | transform: scale(-1, 1) !important; 11 | } 12 | 13 | .DfDisableLayout { 14 | pointer-events: none; 15 | opacity: 0.9; 16 | } 17 | 18 | .DfSubTitle { 19 | font-weight: bolder; 20 | background-color: #eee; 21 | padding: .25rem; 22 | padding-left: 1rem; 23 | color: $color_secondary; 24 | } 25 | 26 | .DfSecondaryColor { 27 | color: $color_secondary; 28 | &:hover, :active { 29 | color: $color_secondary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // This declaration says to TypeScript compiler that it's OK to import *.md files. 2 | declare module '*.md' { 3 | const content: string 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { AnyAccountId, AnySpaceId } from '@subsocial/types' 2 | import { equalAddresses } from 'src/components/substrate' 3 | 4 | function isReservedPolkadotSpace (id: AnySpaceId): boolean { 5 | return id.gten(1001) && id.lten(1217) 6 | } 7 | 8 | /** 9 | * Simple check if this is an id is of a Polkadot ecosystem project. 10 | */ 11 | export function isPolkaProject (id: AnySpaceId): boolean { 12 | // TODO This logic should be imroved later. 13 | return id.eqn(1) || isReservedPolkadotSpace(id) 14 | } 15 | 16 | export function findSpaceIdsThatCanSuggestIfSudo (sudoAcc: AnyAccountId, myAcc: AnyAccountId, spaceIds: AnySpaceId[]): AnySpaceId[] { 17 | const isSudo = equalAddresses(sudoAcc, myAcc) 18 | return !isSudo ? spaceIds : spaceIds.filter(id => !isReservedPolkadotSpace(id)) 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hacks' 2 | export * from './md' 3 | export * from './num' 4 | export * from './text' 5 | -------------------------------------------------------------------------------- /src/utils/md.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { isEmptyStr } from '@subsocial/utils' 3 | 4 | const remark = require('remark') 5 | const strip = require('strip-markdown') 6 | // const squeezeParagraphs = require('remark-squeeze-paragraphs') 7 | 8 | const processMd = remark() 9 | .use(strip) 10 | // .use(squeezeParagraphs) // <-- doesn't work very well: leaves couple sequential new lines 11 | .processSync 12 | 13 | export const mdToText = (md?: string) => { 14 | if (isEmptyStr(md)) return md 15 | 16 | return String(processMd(md) as string) 17 | // strip-markdown renders URLs as: 18 | // http://hello.com 19 | // so we need to fix this issue 20 | .replace(/:/g, ':') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/num.ts: -------------------------------------------------------------------------------- 1 | /** `def` is a default number that will be returned in case the fuction fails to parse `maybeNum` */ 2 | export const tryParseInt = (maybeNum: string, def: number): number => { 3 | try { 4 | return parseInt(maybeNum) 5 | } catch (err) { 6 | return def 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyStr } from '@subsocial/utils' 2 | import truncate from 'lodash.truncate' 3 | 4 | const DEFAULT_SUMMARY_LEN = 300 5 | 6 | const SEPARATOR = /[.,:;!?()[\]{}\s]+/ 7 | 8 | type SummarizeOpt = { 9 | limit?: number, 10 | omission?: string; 11 | } 12 | 13 | /** Shorten a plain text up to `limit` chars. Split by separators. */ 14 | export const summarize = ( 15 | text: string, 16 | { 17 | limit = DEFAULT_SUMMARY_LEN, 18 | omission = '...' 19 | }: SummarizeOpt 20 | ): string => { 21 | if (isEmptyStr(text)) return '' 22 | 23 | text = (text as string).trim() 24 | 25 | return text.length <= limit 26 | ? text 27 | : truncate(text, { 28 | length: limit, 29 | separator: SEPARATOR, 30 | omission 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /subsocial-betanet.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=info 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=https://app.subsocial.network 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=wss://rpc.subsocial.network 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=https://app.subsocial.network/offchain 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=https://app.subsocial.network/ipfs 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://app.subsocial.network:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://app.subsocial.network:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=false 30 | UI_SHOW_NOTIFICATIONS=false 31 | UI_SHOW_ACTIVITY=false 32 | 33 | # SEO settings 34 | # Date of the last update for the sitemap. Expected format: YYYY-MM-DD 35 | SEO_SITEMAP_LASTMOD='2020-11-21' 36 | SEO_SITEMAP_PAGE_SIZE=100 37 | 38 | # The id of the last space reserved at genesis. The first space has id 1. 39 | LAST_RESERVED_SPACE_ID=1000 40 | 41 | # Ids of reserved spaces that have been claimed. 42 | CLAIMED_SPACE_IDS=1,2,3,4,5 43 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Copyright 2017-2019 @polkadot authors & contributors 3 | // This software may be modified and distributed under the terms 4 | // of the Apache-2.0 license. See the LICENSE file for details. 5 | 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | const Enzyme = require('enzyme'); 8 | 9 | Enzyme.configure({ 10 | adapter: new Adapter() 11 | }); 12 | 13 | module.exports = Enzyme; 14 | -------------------------------------------------------------------------------- /test/test.contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/test/test.contract.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 15 | 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ], 23 | "allowJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noEmit": true, 26 | "module": "esnext", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@polkadot/ts", 32 | "./node_modules/@types", 33 | "./src/types" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "**/*.stories.tsx", 38 | "**/*.stories.ts" 39 | ], 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "src/pages/_app.js" 45 | ] 46 | } 47 | --------------------------------------------------------------------------------
7 | Hi there, you are about to request your Subsocial Tokens (SMN). And as you proceed to 8 | the Request Token page, please take a moment and subscribe to join 9 | our Telegram chat and follow 10 | our Twitter account. 11 |
13 | By signing up for our social media you will be among the first to know about any updates 14 | on the Subsocial Platform and the development process, if anything concerning our technology 15 | is worth talking about, you’ll find it there. 16 |
7 | By clicking {`"${Step3ButtonName}"`} button and requesting Tokens in that Telegram chat 8 | you hereby Agree to 9 | our Terms of Use{' '} 10 | and Privacy Policy. 11 |
(Component: React.ComponentType
) { 12 | return function (props: P) { 13 | const myAddress = useMyAddress() 14 | return 15 | } 16 | } 17 | 18 | export const withMyAccount =
) => 19 | withMulti( 20 | Component, 21 | withMyAddress 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/utils/MyEntityLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | import { useResponsiveSize } from '../responsive' 4 | 5 | type Props = React.PropsWithChildren<{ 6 | isMy?: boolean 7 | }> 8 | 9 | export const MyEntityLabel = ({ isMy = false, children }: Props) => { 10 | const { isNotMobile } = useResponsiveSize() 11 | return isNotMobile && isMy 12 | ? {children} 13 | : null 14 | } 15 | export default MyEntityLabel 16 | -------------------------------------------------------------------------------- /src/components/utils/Plularize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pluralize } from '@subsocial/utils' 3 | import BN from 'bn.js' 4 | 5 | type PluralizeProps = { 6 | count: number | BN | string, 7 | singularText: string, 8 | pluralText?: string 9 | }; 10 | 11 | export { pluralize } 12 | 13 | export function Pluralize (props: PluralizeProps) { 14 | const { count, singularText, pluralText } = props 15 | return <>{pluralize(count, singularText, pluralText)}> 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/PrivacyPolicyLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PrivacyPolicyLinks = () => ( 4 |
⚠️ The faucet is temporarily disabled. ⚠️ We are working on a new version of it.
19 | Follow us on Twitter 20 | (@SubsocialChain) 21 | and Telegram 22 | (@Subsocial) 23 | to not miss important announcements. 24 |
Sorry for the inconvenience 🙏.