├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── RELEASE-NOTES.md ├── comments-example-screenshot.png ├── config ├── jest │ ├── cssTransform.js │ ├── fileTransform.js │ └── setup.js └── polyfills.js ├── examples ├── .babelrc ├── Example.js ├── dist │ ├── bundle.js │ └── index.html ├── index.js ├── index.scss ├── webpack.config.babel.js └── webpack.config.live.babel.js ├── index.d.ts ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── previews └── screenshot ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── sass ├── App.scss ├── Comment.scss ├── Context.scss ├── Dialogue.scss ├── EmojiPicker.scss ├── Footer.scss ├── Input.scss ├── PopupWindow.scss ├── Reactions.scss ├── Vote.scss ├── index.scss └── variables.scss ├── src ├── App.test.js ├── assets │ ├── 3BoxCommentsSpinner.svg │ ├── 3BoxLogo.svg │ ├── ArrowDown.svg │ ├── ArrowUp.svg │ ├── Delete.svg │ ├── Dots.svg │ ├── Profile.svg │ └── Reply.svg ├── components │ ├── Comment.jsx │ ├── Context.jsx │ ├── Dialogue.jsx │ ├── Emoji │ │ ├── EmojiIcon.jsx │ │ ├── EmojiPicker.jsx │ │ ├── PopupWindow.jsx │ │ └── emojiData.json │ ├── Footer.jsx │ ├── Input.jsx │ ├── Reactions.jsx │ └── Vote.jsx ├── css │ ├── App.css │ ├── Comment.css │ ├── Context.css │ ├── Dialogue.css │ ├── EmojiPicker.css │ ├── Footer.css │ ├── Input.css │ ├── PopupWindow.css │ ├── Reactions.css │ ├── Vote.css │ ├── index.css │ └── variables.css ├── getAddr.js ├── index.js ├── logo.svg ├── serviceWorker.js └── utils.js ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties" 5 | ], 6 | "ignore": [ 7 | "./src/sass" 8 | ] 9 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true, 7 | "jest": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 7, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "plugins": [ 17 | "react" 18 | ], 19 | "extends": ["eslint:recommended", "plugin:react/recommended"], 20 | "rules": { 21 | "no-multiple-empty-lines": ["error", { "max": 2 }], 22 | "indent": "off", 23 | "no-console": 1, 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/*.js 3 | lib/* 4 | # examples/dist/*.js removed, required for pages 5 | coverage 6 | 7 | .idea 8 | 9 | # lock files 10 | package-lock.json 11 | yarn.lock 12 | 13 | # others 14 | .DS_Store 15 | yarn-error.log 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | yarn-error.log 4 | 5 | # Coverage directory used by tools like istanbul 6 | coverage 7 | .nyc_output 8 | 9 | # Dependency directories 10 | node_modules 11 | 12 | # npm package lock 13 | package-lock.json 14 | yarn.lock 15 | 16 | # project files 17 | src 18 | test 19 | dist 20 | examples 21 | CHANGELOG.md 22 | .travis.yml 23 | .editorconfig 24 | .eslintignore 25 | .eslintrc 26 | .babelrc 27 | .gitignore 28 | 29 | 30 | # lock files 31 | package-lock.json 32 | yarn.lock 33 | 34 | # others 35 | .DS_Store 36 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dinesh Pandiyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/discord/484729862368526356.svg?style=for-the-badge)](https://discordapp.com/invite/Z3f3Cxy) 2 | [![npm](https://img.shields.io/npm/v/3box-comments-react.svg?style=for-the-badge)](https://www.npmjs.com/package/3box-comments-react) 3 | [![Twitter Follow](https://img.shields.io/twitter/follow/3boxdb.svg?style=for-the-badge&label=Twitter)](https://twitter.com/3boxdb) 4 | 5 | # 3Box Comments Plugin 💬 6 | 7 | > ⚠️ This plugin is now deprecated - see the [migration FAQ](https://www.notion.so/threebox/FAQ-3Box-Migration-to-Ceramic-9db767a0aded46279fce9eab97920a4f) for more details 8 | 9 | `3box-comments-react` node package is a drop-in react component that provides Web3 developers with a readymade commenting system for their Ethereum application. Easily add rich, decentralized social discourse to your dApp with one line of code. The 3Box Comments plugin is built using 3Box infrastructure, and handles all logic for creating a comments thread. *Read the docs on [docs.3box.io](https://docs.3box.io/build/plugins/comments)*. 10 | 11 | ### New Social Features: 12 | **Replies** - reply to comments inline (limited to two levels of replies), deleted comments with nested replies maintain the nested replies
13 | **Emoji reactions** - react to each comment with emojis from an inline picker
14 | **Votes** - Upvote or downvote on any comment
15 | *Note: This is a breaking change - this will break any previously used comments, to create a new one, simply update the threadName in the same space* 16 |
17 | 18 | 19 | ### Try the demo [here](https://3box.github.io/3box-comments-react/examples/dist/) 20 | ![Example Screenshot](comments-example-screenshot.png) 21 |
22 | 23 | ## How it Works 24 | #### Architecture 25 | The Comments plugin is built using a standard implementation of [Open Threads](https://docs.3box.io/build/web-apps/messaging/choose#open-threads) which are defined in the [3Box Threads API](https://docs.3box.io/api/messaging) and made available via the [`3Box.js SDK`](https://github.com/3box/3box-js). The Comments plugin also includes UI for inputting and displaying user comments, logic for fetching user profiles, and pagination. The component is configurable to various authentication patterns, and can handle both Web3/3Box logged-in & logged-out states. 26 | 27 | #### Authentication 28 | Without authenticating, users can read messages in the comment thread. However authentication is required to perform more interactive functionality. After the user is authenticated, a user can post a comment, delete their comment, and receive comments from other users in *real-time*. Note: if you are not logged in and the component has not been passed your ethereum address, the component cannot know which comment belongs to you in order to delete. In this case, click the login button just below the input UI. 29 |
30 |
31 | 32 | ## Getting Started 33 | 1. Install the component 34 | 2. Choose your authentication pattern 35 | 3. Configure application settings 36 | 4. Usage 37 | 38 | ### 1. Install the component 39 | 40 | ```shell 41 | npm i -S 3box-comments-react 42 | ``` 43 | 44 | ### 2. Choose your authentication pattern 45 | Depending on *when and how* your dApp handles authentication for web3 and 3Box, you will need to provide a different set of props to the component. Regardless of the context, the component must be passed either an ethereum provider or the user's box object before it mounts. Three acceptable authentication patterns and their respective props are discussed below in A-C: 46 | 47 | **A) Dapp handles web3 and 3Box logins, and they run *before* component is mounted. (recommended)** 48 | 49 | Dapp integrates with `3Box.js SDK` and the `3box-comments-react` component. In this case, the `box` instance returned from `Box.openBox(ethAddr)` via 3Box.js should be passed to the `box` prop in the comments component. The user's current Ethereum address should be passed to the `currentUserAddr` prop to determine `deletePost` access on each comment. 50 | 51 | **B) Dapp handles web3 and 3Box logins, but they haven't run before component is mounted. (recommended)** 52 | 53 | Dapp integrates with `3Box.js SDK` and the `3box-comments-react` component. In this case, the login logic implemented in the dapp should be passed to the Comments component as the `loginFunction` prop, which is run when a user attempts to post a comment. The user's current Ethereum address should be passed to the `currentUserAddr` prop to determine `deletePost` access on each comment. 54 | 55 | **C) Dapp has no web3 and 3Box login logic.** 56 | 57 | Dapp only integrates with the `3box-comments-react` component, but not `3Box.js SDK`. All web3 and 3Box login logic will be handled within the Comments component, though it's required for the `ethereum` object from your dapp's preferred web3 provider be passed to the `ethereum` prop in the component. 58 | 59 | #### Best practice 60 | 61 | For the best UX, we recommend implementing one of the following authentication patterns: A; B; or B with A. 62 | 63 | Each of these patterns allow your application to make the `box` object available in global application state where it can be used by all instances of the Comments component regardless of which page the user is on. This global pattern removes the need for users to authenticate on each individual page they wish to comment on, which would be the case in C. 64 | 65 | ### 3. Configure application settings 66 | 67 | **First, choose a name for your application's 3Box space.** 68 | 69 | Although you are free to choose whichever name you'd like for your app's space, we recommend using the name of your app. If your application already has a 3Box space, you are welcome to use that same one for comments. 70 | 71 | **Next, choose a naming convention for your application's threads.** 72 | 73 | Comment threads need a name, and we recommend that your application creates `threadNames` according to a simple rule. We generally like using a natural identifier, such as community name, page URL, token ID, or other similar means. 74 | 75 | **Then, create an admin 3Box account for your application.** 76 | 77 | Each thread is required to have an admin (`adminEthAddr`), which possesses the rights to moderate the thread. We recommend you create an admin Ethereum account for your application so you can perform these actions. While technically you can use any Ethereum address as an admin account, we recommend [creating a 3Box profile](https://3box.io/hub) for that address so if you need to take action in the thread, others will know and trust you as the admin. 78 | 79 | **Lastly, initialize your application's space.** 80 | 81 | Before threads can be deployed in your dapp, your application's admin account (`adminEthAddr` created in the previous step) must first open the space (`spaceName`) that will be used to store your application's threads. This step *must* be completed before your comment threads can be used by others. This process would likely be done outside the context of your dapp, probably in a test environment. 82 | 83 | Simply open a space by running (via `3Box.js SDK`): 84 | ``` 85 | const box = await Box.openBox(adminEthAddr, ethereum); 86 | const space = await box.openSpace(spaceName, spaceOpts); 87 | ``` 88 | Sign the web3 prompts to complete the space initialization process. 89 |
90 |
91 | 92 | 93 | ### 4. Usage 94 | 95 | #### Example 96 | 97 | ```jsx 98 | import ThreeBoxComments from '3box-comments-react'; 99 | 100 | const MyComponent = ({ handleLogin, box, ethereum, myAddress, currentUser3BoxProfile, adminEthAddr }) => ( 101 | `https://mywebsite.com/user/${address}`} 122 | /> 123 | ); 124 | ``` 125 | 126 | 127 | #### Prop Types 128 | 129 | | Property | Type | Default | Required Case | Description | 130 | | :-------------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 131 | | `spaceName` | String | | Always | Likely your dApp name and / or comment category. A single `spaceName` with different `threadName`s is common practice when building a dApp with multiple Comment threads. | 132 | | `threadName` | String | | Always | A name specific to this Comments thread. | 133 | | `adminEthAddr` | String (Ethereum Address) | | Always | The Ethereum address you wish to give admin rights to for the Comments thread. This user will be able to delete all comments and accept members in a members-only thread. **A thread with a new admin address, despite identical `spaceName` and `threadName`, will create an entirely new thread.**| 134 | | `box` | Object | | A (and likely B) | The `box` instance returned from running `await Box.openBox(address, web3)` somewhere in your dApp.| 135 | | `currentUserAddr` | String (Ethereum Address) | | A & B, optional for C | The current user's Ethereum address. Passing this will help determine whether a user has delete access on each comment. This prop will also let the component fetch that user's 3Box profile on component mount and render that data (profile picture) in the Comment input UI. | 136 | | `loginFunction` | Function | | B | A function from your dApp that handles web3 and 3Box login at the global dApp state. This callback will run when a user attempts to save a comment but a `box` instance doesn't yet exist. Running this function should result in a box instance (from `const box = Box.openBox(address, web3)`) being passed as the `box` prop to this component. | 137 | | `ethereum` | Object | window.ethereum | Always | The `ethereum` object from whichever web3 provider your dApp uses. The `enable` method on this object will be used to get the current user's Ethereum address and that address will be used to `openBox` within the current Component context.| 138 | | `members` | Boolean | False | Optional | A boolean - `true` - to make the thread a members-only thread. Passing `false` will allow all users to post to the thread. **Changing this setting after creating it will result in an entirely different thread** (see [Docs.3box.io](https://Docs.3box.io) for more info). | 139 | | `showCommentCount` | Integer | 30 | Optional | The number of comments rendered in the UI by default on component mount and the number of additional comments revealed after clicking `Load more` in component. | 140 | | `spaceOpts` | Object | | Optional | Optional parameters for threads (see [Docs.3box.io](https://Docs.3box.io) for more info)| 141 | | `threadOpts` | Object | | Optional | Optional parameters for threads (see [Docs.3box.io](https://Docs.3box.io) for more info)| 142 | | `useHovers` | Boolean | False | Optional | Pass true to enable a 3Box profile pop up when hovering over a commenter's name | 143 | | `currentUser3BoxProfile` | Object | | Optional | If the current user has already had their 3Box data fetched at the global dApp state, pass the object returned from `Box.getProfile(profileAddress)` to avoid an extra request. This data will be rendered in the Comment input interface.| 144 | | `userProfileURL` | Function | Defaults to returning user's 3Box profile URL | Optional | A function that returns a correctly formatted URL of a user's profile on the current platform. The function will be passed an Ethereum address within the component, if needed. A user will be redirected to the URL returned from this function when clicking on the name or Ethereum address associated with the comment in the thread.| 145 | 146 | ## Maintainers 147 | [@oed](https://github.com/oed) 148 | 149 | ## License 150 | 151 | MIT 152 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v3.0.5 - 2020-07-18 4 | * fix: crash when user profile is cleared 5 | * chore: update 3box library 6 | 7 | ## v3.0.2 - 2020-06-03 8 | * chore: update did-resolver libraries 9 | 10 | ## v3.0.1 - 2020-05-01 11 | * fix: make ethereum prop required - for initial openThread 12 | * fix: allow empty box when commenting - necessary for passing loginFunction 13 | * fix: authenticate for actions 14 | * feat: add loading animation for all interactions that require login step 15 | 16 | ## v2.0.3 - 2020-04-24 17 | * fix: openSpace and joinThread before posting to thread 18 | 19 | ## v2.0.2 - 2020-04-14 20 | * fix: typescript typings by community members 21 | 22 | ## v2.0.1 - 2020-04-14 23 | * fix: must open space and get thread from joinThread from newly opened space 24 | 25 | ## v2.0.0 - 2020-03-26 26 | * chore: update 3box lib to 1.17.1 27 | * feat: output CSS instead of SCSS 28 | * feat: remove autofocus on main input 29 | 30 | ## v1.0.2 - 2020-02-14 31 | * chore: update 3box lib to 1.17.0 32 | 33 | ## v1.0.1 - 2020-02-05 34 | * feat: autofocus input on click 35 | * feat: maintain emoji order on click 36 | 37 | ## v1.0.0 - 2020-01-13 38 | * Add replies, reactions, and up/downvotes to comments plugin 39 | 40 | ## v0.0.9 - 2020-01-13 41 | * fix: stricter css rules 42 | 43 | ## v0.0.8 - 2020-01-13 44 | * fix: move node-sass from devDep to dep 45 | 46 | ## v0.0.7 - 2020-01-09 47 | * chore: update to new 3box lib v1.16 48 | * feat: use new Create & Auth methods, exposing onUpdate to first mounted thread 49 | 50 | ## v0.0.3 - 2020-01-09 51 | * fix: move missing dep `react-inlinesvg` from devDep to dep 52 | 53 | ## v0.0.2 - 2020-01-06 54 | * fix: do not precompile package 55 | -------------------------------------------------------------------------------- /comments-example-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3box/3box-comments-react/3147f73a5b5cd37247e1d45305e63d98a9067e71/comments-example-screenshot.png -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | process(src, filename, config, options) { 6 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /config/jest/setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | // React 16 Enzyme adapter 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | // Make Enzyme functions available in all test files without importing 6 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties" 5 | ] 6 | } -------------------------------------------------------------------------------- /examples/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '3box'; 3 | 4 | import Comments from '../src/index'; 5 | import './index.scss'; 6 | 7 | class Example extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | box: {}, 12 | myProfile: {}, 13 | myAddress: '', 14 | isReady: false, 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | this.handleLogin(); 20 | } 21 | 22 | handleLogin = async () => { 23 | const addresses = await window.ethereum.enable(); 24 | const myAddress = addresses[0]; 25 | 26 | const box = await Box.openBox(myAddress, window.ethereum, {}); 27 | const myProfile = await Box.getProfile(myAddress); 28 | 29 | box.onSyncDone(() => this.setState({ box })); 30 | this.setState({ box, myProfile, myAddress, isReady: true }); 31 | } 32 | 33 | render() { 34 | const { 35 | box, 36 | myAddress, 37 | isReady, 38 | } = this.state; 39 | 40 | return ( 41 |
42 |
43 |
44 |

45 | 3Box Comments Demo 46 |

47 |
48 |

49 | Your super cool dApp 50 |

51 |
52 |
53 | 54 |
55 | {isReady && ( 56 | 67 | )} 68 |
69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | export default Example; 76 | -------------------------------------------------------------------------------- /examples/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3Box Comments Demo 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Example from './Example'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | // AppContainer is a necessary wrapper component for HMR 7 | 8 | const render = (Component) => { 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | }; 16 | 17 | render(Example); 18 | 19 | // Hot Module Replacement API 20 | if (module.hot) { 21 | module.hot.accept('./Example', () => { 22 | render(Example) 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /examples/index.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | width: 100vw; 3 | height: 100vh; 4 | 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: center; 8 | flex-direction: column; 9 | } 10 | 11 | .page { 12 | width: 100vw; 13 | display: flex; 14 | justify-content: flex-start; 15 | align-items: center; 16 | flex-direction: column; 17 | 18 | .page_description { 19 | margin-bottom: 10px; 20 | color: white; 21 | font-size: 36px; 22 | } 23 | } 24 | 25 | .page_dapp { 26 | width: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: flex-start; 31 | background-color: #ec5d85; 32 | height: 36vh; 33 | padding-top: 50px; 34 | } 35 | 36 | .userscontainer { 37 | margin-top: 60px; 38 | width: 100%; 39 | display: flex; 40 | justify-content: center; 41 | align-items: flex-start; 42 | } 43 | 44 | .page_content { 45 | border: 1px solid #dadada; 46 | border-radius: 8px; 47 | height: 25vh; 48 | width: 100%; 49 | max-width: 600px; 50 | background-color: #fcfcfc; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | 55 | p { 56 | color: #adadad; 57 | } 58 | } 59 | 60 | @media only screen and (max-width: 600px) { 61 | .userscontainer { 62 | width: 70%; 63 | } 64 | 65 | .page_dapp { 66 | padding: 20px 0 30px 0; 67 | } 68 | 69 | .page_description { 70 | width: 80%; 71 | } 72 | 73 | .page_content { 74 | width: 80%; 75 | } 76 | } -------------------------------------------------------------------------------- /examples/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; //eslint-disable-line 3 | import CleanWebpackPlugin from 'clean-webpack-plugin'; 4 | 5 | export default () => ({ 6 | mode: 'production', 7 | entry: { 8 | index: path.join(__dirname, './index.js') 9 | }, 10 | 11 | output: { 12 | filename: 'bundle.js', 13 | path: path.resolve(__dirname, 'dist') 14 | }, 15 | 16 | module: { 17 | rules: [{ 18 | test: /.jsx?$/, 19 | exclude: /node_modules/, 20 | 21 | use: [{ 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env', '@babel/preset-react'] 25 | } 26 | }] 27 | }, 28 | { 29 | test:/\.(s*)css$/, 30 | loader: 'style-loader!css-loader!sass-loader' 31 | }, { 32 | test: /\.svg$/, 33 | loader: 'svg-inline-loader?classPrefix' 34 | } 35 | ] 36 | }, 37 | 38 | resolve: { 39 | extensions: ['.js', '.jsx', '.scss', '.svg', '.json', 'css'] 40 | }, 41 | 42 | plugins: [ 43 | // Clean dist folder 44 | new CleanWebpackPlugin(['./dist/build.js']) 45 | ] 46 | }); -------------------------------------------------------------------------------- /examples/webpack.config.live.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | export default () => ({ 5 | mode: 'development', 6 | entry: [ 7 | 'react-hot-loader/patch', 8 | // activate HMR for React 9 | 10 | 'webpack-dev-server/client?http://localhost:8080', 11 | // bundle the client for webpack-dev-server 12 | // and connect to the provided endpoint 13 | 14 | 'webpack/hot/only-dev-server', 15 | // bundle the client for hot reloading 16 | // only- means to only hot reload for successful updates 17 | 18 | './examples/index.js' 19 | // the entry point of our app 20 | ], 21 | 22 | output: { 23 | filename: 'bundle.js', 24 | path: path.resolve(__dirname, 'dist'), 25 | publicPath: '/' 26 | // necessary for HMR to know where to load the hot update chunks 27 | }, 28 | 29 | devtool: 'inline-source-map', 30 | 31 | devServer: { 32 | hot: true, 33 | // enable HMR on the server 34 | 35 | contentBase: path.resolve(__dirname, 'dist'), 36 | // match the output path 37 | 38 | publicPath: '/', 39 | // match the output `publicPath` 40 | 41 | stats: 'minimal' 42 | }, 43 | 44 | module: { 45 | rules: [{ 46 | test: /.jsx?$/, 47 | exclude: /node_modules/, 48 | use: [{ 49 | loader: 'babel-loader', 50 | options: { 51 | presets: ['@babel/preset-env', '@babel/preset-react'] 52 | } 53 | }] 54 | }, 55 | { 56 | test:/\.(s*)css$/, 57 | loader: 'style-loader!css-loader!sass-loader' 58 | }, { 59 | test: /\.svg$/, 60 | loader: 'svg-inline-loader?classPrefix' 61 | } 62 | ] 63 | }, 64 | 65 | resolve: { 66 | extensions: ['.js', '.jsx', '.scss', '.svg', '.json', 'css'] 67 | }, 68 | 69 | plugins: [new webpack.HotModuleReplacementPlugin()], 70 | optimization: { 71 | namedModules: true 72 | } 73 | }); -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | declare class ThreeBoxComments extends React.Component { } 4 | 5 | interface ThreeBoxCommentsProps { 6 | box?: any; 7 | loginFunction?: any; 8 | ethereum?: any; 9 | threadOpts?: any; 10 | currentUser3BoxProfile?: any; 11 | spaceOpts?: any; 12 | members?: boolean; 13 | useHovers?: boolean; 14 | showCommentCount?: number; 15 | currentUserAddr?: string; 16 | userProfileURL?: (url: string) => string; 17 | 18 | spaceName: string; 19 | threadName: string; 20 | adminEthAddr: string; 21 | } 22 | 23 | export default ThreeBoxComments; 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/manifest'); 2 | require('./dist/vendor'); 3 | module.exports = require('./dist/index').default; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageReporters: [ 3 | 'json', 4 | 'lcov', 5 | 'text-summary' 6 | ], 7 | moduleFileExtensions: [ 8 | 'js', 9 | 'jsx', 10 | 'scss' 11 | ], 12 | modulePaths: [ 13 | './src' 14 | ], 15 | setupFiles: [ 16 | '/config/jest/setup.js' 17 | ], 18 | transform: { 19 | '^.+\\.(js|jsx)$': '/node_modules/babel-jest', 20 | '^.+\\.css$': '/config/jest/cssTransform.js', 21 | '^(?!.*\\.(js|jsx|css|json)$)': '/config/jest/fileTransform.js' 22 | }, 23 | transformIgnorePatterns: [ 24 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$' 25 | ] 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3box-comments-react", 3 | "version": "3.0.5", 4 | "description": "Comments component for decentralized applications by 3Box", 5 | "main": "lib/index.js", 6 | "author": "3box.io", 7 | "license": "MIT", 8 | "contributors": [ 9 | "Kenzo Nakamura " 10 | ], 11 | "keywords": [ 12 | "comments", 13 | "3Box", 14 | "decentralized", 15 | "ethereum" 16 | ], 17 | "scripts": { 18 | "build": "webpack --config webpack.config.babel.js", 19 | "build:es5": "rm -rf ./lib; npm run scss; ./node_modules/.bin/babel src --out-dir lib --source-maps inline --copy-files --ignore=src/__tests__/,src/__mocks__/,src/sass", 20 | "build-examples": "webpack --config examples/webpack.config.babel.js --progress", 21 | "clean": "rm -rf dist coverage", 22 | "coverage": "jest --coverage", 23 | "scss": "node-sass sass -o src/css", 24 | "watch": "node-sass --watch scss sass -o src/css", 25 | "lint": "eslint ./src", 26 | "prepublish": "npm run clean && npm run test && npm run build && npm run build:es5", 27 | "start": "webpack-dev-server --config examples/webpack.config.live.babel.js; npm run watch", 28 | "test": "npm run lint && npm run coverage" 29 | }, 30 | "devDependencies": { 31 | "@babel/cli": "^7.7.7", 32 | "@babel/core": "^7.1.2", 33 | "@babel/plugin-proposal-class-properties": "^7.5.5", 34 | "@babel/preset-env": "^7.1.0", 35 | "@babel/preset-react": "^7.0.0", 36 | "@babel/register": "^7.0.0", 37 | "babel-eslint": "^10.0.1", 38 | "babel-jest": "^23.6.0", 39 | "babel-loader": "^8.0.4", 40 | "chai": "^4.1.2", 41 | "clean-webpack-plugin": "^0.1.16", 42 | "css-loader": "^1.0.1", 43 | "enzyme": "^3.3.0", 44 | "enzyme-adapter-react-16": "^1.1.1", 45 | "eslint": "^5.8.0", 46 | "eslint-plugin-react": "^7.4.0", 47 | "jest-cli": "^23.6.0", 48 | "prop-types": "^15.6.0", 49 | "react": "^16.6.0", 50 | "react-addons-test-utils": "^15.5.1", 51 | "react-dom": "^16.6.0", 52 | "react-hot-loader": "^4.12.19", 53 | "react-test-renderer": "^16.0.0", 54 | "regenerator-runtime": "^0.12.1", 55 | "sass-loader": "^7.0.1", 56 | "style-loader": "^0.23.1", 57 | "webpack": "^4.9.1", 58 | "webpack-cli": "^3.1.2", 59 | "webpack-dev-server": "^3.1.4" 60 | }, 61 | "peerDependencies": { 62 | "react": ">= 16.3.0", 63 | "react-dom": ">= 16.3.0" 64 | }, 65 | "resolutions": { 66 | "babel-core": "7.0.0-bridge.0" 67 | }, 68 | "dependencies": { 69 | "3box": "^1.20.0", 70 | "3id-resolver": "^1.0.0", 71 | "@babel/runtime": "^7.6.0", 72 | "babel-core": "^7.0.0-bridge.0", 73 | "did-resolver": "^1.1.0", 74 | "emoji-js": "^3.4.1", 75 | "ethereum-blockies-base64": "^1.0.2", 76 | "multicodec": "^0.5.6", 77 | "node-sass": "^4.13.0", 78 | "profile-hover": "^1.1.1", 79 | "react-inlinesvg": "^1.2.0", 80 | "react-linkify": "^1.0.0-alpha", 81 | "svg-inline-loader": "^0.8.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /previews/screenshot: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3box/3box-comments-react/3147f73a5b5cd37247e1d45305e63d98a9067e71/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /sass/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sass/Comment.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .comment { 4 | width: 100%; 5 | min-height: $commentPictureDimension; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: flex-start; 9 | align-items: flex-start; 10 | position: relative; 11 | 12 | &:hover { 13 | .comment_control { 14 | display: flex; 15 | } 16 | } 17 | 18 | .inlinePicker { 19 | padding: 0 8px 0 10px !important; 20 | border-top-right-radius: 0; 21 | border-bottom-right-radius: 0; 22 | 23 | &:hover { 24 | background-color: $lightBorderColor; 25 | } 26 | } 27 | } 28 | 29 | .comment.isMyComment { 30 | &:hover { 31 | .comment_content_context_main_user_delete { 32 | visibility: visible !important; 33 | } 34 | } 35 | } 36 | 37 | .comment.isDeletedComment { 38 | border: 1px solid $lightBorderColor; 39 | background-color: #fafafa; 40 | border-radius: 4px; 41 | 42 | p { 43 | color: $lighterFont; 44 | font-size: 13px; 45 | } 46 | } 47 | 48 | .nestedDialogue { 49 | .comment_content { 50 | min-width: calc(100% - #{$commentPictureDimension}); 51 | } 52 | } 53 | 54 | .comment_wrapper { 55 | display: flex; 56 | min-width: 100%; 57 | min-height: 100%; 58 | width: 100%; 59 | height: 100%; 60 | } 61 | 62 | .comment_picture { 63 | background-color: grey; 64 | object-fit: cover; 65 | border-radius: 50%; 66 | } 67 | 68 | .comment_picture-bgWhite { 69 | background-color: white !important; 70 | } 71 | 72 | .comment_picture.originalComment { 73 | min-width: $commentPictureDimension; 74 | min-height: $commentPictureDimension; 75 | max-width: $commentPictureDimension; 76 | max-height: $commentPictureDimension; 77 | } 78 | 79 | .comment_picture.nestedComment { 80 | min-width: $commentPictureDimension-nested; 81 | min-height: $commentPictureDimension-nested; 82 | max-width: $commentPictureDimension-nested; 83 | max-height: $commentPictureDimension-nested; 84 | } 85 | 86 | .comment_control { 87 | display: none; 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | justify-content: space-between; 92 | align-items: center; 93 | border: 1px solid $borderColor; 94 | border-radius: 4px; 95 | height: $reactionHeight; 96 | 97 | .sc-user-input--emoji-icon { 98 | height: 16px; 99 | width: auto; 100 | cursor: pointer; 101 | } 102 | } 103 | 104 | .comment_control.show { 105 | display: flex !important; 106 | } 107 | 108 | .comment_content { 109 | width: 100%; 110 | box-sizing: border-box; 111 | padding-left: 12px; 112 | display: flex; 113 | flex-direction: column; 114 | justify-content: flex-start; 115 | align-items: flex-start; 116 | margin-bottom: 4px; 117 | } 118 | 119 | .comment.isDeletedComment { 120 | justify-content: center; 121 | align-items: center; 122 | } 123 | 124 | .comment_content_context { 125 | width: 100%; 126 | display: flex; 127 | flex-direction: column; 128 | justify-content: flex-start; 129 | align-items: flex-start; 130 | margin-bottom: 4px; 131 | } 132 | 133 | .comment_content_context_main { 134 | display: flex; 135 | justify-content: space-between; 136 | align-items: center; 137 | width: 100%; 138 | } 139 | 140 | .comment_content_context_main_user { 141 | display: flex; 142 | justify-content: flex-start; 143 | align-items: flex-end; 144 | position: relative; 145 | } 146 | 147 | .comment_content_context_main_user_info { 148 | display: flex; 149 | justify-content: flex-start; 150 | align-items: flex-end; 151 | } 152 | 153 | .comment_content_context_main_user_delete { 154 | visibility: hidden; 155 | position: absolute; 156 | right: -36px; 157 | top: 1px; 158 | } 159 | 160 | .comment_content_context_main_user_delete_button { 161 | border: none; 162 | background-color: transparent; 163 | } 164 | 165 | .comment_content_context_main_user_delete_button_icon { 166 | height: 16px; 167 | width: auto; 168 | cursor: pointer; 169 | opacity: .35; 170 | } 171 | 172 | .comment_content_context_main_user_reply_button { 173 | border: none; 174 | background-color: transparent; 175 | cursor: pointer; 176 | display: flex; 177 | justify-content: center; 178 | align-items: center; 179 | color: $lighterFont; 180 | height: 100%; 181 | border-top-right-radius: 4px; 182 | border-bottom-right-radius: 4px; 183 | 184 | &:hover { 185 | background-color: $lightBorderColor; 186 | } 187 | } 188 | 189 | .comment_content_context_main_user_reply_button_icon { 190 | height: 15px; 191 | width: auto; 192 | opacity: .35; 193 | margin: 2px 3px 0 0; 194 | } 195 | 196 | .comment_reactions { 197 | display: flex; 198 | justify-content: flex-start; 199 | align-items: flex-start; 200 | margin: 8px 0 4px 0; 201 | } 202 | 203 | .comment_control_mobile { 204 | display: none; 205 | } 206 | 207 | .comment_reactions.nestedComment { 208 | display: flex; 209 | margin-top: 8px; 210 | } 211 | 212 | .comment_content_context_time { 213 | font-size: 11px; 214 | color: $lighterFont; 215 | } 216 | 217 | .comment_content_context_main_user_info_username { 218 | font-size: 16px; 219 | font-weight: 800; 220 | white-space: nowrap; 221 | text-align: left; 222 | color: rgb(32, 32, 38); 223 | 224 | &:hover { 225 | text-decoration: underline; 226 | } 227 | } 228 | 229 | .comment_content_context_main_user_info_address { 230 | font-size: 12px; 231 | color: $lighterFont; 232 | margin-left: 8px; 233 | padding-bottom: 2px; 234 | white-space: nowrap; 235 | } 236 | 237 | .comment_content_text { 238 | font-size: 14px; 239 | text-align: left; 240 | word-break: break-word; 241 | } 242 | 243 | .comment_loading { 244 | min-width: 30px; 245 | min-height: 30px; 246 | max-width: 30px; 247 | max-height: 30px; 248 | position: absolute; 249 | right: -36px; 250 | top: -4px; 251 | } 252 | 253 | .dialogue { 254 | .dialogue_button_container { 255 | height: 0; 256 | } 257 | } 258 | 259 | @media only screen and (max-width: 600px) { 260 | .comment-mobile { 261 | .comment_picture { 262 | min-width: $commentPictureDimension-mobile; 263 | min-height: $commentPictureDimension-mobile; 264 | max-width: $commentPictureDimension-mobile; 265 | max-height: $commentPictureDimension-mobile; 266 | } 267 | 268 | .comment_content_context_main_user_info { 269 | flex-direction: column; 270 | align-items: flex-start; 271 | } 272 | 273 | .comment_content_context_main_user_info_address { 274 | margin-left: 0; 275 | } 276 | 277 | .comment_content_text { 278 | font-size: 15px; 279 | } 280 | 281 | .comment.isMyComment { 282 | .comment_content_context_main_user_delete { 283 | visibility: visible !important; 284 | right: -26px; 285 | } 286 | } 287 | } 288 | 289 | .comment_content { 290 | padding-left: 8px; 291 | } 292 | 293 | .comment_picture.nestedComment { 294 | min-width: $commentPictureDimension-nested-mobile; 295 | min-height: $commentPictureDimension-nested-mobile; 296 | max-width: $commentPictureDimension-nested-mobile; 297 | max-height: $commentPictureDimension-nested-mobile; 298 | } 299 | 300 | .comment_reactions { 301 | flex-direction: column; 302 | } 303 | 304 | .comment_control_mobile { 305 | display: flex; 306 | position: absolute; 307 | top: 5px; 308 | right: 0; 309 | height: $reactionHeight; 310 | } 311 | 312 | .comment_control_mobile_icon { 313 | height: 3px; 314 | width: auto; 315 | opacity: .35; 316 | margin: 2px 3px 0 0; 317 | } 318 | 319 | .comment_control.showOnMobile { 320 | display: flex !important; 321 | } 322 | 323 | .comment_control { 324 | background-color: white; 325 | z-index: 5; 326 | top: -28px; 327 | width: fit-content;; 328 | } 329 | 330 | .comment { 331 | &:hover { 332 | .comment_control { 333 | display: none; 334 | } 335 | } 336 | } 337 | } -------------------------------------------------------------------------------- /sass/Context.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .context { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | height: 68px; 8 | } 9 | 10 | .context_text { 11 | position: relative; 12 | color: $lightestFont; 13 | font-size: 14px; 14 | font-weight: 600; 15 | } 16 | 17 | .context_loading { 18 | position: absolute; 19 | min-width: 40px; 20 | min-height: 40px; 21 | max-width: 40px; 22 | max-height: 40px; 23 | left: -44px; 24 | top: -11px; 25 | opacity: .6; 26 | } 27 | 28 | @media only screen and (max-width: 600px) { 29 | .comment-mobile { 30 | .context { 31 | height: 50px; 32 | margin-top: 20px; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /sass/Dialogue.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .dialogue { 4 | width: 100%; 5 | margin: 6px 0 6px 0; 6 | } 7 | 8 | .nestedDialogue.dialogue { 9 | margin-left: 68px; 10 | width: calc(100% - 68px); 11 | margin-top: 14px; 12 | 13 | .dialogue_grid { 14 | row-gap: 10px; 15 | } 16 | 17 | // second nested dialogue 18 | .nestedDialogue.dialogue { 19 | margin-left: 54px; 20 | width: calc(100% - 54px); 21 | } 22 | } 23 | 24 | .dialogue_grid { 25 | width: 100%; 26 | display: inline-grid; 27 | row-gap: 22px; 28 | grid-template-columns: repeat(1, 100% [col-start]); 29 | margin-bottom: 10px; 30 | } 31 | 32 | .dialogue_button_container { 33 | width: 100%; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | height: 70px; 38 | } 39 | 40 | .dialogue_button { 41 | height: 36px; 42 | width: 120px; 43 | background-color: $lightestFont; 44 | border-radius: 20px; 45 | border: none; 46 | color: white; 47 | cursor: pointer; 48 | transition: .2s ease-in-out all; 49 | font-size: 14px; 50 | 51 | &:hover { 52 | background-color: $lighterFont; 53 | } 54 | } 55 | 56 | @media only screen and (max-width: 600px) { 57 | .nestedDialogue.dialogue { 58 | margin-left: 40px; 59 | width: calc(100% - 40px); 60 | margin-top: 14px; 61 | 62 | .dialogue_grid { 63 | row-gap: 10px; 64 | } 65 | 66 | // second nested dialogue 67 | .nestedDialogue.dialogue { 68 | margin-left: 30px; 69 | width: calc(100% - 30px); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /sass/EmojiPicker.scss: -------------------------------------------------------------------------------- 1 | .sc-emoji-picker { 2 | overflow: auto; 3 | width: 100%; 4 | max-height: calc(100% - 40px); 5 | box-sizing: border-box; 6 | padding: 15px; 7 | } 8 | 9 | .sc-emoji-picker--category { 10 | display: flex; 11 | flex-direction: row; 12 | flex-wrap: wrap; 13 | } 14 | 15 | .sc-emoji-picker--category-title { 16 | min-width: 100%; 17 | color: #b8c3ca; 18 | font-weight: 200; 19 | font-size: 13px; 20 | margin: 5px; 21 | letter-spacing: 1px; 22 | } 23 | 24 | .sc-emoji-picker--emoji { 25 | margin: 5px; 26 | width: 30px; 27 | line-height: 30px; 28 | text-align: center; 29 | cursor: pointer; 30 | vertical-align: middle; 31 | font-size: 28px; 32 | transition: transform 60ms ease-out,-webkit-transform 60ms ease-out; 33 | transition-delay: 60ms; 34 | } 35 | 36 | .sc-emoji-picker--emoji:hover { 37 | transform: scale(1.4); 38 | } 39 | 40 | .onClickOutside { 41 | position: fixed; 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | height: 100%; 46 | } -------------------------------------------------------------------------------- /sass/Footer.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .comments_footer { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | margin-bottom: 80px; 8 | width: 100%; 9 | margin-top: 38px; 10 | } 11 | 12 | .footer_text { 13 | color: $lightestFont; 14 | font-weight: 600; 15 | font-size: 13px; 16 | display: flex; 17 | justify-content: flex-end; 18 | align-items: center; 19 | width: fit-content; 20 | 21 | a { 22 | display: flex; 23 | justify-content: flex-end; 24 | align-items: center; 25 | } 26 | } 27 | 28 | .footer_text_image { 29 | height: 18px; 30 | margin-left: 8px; 31 | opacity: .3; 32 | } -------------------------------------------------------------------------------- /sass/Input.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .input { 4 | position: relative; 5 | width: 100%; 6 | z-index: 1; 7 | } 8 | 9 | .input_form { 10 | font-size: 15px; 11 | min-height: calc(#{$commentPictureDimension} + 14px); 12 | height: calc(#{$commentPictureDimension} + 14px); 13 | box-sizing: border-box; 14 | min-width: calc(100% + 16px); 15 | width: 100%; 16 | border-radius: 37px; 17 | border: 1px solid $borderColor; 18 | margin-left: -8px; 19 | padding: 25px 56px 25px 80px; 20 | resize: none; 21 | overflow-y: hidden; 22 | line-height: 22px; 23 | 24 | &:focus { 25 | outline-width: 0; 26 | } 27 | } 28 | 29 | ::placeholder { 30 | font-size: 15px; 31 | white-space: nowrap; 32 | } 33 | 34 | .input_form.hidePlaceholder::placeholder { 35 | color: transparent !important; 36 | } 37 | 38 | .input_user { 39 | position: absolute; 40 | left: 0; 41 | top: 7px; 42 | min-width: $commentPictureDimension; 43 | min-height: $commentPictureDimension; 44 | max-width: $commentPictureDimension; 45 | max-height: $commentPictureDimension; 46 | border-radius: 50%; 47 | // background-color: #1168df; 48 | object-fit: cover; 49 | } 50 | 51 | .input_login { 52 | position: absolute; 53 | left: 72px; 54 | bottom: -24px; 55 | color: $lighterFont; 56 | border: none; 57 | font-weight: 600; 58 | font-size: 14px; 59 | cursor: pointer; 60 | padding: 0; 61 | } 62 | 63 | .input_login_loading { 64 | min-height: 52px; 65 | min-width: 52px; 66 | max-height: 52px; 67 | max-width: 52px; 68 | } 69 | 70 | .sc-user-input--picker-wrapper { 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | } 75 | 76 | .sc-user-input--picker-wrapper.inputPicker { 77 | position: absolute; 78 | right: 0; 79 | top: 7px; 80 | min-width: $commentPictureDimension; 81 | min-height: $commentPictureDimension; 82 | max-width: $commentPictureDimension; 83 | max-height: $commentPictureDimension; 84 | border-radius: 50%; 85 | display: flex; 86 | justify-content: center; 87 | align-items: center; 88 | } 89 | 90 | // nested reply input 91 | .comment { 92 | .sc-user-input--picker-wrapper.inputPicker { 93 | top: 5.5px; 94 | min-width: 46px; 95 | min-height: 46px; 96 | max-width: 46px; 97 | max-height: 46px; 98 | } 99 | } 100 | 101 | .sc-user-input--picker-wrapper.inlinePicker { 102 | width: 22px; 103 | padding: 0 10px !important; 104 | border-top-right-radius: 4px; 105 | border-bottom-right-radius: 4px; 106 | top: 0; 107 | height: $reactionHeight; 108 | cursor: pointer; 109 | 110 | &:hover { 111 | background-color: $lightBorderColor; 112 | } 113 | } 114 | 115 | .input_emptyUser { 116 | position: absolute; 117 | left: -1px; 118 | top: 6px; 119 | min-width: $commentPictureDimension; 120 | min-height: $commentPictureDimension; 121 | max-width: $commentPictureDimension; 122 | max-height: $commentPictureDimension; 123 | border-radius: 50%; 124 | background-color: transparent; 125 | border: 1px solid $borderColor; 126 | overflow: hidden; 127 | display: flex; 128 | justify-content: center; 129 | align-items: center; 130 | } 131 | 132 | .comment { 133 | .input_form { 134 | margin-top: 6px; 135 | border-radius: 4px; 136 | padding: 10px 40px 10px 12px; 137 | margin-left: $inputMarginLeft; 138 | min-height: 46px; 139 | height: 46px; 140 | width: calc(100% - #{$inputMarginLeft}); 141 | min-width: calc(100% - #{$inputMarginLeft}); 142 | } 143 | 144 | .input_emptyUser { 145 | display: none; 146 | } 147 | 148 | .input_user { 149 | display: none; 150 | } 151 | 152 | .input_login { 153 | display: none; 154 | } 155 | 156 | .input_commentAs { 157 | display: none; 158 | } 159 | 160 | .input_postLoading_spinner { 161 | min-width: 42px; 162 | min-height: 42px; 163 | max-width: 42px; 164 | max-height: 42px; 165 | } 166 | 167 | .input_postLoading { 168 | top: 8px; 169 | } 170 | } 171 | 172 | .nestedDialogue.dialogue { 173 | .input_form { 174 | border-radius: 4px; 175 | margin-left: 0 !important; 176 | width: calc(100% + 2px) !important; 177 | min-width: calc(100% + 2px) !important; 178 | border: 1px solid $lightBorderColor; 179 | margin-bottom: 10px; 180 | } 181 | } 182 | 183 | .input_emptyUser_icon { 184 | min-width: 30px; 185 | min-height: 30px; 186 | max-width: 30px; 187 | max-height: 30px; 188 | opacity: .6; 189 | } 190 | 191 | .input_send { 192 | visibility: hidden; 193 | display: none; 194 | } 195 | 196 | .input_postLoading { 197 | position: absolute; 198 | top: 10px; 199 | left: calc(50% - #{$inputMarginLeft}); 200 | display: flex; 201 | justify-content: center; 202 | align-items: center; 203 | } 204 | 205 | .input_postLoading_spinner { 206 | min-height: 52px; 207 | min-width: 52px; 208 | max-height: 52px; 209 | max-width: 52px; 210 | opacity: .6; 211 | } 212 | 213 | .input_postLoading_text { 214 | color: $lighterFont; 215 | font-size: 13px; 216 | display: flex; 217 | justify-content: center; 218 | align-items: center; 219 | margin-left: -4px; 220 | } 221 | 222 | .input_commentAs { 223 | position: absolute; 224 | left: 72px; 225 | top: -34px; 226 | color: $lighterFont; 227 | font-size: 12px; 228 | opacity: 0; 229 | transition: .2s ease-in-out all; 230 | white-space: nowrap; 231 | overflow: hidden; 232 | } 233 | 234 | .showLoggedInAs { 235 | opacity: 1; 236 | } 237 | 238 | .comment_loading { 239 | min-width: 30px; 240 | min-height: 30px; 241 | max-width: 30px; 242 | max-height: 30px; 243 | } 244 | 245 | @media only screen and (max-width: 600px) { 246 | .comment-mobile { 247 | ::placeholder { 248 | font-size: 16px; 249 | } 250 | 251 | .sc-user-input--picker-wrapper { 252 | display: none; 253 | } 254 | 255 | .comment { 256 | .sc-user-input--picker-wrapper { 257 | display: flex; 258 | } 259 | } 260 | 261 | .input_user { 262 | top: 7px; 263 | left: -1px; 264 | min-width: $commentPictureDimension-mobile; 265 | min-height: $commentPictureDimension-mobile; 266 | max-width: $commentPictureDimension-mobile; 267 | max-height: $commentPictureDimension-mobile; 268 | } 269 | 270 | .input_emptyUser { 271 | top: 6px; 272 | left: -1px; 273 | min-width: $commentPictureDimension-mobile; 274 | min-height: $commentPictureDimension-mobile; 275 | max-width: $commentPictureDimension-mobile; 276 | max-height: $commentPictureDimension-mobile; 277 | } 278 | 279 | .input_emptyUser_icon { 280 | min-width: 30px; 281 | min-height: 30px; 282 | max-width: 30px; 283 | max-height: 30px; 284 | opacity: .6; 285 | } 286 | 287 | .input_form { 288 | font-size: 15px; 289 | min-height: 64px; 290 | height: 64px; 291 | box-sizing: border-box; 292 | min-width: calc(100% + 16px); 293 | border-radius: 37px; 294 | border: 1px solid $borderColor; 295 | margin-left: -8px; 296 | padding: 20px 52px 20px $inputMarginLeft; 297 | } 298 | 299 | .comment { 300 | .input_form { 301 | font-size: 15px; 302 | margin-top: 6px; 303 | border-radius: 4px; 304 | padding: 10px 12px; 305 | margin-left: 58px; 306 | min-height: 46px; 307 | height: 46px; 308 | width: calc(100% - 58px); 309 | min-width: calc(100% - 58px); 310 | } 311 | 312 | .nestedDialogue.dialogue { 313 | .input_form { 314 | width: 100% !important; 315 | min-width: 100% !important; 316 | 317 | .input_send { 318 | bottom: 35px; 319 | right: 24px; 320 | } 321 | } 322 | } 323 | } 324 | 325 | .input_send { 326 | position: absolute; 327 | right: 0px; 328 | bottom: 9px; 329 | border-radius: 50%; 330 | background-color: transparent; 331 | padding: 0; 332 | border: 0; 333 | min-width: $commentPictureDimension-mobile; 334 | min-height: $commentPictureDimension-mobile; 335 | max-width: $commentPictureDimension-mobile; 336 | max-height: $commentPictureDimension-mobile; 337 | opacity: .6; 338 | display: flex; 339 | justify-content: center; 340 | align-items: center; 341 | } 342 | 343 | .comment { 344 | .input_send { 345 | bottom: 25px; 346 | right: 24px; 347 | min-width: 20px; 348 | min-height: 20px; 349 | max-width: 20px; 350 | max-height: 20px; 351 | } 352 | } 353 | 354 | .nestedDialogue { 355 | .input_send { 356 | bottom: 33px; 357 | right: 24px; 358 | } 359 | } 360 | 361 | .input_send-visible { 362 | visibility: visible; 363 | } 364 | 365 | .input_send_icon { 366 | width: 26px; 367 | opacity: .5; 368 | } 369 | 370 | .input_commentAs { 371 | left: 62px; 372 | } 373 | 374 | .input_login { 375 | left: 62px; 376 | bottom: -20px; 377 | background-color: transparent; 378 | } 379 | 380 | .input_user-empty { 381 | background-color: transparent; 382 | left: 12.5px; 383 | top: 18px; 384 | min-width: 28px; 385 | min-height: 28px; 386 | max-width: 28px; 387 | max-height: 28px; 388 | border-radius: 0; 389 | opacity: .7; 390 | } 391 | 392 | .input_postLoading { 393 | position: absolute; 394 | top: 6px; 395 | left: calc(50% - 58px); 396 | display: flex; 397 | justify-content: center; 398 | align-items: center; 399 | } 400 | 401 | .input_postLoading_spinner { 402 | min-height: 52px; 403 | min-width: 52px; 404 | max-height: 52px; 405 | max-width: 52px; 406 | } 407 | } 408 | 409 | .inputPicker { 410 | display: none !important; 411 | } 412 | } -------------------------------------------------------------------------------- /sass/PopupWindow.scss: -------------------------------------------------------------------------------- 1 | .sc-popup-window { 2 | position: relative; 3 | width: 0; 4 | // width: 150px; 5 | } 6 | 7 | .sc-user-input--emoji-icon { 8 | height: 22px; 9 | cursor: pointer; 10 | align-self: center; 11 | opacity: .4; 12 | } 13 | 14 | .sc-popup-window--cointainer { 15 | position: absolute; 16 | bottom: 24px; 17 | right: -65px; 18 | // right: 100px; 19 | width: 330px; 20 | max-height: 260px; 21 | height: 260px; 22 | box-shadow: 0px 7px 40px 2px rgba(148, 149, 150, 0.3); 23 | background: white; 24 | border-radius: 10px; 25 | outline: none; 26 | transition: 0.2s ease-in-out; 27 | z-index: 1; 28 | padding: 0px 5px 5px 5px; 29 | box-sizing: border-box; 30 | } 31 | 32 | .sc-popup-window--cointainer.closed { 33 | opacity: 0; 34 | visibility: hidden; 35 | bottom: 14px; 36 | } 37 | 38 | .sc-popup-window--cointainer:after { 39 | content: ""; 40 | width: 14px; 41 | height: 14px; 42 | background: white; 43 | position: absolute; 44 | z-index: -1; 45 | bottom: -6px; 46 | right: 28px; 47 | transform: rotate(45deg); 48 | border-radius: 2px; 49 | } 50 | 51 | .sc-popup-window--search { 52 | width: 290px; 53 | box-sizing: border-box; 54 | margin: auto; 55 | display: block; 56 | border-width: 0px 0px 1px 0px; 57 | color: #565867; 58 | padding-left: 25px; 59 | height: 40px; 60 | font-size: 14px; 61 | background-image: url(https://js.intercomcdn.com/images/search@2x.32fca88e.png); 62 | background-size: 16px 16px; 63 | background-repeat: no-repeat; 64 | background-position: 0 12px; 65 | outline: none; 66 | } 67 | 68 | .sc-popup-window--search::placeholder { 69 | color: #C1C7CD; 70 | } 71 | 72 | // .sc-user-input--file-icon-wrapper 73 | .sc-user-input--emoji-icon-wrapper { 74 | background: none; 75 | border: none; 76 | padding: 2px 2px 2px 2px; 77 | margin: 0; 78 | display: flex; 79 | flex-direction: column; 80 | justify-content: center; 81 | cursor: pointer; 82 | } 83 | 84 | .sc-user-input--emoji-icon-wrapper:focus { 85 | outline: none; 86 | } 87 | 88 | .sc-user-input--emoji-icon-wrapper:focus .sc-user-input--emoji-icon path, 89 | .sc-user-input--emoji-icon-wrapper:focus .sc-user-input--emoji-icon circle, 90 | .sc-user-input--emoji-icon.active path, 91 | .sc-user-input--emoji-icon.active circle, 92 | .sc-user-input--emoji-icon:hover path, 93 | .sc-user-input--emoji-icon:hover circle { 94 | fill: rgba(86, 88, 103, 1); 95 | } -------------------------------------------------------------------------------- /sass/Reactions.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .reactions { 4 | position: relative; 5 | 6 | min-height: $reactionHeight; 7 | height: fit-content; 8 | display: flex; 9 | justify-content: flex-start; 10 | 11 | .sc-user-input--picker-wrapper { 12 | min-width: 0; 13 | min-height: 0; 14 | position: relative; 15 | top: 0; 16 | padding: 7px 10px 5px; 17 | 18 | .sc-user-input--emoji-icon { 19 | height: 16px; 20 | width: auto; 21 | cursor: pointer; 22 | } 23 | } 24 | 25 | .hint { 26 | display: none; 27 | position: absolute; 28 | left: 0; 29 | bottom: -42px; 30 | color: $tooltipFont; 31 | background-color: $darkBackgroundColor; 32 | font-size: 12px; 33 | transition: .2s ease-in-out all; 34 | white-space: nowrap; 35 | overflow: hidden; 36 | padding: 2px 10px; 37 | border-radius: 4px; 38 | z-index: 10; 39 | 40 | &.visible { 41 | display: block; 42 | } 43 | } 44 | } 45 | 46 | .emoji-bar { 47 | visibility: visible !important; 48 | width: fit-content; 49 | min-width: fit-content; 50 | 51 | display: inline-grid; 52 | row-gap: 4px; 53 | column-gap: 6px; 54 | grid-template-columns: repeat(8, 46px [col-start]); 55 | 56 | .emoji-item { 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | color: #0366D6; 61 | cursor: pointer; 62 | min-width: 46px; 63 | height: $reactionHeight; 64 | font-size: 13px; 65 | border: 1px solid $borderColor; 66 | border-radius: 4px; 67 | 68 | &.has_reacted { 69 | background-color: $activeIconBackgroundColor; 70 | } 71 | 72 | &:hover { 73 | background-color: $lightBorderColor; 74 | } 75 | } 76 | } 77 | 78 | .nestedDialogue.dialogue { 79 | .emoji-bar { 80 | grid-template-columns: repeat(7, 46px [col-start]); 81 | } 82 | 83 | // second nested dialogue 84 | .nestedDialogue.dialogue { 85 | .emoji-bar { 86 | grid-template-columns: repeat(6, 46px [col-start]); 87 | } 88 | } 89 | } 90 | 91 | @media only screen and (max-width: 730px) { 92 | .emoji-bar { 93 | grid-template-columns: repeat(7, 46px [col-start]); 94 | } 95 | 96 | .nestedDialogue.dialogue { 97 | .emoji-bar { 98 | grid-template-columns: repeat(6, 46px [col-start]); 99 | } 100 | 101 | // second nested dialogue 102 | .nestedDialogue.dialogue { 103 | .emoji-bar { 104 | grid-template-columns: repeat(5, 46px [col-start]); 105 | } 106 | } 107 | } 108 | } 109 | 110 | @media only screen and (max-width: 678px) { 111 | .emoji-bar { 112 | grid-template-columns: repeat(6, 46px [col-start]); 113 | } 114 | 115 | .nestedDialogue.dialogue { 116 | .emoji-bar { 117 | grid-template-columns: repeat(5, 46px [col-start]); 118 | } 119 | 120 | // second nested dialogue 121 | .nestedDialogue.dialogue { 122 | .emoji-bar { 123 | grid-template-columns: repeat(4, 46px [col-start]); 124 | } 125 | } 126 | } 127 | } 128 | 129 | @media only screen and (max-width: 600px) { 130 | .emoji-bar { 131 | grid-template-columns: repeat(4, 46px [col-start]); 132 | } 133 | 134 | .nestedDialogue.dialogue { 135 | .emoji-bar { 136 | grid-template-columns: repeat(4, 46px [col-start]); 137 | } 138 | 139 | // second nested dialogue 140 | .nestedDialogue.dialogue { 141 | .emoji-bar { 142 | grid-template-columns: repeat(3, 46px [col-start]); 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /sass/Vote.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .comment_vote { 4 | display: flex; 5 | justify-content: flex-start; 6 | align-items: center; 7 | border: 1px solid $borderColor; 8 | border-radius: 4px; 9 | height: $reactionHeight; 10 | margin-right: 8px; 11 | } 12 | 13 | .vote_btn { 14 | border: none; 15 | background-color: transparent; 16 | padding: 0 2px; 17 | width: 28px; 18 | height: 100%; 19 | width: 30px; 20 | position: relative; 21 | cursor: pointer; 22 | 23 | &:hover { 24 | background-color: $lightBorderColor; 25 | 26 | .vote_icon { 27 | &.upvote { 28 | fill: $positiveColor; 29 | } 30 | 31 | &.downvote { 32 | fill: $negativeColor; 33 | } 34 | } 35 | } 36 | 37 | .vote_icon { 38 | height: 16px; 39 | width: auto; 40 | opacity: .4; 41 | 42 | &.upvote.voted { 43 | fill: $positiveColor; 44 | } 45 | 46 | &.downvote.voted { 47 | fill: $negativeColor; 48 | } 49 | } 50 | } 51 | 52 | .vote_btn-middle { 53 | // border-right: 1px solid $borderColor; 54 | // border-left: 1px solid $borderColor; 55 | } 56 | 57 | .count { 58 | width: 30px; 59 | opacity: .8; 60 | height: 100%; 61 | display: flex; 62 | justify-content: center; 63 | align-items: center; 64 | font-size: 13px; 65 | 66 | &.positive { 67 | color: $positiveColor; 68 | } 69 | 70 | &.negative { 71 | color: $negativeColor; 72 | ; 73 | } 74 | } 75 | 76 | @media only screen and (max-width: 600px) { 77 | .comment_vote { 78 | margin-bottom: 6px; 79 | } 80 | } -------------------------------------------------------------------------------- /sass/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | textarea { 13 | margin: 0; 14 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 15 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 16 | sans-serif; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | box-shadow: none; 20 | -webkit-appearance: none; 21 | } 22 | 23 | code { 24 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 25 | monospace; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | } 31 | 32 | .threebox-comments-react { 33 | max-width: $maxCommentWidth; 34 | width: 100%; 35 | min-width: $minCommentWidth; 36 | } 37 | 38 | .comment-mobile { 39 | .threebox-comments-react { 40 | min-width: $minCommentWidth-mobile; 41 | } 42 | } -------------------------------------------------------------------------------- /sass/variables.scss: -------------------------------------------------------------------------------- 1 | $lightFont:#54545e; 2 | $lighterFont:#7d7d7d; 3 | $lightestFont:#c1c1c1; 4 | $tooltipFont:#f1f8ff; 5 | 6 | $maxCommentWidth: 600px; 7 | $minCommentWidth: 300px; 8 | 9 | $minCommentWidth-mobile: 260px; 10 | 11 | $commentPictureDimension: 56px; 12 | $commentPictureDimension-nested: 42px; 13 | // $commentPictureDimension: 60px; 14 | 15 | $commentPictureDimension-mobile: 50px; 16 | $commentPictureDimension-nested-mobile: 40px; 17 | 18 | $borderColor:#dadada; 19 | $lightBorderColor:#ebebeb; 20 | $buttonColor:#0366D6; 21 | $activeIconBackgroundColor:#f1f8ff; 22 | $darkBackgroundColor:#38383e; 23 | 24 | $positiveColor:#005eff; 25 | $negativeColor:#ff0051; 26 | 27 | $reactionHeight: 22px; 28 | 29 | $inputMarginLeft: 68px; -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import ReactDOM from 'react-dom'; 3 | // import App from './index'; 4 | 5 | it('renders without crashing', () => { 6 | expect(1 + 2).toEqual(3); 7 | }); 8 | 9 | // it('renders without crashing', () => { 10 | // const div = document.createElement('div'); 11 | // ReactDOM.render(, div); 12 | // ReactDOM.unmountComponentAtNode(div); 13 | // }); 14 | -------------------------------------------------------------------------------- /src/assets/3BoxCommentsSpinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/assets/3BoxLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Combined Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/ArrowDown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/ArrowUp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/Dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/Profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/Reply.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ProfileHover from 'profile-hover'; 3 | import PropTypes from 'prop-types'; 4 | import Linkify from 'react-linkify'; 5 | import makeBlockie from 'ethereum-blockies-base64'; 6 | import SVG from 'react-inlinesvg'; 7 | 8 | import { 9 | timeSince, 10 | shortenEthAddr, 11 | filterComments, 12 | REPLIABLE_COMMENT_LEVEL_MAX, 13 | encodeMessage, 14 | aggregateReactions, 15 | } from '../utils'; 16 | 17 | import EmojiIcon from './Emoji/EmojiIcon'; 18 | import EmojiPicker from './Emoji/EmojiPicker'; 19 | import PopupWindow from './Emoji/PopupWindow'; 20 | import ArrowUp from '../assets/ArrowUp.svg'; 21 | import ArrowDown from '../assets/ArrowDown.svg'; 22 | import Delete from '../assets/Delete.svg'; 23 | import Reply from '../assets/Reply.svg'; 24 | import Dots from '../assets/Dots.svg'; 25 | import Loading from '../assets/3BoxCommentsSpinner.svg'; 26 | import Input from './Input'; 27 | import Vote from './Vote'; 28 | import Reactions from './Reactions'; 29 | import '../css/Comment.css'; 30 | import '../css/Vote.css'; 31 | 32 | class Comment extends Component { 33 | constructor(props) { 34 | super(props); 35 | 36 | this.state = { 37 | loadingPost: false, 38 | showControlsOnMobile: false, 39 | emojiPickerIsOpen: false, 40 | emojiFilter: '', 41 | }; 42 | this.upvote = () => { this.vote(1); } 43 | this.downvote = () => { this.vote(-1); } 44 | } 45 | 46 | async componentDidMount() { 47 | this.emojiPickerButton = document.querySelector('#sc-emoji-picker-button'); 48 | } 49 | 50 | handleLoadingState = () => this.setState({ loadingPost: !this.state.loadingPost }); 51 | 52 | vote = async (direction) => { 53 | const { 54 | updateComments, 55 | login, 56 | comment, 57 | hasAuthed, 58 | noWeb3 59 | } = this.props; 60 | const { disableVote } = this.state; 61 | 62 | if (noWeb3 || disableVote) return; 63 | 64 | this.setState({ loadingPost: true }); 65 | if (!hasAuthed) await login(); 66 | 67 | try { 68 | const myVote = this.getMyVote(); 69 | if (myVote) { 70 | if (myVote.message.data === direction) { 71 | // undo vote 72 | await this.props.thread.deletePost(myVote.postId); 73 | } else { 74 | // re-vote 75 | await this.props.thread.deletePost(myVote.postId); 76 | const message = encodeMessage("vote", direction, comment.postId); 77 | await this.props.thread.post(message); 78 | } 79 | } else { 80 | const message = encodeMessage("vote", direction, comment.postId); 81 | await this.props.thread.post(message); 82 | } 83 | 84 | await updateComments(); 85 | this.setState({ loadingPost: false }); 86 | } catch (error) { 87 | console.error('There was an error saving your vote', error); 88 | } 89 | } 90 | 91 | deleteComment = async (commentId, e) => { 92 | e.preventDefault(); 93 | const { 94 | login, 95 | hasAuthed, 96 | } = this.props; 97 | 98 | if (!hasAuthed) { 99 | this.setState({ loadingPost: true }); 100 | await login(); 101 | } 102 | 103 | try { 104 | this.setState({ loadingPost: false }); 105 | await this.props.thread.deletePost(commentId); 106 | } catch (error) { 107 | console.error('There was an error deleting your comment', error); 108 | } 109 | } 110 | 111 | getMyVote = () => { 112 | const { 113 | currentUserAddr, 114 | profiles, 115 | comment 116 | } = this.props; 117 | 118 | const votes = comment.children ? filterComments(comment.children, "vote") : []; 119 | 120 | const currentUserAddrNormalized = currentUserAddr && currentUserAddr.toLowerCase(); 121 | const myVotes = votes.filter(v => { 122 | const profile = profiles[v.author]; 123 | const voterAddr = profile && profile.ethAddr.toLowerCase(); 124 | return voterAddr === currentUserAddrNormalized 125 | }); 126 | return myVotes && myVotes.length > 0 ? myVotes[0] : null; 127 | } 128 | 129 | toggleEmojiPicker = (e) => { 130 | const { emojiPickerIsOpen } = this.state; 131 | e.stopPropagation(); 132 | e.preventDefault(); 133 | this.setState({ emojiPickerIsOpen: !emojiPickerIsOpen }); 134 | } 135 | 136 | renderEmojiPopup = () => ( 137 | 142 | 146 | 147 | ) 148 | 149 | closeEmojiPicker = (e) => { 150 | e.stopPropagation(); 151 | e.preventDefault(); 152 | this.setState({ emojiPickerIsOpen: false }); 153 | } 154 | 155 | _handleEmojiPicked = (emoji) => { 156 | this.addReaction(emoji); 157 | this.setState({ emojiPickerIsOpen: false }); 158 | } 159 | 160 | handleEmojiFilterChange = (event) => { 161 | const emojiFilter = event.target.value; 162 | this.setState({ emojiFilter }); 163 | } 164 | 165 | addReaction = async (emoji) => { 166 | const { 167 | login, 168 | updateComments, 169 | comment, 170 | noWeb3, 171 | hasAuthed, 172 | } = this.props; 173 | 174 | this.setState({ loadingPost: true }); 175 | 176 | if (noWeb3) return; 177 | 178 | if (!hasAuthed) await login(); 179 | 180 | try { 181 | console.log("react with emoji", emoji); 182 | const myReactions = this.getMyReactions(); 183 | 184 | if (myReactions) { 185 | const reactions = aggregateReactions(myReactions); 186 | if (reactions[emoji]) { 187 | console.log("ignore because you already reacted with this emoji", emoji); 188 | } else { 189 | const message = encodeMessage("reaction", emoji, comment.postId); 190 | await this.props.thread.post(message); 191 | this.setState({}) 192 | } 193 | } else { 194 | const message = encodeMessage("reaction", emoji, comment.postId); 195 | await this.props.thread.post(message); 196 | } 197 | 198 | await updateComments(); 199 | this.setState({ loadingPost: false }); 200 | } catch (error) { 201 | console.error('There was an error saving your reaction', error); 202 | } 203 | } 204 | 205 | getMyReactions = () => { 206 | const { 207 | currentUserAddr, 208 | reactions, 209 | profiles 210 | } = this.props; 211 | 212 | const currentUserAddrNormalized = currentUserAddr && currentUserAddr.toLowerCase(); 213 | const myReactions = reactions.filter(r => { 214 | const profile = profiles[r.author]; 215 | const reactionAddr = profile && profile.ethAddr.toLowerCase(); 216 | return reactionAddr === currentUserAddrNormalized 217 | }); 218 | return myReactions; 219 | } 220 | 221 | handleShowControlsOnMobile = () => { 222 | const { showControlsOnMobile } = this.state; 223 | this.setState({ showControlsOnMobile: !showControlsOnMobile }) 224 | } 225 | 226 | render() { 227 | const { 228 | loadingPost, 229 | emojiPickerIsOpen, 230 | showControlsOnMobile, 231 | } = this.state; 232 | 233 | const { 234 | comment, 235 | profile, 236 | isMyComment, 237 | useHovers, 238 | isMyAdmin, 239 | isCommenterAdmin, 240 | thread, 241 | currentUserAddr, 242 | currentUser3BoxProfile, 243 | ethereum, 244 | isLoading3Box, 245 | updateComments, 246 | adminEthAddr, 247 | box, 248 | loginFunction, 249 | login, 250 | profiles, 251 | hasAuthed, 252 | votes, 253 | reactions, 254 | isNestedComment, 255 | showReply, 256 | toggleReplyInput, 257 | noWeb3, 258 | } = this.props; 259 | 260 | const profilePicture = profile.ethAddr && 261 | (profile.image ? `https://ipfs.infura.io/ipfs/${profile.image[0].contentUrl['/']}` 262 | : makeBlockie(profile.ethAddr)); 263 | const canDelete = isMyComment || isMyAdmin; 264 | 265 | let voted = 0; 266 | const myVote = this.getMyVote(); 267 | if (myVote) voted = myVote.message.data; 268 | 269 | const count = votes.reduce((sum, v) => (sum + v.message.data), 0); 270 | const isDeletedComment = comment.message.category === 'deleted'; 271 | 272 | return ( 273 |
274 | 275 | {!isDeletedComment ? ( 276 | <> 277 |
280 | 285 | {profilePicture ? ( 286 | profile 291 | ) :
} 292 | 293 | 294 |
295 | 350 | 351 |
352 | 353 | {comment.message.data} 354 | 355 |
356 | 357 | {(count !== 0 || !!reactions.length) && ( 358 |
359 | {count !== 0 && ( 360 | 380 | )} 381 | 382 | {!!reactions.length && ( 383 | 404 | )} 405 |
406 | )} 407 |
408 |
409 | 410 | {(!loadingPost && comment.message.nestLevel < REPLIABLE_COMMENT_LEVEL_MAX && showReply === comment.postId) && ( 411 | { this.setState({ showReply: '' }) }} 430 | isNestedInput 431 | /> 432 | )} 433 | 434 |
435 | { 436 | count === 0 && ( 437 | <> 438 | 445 | 446 | 453 | 454 | )} 455 | 456 | 462 | 463 | {comment.message.nestLevel < REPLIABLE_COMMENT_LEVEL_MAX && ( 464 | 471 | )} 472 |
473 | 474 |
475 | 476 |
477 | 478 | ) :

This comment was deleted

} 479 |
480 | ); 481 | } 482 | } 483 | 484 | export default Comment; 485 | 486 | Comment.propTypes = { 487 | thread: PropTypes.object, 488 | isMyAdmin: PropTypes.bool.isRequired, 489 | isCommenterAdmin: PropTypes.bool.isRequired, 490 | useHovers: PropTypes.bool.isRequired, 491 | isMyComment: PropTypes.bool.isRequired, 492 | hasAuthed: PropTypes.bool.isRequired, 493 | comment: PropTypes.object.isRequired, 494 | profile: PropTypes.object.isRequired, 495 | box: PropTypes.object, 496 | loginFunction: PropTypes.func, 497 | currentUserAddr: PropTypes.string, 498 | adminEthAddr: PropTypes.string, 499 | showReply: PropTypes.string, 500 | currentUser3BoxProfile: PropTypes.object, 501 | ethereum: PropTypes.object, 502 | isLoading3Box: PropTypes.bool, 503 | isNestedComment: PropTypes.bool, 504 | noWeb3: PropTypes.bool.isRequired, 505 | updateComments: PropTypes.func.isRequired, 506 | toggleReplyInput: PropTypes.func.isRequired, 507 | login: PropTypes.func.isRequired, 508 | profiles: PropTypes.object, 509 | votes: PropTypes.array, 510 | reactions: PropTypes.array, 511 | }; 512 | 513 | Comment.defaultProps = { 514 | thread: {}, 515 | votes: [], 516 | reactions: [], 517 | isNestedComment: false, 518 | }; 519 | -------------------------------------------------------------------------------- /src/components/Context.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SVG from 'react-inlinesvg'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Loading from '../assets/3BoxCommentsSpinner.svg'; 6 | import '../css/Context.css'; 7 | 8 | const Context = ({ dialogueLength, isLoading }) => ( 9 |
10 | 11 | {isLoading && } 12 | {`${isLoading ? '' : dialogueLength || 'No'} comments`} 13 | 14 |
15 | ) 16 | 17 | export default Context; 18 | 19 | Context.propTypes = { 20 | dialogueLength: PropTypes.number, 21 | isLoading: PropTypes.bool, 22 | }; 23 | 24 | Context.defaultProps = { 25 | dialogueLength: null, 26 | isLoading: false, 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Dialogue.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { filterComments, REPLY_THREAD_SHOW_COMMENT_COUNT } from '../utils'; 5 | 6 | import Comment from './Comment'; 7 | import '../css/Dialogue.css'; 8 | 9 | class Dialogue extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | showCommentCount: this.props.showCommentCount || 30 15 | } 16 | } 17 | 18 | handleLoadMore = async () => { 19 | const { showCommentCount } = this.state; 20 | const { dialogue } = this.props; 21 | const newCount = showCommentCount + showCommentCount; 22 | let showLoadButton = true; 23 | if (newCount >= dialogue.length) showLoadButton = false; 24 | this.setState({ showCommentCount: newCount, showLoadButton }); 25 | } 26 | 27 | render() { 28 | const { 29 | dialogue, 30 | profiles, 31 | thread, 32 | useHovers, 33 | currentUserAddr, 34 | adminEthAddr, 35 | box, 36 | loginFunction, 37 | currentUser3BoxProfile, 38 | ethereum, 39 | isLoading3Box, 40 | updateComments, 41 | login, 42 | hasAuthed, 43 | isNestedComment, 44 | toggleReplyInput, 45 | showReply, 46 | noWeb3, 47 | } = this.props; 48 | 49 | const { showCommentCount } = this.state; 50 | 51 | let showLoadButton = false; 52 | if (dialogue.length > showCommentCount) showLoadButton = true; 53 | 54 | return ( 55 |
56 |
57 | {dialogue.slice(0, showCommentCount).map(comment => { 58 | const profile = profiles[comment.author]; 59 | const commentAddr = profile && profile.ethAddr.toLowerCase(); 60 | const currentUserAddrNormalized = currentUserAddr && currentUserAddr.toLowerCase(); 61 | const adminEthAddrNormalized = adminEthAddr.toLowerCase(); 62 | 63 | const children_comments = comment.children ? filterComments(comment.children, "comment", "deleted") : []; 64 | const votes = comment.children ? filterComments(comment.children, "vote") : []; 65 | const reactions = comment.children ? filterComments(comment.children, "reaction") : []; 66 | return ( 67 |
68 | 95 | 96 | {(!!children_comments.length) && ( 97 | 118 | )} 119 |
120 | ) 121 | })} 122 |
123 | 124 |
125 | {showLoadButton && ( 126 | 132 | )} 133 |
134 |
135 | ); 136 | } 137 | } 138 | 139 | export default Dialogue; 140 | 141 | Dialogue.propTypes = { 142 | dialogue: PropTypes.array, 143 | profiles: PropTypes.object, 144 | thread: PropTypes.object, 145 | box: PropTypes.object, 146 | currentUserAddr: PropTypes.string, 147 | showReply: PropTypes.string, 148 | useHovers: PropTypes.bool, 149 | hasAuthed: PropTypes.bool, 150 | loginFunction: PropTypes.func, 151 | 152 | 153 | showCommentCount: PropTypes.number.isRequired, 154 | adminEthAddr: PropTypes.string.isRequired, 155 | 156 | currentUser3BoxProfile: PropTypes.object, 157 | ethereum: PropTypes.object, 158 | isLoading3Box: PropTypes.bool, 159 | isNestedComment: PropTypes.bool, 160 | noWeb3: PropTypes.bool.isRequired, 161 | updateComments: PropTypes.func.isRequired, 162 | login: PropTypes.func.isRequired, 163 | toggleReplyInput: PropTypes.func.isRequired, 164 | }; 165 | 166 | Dialogue.defaultProps = { 167 | dialogue: [], 168 | profiles: {}, 169 | thread: {}, 170 | box: {}, 171 | currentUserAddr: null, 172 | useHovers: false, 173 | isNestedComment: false, 174 | }; 175 | -------------------------------------------------------------------------------- /src/components/Emoji/EmojiIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import '../../css/PopupWindow.css'; 5 | import '../../css/EmojiPicker.css'; 6 | 7 | const EmojiIcon = ({ tooltip, onClick, isActive, isInlinePicker }) => ( 8 |
{ }} 11 | > 12 | {tooltip} 13 | 14 | 46 | 47 | {isActive && ( 48 |
onClick()} 51 | onKeyPress={() => onClick()} 52 | role="button" 53 | tabIndex={0} 54 | /> 55 | )} 56 |
57 | ); 58 | 59 | EmojiIcon.propTypes = { 60 | tooltip: PropTypes.object, 61 | onClick: PropTypes.func.isRequired, 62 | isActive: PropTypes.bool, 63 | isInlinePicker: PropTypes.bool, 64 | }; 65 | 66 | export default EmojiIcon; 67 | -------------------------------------------------------------------------------- /src/components/Emoji/EmojiPicker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EmojiConvertor from 'emoji-js'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import emojiData from './emojiData.json'; 6 | 7 | const emojiConvertor = new EmojiConvertor(); 8 | emojiConvertor.init_env(); 9 | 10 | const EmojiPicker = ({ onEmojiPicked, filter }) => ( 11 |
12 | {emojiData.map((category) => { 13 | const filteredEmojis = category.emojis.filter(({ name }) => name.includes(filter)); 14 | return ( 15 |
16 | {filteredEmojis.length > 0 && ( 17 |
18 | {category.name} 19 |
20 | )} 21 | 22 | {filteredEmojis.map(({ char }) => { 23 | return ( 24 | onEmojiPicked(char)} 28 | > 29 | {char} 30 | 31 | ); 32 | })} 33 |
34 | ); 35 | })} 36 |
37 | ); 38 | 39 | EmojiPicker.propTypes = { 40 | onEmojiPicked: PropTypes.func.isRequired, 41 | filter: PropTypes.string, 42 | }; 43 | 44 | export default EmojiPicker; 45 | -------------------------------------------------------------------------------- /src/components/Emoji/PopupWindow.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class PopupWindow extends Component { 5 | 6 | componentDidMount() { 7 | this.scLauncher = document.querySelector('.threebox-comments-react'); 8 | this.scLauncher.addEventListener('click', this.interceptLauncherClick); 9 | } 10 | 11 | componentWillUnmount() { 12 | this.scLauncher.removeEventListener('click', this.interceptLauncherClick); 13 | } 14 | 15 | interceptLauncherClick = (e) => { 16 | const { isOpen, onClickedOutside } = this.props; 17 | const clickedOutside = !this.emojiPopup.contains(e.target) && isOpen; 18 | clickedOutside && onClickedOutside(e); 19 | } 20 | 21 | render() { 22 | const { isOpen, children, onInputChange } = this.props; 23 | 24 | return ( 25 |
this.emojiPopup = e} onClick={(e) => { e.preventDefault(); e.stopPropagation() }}> 26 |
27 | 32 | {children} 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | PopupWindow.propTypes = { 40 | isOpen: PropTypes.bool, 41 | children: PropTypes.object, 42 | onInputChange: PropTypes.func, 43 | onClickedOutside: PropTypes.func, 44 | }; 45 | 46 | export default PopupWindow; 47 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SVG from 'react-inlinesvg'; 3 | 4 | import Logo from '../assets/3BoxLogo.svg'; 5 | import '../css/Footer.css'; 6 | 7 | const Footer = () => ( 8 |
9 | 10 | Decentralized comments by 11 | 12 | 13 | 14 | 15 |
16 | ) 17 | 18 | export default Footer; -------------------------------------------------------------------------------- /src/components/Input.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import makeBlockie from 'ethereum-blockies-base64'; 3 | import SVG from 'react-inlinesvg'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { shortenEthAddr, checkIsMobileDevice, encodeMessage } from '../utils'; 7 | 8 | import EmojiIcon from './Emoji/EmojiIcon'; 9 | import PopupWindow from './Emoji/PopupWindow'; 10 | import EmojiPicker from './Emoji/EmojiPicker'; 11 | import Loading from '../assets/3BoxCommentsSpinner.svg'; 12 | import Logo from '../assets/3BoxLogo.svg'; 13 | import Profile from '../assets/Profile.svg'; 14 | import '../css/Input.css'; 15 | import '../css/PopupWindow.css'; 16 | 17 | class Input extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | user: '', 22 | comment: '', 23 | time: '', 24 | emojiFilter: '', 25 | disableComment: true, 26 | postLoading: false, 27 | emojiPickerIsOpen: false, 28 | isMobile: checkIsMobileDevice() 29 | } 30 | this.inputRef = React.createRef(); 31 | } 32 | 33 | async componentDidMount() { 34 | const el = this.inputRef.current; 35 | el.addEventListener("keydown", this.searchEnter, false); 36 | this.emojiPickerButton = document.querySelector('#sc-emoji-picker-button'); 37 | 38 | this.setState({ disableComment: false }); 39 | 40 | document.addEventListener('input', (event) => { 41 | if (event.target.tagName.toLowerCase() !== 'textarea') return; 42 | this.autoExpand(event.target); 43 | }, false); 44 | } 45 | 46 | autoExpand = (field) => { 47 | var height = field.scrollHeight; 48 | field.style.height = (height - 2) + 'px'; 49 | }; 50 | 51 | componentWillUnmount() { 52 | const el = document.getElementsByClassName('input_form')[0]; 53 | el.removeEventListener("keydown", this.searchEnter, false); 54 | } 55 | 56 | handleCommentText = (event) => { 57 | const { noWeb3 } = this.props; 58 | if (!noWeb3) this.setState({ comment: event.target.value }); 59 | } 60 | 61 | searchEnter = (event) => { 62 | const { comment, isMobile } = this.state; 63 | const { isLoading } = this.props; 64 | const updatedComment = comment.replace(/(\r\n|\n|\r)/gm, ""); 65 | 66 | if (event.keyCode === 13 && !event.shiftKey && updatedComment && !isMobile && !isLoading) { 67 | this.saveComment(); 68 | } else if (event.keyCode === 13 && !event.shiftKey && !isMobile && !updatedComment) { 69 | event.preventDefault(); 70 | } 71 | } 72 | 73 | handleLoggedInAs = () => { 74 | const { showLoggedInAs } = this.state; 75 | this.setState({ showLoggedInAs: !showLoggedInAs }); 76 | } 77 | 78 | toggleEmojiPicker = (e) => { 79 | e.preventDefault(); 80 | if (!this.state.emojiPickerIsOpen) { 81 | this.setState({ emojiPickerIsOpen: true }); 82 | } 83 | } 84 | 85 | closeEmojiPicker = (e) => { 86 | if (this.emojiPickerButton.contains(e.target)) { 87 | e.stopPropagation(); 88 | e.preventDefault(); 89 | } 90 | this.setState({ emojiPickerIsOpen: false }); 91 | } 92 | 93 | _handleEmojiPicked = (emoji) => { 94 | const { comment } = this.state; 95 | let newComment = comment; 96 | const updatedComment = newComment += emoji; 97 | this.setState({ emojiPickerIsOpen: false, comment: updatedComment }); 98 | } 99 | 100 | handleEmojiFilterChange = (event) => { 101 | const emojiFilter = event.target.value; 102 | this.setState({ emojiFilter }); 103 | } 104 | 105 | _renderEmojiPopup = () => ( 106 | 111 | 115 | 116 | ) 117 | 118 | saveComment = async () => { 119 | const { 120 | updateComments, 121 | noWeb3, 122 | parentId, 123 | grandParentId, 124 | onSubmit, 125 | login, 126 | isNestedInput, 127 | currentNestLevel, 128 | hasAuthed 129 | } = this.props; 130 | 131 | const { comment, disableComment, isMobile } = this.state; 132 | const updatedComment = comment.replace(/(\r\n|\n|\r)/gm, ""); 133 | this.inputRef.current.blur(); 134 | this.inputRef.current.style.height = isNestedInput ? '46px' : isMobile ? '64px' : '70px'; 135 | 136 | if (noWeb3) return console.log('No web3'); 137 | if (disableComment || !updatedComment) return console.log('comment is empty or disabled') 138 | 139 | this.setState({ postLoading: true, comment: '' }); 140 | if (!hasAuthed) await login(); 141 | 142 | try { 143 | const grandParentIdToUse = currentNestLevel === 2 && grandParentId; 144 | const message = encodeMessage("comment", comment, parentId, currentNestLevel, grandParentIdToUse); 145 | 146 | if (!this.props.thread || !Object.keys(this.props.thread).length) return console.log('Thread is empty'); 147 | await this.props.thread.post(message); 148 | 149 | await updateComments(); 150 | this.setState({ postLoading: false }); 151 | } catch (error) { 152 | console.error('There was an error saving your comment', error); 153 | } 154 | 155 | if (onSubmit) onSubmit(); 156 | } 157 | 158 | render() { 159 | const { 160 | comment, 161 | postLoading, 162 | showLoggedInAs, 163 | isMobile, 164 | emojiPickerIsOpen, 165 | } = this.state; 166 | 167 | const { 168 | currentUser3BoxProfile, 169 | currentUserAddr, 170 | login, 171 | isLoading3Box, 172 | hasAuthed, 173 | box, 174 | showReply, 175 | toggleReplyInput, 176 | noWeb3 177 | } = this.props; 178 | 179 | const updatedProfilePicture = (currentUser3BoxProfile && currentUser3BoxProfile.image) ? `https://ipfs.infura.io/ipfs/${currentUser3BoxProfile.image[0].contentUrl['/']}` 180 | : currentUserAddr && makeBlockie(currentUserAddr); 181 | const isBoxEmpty = !box || !Object.keys(box).length; 182 | 183 | return ( 184 | <> 185 |
186 | {updatedProfilePicture ? ( 187 | Profile 192 | ) : ( 193 |
194 | 199 |
200 | )} 201 | 202 | {postLoading ? ( 203 |
204 | 209 | 210 | 211 | 212 |
213 | ) :
214 | } 215 | 216 |

217 | {(!noWeb3 && !currentUserAddr) ? 'You will log in upon commenting' : ''} 218 | {(currentUserAddr && currentUser3BoxProfile) ? `Commenting as ${currentUser3BoxProfile.name || shortenEthAddr(currentUserAddr)}` : ''} 219 | {noWeb3 ? 'Cannot comment without Web3' : ''} 220 |

221 | 222 |