├── .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 |
36 | {story()} 37 |
38 | )) 39 | 40 | configure(require.context('../src', true, /\.stories\.tsx?$/), module) 41 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/.storybook/style.css -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | module.exports = ({ config, isServer }) => { 4 | 5 | if (!isServer) { 6 | config.node = { 7 | fs: 'empty' 8 | } 9 | } 10 | 11 | config.module.rules.push( 12 | 13 | // Loader for static files managed by Next.js 14 | { 15 | test: /\.(png|svg|eot|otf|ttf|woff|woff2)$/, 16 | use: { 17 | loader: 'url-loader', 18 | options: { 19 | limit: 8192, 20 | publicPath: '/_next/static/', 21 | outputPath: 'static/', 22 | name: '[name].[ext]' 23 | } 24 | } 25 | }, 26 | 27 | // TypeScript loader 28 | { 29 | test: /\.(ts|tsx)$/, 30 | exclude: /(node_modules)/, 31 | use: [ 32 | { 33 | loader: require.resolve('babel-loader') 34 | }, 35 | ], 36 | } 37 | ); 38 | 39 | config.resolve.extensions.push('.ts', '.tsx'); 40 | 41 | // TSConfig, uses the same file as packages 42 | config.resolve.plugins = config.resolve.plugins || []; 43 | config.resolve.plugins.push( 44 | new TsconfigPathsPlugin({ 45 | configFile: path.resolve(__dirname, '../tsconfig.json'), 46 | }) 47 | ); 48 | 49 | // Stories parser 50 | config.module.rules.push({ 51 | test: /\.stories\.tsx?$/, 52 | loaders: [require.resolve('@storybook/source-loader')], 53 | enforce: 'pre', 54 | }); 55 | 56 | return config; 57 | }; 58 | -------------------------------------------------------------------------------- /deployment.extras.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: production-ingress-substrate-ui 5 | namespace: poc3-122 6 | annotations: 7 | kubernetes.io/ingress.class: traefik 8 | traefik.frontend.entryPoints: "https,http" 9 | spec: 10 | rules: 11 | - host: substrate-ui.parity.io 12 | http: 13 | paths: 14 | - backend: 15 | serviceName: production-service 16 | servicePort: 80 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Ingress 20 | metadata: 21 | name: production-ingress-substrate-ui-light 22 | namespace: poc3-122 23 | annotations: 24 | kubernetes.io/ingress.class: traefik 25 | traefik.frontend.entryPoints: "https,http" 26 | spec: 27 | rules: 28 | - host: substrate-ui-light.parity.io 29 | http: 30 | paths: 31 | - backend: 32 | serviceName: production-service 33 | servicePort: 80 34 | -------------------------------------------------------------------------------- /deployment.template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | data: 4 | # AZURE_DOCKER_REGISTRY_CONFIG is base64 of this: 5 | # {"auths":{"parity.azurecr.io":{"username":"parity","password":"","email":"admin@parity.io","auth":""}}} 6 | .dockerconfigjson: $AZURE_DOCKER_REGISTRY_CONFIG 7 | kind: Secret 8 | metadata: 9 | name: azure-docker-registry-key 10 | type: kubernetes.io/dockerconfigjson 11 | --- 12 | apiVersion: extensions/v1beta1 13 | kind: Deployment 14 | metadata: 15 | name: $CI_ENVIRONMENT_SLUG-backend 16 | spec: 17 | replicas: $REPLICAS 18 | template: 19 | metadata: 20 | labels: 21 | app: $CI_ENVIRONMENT_SLUG 22 | component: backend 23 | spec: 24 | containers: 25 | - name: $CI_ENVIRONMENT_SLUG-backend 26 | image: $DOCKER_IMAGE_FULL_NAME 27 | imagePullPolicy: Always 28 | ports: 29 | - containerPort: 80 30 | imagePullSecrets: 31 | - name: azure-docker-registry-key 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: $CI_ENVIRONMENT_SLUG-service 37 | spec: 38 | selector: 39 | app: $CI_ENVIRONMENT_SLUG 40 | ports: 41 | - name: http 42 | port: 80 43 | targetPort: 80 44 | protocol: TCP 45 | --- 46 | apiVersion: extensions/v1beta1 47 | kind: Ingress 48 | metadata: 49 | name: $CI_ENVIRONMENT_SLUG-ingress 50 | annotations: 51 | kubernetes.io/ingress.class: traefik 52 | traefik.frontend.entryPoints: "https,http" 53 | spec: 54 | rules: 55 | - host: $AUTODEVOPS_HOST 56 | http: 57 | paths: 58 | - backend: 59 | serviceName: $CI_ENVIRONMENT_SLUG-service 60 | servicePort: 80 61 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 as builder 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y libusb-1.0-0-dev build-essential git curl gnupg gcc-4.8 g++-4.8 \ 5 | && export CXX=g++-4.8 6 | 7 | RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - 8 | RUN apt-get install -y nodejs 9 | RUN npm install yarn -g 10 | 11 | COPY package.json yarn.lock* ./ 12 | RUN yarn install --no-optional 13 | 14 | WORKDIR /app 15 | COPY . . 16 | 17 | RUN yarn 18 | 19 | COPY localhost.env .env 20 | RUN NODE_ENV=production yarn build 21 | 22 | FROM node:10-slim 23 | 24 | COPY --from=builder /app . 25 | 26 | RUN yarn add pm2 -G 27 | 28 | ENTRYPOINT [ "yarn", "pm2", "start", "--no-daemon" ] 29 | CMD [ "yarn start" ] 30 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: "3" 3 | services: 4 | web-ui: 5 | build: 6 | context: .. 7 | dockerfile: ./docker/Dockerfile 8 | image: dappforce/subsocial-ui:latest 9 | container_name: subsocial-web-ui 10 | restart: on-failure 11 | network_mode: "host" 12 | 13 | nginx: 14 | build: ./nginx 15 | container_name: subsocial-proxy 16 | image: dappforce/subsocial-proxy:latest 17 | restart: on-failure 18 | network_mode: "host" 19 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN rm /etc/nginx/conf.d/* 4 | 5 | COPY ./default.conf /etc/nginx/conf.d/ 6 | 7 | EXPOSE 80 8 | 9 | CMD [ "nginx", "-g", "daemon off;" ] -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | 4 | server_name _; 5 | 6 | server_tokens off; 7 | 8 | location / { 9 | proxy_pass http://localhost:3003; 10 | } 11 | 12 | location /bc/ { 13 | rewrite ^/bc(.*) /$1 break; 14 | proxy_pass http://localhost:3002; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /export-env.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const { writeFileSync } = require('fs') 4 | 5 | require('dotenv').config() 6 | 7 | const varsToExport = [ 8 | 'NODE_ENV', 9 | 'LOG_LEVEL', 10 | 'APP_NAME', 11 | 'APP_BASE_URL', 12 | 'SUBSTRATE_URL', 13 | 'OFFCHAIN_URL', 14 | 'OFFCHAIN_WS', 15 | 'APPS_URL', 16 | 'IPFS_URL', 17 | 'UI_SHOW_ADVANCED', 18 | 'UI_SHOW_SEARCH', 19 | 'UI_SHOW_FEED', 20 | 'UI_SHOW_NOTIFICATIONS', 21 | 'UI_SHOW_ACTIVITY', 22 | 'SEO_SITEMAP_LASTMOD', 23 | 'SEO_SITEMAP_PAGE_SIZE', 24 | 'LAST_RESERVED_SPACE_ID', 25 | 'CLAIMED_SPACE_IDS', 26 | 'DAG_HTTP_METHOD' 27 | ] 28 | 29 | function getSerializedVal (varName) { 30 | const val = process.env[varName] 31 | return typeof val === 'string' ? `'${val}'` : val 32 | } 33 | 34 | const vals = varsToExport 35 | .map(varName => `${varName}: ${getSerializedVal(varName)}`) 36 | .join(',\n ') 37 | 38 | const jsFile = `${__dirname}/public/env.js` 39 | 40 | console.log(`Export .env to ${jsFile}`) 41 | 42 | writeFileSync(jsFile, 43 | `// WARN: This is a generated file. Do not modify! 44 | 45 | if (!window.process) window.process = {}; 46 | if (!window.process.ENV) window.process.ENV = {}; 47 | 48 | window.process.env = { 49 | ${vals} 50 | }; 51 | `, 'utf8' 52 | ) 53 | -------------------------------------------------------------------------------- /localhost.env: -------------------------------------------------------------------------------- 1 | # Logger level 2 | LOG_LEVEL=debug 3 | 4 | # The name of this application 5 | APP_NAME='Subsocial' 6 | 7 | APP_BASE_URL=http://localhost:3003 8 | 9 | # Substrate Node config 10 | SUBSTRATE_URL=ws://127.0.0.1:9944 11 | 12 | # Offchain config 13 | OFFCHAIN_URL=http://127.0.0.1:3001 14 | 15 | # IPFS config 16 | # Port 5001 - IPFS Go with write access. 17 | # Port 8080 - Read only. 18 | IPFS_URL=http://127.0.0.1:8080 19 | 20 | # Notifications Web Socket 21 | OFFCHAIN_WS=ws://127.0.0.1:3011 22 | 23 | # JS Apps config 24 | APPS_URL=http://127.0.0.1:3002 25 | 26 | # UI settings 27 | UI_SHOW_ADVANCED=true 28 | UI_SHOW_SEARCH=true 29 | UI_SHOW_FEED=true 30 | UI_SHOW_NOTIFICATIONS=true 31 | UI_SHOW_ACTIVITY=true 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 | 44 | DAG_HTTP_METHOD='GET' 45 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /patch-polkadot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Patch Polkadot JS files with 'window'" 4 | cp -rf ./fixes/fixed-polkadot-ext.js ./node_modules/@polkadot/extension-dapp/index.js 5 | -------------------------------------------------------------------------------- /patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./patch-polkadot.sh 4 | yarn export-env 5 | -------------------------------------------------------------------------------- /public/chrome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/Merriweather-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/public/fonts/Merriweather-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/NotoSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/public/fonts/NotoSerif-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/PTSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/public/fonts/PTSerif-Bold.ttf -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Subsocial Network Portal 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Subsocial", 3 | "name": "A decentralized social network", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "256x256 128x128 64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/subsocial-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappforce/subsocial-web-app/24ca2fe94aedf98e1c9e349b3e2e82b40821a4e6/public/subsocial-sign.png -------------------------------------------------------------------------------- /public/subsocial-sign.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/substrate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yarn run dev 4 | -------------------------------------------------------------------------------- /src/components/activity/FeedActivities.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isDef } from '@subsocial/utils' 3 | import { LoadMoreFn } from './NotificationUtils' 4 | import { PostWithAllDetails } from '@subsocial/types' 5 | import PostPreview from '../posts/view-post/PostPreview' 6 | import { LoadMoreProps, ActivityProps } from './types' 7 | import { SubsocialApi } from '@subsocial/api/subsocial' 8 | import BN from 'bn.js' 9 | import { InnerActivities } from './InnerActivities' 10 | 11 | const postsFromActivity = async (subsocial: SubsocialApi, postIds: BN[]): Promise => { 12 | const posts = await subsocial.findPublicPostsWithAllDetails(postIds) 13 | 14 | return posts.filter(x => isDef(x.space)) 15 | } 16 | 17 | export const getLoadMoreFeedFn = (getActivity: LoadMoreFn, keyId: 'post_id' | 'comment_id') => 18 | async (props: LoadMoreProps) => { 19 | const { subsocial, address, page, size } = props 20 | 21 | if (!address) return [] 22 | 23 | const offset = (page - 1) * size 24 | const activity = await getActivity(address, offset, size) || [] 25 | const postIds = activity.map(x => new BN(x[keyId])) 26 | 27 | return postsFromActivity(subsocial, postIds) 28 | } 29 | 30 | export const FeedActivities = (props: ActivityProps) => 33 | } 34 | /> 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/activity/InnerActivities.tsx: -------------------------------------------------------------------------------- 1 | import { InnerActivitiesProps } from './types' 2 | import { useSubsocialApi } from '../utils/SubsocialApiContext' 3 | import { useState, useEffect, useCallback } from 'react' 4 | import { notDef } from '@subsocial/utils' 5 | import { InfiniteListByPage } from '../lists/InfiniteList' 6 | import { Loading } from '../utils' 7 | 8 | export function InnerActivities ({ address, title, getCount, totalCount, noDataDesc, loadingLabel, loadMore, ...otherProps }: InnerActivitiesProps) { 9 | const { subsocial, isApiReady } = useSubsocialApi() 10 | const [ total, setTotalCount ] = useState(totalCount) 11 | 12 | useEffect(() => { 13 | if (!address || !getCount) return 14 | 15 | getCount 16 | ? getCount(address).then(setTotalCount) 17 | : setTotalCount(0) 18 | }, [ address ]) 19 | 20 | const noData = notDef(total) 21 | 22 | const Activities = useCallback(() => loadMore({ subsocial, address, page, size})} 25 | loadingLabel={loadingLabel} 26 | title={title ? `${title} (${total})` : null} 27 | noDataDesc={noDataDesc} 28 | totalCount={total || 0} 29 | 30 | />, [ isApiReady, noData, total ]) 31 | 32 | if (!isApiReady || noData) return 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /src/components/activity/MyFeed.tsx: -------------------------------------------------------------------------------- 1 | import { getLoadMoreFeedFn, FeedActivities } from './FeedActivities' 2 | import { BaseActivityProps } from './types' 3 | import { getFeedCount, getNewsFeed } from '../utils/OffchainUtils' 4 | import { useMyAddress } from '../auth/MyAccountContext' 5 | import NotAuthorized from '../auth/NotAuthorized' 6 | import { PageContent } from '../main/PageWrapper' 7 | 8 | const TITLE = 'My feed' 9 | const loadingLabel = 'Loading your feed...' 10 | 11 | type MyFeedProps = { 12 | title?: string 13 | } 14 | 15 | const loadMoreFeed = getLoadMoreFeedFn(getNewsFeed, 'post_id') 16 | 17 | export const InnerMyFeed = (props: BaseActivityProps) => 24 | 25 | 26 | export const MyFeed = ({ title }: MyFeedProps) => { 27 | const myAddress = useMyAddress() 28 | 29 | if (!myAddress) return 30 | 31 | return 32 | 33 | 34 | } 35 | 36 | export default MyFeed 37 | -------------------------------------------------------------------------------- /src/components/activity/MyNotifications.tsx: -------------------------------------------------------------------------------- 1 | import { useMyAddress } from '../auth/MyAccountContext' 2 | import NotAuthorized from '../auth/NotAuthorized' 3 | import { PageContent } from '../main/PageWrapper' 4 | import { Notifications } from './Notifications' 5 | 6 | const NOTIFICATION_TITLE = 'My notifications' 7 | 8 | export const MyNotifications = () => { 9 | const myAddress = useMyAddress() 10 | 11 | if (!myAddress) return 12 | 13 | return 14 | 15 | 16 | } 17 | 18 | export default MyNotifications 19 | -------------------------------------------------------------------------------- /src/components/activity/Notification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DfBgImageLink } from '../utils/DfBgImg' 3 | import { nonEmptyStr } from '@subsocial/utils' 4 | import Avatar from '../profiles/address-views/Avatar' 5 | import Name from '../profiles/address-views/Name' 6 | import { MutedDiv } from '../utils/MutedText' 7 | import { NotificationType } from './NotificationUtils' 8 | import Link from 'next/link' 9 | 10 | export function Notification (props: NotificationType) { 11 | const { address, notificationMessage, details, image = '', owner, links } = props 12 | const avatar = owner?.content?.avatar 13 | 14 | return 15 | 16 | 17 |
18 |
19 | 20 | {notificationMessage} 21 |
22 | {details} 23 |
24 | {nonEmptyStr(image) && } 25 |
26 | 27 | } 28 | 29 | export default Notification 30 | -------------------------------------------------------------------------------- /src/components/activity/types.ts: -------------------------------------------------------------------------------- 1 | import { ParsedPaginationQuery } from '../utils/getIds' 2 | import { SubsocialApi } from '@subsocial/api/subsocial' 3 | import { ActivityStore } from './NotificationUtils' 4 | 5 | export type LoadMoreProps = ParsedPaginationQuery & { 6 | subsocial: SubsocialApi 7 | address?: string, 8 | activityStore?: ActivityStore 9 | } 10 | 11 | type GetCountFn = (account: string) => Promise 12 | 13 | export type BaseActivityProps = { 14 | address: string, 15 | totalCount?: number, 16 | title?: string, 17 | } 18 | 19 | export type ActivityProps = BaseActivityProps & { 20 | loadMore: (props: LoadMoreProps) => Promise, 21 | getCount?: GetCountFn, 22 | noDataDesc?: string, 23 | loadingLabel?: string 24 | } 25 | 26 | export type InnerActivitiesProps = ActivityProps & { 27 | renderItem: (item: T, index: number) => JSX.Element, 28 | } 29 | -------------------------------------------------------------------------------- /src/components/api/useSubsocialEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, DependencyList, useMemo } from 'react' 2 | import { SubsocialConsts, useSubsocialApi } from '../utils/SubsocialApiContext' 3 | import { isFunction } from '@polkadot/util' 4 | import { SubsocialApi } from '@subsocial/api/subsocial' 5 | import { SubsocialSubstrateApi } from '@subsocial/api/substrate' 6 | import { SubsocialIpfsApi } from '@subsocial/api/ipfs' 7 | 8 | type Apis = { 9 | subsocial: SubsocialApi 10 | substrate: SubsocialSubstrateApi 11 | ipfs: SubsocialIpfsApi 12 | consts: SubsocialConsts 13 | } 14 | 15 | type EffectCallbackResult = void | (() => void | undefined) 16 | type EffectCallback = (apis: Apis) => EffectCallbackResult 17 | 18 | /** Effect callback will be called only if API is ready. */ 19 | export default function useSubsocialEffect ( 20 | effect: EffectCallback, 21 | deps: DependencyList = [] 22 | ): void { 23 | 24 | const _deps = useMemo(() => JSON.stringify(deps), deps) 25 | const apis = useSubsocialApi() 26 | const isReady = apis.isApiReady 27 | 28 | // console.log('useSubsocialEffect: deps:', _deps) 29 | 30 | useEffect(() => { 31 | if (isReady && isFunction(effect)) { 32 | // At this point all APIs should be initialized and ready to use. 33 | // That's why we can treat them as defined here and cast to their types. 34 | return effect({ 35 | subsocial: apis.subsocial as SubsocialApi, 36 | substrate: apis.substrate as SubsocialSubstrateApi, 37 | ipfs: apis.ipfs as SubsocialIpfsApi, 38 | consts: apis.consts 39 | }) 40 | } 41 | }, [ isReady, _deps ]) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/auth/AuthButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button, { ButtonSize, ButtonType } from 'antd/lib/button' 3 | import { useAuth, ModalKind } from './AuthContext' 4 | import { useMyAccount } from './MyAccountContext' 5 | import { useSubsocialApi } from '../utils/SubsocialApiContext' 6 | 7 | type InnerAuthButtonProps = { 8 | type?: ButtonType 9 | size?: ButtonSize 10 | title?: string 11 | className?: string 12 | } 13 | 14 | type OpenAuthButton = InnerAuthButtonProps & { 15 | kind: ModalKind 16 | } 17 | 18 | export function OpenAuthButton ({ 19 | type = 'default', 20 | size, 21 | title = 'Click me', 22 | kind = 'OnBoarding', 23 | className 24 | }: OpenAuthButton) { 25 | const { isApiReady } = useSubsocialApi() 26 | const { openSignInModal } = useAuth() 27 | 28 | return 37 | } 38 | 39 | type SignInButtonProps = InnerAuthButtonProps & { 40 | isPrimary?: boolean 41 | }; 42 | 43 | export const SignInMobileStub = () => 44 | 45 | export const SignInButton = ({ 46 | isPrimary, 47 | size, 48 | title = 'Sign in' 49 | }: SignInButtonProps) => 55 | 56 | type SwitchAccountButtonProps = InnerAuthButtonProps 57 | 58 | export const SwitchAccountButton = ({ 59 | size, 60 | title = 'Switch account' 61 | }: SwitchAccountButtonProps) => ( 62 | 69 | ) 70 | 71 | type SignOutButtonProps = InnerAuthButtonProps 72 | 73 | export function SignOutButton ({ 74 | size, 75 | title = 'Sign out' 76 | }: SignOutButtonProps) { 77 | const { signOut } = useMyAccount() 78 | 79 | return
80 | 87 |
88 | } 89 | -------------------------------------------------------------------------------- /src/components/auth/AuthorizationPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useMyAddress } from './MyAccountContext' 3 | import { MyAccountPopup } from '../profiles/address-views' 4 | import { SignInButton } from './AuthButtons' 5 | import { NewPostButtonInTopMenu } from '../posts/NewPostButtonInTopMenu' 6 | 7 | export const AuthorizationPanel = () => { 8 | const address = useMyAddress() 9 | return <> 10 | {address 11 | ? <> 12 | 13 | 14 | 15 | : 16 | } 17 | 18 | } 19 | 20 | export default AuthorizationPanel 21 | -------------------------------------------------------------------------------- /src/components/auth/NotAuthorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NoData from '../utils/EmptyList' 3 | import { AuthorizationPanel } from './AuthorizationPanel' 4 | 5 | export const NotAuthorized = () => 6 | 7 | 8 | 9 | 10 | export default NotAuthorized 11 | -------------------------------------------------------------------------------- /src/components/auth/OnlySudo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import AccountId from '@polkadot/types/generic/AccountId' 3 | import useSubsocialEffect from '../api/useSubsocialEffect' 4 | import NoData from '../utils/EmptyList' 5 | import { Loading } from '../utils' 6 | import { useMyAddress } from './MyAccountContext' 7 | import { equalAddresses } from '../substrate' 8 | 9 | export const NotSudo = React.memo(() => 10 | 11 | ) 12 | 13 | type OnlySudoProps = React.PropsWithChildren<{}> 14 | 15 | export const OnlySudo = ({ children }: OnlySudoProps) => { 16 | const myAddress = useMyAddress() 17 | const [ sudo, setSudo ] = useState() 18 | 19 | useSubsocialEffect(({ substrate }) => { 20 | const load = async () => { 21 | const api = await substrate.api 22 | const sudo = await api.query.sudo.key() 23 | setSudo(sudo) 24 | } 25 | load() 26 | }, []) 27 | 28 | const iAmSudo = equalAddresses(myAddress, sudo) 29 | 30 | return sudo 31 | ? <> 32 | {/*
Sudo: {sudo.toString()}
*/} 33 | {iAmSudo ? children : } 34 | 35 | : 36 | } 37 | -------------------------------------------------------------------------------- /src/components/comments/CommentEditor.module.sass: -------------------------------------------------------------------------------- 1 | .DfCommentEditor 2 | 3 | \:global .CodeMirror-scroll 4 | min-height: auto !important 5 | -------------------------------------------------------------------------------- /src/components/comments/helpers/index.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .BumbleContent 4 | display: inline-block 5 | margin-top: $space_tiny !important 6 | padding: $space_small $space_normal 7 | border-radius: 15px 8 | border-top-left-radius: 0 9 | background-color: #fff 10 | box-shadow: $shadow 11 | 12 | .DfMdEditor 13 | margin-top: $space_small 14 | 15 | .DfCommentAlert 16 | margin-top: -$space_small 17 | margin-right: -$space_normal 18 | margin-left: -$space_normal 19 | margin-bottom: $space_small 20 | border-top-left-radius: 0 !important 21 | border-top-right-radius: 15px !important -------------------------------------------------------------------------------- /src/components/comments/helpers/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HiddenPostAlert } from 'src/components/posts/view-post' 3 | import { DfMd } from 'src/components/utils/DfMd' 4 | import { CommentData } from '@subsocial/types/dto' 5 | import styles from './index.module.sass' 6 | import { HasPostId } from 'src/components/urls' 7 | 8 | type CommentBodyProps = { 9 | comment: CommentData 10 | } 11 | 12 | export const CommentBody = ({ comment: { struct, content } }: CommentBodyProps) => { 13 | return
14 | 15 | 16 |
17 | } 18 | 19 | const FAKE = 'fake' 20 | 21 | export const isFakeId = (comment: HasPostId) => comment.id.toString().startsWith(FAKE) 22 | -------------------------------------------------------------------------------- /src/components/forms/AntForms.module.sass: -------------------------------------------------------------------------------- 1 | $space: 1rem 2 | 3 | .DfForm 4 | margin: 2rem 0 5 | 6 | \:global .ant-form-item 7 | margin-bottom: $space 8 | 9 | \:global .ant-btn 10 | margin-right: $space 11 | -------------------------------------------------------------------------------- /src/components/forms/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './AntForms' 2 | export * from './messages' 3 | -------------------------------------------------------------------------------- /src/components/forms/messages.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { pluralize } from '../utils/Plularize' 3 | 4 | export function minLenError (fieldName: string, minLen: number | BN): string { 5 | return `${fieldName} is too short. Minimum length is ${pluralize(minLen, 'char')}.` 6 | } 7 | 8 | export function maxLenError (fieldName: string, maxLen: number | BN): string { 9 | return `${fieldName} is too long. Maximum length is ${pluralize(maxLen, 'char')}.` 10 | } 11 | -------------------------------------------------------------------------------- /src/components/lists/DataList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { List } from 'antd' 3 | import { PaginationConfig } from 'antd/lib/pagination' 4 | import Section from 'src/components/utils/Section' 5 | import NoData from 'src/components/utils/EmptyList' 6 | 7 | export type DataListOptProps = { 8 | title?: React.ReactNode, 9 | level?: number, 10 | noDataDesc?: React.ReactNode, 11 | noDataExt?: React.ReactNode, 12 | className?: string, 13 | } 14 | 15 | export type DataListProps = DataListOptProps & { 16 | totalCount?: number, 17 | dataSource: T[], 18 | renderItem: (item: T, index: number) => JSX.Element, 19 | paginationConfig?: PaginationConfig, 20 | children?: React.ReactNode 21 | } 22 | 23 | export function DataList (props: DataListProps) { 24 | const { 25 | dataSource, 26 | totalCount, 27 | renderItem, 28 | className, 29 | title, 30 | level, 31 | noDataDesc = null, 32 | noDataExt, 33 | paginationConfig, 34 | children 35 | } = props 36 | 37 | const total = totalCount || dataSource.length 38 | 39 | const hasData = total > 0 40 | 41 | const list = hasData 42 | ? 49 | 50 | {renderItem(item, index)} 51 | 52 | } 53 | > 54 | {children} 55 | 56 | : {noDataExt} 57 | 58 | const renderTitle = () => 59 |
{title}
60 | 61 | return !title 62 | ? list 63 | :
{list}
64 | } 65 | 66 | export default DataList 67 | -------------------------------------------------------------------------------- /src/components/lists/utils.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { DEFAULT_FIRST_PAGE, DEFAULT_PAGE_SIZE } from 'src/config/ListData.config' 4 | 5 | type ParamsHookProps = { 6 | triggers?: any[] 7 | defaultSize: number 8 | } 9 | 10 | export const useLinkParams = ({ triggers = [], defaultSize }: ParamsHookProps) => { 11 | const { pathname, asPath } = useRouter() 12 | 13 | return useCallback((page: number, currentSize?: number) => { 14 | const size = currentSize || defaultSize 15 | const sizeParam = size && size !== DEFAULT_PAGE_SIZE ? `&size=${size}` : '' 16 | const pageParam = page !== DEFAULT_FIRST_PAGE ? `page=${page}` : '' 17 | const params = `${pageParam}${sizeParam}` 18 | const query = params ? `?${params}` : '' 19 | return { 20 | href: `${pathname}${query}`, 21 | as: `${asPath.split('?')[0]}${query}` 22 | } 23 | }, [ pathname, asPath, ...triggers ]) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/main/LatestPosts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PostWithAllDetails } from '@subsocial/types' 3 | import PostPreview from '../posts/view-post/PostPreview' 4 | import DataList from '../lists/DataList' 5 | 6 | type Props = { 7 | postsData: PostWithAllDetails[] 8 | type: 'post' | 'comment' 9 | } 10 | 11 | export const LatestPosts = (props: Props) => { 12 | const { postsData = [], type } = props 13 | const posts = postsData.filter((x) => typeof x.post.struct !== 'undefined') 14 | 15 | if (posts.length === 0) { 16 | return null 17 | } 18 | 19 | return 23 | 24 | } 25 | /> 26 | } 27 | -------------------------------------------------------------------------------- /src/components/main/LatestSpaces.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Space } from '@subsocial/types/substrate/interfaces' 3 | import { ViewSpace } from '../spaces/ViewSpace' 4 | import { SpaceData } from '@subsocial/types/dto' 5 | import { CreateSpaceButton, AllSpacesLink } from '../spaces/helpers' 6 | import DataList from '../lists/DataList' 7 | import { ButtonLink } from 'src/components/utils/ButtonLink' 8 | 9 | type Props = { 10 | spacesData: SpaceData[] 11 | canHaveMoreSpaces?: boolean 12 | } 13 | 14 | export const LatestSpaces = (props: Props) => { 15 | const { spacesData = [], canHaveMoreSpaces = true } = props 16 | const spaces = spacesData.filter((x) => typeof x.struct !== 'undefined') 17 | 18 | return <> 19 | 21 | {'Latest spaces'} 22 | {canHaveMoreSpaces && } 23 | } 24 | dataSource={spaces} 25 | noDataDesc='No spaces found' 26 | noDataExt={} 27 | renderItem={(item) => 28 | 35 | } 36 | /> 37 | {canHaveMoreSpaces && 38 | 39 | See all spaces 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/components/onboarding/OnBoardingCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAuth, StepsEnum } from '../auth/AuthContext' 3 | import { OnBoarding, OnBoardingButton, CurrentStep } from '.' 4 | 5 | const onBoadingTitle =

Get started with Subsocial

6 | 7 | type OnBoardingCardViewProps = { 8 | initialized?: boolean 9 | } 10 | 11 | export const OnBoardingCardView = ({ initialized }: OnBoardingCardViewProps) => ( 12 |
13 | {onBoadingTitle} 14 | 15 | 16 |
17 | ) 18 | 19 | export const OnBoardingCard = () => { 20 | const { state: { currentStep, showOnBoarding } } = useAuth() 21 | 22 | const initialized = currentStep !== StepsEnum.Disabled 23 | if (!showOnBoarding) return null 24 | 25 | return 26 | } 27 | 28 | type OnBoardingMobileCardViewProps = CurrentStep 29 | 30 | export const OnBoardingMobileCardView = ({ currentStep }: OnBoardingMobileCardViewProps) => ( 31 |
32 | Join Subsocial. Step {currentStep + 1}/3 33 | 34 |
35 | ) 36 | 37 | export const OnBoardingMobileCard = () => { 38 | const { state: { currentStep, showOnBoarding } } = useAuth() 39 | 40 | if (!showOnBoarding || currentStep === StepsEnum.Disabled) return null 41 | 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /src/components/onboarding/OnBoardingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { OnBoarding, stepItems } from './OnBoarding' 3 | import { useAuth } from '../auth/AuthContext' 4 | import Button from 'antd/lib/button' 5 | import { PageContent } from '../main/PageWrapper' 6 | 7 | type Props = { 8 | title?: string, 9 | onlyStep?: number 10 | } 11 | 12 | export const OnBoardingPage = ({ 13 | title = 'Get started with Subsocial', 14 | onlyStep 15 | }: Props) => { 16 | const { state: { currentStep } } = useAuth() 17 | const step = onlyStep || currentStep 18 | const desc = stepItems[step].content 19 | 20 | return ( 21 | 22 |
23 |

{title}

24 | {!onlyStep && } 25 |
26 | {desc} 27 |
28 | 29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | export default OnBoardingPage 37 | -------------------------------------------------------------------------------- /src/components/onboarding/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './OnBoarding' 2 | export * from './OnBoardingCard' 3 | export * from './OnBoardingPage' 4 | -------------------------------------------------------------------------------- /src/components/posts/HiddenPostButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Post } from '@subsocial/types/substrate/interfaces' 3 | import HiddenButton from '../utils/HiddenButton' 4 | import { PostUpdate, OptionId, OptionBool, OptionIpfsContent } from '@subsocial/types/substrate/classes' 5 | import { isComment } from './view-post' 6 | 7 | type HiddenPostButtonProps = { 8 | post: Post, 9 | asLink?: boolean 10 | }; 11 | 12 | export function HiddenPostButton (props: HiddenPostButtonProps) { 13 | const { post } = props 14 | const hidden = post.hidden.valueOf() 15 | 16 | const newTxParams = () => { 17 | const update = new PostUpdate( 18 | { 19 | // If we provide a new space_id in update, it will move this post to another space. 20 | space_id: new OptionId(), 21 | content: new OptionIpfsContent(), 22 | hidden: new OptionBool(!hidden) // TODO has no implementation on UI 23 | }) 24 | return [ post.id, update ] 25 | } 26 | 27 | return 28 | } 29 | 30 | export default HiddenPostButton 31 | -------------------------------------------------------------------------------- /src/components/posts/NewPostButtonInTopMenu.module.sass: -------------------------------------------------------------------------------- 1 | @import 'src/styles/subsocial-vars.scss' 2 | 3 | .DfSpacePreview 4 | 5 | \:global .ProfileDetails 6 | align-items: center 7 | padding: $space_small $space_normal 8 | 9 | border: 1px solid transparent 10 | border-radius: $border_radius_normal 11 | cursor: pointer 12 | 13 | \:global .handle 14 | // font-size: $font_normal 15 | 16 | &:hover 17 | border-color: $color_link 18 | // background-color: $color_page_bg 19 | 20 | \:global .handle 21 | color: $color_link 22 | -------------------------------------------------------------------------------- /src/components/posts/PostPreviewList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import BN from 'bn.js' 3 | import { Loading } from '../utils' 4 | import useSubsocialEffect from '../api/useSubsocialEffect' 5 | import { PostWithAllDetails } from '@subsocial/types' 6 | import PostPreview from './view-post/PostPreview' 7 | 8 | type OuterProps = { 9 | postIds: BN[] 10 | } 11 | 12 | type ResolvedProps = { 13 | posts: PostWithAllDetails[] 14 | } 15 | 16 | export function withLoadPostsWithSpaces

(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 | 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 | ? 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 | 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